├── .gitattributes ├── lombok.config ├── document ├── web-docs │ ├── docs │ │ ├── .vuepress │ │ │ ├── configs │ │ │ │ ├── navbar │ │ │ │ │ ├── index.ts │ │ │ │ │ └── zh.ts │ │ │ │ ├── sidebar │ │ │ │ │ ├── index.ts │ │ │ │ │ └── zh.ts │ │ │ │ ├── index.ts │ │ │ │ ├── meta.ts │ │ │ │ └── head.ts │ │ │ └── public │ │ │ │ ├── robots.txt │ │ │ │ └── images │ │ │ │ ├── hero.png │ │ │ │ ├── logo.png │ │ │ │ └── icons │ │ │ │ └── safari-pinned-tab.svg │ │ ├── image │ │ │ ├── faq-api-test.png │ │ │ ├── faq-flame1.png │ │ │ ├── faq-flame2.png │ │ │ ├── faq-execution-time1.png │ │ │ ├── faq-execution-time2.png │ │ │ └── user-g-business-exception.png │ │ ├── README.md │ │ └── guide │ │ │ ├── changelog.md │ │ │ ├── introduction.md │ │ │ ├── FAQ.md │ │ │ ├── i18n.md │ │ │ ├── spel.md │ │ │ ├── custom.md │ │ │ ├── principle.md │ │ │ └── getting-started.md │ ├── tsconfig.json │ └── package.json └── image │ ├── wechat-qrcode.jpg │ ├── performance-test.png │ ├── alipay-receipt-code.jpg │ └── wechat-appreciation-code.jpg ├── spel-validator-javax ├── README.md ├── src │ ├── test │ │ └── java │ │ │ └── cn │ │ │ └── sticki │ │ │ └── spel │ │ │ └── validator │ │ │ └── javax │ │ │ ├── enums │ │ │ └── ExampleEnum.java │ │ │ ├── ConstrainTest.java │ │ │ ├── bean │ │ │ └── SpelValidTestBean.java │ │ │ └── util │ │ │ └── JavaxSpelValidator.java │ └── main │ │ └── java │ │ └── cn │ │ └── sticki │ │ └── spel │ │ └── validator │ │ └── javax │ │ ├── SpelValid.java │ │ └── SpelValidator.java └── pom.xml ├── spel-validator-jakarta ├── README.md ├── src │ ├── test │ │ └── java │ │ │ └── cn │ │ │ └── sticki │ │ │ └── spel │ │ │ └── validator │ │ │ └── jakarta │ │ │ ├── enums │ │ │ └── ExampleEnum.java │ │ │ ├── ConstrainTest.java │ │ │ ├── bean │ │ │ ├── SpelValidTestBean.java │ │ │ ├── I18nTestBean.java │ │ │ └── ParentClassTestBean.java │ │ │ └── JakartaSpelValidator.java │ └── main │ │ └── java │ │ └── cn │ │ └── sticki │ │ └── spel │ │ └── validator │ │ └── jakarta │ │ ├── SpelValid.java │ │ └── SpelValidator.java └── pom.xml ├── spel-validator-test-report ├── README.md └── pom.xml ├── spel-validator-constrain ├── src │ ├── test │ │ ├── resources │ │ │ ├── testMessages.properties │ │ │ ├── testMessages2.properties │ │ │ └── logback-test.xml │ │ └── java │ │ │ └── cn │ │ │ └── sticki │ │ │ └── spel │ │ │ └── validator │ │ │ └── constrain │ │ │ ├── ResourceMessageTest.java │ │ │ ├── bean │ │ │ └── SpelNotBlankTestBean.java │ │ │ └── MessageInterpolatorTest.java │ └── main │ │ ├── resources │ │ └── cn │ │ │ └── sticki │ │ │ └── spel │ │ │ └── validator │ │ │ ├── ValidationMessages_pt_PT.properties │ │ │ ├── ValidationMessages_pt_BR.properties │ │ │ ├── ValidationMessages_en.properties │ │ │ ├── ValidationMessages_sk.properties │ │ │ ├── ValidationMessages_tr.properties │ │ │ ├── ValidationMessages.properties │ │ │ ├── ValidationMessages_pt.properties │ │ │ ├── ValidationMessages_es.properties │ │ │ ├── ValidationMessages_it.properties │ │ │ ├── ValidationMessages_nl.properties │ │ │ ├── ValidationMessages_de.properties │ │ │ ├── ValidationMessages_da.properties │ │ │ ├── ValidationMessages_fr.properties │ │ │ ├── ValidationMessages_zh.properties │ │ │ ├── ValidationMessages_zh_TW.properties │ │ │ ├── ValidationMessages_ro.properties │ │ │ ├── ValidationMessages_pl.properties │ │ │ ├── ValidationMessages_hu.properties │ │ │ ├── ValidationMessages_zh_CN.properties │ │ │ ├── ValidationMessages_cs.properties │ │ │ ├── ValidationMessages_mn_MN.properties │ │ │ ├── ValidationMessages_ko.properties │ │ │ ├── ValidationMessages_ar.properties │ │ │ └── ValidationMessages_ja.properties │ │ └── java │ │ └── cn │ │ └── sticki │ │ └── spel │ │ └── validator │ │ ├── constraintvalidator │ │ ├── SpelNullValidator.java │ │ ├── SpelNotNullValidator.java │ │ ├── SpelPastValidator.java │ │ ├── SpelFutureValidator.java │ │ ├── SpelPastOrPresentValidator.java │ │ ├── SpelFutureOrPresentValidator.java │ │ ├── SpelAssertValidator.java │ │ ├── SpelMaxValidator.java │ │ ├── SpelMinValidator.java │ │ ├── SpelNotBlankValidator.java │ │ ├── SpelNotEmptyValidator.java │ │ └── SpelSizeValidator.java │ │ └── constrain │ │ ├── SpelNull.java │ │ ├── SpelNotNull.java │ │ ├── SpelAssert.java │ │ ├── SpelNotBlank.java │ │ ├── SpelNotEmpty.java │ │ ├── SpelDigits.java │ │ ├── SpelMax.java │ │ ├── SpelMin.java │ │ ├── SpelSize.java │ │ ├── SpelPast.java │ │ ├── SpelFuture.java │ │ ├── SpelPastOrPresent.java │ │ └── SpelFutureOrPresent.java ├── README.md └── pom.xml ├── spel-validator-core ├── README.md ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ ├── spring │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ │ │ └── spring.factories │ │ └── java │ │ │ └── cn │ │ │ └── sticki │ │ │ └── spel │ │ │ └── validator │ │ │ └── core │ │ │ ├── exception │ │ │ ├── SpelArgumentException.java │ │ │ ├── SpelParserException.java │ │ │ ├── SpelValidatorException.java │ │ │ └── SpelNotSupportedTypeException.java │ │ │ ├── SpelValidContext.java │ │ │ ├── parse │ │ │ ├── EnableSpelValidatorBeanRegistrar.java │ │ │ └── SpelValidatorBeanRegistrar.java │ │ │ ├── result │ │ │ ├── FieldError.java │ │ │ ├── FieldValidResult.java │ │ │ └── ObjectValidResult.java │ │ │ ├── util │ │ │ ├── BigDecimalUtil.java │ │ │ ├── CalcLengthUtil.java │ │ │ └── NumberComparatorUtil.java │ │ │ ├── manager │ │ │ ├── AnnotationMethodManager.java │ │ │ └── ValidatorInstanceManager.java │ │ │ ├── SpelConstraintValidator.java │ │ │ ├── SpelConstraint.java │ │ │ └── message │ │ │ ├── ResourceBundleMessageResolver.java │ │ │ └── ValidatorMessageInterpolator.java │ └── test │ │ ├── java │ │ └── cn │ │ │ └── sticki │ │ │ └── spel │ │ │ └── validator │ │ │ └── core │ │ │ ├── constraint │ │ │ ├── SpelNotNullTest.java │ │ │ └── SpelNotNullValidator.java │ │ │ └── SpelValidExecutorTest.java │ │ └── resources │ │ └── logback-test.xml └── pom.xml ├── spel-validator-test ├── src │ └── main │ │ ├── java │ │ └── cn │ │ │ └── sticki │ │ │ └── spel │ │ │ └── validator │ │ │ └── test │ │ │ └── util │ │ │ ├── ID.java │ │ │ ├── IGetter.java │ │ │ ├── BaseSpelValidator.java │ │ │ ├── BeanUtil.java │ │ │ ├── VerifyFailedField.java │ │ │ ├── LogContext.java │ │ │ └── ConstraintViolationSet.java │ │ └── resources │ │ └── logback-test.xml ├── README.md └── pom.xml ├── .github └── workflows │ ├── mvn-test.yml │ ├── release-docs.yml │ └── release-maven.yml └── .gitignore /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.addLombokGeneratedAnnotation = true -------------------------------------------------------------------------------- /document/web-docs/docs/.vuepress/configs/navbar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './zh.js' 2 | -------------------------------------------------------------------------------- /document/web-docs/docs/.vuepress/configs/sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './zh.js' 2 | -------------------------------------------------------------------------------- /document/image/wechat-qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stick-i/spel-validator/HEAD/document/image/wechat-qrcode.jpg -------------------------------------------------------------------------------- /document/image/performance-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stick-i/spel-validator/HEAD/document/image/performance-test.png -------------------------------------------------------------------------------- /document/image/alipay-receipt-code.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stick-i/spel-validator/HEAD/document/image/alipay-receipt-code.jpg -------------------------------------------------------------------------------- /spel-validator-javax/README.md: -------------------------------------------------------------------------------- 1 | # 模块说明 2 | 3 | 此模块是可以直接使用的,基于 `javax.validation-api` 的参数校验工具模块。 4 | 5 | 内包含启动注解和一些常用的约束注解,可以直接使用。 6 | -------------------------------------------------------------------------------- /document/web-docs/docs/.vuepress/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent:* 2 | 3 | Allow: / 4 | 5 | Sitemap: https://spel-validator.sticki.cn/sitemap.xml -------------------------------------------------------------------------------- /document/image/wechat-appreciation-code.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stick-i/spel-validator/HEAD/document/image/wechat-appreciation-code.jpg -------------------------------------------------------------------------------- /document/web-docs/docs/image/faq-api-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stick-i/spel-validator/HEAD/document/web-docs/docs/image/faq-api-test.png -------------------------------------------------------------------------------- /document/web-docs/docs/image/faq-flame1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stick-i/spel-validator/HEAD/document/web-docs/docs/image/faq-flame1.png -------------------------------------------------------------------------------- /document/web-docs/docs/image/faq-flame2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stick-i/spel-validator/HEAD/document/web-docs/docs/image/faq-flame2.png -------------------------------------------------------------------------------- /document/web-docs/docs/.vuepress/configs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './head' 2 | export * from './navbar/index.js' 3 | export * from './sidebar/index.js' 4 | -------------------------------------------------------------------------------- /document/web-docs/docs/.vuepress/configs/meta.ts: -------------------------------------------------------------------------------- 1 | import config from '../../../package.json' 2 | 3 | // 获取版本号 4 | export const version = config.version; 5 | -------------------------------------------------------------------------------- /document/web-docs/docs/image/faq-execution-time1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stick-i/spel-validator/HEAD/document/web-docs/docs/image/faq-execution-time1.png -------------------------------------------------------------------------------- /document/web-docs/docs/image/faq-execution-time2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stick-i/spel-validator/HEAD/document/web-docs/docs/image/faq-execution-time2.png -------------------------------------------------------------------------------- /document/web-docs/docs/.vuepress/public/images/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stick-i/spel-validator/HEAD/document/web-docs/docs/.vuepress/public/images/hero.png -------------------------------------------------------------------------------- /document/web-docs/docs/.vuepress/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stick-i/spel-validator/HEAD/document/web-docs/docs/.vuepress/public/images/logo.png -------------------------------------------------------------------------------- /spel-validator-jakarta/README.md: -------------------------------------------------------------------------------- 1 | # 模块说明 2 | 3 | 此模块是可以直接使用的,基于 `jakarta.validation-api` 的参数校验工具模块。 4 | 5 | 内包含启动注解和一些常用的约束注解,可以直接使用。 6 | 7 | 注意:此模块最低支持的 JDK 版本为 11。 -------------------------------------------------------------------------------- /spel-validator-test-report/README.md: -------------------------------------------------------------------------------- 1 | # 此模块仅用于 jacoco 聚合覆盖率报告 2 | 3 | 聚合后的报告会生成在 `target/site/jacoco-aggregate` 目录下。 4 | 5 | 需要聚合的模块,在此模块中引入pom依赖即可,测试用例仍然写在各自模块中。 6 | -------------------------------------------------------------------------------- /document/web-docs/docs/image/user-g-business-exception.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stick-i/spel-validator/HEAD/document/web-docs/docs/image/user-g-business-exception.png -------------------------------------------------------------------------------- /spel-validator-constrain/src/test/resources/testMessages.properties: -------------------------------------------------------------------------------- 1 | test.info=Test info 2 | cn.sticki.spel.validator.constraint.Size.message=size must be between {0} and {1} (test) -------------------------------------------------------------------------------- /spel-validator-constrain/src/test/resources/testMessages2.properties: -------------------------------------------------------------------------------- 1 | test.info=Test info2 2 | cn.sticki.spel.validator.constraint.Size.message=size must be between {0} and {1} (test2) -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_pt_PT.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.Past.message = deve ser uma data no passado 2 | cn.sticki.spel.validator.constraint.Size.message = tamanho deve estar entre {0} e {1} 3 | -------------------------------------------------------------------------------- /spel-validator-core/README.md: -------------------------------------------------------------------------------- 1 | # 模块说明 2 | 3 | 此模块为校验器核心模块,提供了校验器的基本功能,包括校验器的注册、执行、结果汇总等。 4 | 5 | 该模块可以被单独使用,只需要遵循校验器的规范 [SpelConstraint](src/main/java/cn/sticki/spel/validator/core/SpelConstraint.java), 6 | 并实现自己需要的约束注解即可。 7 | 8 | 但一般情况下,建议直接使用 `-javax` 或 `-jakarta` 模块,因为这两个模块已经提供了一些常用的约束注解和启动注解。 9 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_pt_BR.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.Max.message = deve ser menor que ou igual \u00E0 {0} 2 | cn.sticki.spel.validator.constraint.Min.message = deve ser maior que ou igual \u00E0 {0} 3 | -------------------------------------------------------------------------------- /spel-validator-test/src/main/java/cn/sticki/spel/validator/test/util/ID.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.test.util; 2 | 3 | /** 4 | * ID接口,用于给测试用例标号,方便查看测试结果 5 | * 6 | * @author 阿杆 7 | * @version 1.0 8 | * @since 2024/6/15 9 | */ 10 | public interface ID { 11 | 12 | int getId(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.core.parse.SpelParser 2 | cn.sticki.spel.validator.core.SpelValidExecutor 3 | cn.sticki.spel.validator.core.manager.AnnotationMethodManager 4 | cn.sticki.spel.validator.core.util.CalcLengthUtil -------------------------------------------------------------------------------- /spel-validator-constrain/README.md: -------------------------------------------------------------------------------- 1 | # 模块说明 2 | 3 | 此模块内包含了一些常用的约束校验器,用于校验对象的属性是否符合预期。 4 | 5 | 同时这些校验器的测试用例也在此模块中。 6 | 7 | 刚开始这些约束器都是在 `-core` 模块里的。 8 | 但是由于这些约束器的测试工具依赖了 `-core` 模块,然后 `-core` 模块又依赖了 `-test`,这样就形成了循环依赖,导致编译不通过。 9 | 所以将这些约束器单独提取出来,放到一个新的模块中。 10 | 11 | 至于为什么需要将 `-test` 独立出来,可以看看 `-test` 模块的 [README.md](../spel-validator-test/README.md) 文件。 -------------------------------------------------------------------------------- /spel-validator-test/src/main/java/cn/sticki/spel/validator/test/util/IGetter.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.test.util; 2 | 3 | /** 4 | * getter,用于获取对象的属性值 5 | * 6 | * @author 阿杆 7 | * @version 1.0 8 | * @since 2024/6/15 9 | */ 10 | @FunctionalInterface 11 | public interface IGetter extends java.io.Serializable { 12 | 13 | @SuppressWarnings("unused") 14 | R apply(T t); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | # 这是一个简单的预热操作,用于在 SpringBoot 程序启动时预加载一些类,这样 static 代码块就会提前执行 2 | # 当然,它不是必须的 3 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 4 | cn.sticki.spel.validator.core.parse.SpelParser,\ 5 | cn.sticki.spel.validator.core.SpelValidExecutor,\ 6 | cn.sticki.spel.validator.core.manager.AnnotationMethodManager,\ 7 | cn.sticki.spel.validator.core.util.CalcLengthUtil -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/exception/SpelArgumentException.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.exception; 2 | 3 | /** 4 | * 参数异常 5 | * 6 | * @author 阿杆 7 | * @version 1.0 8 | * @since 2024/5/3 9 | */ 10 | public class SpelArgumentException extends SpelValidatorException { 11 | 12 | public SpelArgumentException(String message) { 13 | super(message); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /spel-validator-test/README.md: -------------------------------------------------------------------------------- 1 | # 模块说明 2 | 3 | 此模板内包含一些测试使用的工具,其他模块引入此模块时都需要以 `test` 的方式引入。 4 | 5 | 刚开始只有一个 `spel-validator`,里面包含了所有的代码。 6 | 7 | 后来需要兼容 `javax` 和 `jakarta` 的包,所以将代码分成了三个模块,`spel-validator-core`、`spel-validator-jakarta` 和 `spel-validator-javax`。 8 | 此时测试工具代码在 `spel-validator-core` 的测试目录中,但 `-javax` 和 `-jakarta` 模块也需要使用这些测试工具, 9 | 而测试目录的代码不会被编译到 jar 包中,无法传递给其他模块。 10 | 11 | 然后我就把测试代码单独提取出来,放到了 `spel-validator-test` 模块中,再让其他三个模块进行引入。 12 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_en.properties: -------------------------------------------------------------------------------- 1 | # This file is intentionally left empty. All calls to this bundle will 2 | # be delegated to the parent bundle ValidationMessages (which contains 3 | # English messages). 4 | # 5 | # Not providing this bundle would cause the bundle for the default 6 | # locale to take precedence over the base bundle. If the default locale 7 | # is not English but one, for which a resource bundle exists (e.g. German), 8 | # the English texts would never be returned. 9 | -------------------------------------------------------------------------------- /document/web-docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig-vuepress/base.json", 3 | "compilerOptions": { 4 | "lib": [ 5 | "DOM", 6 | "ES2022" 7 | ], 8 | "module": "ESNext", 9 | "moduleResolution": "Bundler", 10 | "target": "ES2022", 11 | "declaration": false 12 | }, 13 | "include": [ 14 | "**/.vuepress/**/*" 15 | ], 16 | "exclude": [ 17 | "node_modules", 18 | ".cache", 19 | ".temp", 20 | "dist" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/exception/SpelParserException.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.exception; 2 | 3 | /** 4 | * 表达式解析异常 5 | * 6 | * @author 阿杆 7 | * @version 1.0 8 | * @since 2024/4/29 9 | */ 10 | public class SpelParserException extends SpelValidatorException { 11 | 12 | public SpelParserException(String message) { 13 | super(message); 14 | } 15 | 16 | public SpelParserException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | public SpelParserException(Throwable cause) { 21 | super(cause); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/exception/SpelValidatorException.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.exception; 2 | 3 | /** 4 | * Spel 验证器异常,是项目中所有异常的父类。 5 | * 6 | * @author 阿杆 7 | * @version 1.0 8 | * @since 2024/4/29 9 | */ 10 | public class SpelValidatorException extends RuntimeException { 11 | 12 | public SpelValidatorException(String message) { 13 | super(message); 14 | } 15 | 16 | public SpelValidatorException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | public SpelValidatorException(Throwable cause) { 21 | super(cause); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/mvn-test.yml: -------------------------------------------------------------------------------- 1 | name: run mvn test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'v*' 7 | - 'main' 8 | types: [opened, synchronize, reopened] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | run-mvn-test: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-java@v4 18 | with: 19 | distribution: 'zulu' 20 | java-version: '11' 21 | cache: maven 22 | 23 | - run: | 24 | java -version 25 | mvn -v 26 | 27 | - run: mvn verify 28 | - run: mvn clean test coveralls:report -DrepoToken="${{secrets.COVERALLS_TOKEN}}" -f pom.xml 29 | -------------------------------------------------------------------------------- /document/web-docs/docs/.vuepress/configs/sidebar/zh.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarOptions } from '@vuepress/theme-default' 2 | 3 | export const sidebarZh: SidebarOptions = { 4 | '/guide/': [ 5 | { 6 | text: '基础', 7 | children: [ 8 | '/guide/introduction.md', 9 | '/guide/getting-started.md', 10 | '/guide/user-guide.md', 11 | '/guide/spel.md', 12 | '/guide/i18n.md', 13 | ], 14 | }, 15 | { 16 | text: '进阶', 17 | children: ['/guide/principle.md', '/guide/custom.md'], 18 | }, 19 | { 20 | text: '其他', 21 | children: ['/guide/annotation-guide.md', '/guide/changelog.md', '/guide/FAQ.md'], 22 | }, 23 | ], 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### IntelliJ IDEA ### 7 | /.idea/ 8 | *.iws 9 | *.iml 10 | *.ipr 11 | 12 | ### Eclipse ### 13 | .apt_generated 14 | .classpath 15 | .factorypath 16 | .project 17 | .settings 18 | .springBeans 19 | .sts4-cache 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | build/ 28 | !**/src/main/**/build/ 29 | !**/src/test/**/build/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | ### Mac OS ### 35 | .DS_Store 36 | 37 | ### vuepress ### 38 | /document/web-docs/node_modules/ 39 | /document/web-docs/docs/.vuepress/.cache/ 40 | /document/web-docs/docs/.vuepress/.temp/ 41 | /document/web-docs/docs/.vuepress/dist/ 42 | -------------------------------------------------------------------------------- /spel-validator-core/src/test/java/cn/sticki/spel/validator/core/constraint/SpelNotNullTest.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.constraint; 2 | 3 | import cn.sticki.spel.validator.core.SpelConstraint; 4 | 5 | import java.lang.annotation.Documented; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.Target; 8 | 9 | import static java.lang.annotation.ElementType.FIELD; 10 | import static java.lang.annotation.ElementType.TYPE; 11 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 12 | 13 | /** 14 | * 注解测试类 15 | * 16 | * @author 阿杆 17 | * @since 2024/11/4 18 | */ 19 | @Documented 20 | @Retention(RUNTIME) 21 | @Target({FIELD, TYPE}) 22 | @SpelConstraint(validatedBy = SpelNotNullValidator.class) 23 | public @interface SpelNotNullTest { 24 | } 25 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/SpelValidContext.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core; 2 | 3 | import lombok.Builder; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.ToString; 6 | 7 | import java.util.Locale; 8 | 9 | /** 10 | * Spel校验上下文 11 | * 12 | * @author 阿杆 13 | * @since 2025/4/10 14 | */ 15 | @Builder 16 | @ToString 17 | @EqualsAndHashCode 18 | public class SpelValidContext { 19 | 20 | Locale locale; 21 | 22 | private static final SpelValidContext DEFAULT = SpelValidContext.builder().build(); 23 | 24 | public static SpelValidContext getDefault() { 25 | return DEFAULT; 26 | } 27 | 28 | public Locale getLocale() { 29 | return locale == null ? Locale.getDefault() : locale; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /spel-validator-core/src/test/java/cn/sticki/spel/validator/core/constraint/SpelNotNullValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.constraint; 2 | 3 | import cn.sticki.spel.validator.core.SpelConstraintValidator; 4 | import cn.sticki.spel.validator.core.result.FieldValidResult; 5 | 6 | import java.lang.reflect.Field; 7 | 8 | /** 9 | * Test 10 | * 11 | * @author 阿杆 12 | * @version 1.0 13 | * @since 2024/5/1 14 | */ 15 | public class SpelNotNullValidator implements SpelConstraintValidator { 16 | 17 | private SpelNotNullValidator() { 18 | } 19 | 20 | @Override 21 | public FieldValidResult isValid(SpelNotNullTest annotation, Object obj, Field field) throws IllegalAccessException { 22 | return FieldValidResult.of(field.get(obj) != null); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constraintvalidator/SpelNullValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constraintvalidator; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelNull; 4 | import cn.sticki.spel.validator.core.SpelConstraintValidator; 5 | import cn.sticki.spel.validator.core.result.FieldValidResult; 6 | 7 | import java.lang.reflect.Field; 8 | 9 | /** 10 | * {@link SpelNull} 注解校验器。 11 | * 12 | * @author 阿杆 13 | * @version 1.0 14 | * @since 2024/5/5 15 | */ 16 | public class SpelNullValidator implements SpelConstraintValidator { 17 | 18 | @Override 19 | public FieldValidResult isValid(SpelNull annotation, Object obj, Field field) throws IllegalAccessException { 20 | return FieldValidResult.of(field.get(obj) == null); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/exception/SpelNotSupportedTypeException.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.exception; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.Set; 6 | 7 | /** 8 | * 不支持的类型异常 9 | * 10 | * @author 阿杆 11 | * @version 1.0 12 | * @since 2024/5/3 13 | */ 14 | @Getter 15 | public class SpelNotSupportedTypeException extends SpelValidatorException { 16 | 17 | private final Class clazz; 18 | 19 | private final Set> supperType; 20 | 21 | public SpelNotSupportedTypeException(Class clazz, Set> supperType) { 22 | super("Class type not supported, current type: " + clazz.getName() + ", supper type: " + supperType.toString()); 23 | this.clazz = clazz; 24 | this.supperType = supperType; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/parse/EnableSpelValidatorBeanRegistrar.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.parse; 2 | 3 | import org.springframework.context.annotation.Import; 4 | 5 | import java.lang.annotation.*; 6 | 7 | /** 8 | * 启用 {@link SpelValidatorBeanRegistrar},启用后可以在 spel-validator 的相关注解中引用 Spring Bean。 9 | *

10 | * 例如: 11 | *

12 |  * @SpelAssert(assertTrue = "@userService.getUserById(#this.userId) != null")
13 |  * private Integer userId;
14 |  * 
15 | * 16 | * @author 阿杆 17 | * @version 1.0 18 | * @since 2024/5/4 19 | */ 20 | @SuppressWarnings("unused") 21 | @Documented 22 | @Target(ElementType.TYPE) 23 | @Retention(RetentionPolicy.RUNTIME) 24 | @Import(SpelValidatorBeanRegistrar.class) 25 | public @interface EnableSpelValidatorBeanRegistrar { 26 | } 27 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constraintvalidator/SpelNotNullValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constraintvalidator; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelNotNull; 4 | import cn.sticki.spel.validator.core.SpelConstraintValidator; 5 | import cn.sticki.spel.validator.core.result.FieldValidResult; 6 | 7 | import java.lang.reflect.Field; 8 | 9 | /** 10 | * {@link SpelNotNull} 注解校验器。 11 | * 12 | * @author 阿杆 13 | * @version 1.0 14 | * @since 2024/5/1 15 | */ 16 | public class SpelNotNullValidator implements SpelConstraintValidator { 17 | 18 | @Override 19 | public FieldValidResult isValid(SpelNotNull annotation, Object obj, Field field) throws IllegalAccessException { 20 | return FieldValidResult.of(field.get(obj) != null); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/result/FieldError.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.result; 2 | 3 | import lombok.Getter; 4 | import lombok.ToString; 5 | 6 | /** 7 | * 字段错误信息 8 | * 9 | * @author 阿杆 10 | * @version 1.0 11 | * @since 2024/4/29 12 | */ 13 | @ToString 14 | @Getter 15 | public class FieldError { 16 | 17 | /** 18 | * 字段名称 19 | */ 20 | private final String fieldName; 21 | 22 | /** 23 | * 错误信息 24 | */ 25 | private final String errorMessage; 26 | 27 | public FieldError(String fieldName, String errorMessage) { 28 | this.fieldName = fieldName; 29 | this.errorMessage = errorMessage; 30 | } 31 | 32 | public static FieldError of(String fieldName, String errorMessage) { 33 | return new FieldError(fieldName, errorMessage); 34 | } 35 | } -------------------------------------------------------------------------------- /document/web-docs/docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | title: 首页 4 | heroImage: ./images/hero.png 5 | actions: 6 | - text: 快速开始 7 | link: /guide/getting-started.html 8 | type: primary 9 | 10 | - text: 项目介绍 11 | link: /guide/introduction.html 12 | type: secondary 13 | 14 | features: 15 | - title: 易上手 16 | details: 设计直观,简单易用,与 jakarta.validation-api 使用方式高度相似,学习成本低,上手快。 17 | - title: 极致灵活 18 | details: 利用 SpEL(Spring Expression Language)的强大功能,轻松实现复杂验证逻辑,且能够直接调用已注入的 Spring Beans 进行验证。 19 | - title: 无缝集成 20 | details: 扩展自 jakarta.validation-api 包,只新增不修改,无缝集成到现有项目中。 21 | - title: 上下文感知 22 | details: 能够在整个对象的上下文中进行验证,支持跨字段的逻辑验证,提高数据完整性检查的效率和准确性。 23 | - title: 异常友好 24 | details: 验证失败时自动整合进 jakarta.validation-api 的异常管理体系,简化错误处理流程。 25 | - title: 完全可定制 26 | details: 支持创建自定义约束注解,允许根据具体业务需求灵活定义验证规则。 27 | 28 | footer: Apache-2.0 license | 版权所有 © 2024-至今 阿杆 29 | --- 30 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/parse/SpelValidatorBeanRegistrar.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.parse; 2 | 3 | import lombok.Getter; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.springframework.context.ApplicationContext; 7 | import org.springframework.context.ApplicationContextAware; 8 | 9 | /** 10 | * ApplicationContext工具类,便于在一些非Spring管理的类中使用ApplicationContext的功能 11 | * 12 | * @author 阿杆 13 | * @version 1.0 14 | * @since 2024/4/29 15 | */ 16 | @Slf4j 17 | public class SpelValidatorBeanRegistrar implements ApplicationContextAware { 18 | 19 | @Getter 20 | private static ApplicationContext applicationContext; 21 | 22 | @Override 23 | public void setApplicationContext(@NotNull ApplicationContext applicationContext) { 24 | SpelValidatorBeanRegistrar.applicationContext = applicationContext; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /spel-validator-javax/src/test/java/cn/sticki/spel/validator/javax/enums/ExampleEnum.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.javax.enums; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * 示例枚举 7 | * 8 | * @author 阿杆 9 | * @version 1.0 10 | * @since 2024/5/1 11 | */ 12 | @Getter 13 | public enum ExampleEnum { 14 | 15 | ONE(1), 16 | 17 | TWO(2), 18 | 19 | THREE(3), 20 | 21 | FOUR(4), 22 | 23 | FIVE(5); 24 | 25 | private final Integer code; 26 | 27 | ExampleEnum(Integer code) { 28 | this.code = code; 29 | } 30 | 31 | public static ExampleEnum getByCode(Integer code) { 32 | if (code == null) { 33 | throw new IllegalArgumentException("code can not be null"); 34 | } 35 | for (ExampleEnum value : values()) { 36 | if (value.code.equals(code)) { 37 | return value; 38 | } 39 | } 40 | return null; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /spel-validator-jakarta/src/test/java/cn/sticki/spel/validator/jakarta/enums/ExampleEnum.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.jakarta.enums; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * 示例枚举 7 | * 8 | * @author 阿杆 9 | * @version 1.0 10 | * @since 2024/5/1 11 | */ 12 | @Getter 13 | public enum ExampleEnum { 14 | 15 | ONE(1), 16 | 17 | TWO(2), 18 | 19 | THREE(3), 20 | 21 | FOUR(4), 22 | 23 | FIVE(5); 24 | 25 | private final Integer code; 26 | 27 | ExampleEnum(Integer code) { 28 | this.code = code; 29 | } 30 | 31 | public static ExampleEnum getByCode(Integer code) { 32 | if (code == null) { 33 | throw new IllegalArgumentException("code can not be null"); 34 | } 35 | for (ExampleEnum value : values()) { 36 | if (value.code.equals(code)) { 37 | return value; 38 | } 39 | } 40 | return null; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /document/web-docs/docs/.vuepress/configs/navbar/zh.ts: -------------------------------------------------------------------------------- 1 | import { version } from '../meta' 2 | import type { NavbarOptions } from "@vuepress/theme-default"; 3 | 4 | export const navbarZh: NavbarOptions = [ 5 | { 6 | text: '指南', 7 | children: [ 8 | '/guide/introduction.md', 9 | '/guide/getting-started.md', 10 | '/guide/user-guide.md', 11 | '/guide/spel.md', 12 | '/guide/custom.md', 13 | '/guide/i18n.md', 14 | '/guide/principle.md', 15 | '/guide/FAQ.md', 16 | ], 17 | }, 18 | { 19 | text: `v${version}`, 20 | children: [ 21 | { 22 | text: '更新日志', 23 | link: '/guide/changelog.html', 24 | }, 25 | { 26 | text: 'GitHub Releases', 27 | link: 'https://github.com/stick-i/spel-validator/releases', 28 | }, 29 | ], 30 | }, 31 | ] 32 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constraintvalidator/SpelPastValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constraintvalidator; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelPast; 4 | import cn.sticki.spel.validator.core.result.FieldValidResult; 5 | 6 | import java.lang.reflect.Field; 7 | 8 | /** 9 | * {@link SpelPast} 注解校验器。 10 | * 11 | * @author 阿杆 12 | * @version 1.0 13 | * @since 2025/07/20 14 | */ 15 | public class SpelPastValidator extends AbstractSpelTemporalValidator { 16 | 17 | @Override 18 | public FieldValidResult isValid(SpelPast annotation, Object obj, Field field) throws IllegalAccessException { 19 | Object fieldValue = field.get(obj); 20 | return super.isValid(fieldValue); 21 | } 22 | 23 | @Override 24 | protected boolean isValidTemporal(Object temporal) { 25 | Object now = getNow(temporal); 26 | return compareTemporal(temporal, now) < 0; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /spel-validator-test/src/main/java/cn/sticki/spel/validator/test/util/BaseSpelValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.test.util; 2 | 3 | import cn.sticki.spel.validator.core.SpelValidContext; 4 | import cn.sticki.spel.validator.core.SpelValidExecutor; 5 | import cn.sticki.spel.validator.core.result.ObjectValidResult; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * 测试验证工具 11 | * 12 | * @author 阿杆 13 | * @since 2024/10/31 14 | */ 15 | public class BaseSpelValidator extends AbstractSpelValidator { 16 | 17 | private static final BaseSpelValidator INSTANCE = new BaseSpelValidator(); 18 | 19 | public static boolean check(List verifyObjectList) { 20 | return INSTANCE.checkConstraintResult(verifyObjectList); 21 | } 22 | 23 | @Override 24 | public ObjectValidResult validate(Object obj, String[] spelGroups, SpelValidContext context) { 25 | return SpelValidExecutor.validateObject(obj, spelGroups, context); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constraintvalidator/SpelFutureValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constraintvalidator; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelFuture; 4 | import cn.sticki.spel.validator.core.result.FieldValidResult; 5 | 6 | import java.lang.reflect.Field; 7 | 8 | /** 9 | * {@link SpelFuture} 注解校验器。 10 | * 11 | * @author 阿杆 12 | * @version 1.0 13 | * @since 2025/07/20 14 | */ 15 | public class SpelFutureValidator extends AbstractSpelTemporalValidator { 16 | 17 | @Override 18 | public FieldValidResult isValid(SpelFuture annotation, Object obj, Field field) throws IllegalAccessException { 19 | Object fieldValue = field.get(obj); 20 | return super.isValid(fieldValue); 21 | } 22 | 23 | @Override 24 | protected boolean isValidTemporal(Object temporal) { 25 | Object now = getNow(temporal); 26 | return compareTemporal(temporal, now) > 0; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constraintvalidator/SpelPastOrPresentValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constraintvalidator; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelPastOrPresent; 4 | import cn.sticki.spel.validator.core.result.FieldValidResult; 5 | 6 | import java.lang.reflect.Field; 7 | 8 | /** 9 | * {@link SpelPastOrPresent} 注解校验器。 10 | * 11 | * @author 阿杆 12 | * @version 1.0 13 | * @since 2025/07/20 14 | */ 15 | public class SpelPastOrPresentValidator extends AbstractSpelTemporalValidator { 16 | 17 | @Override 18 | public FieldValidResult isValid(SpelPastOrPresent annotation, Object obj, Field field) throws IllegalAccessException { 19 | Object fieldValue = field.get(obj); 20 | return super.isValid(fieldValue); 21 | } 22 | 23 | @Override 24 | protected boolean isValidTemporal(Object temporal) { 25 | Object now = getNow(temporal); 26 | return compareTemporal(temporal, now) <= 0; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constraintvalidator/SpelFutureOrPresentValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constraintvalidator; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelFutureOrPresent; 4 | import cn.sticki.spel.validator.core.result.FieldValidResult; 5 | 6 | import java.lang.reflect.Field; 7 | 8 | /** 9 | * {@link SpelFutureOrPresent} 注解校验器。 10 | * 11 | * @author 阿杆 12 | * @version 1.0 13 | * @since 2025/07/20 14 | */ 15 | public class SpelFutureOrPresentValidator extends AbstractSpelTemporalValidator { 16 | 17 | @Override 18 | public FieldValidResult isValid(SpelFutureOrPresent annotation, Object obj, Field field) throws IllegalAccessException { 19 | Object fieldValue = field.get(obj); 20 | return super.isValid(fieldValue); 21 | } 22 | 23 | @Override 24 | protected boolean isValidTemporal(Object temporal) { 25 | Object now = getNow(temporal); 26 | return compareTemporal(temporal, now) >= 0; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /spel-validator-test/src/main/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | [%highlight(%level)][%X{fullClassName}][%X{id}][%X{fieldName}] - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /spel-validator-core/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | [%highlight(%level)][%X{fullClassName}][%X{id}][%X{fieldName}] - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | [%highlight(%level)][%X{fullClassName}][%X{id}][%X{fieldName}] - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constraintvalidator/SpelAssertValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constraintvalidator; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelAssert; 4 | import cn.sticki.spel.validator.core.SpelConstraintValidator; 5 | import cn.sticki.spel.validator.core.exception.SpelArgumentException; 6 | import cn.sticki.spel.validator.core.parse.SpelParser; 7 | import cn.sticki.spel.validator.core.result.FieldValidResult; 8 | 9 | import java.lang.reflect.Field; 10 | 11 | /** 12 | * {@link SpelAssert} 注解校验器。 13 | * 14 | * @author 阿杆 15 | * @version 1.0 16 | * @since 2024/5/1 17 | */ 18 | public class SpelAssertValidator implements SpelConstraintValidator { 19 | 20 | @Override 21 | public FieldValidResult isValid(SpelAssert annotation, Object obj, Field field) { 22 | if (annotation.assertTrue().isEmpty()) { 23 | throw new SpelArgumentException("assertTrue must not be empty"); 24 | } 25 | 26 | return FieldValidResult.of(SpelParser.parse(annotation.assertTrue(), obj, Boolean.class)); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /spel-validator-javax/src/test/java/cn/sticki/spel/validator/javax/ConstrainTest.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.javax; 2 | 3 | import cn.sticki.spel.validator.javax.bean.ExampleTestBean; 4 | import cn.sticki.spel.validator.javax.bean.SpelValidTestBean; 5 | import cn.sticki.spel.validator.javax.util.JavaxSpelValidator; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | 9 | /** 10 | * 约束类测试 11 | * 12 | * @author 阿杆 13 | * @version 1.0 14 | * @since 2024/6/15 15 | */ 16 | public class ConstrainTest { 17 | 18 | /** 19 | * 这是一个测试示例 20 | */ 21 | @Test 22 | void testExample() { 23 | boolean verified = JavaxSpelValidator.check(ExampleTestBean.testCase()); 24 | Assertions.assertTrue(verified); 25 | 26 | boolean innerTest = JavaxSpelValidator.check(ExampleTestBean.innerTestCase()); 27 | Assertions.assertTrue(innerTest); 28 | } 29 | 30 | @Test 31 | void testSpelValid() { 32 | boolean verified = JavaxSpelValidator.check(SpelValidTestBean.paramTestCase()); 33 | Assertions.assertTrue(verified); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/util/BigDecimalUtil.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.util; 2 | 3 | import cn.sticki.spel.validator.core.exception.SpelArgumentException; 4 | 5 | import java.math.BigDecimal; 6 | 7 | /** 8 | * BigDecimal工具 9 | * 10 | * @author oddfar 11 | * @since 2024/8/25 12 | */ 13 | public class BigDecimalUtil { 14 | 15 | private BigDecimalUtil() { 16 | } 17 | 18 | public static BigDecimal valueOf(Object val) { 19 | try { 20 | if (val instanceof BigDecimal) { 21 | return (BigDecimal) val; 22 | } else if (val instanceof Double) { 23 | return BigDecimal.valueOf((Double) val); 24 | } else if (val instanceof Float) { 25 | return BigDecimal.valueOf((Float) val); 26 | } else { 27 | return new BigDecimal(String.valueOf(val)); 28 | } 29 | } catch (NumberFormatException e) { 30 | // 如果转换失败 31 | throw new SpelArgumentException("Value [" + val + "] can not convert to BigDecimal."); 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constraintvalidator/SpelMaxValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constraintvalidator; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelMax; 4 | import cn.sticki.spel.validator.core.result.FieldValidResult; 5 | import cn.sticki.spel.validator.core.util.NumberComparatorUtil; 6 | 7 | import java.lang.reflect.Field; 8 | 9 | /** 10 | * {@link SpelMax} 注解校验器。 11 | * 12 | * @author 阿杆 13 | * @version 1.0 14 | * @since 2024/9/29 15 | */ 16 | public class SpelMaxValidator extends AbstractSpelNumberCompareValidator { 17 | 18 | @Override 19 | protected boolean compare(SpelMax anno, Number fieldValue, Number compareValue) { 20 | int compareResult = NumberComparatorUtil.compare(fieldValue, compareValue, NumberComparatorUtil.GREATER_THAN); 21 | return anno.inclusive() ? compareResult <= 0 : compareResult < 0; 22 | } 23 | 24 | @Override 25 | public FieldValidResult isValid(SpelMax annotation, Object obj, Field field) throws IllegalAccessException { 26 | return super.isValid(annotation, field.get(obj), annotation.value(), obj); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constraintvalidator/SpelMinValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constraintvalidator; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelMin; 4 | import cn.sticki.spel.validator.core.result.FieldValidResult; 5 | import cn.sticki.spel.validator.core.util.NumberComparatorUtil; 6 | 7 | import java.lang.reflect.Field; 8 | 9 | /** 10 | * {@link SpelMin} 注解校验器。 11 | * 12 | * @author 阿杆 13 | * @version 1.0 14 | * @since 2024/9/29 15 | */ 16 | public class SpelMinValidator extends AbstractSpelNumberCompareValidator { 17 | 18 | @Override 19 | protected boolean compare(SpelMin anno, Number fieldValue, Number compareValue) { 20 | int compareResult = NumberComparatorUtil.compare(fieldValue, compareValue, NumberComparatorUtil.LESS_THAN); 21 | return anno.inclusive() ? compareResult >= 0 : compareResult > 0; 22 | } 23 | 24 | @Override 25 | public FieldValidResult isValid(SpelMin annotation, Object obj, Field field) throws IllegalAccessException { 26 | return super.isValid(annotation, field.get(obj), annotation.value(), obj); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /spel-validator-test/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cn.sticki 8 | spel-validator-root 9 | 0.6.1-beta 10 | 11 | 12 | Spel Validator Test 13 | spel-validator-test 14 | 15 | 16 | 17 | cn.sticki 18 | spel-validator-core 19 | 20 | 21 | 22 | org.junit.jupiter 23 | junit-jupiter 24 | 25 | 26 | 27 | ch.qos.logback 28 | logback-classic 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/manager/AnnotationMethodManager.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.manager; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.reflect.Method; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | 7 | /** 8 | * 注解类方法管理器。 9 | * 10 | * @author 阿杆 11 | * @version 1.0 12 | * @since 2024/5/9 13 | */ 14 | public class AnnotationMethodManager { 15 | 16 | private AnnotationMethodManager() { 17 | } 18 | 19 | private final static ConcurrentHashMap METHOD_CACHE = new ConcurrentHashMap<>(); 20 | 21 | /** 22 | * 获取方法。 23 | * 24 | * @param clazz 注解类 25 | * @param methodName 方法名 26 | * @return 方法,如果不存在则返回null 27 | */ 28 | public static Method get(Class clazz, String methodName) { 29 | // 由于注解类的方法无法重载,可以通过注解类和方法名来唯一确定一个方法 30 | return METHOD_CACHE.computeIfAbsent(clazz.toString() + "#" + methodName, s -> { 31 | try { 32 | return clazz.getMethod(methodName); 33 | } catch (NoSuchMethodException e) { 34 | return null; 35 | } 36 | }); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constraintvalidator/SpelNotBlankValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constraintvalidator; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelNotBlank; 4 | import cn.sticki.spel.validator.core.SpelConstraintValidator; 5 | import cn.sticki.spel.validator.core.result.FieldValidResult; 6 | import org.springframework.util.StringUtils; 7 | 8 | import java.lang.reflect.Field; 9 | import java.util.Collections; 10 | import java.util.Set; 11 | 12 | /** 13 | * {@link SpelNotBlank} 注解校验器。 14 | * 15 | * @author 阿杆 16 | * @version 1.0 17 | * @since 2024/5/5 18 | */ 19 | public class SpelNotBlankValidator implements SpelConstraintValidator { 20 | 21 | private static final Set> SUPPORT_TYPE = Collections.singleton(CharSequence.class); 22 | 23 | @Override 24 | public FieldValidResult isValid(SpelNotBlank annotation, Object obj, Field field) throws IllegalAccessException { 25 | CharSequence fieldValue = (CharSequence) field.get(obj); 26 | return FieldValidResult.of(StringUtils.hasText(fieldValue)); 27 | } 28 | 29 | @Override 30 | public Set> supportType() { 31 | return SUPPORT_TYPE; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constraintvalidator/SpelNotEmptyValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constraintvalidator; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelNotEmpty; 4 | import cn.sticki.spel.validator.core.SpelConstraintValidator; 5 | import cn.sticki.spel.validator.core.result.FieldValidResult; 6 | import cn.sticki.spel.validator.core.util.CalcLengthUtil; 7 | 8 | import java.lang.reflect.Field; 9 | import java.util.Set; 10 | 11 | /** 12 | * {@link SpelNotEmpty} 注解校验器。 13 | * 14 | * @author 阿杆 15 | * @version 1.0 16 | * @since 2024/5/5 17 | */ 18 | public class SpelNotEmptyValidator implements SpelConstraintValidator { 19 | 20 | @Override 21 | public FieldValidResult isValid(SpelNotEmpty annotation, Object obj, Field field) throws IllegalAccessException { 22 | Object object = field.get(obj); 23 | if (object == null) { 24 | return FieldValidResult.of(false); 25 | } 26 | int size = CalcLengthUtil.calcFieldSize(object); 27 | return FieldValidResult.of(size > 0); 28 | } 29 | 30 | @Override 31 | public Set> supportType() { 32 | return CalcLengthUtil.SUPPORT_TYPE; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /document/web-docs/docs/.vuepress/configs/head.ts: -------------------------------------------------------------------------------- 1 | import type {HeadConfig} from 'vuepress/core' 2 | 3 | export const head: HeadConfig[] = [ 4 | [ 5 | 'link', 6 | { 7 | rel: 'icon', 8 | type: 'image/png', 9 | sizes: '16x16', 10 | href: `/images/logo.png`, 11 | }, 12 | ], 13 | [ 14 | 'link', 15 | { 16 | rel: 'icon', 17 | type: 'image/png', 18 | sizes: '32x32', 19 | href: `/images/logo.png`, 20 | }, 21 | ], 22 | ['link', {rel: 'manifest', href: '/manifest.webmanifest'}], 23 | ['meta', {name: 'application-name', content: 'SpEL Validator'}], 24 | ['meta', {name: 'apple-mobile-web-app-title', content: 'SpEL Validator'}], 25 | ['meta', {name: 'apple-mobile-web-app-status-bar-style', content: 'black'}], 26 | [ 27 | 'link', 28 | {rel: 'apple-touch-icon', href: `/images/logo.png`}, 29 | ], 30 | [ 31 | 'link', 32 | { 33 | rel: 'mask-icon', 34 | href: '/images/logo.png', 35 | color: '#3eaf7c', 36 | }, 37 | ], 38 | ['meta', {name: 'msapplication-TileColor', content: '#3eaf7c'}], 39 | ['meta', {name: 'theme-color', content: '#3eaf7c'}], 40 | ] 41 | -------------------------------------------------------------------------------- /spel-validator-constrain/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cn.sticki 8 | spel-validator-root 9 | 0.6.1-beta 10 | 11 | 12 | Spel Validator Constrain 13 | spel-validator-constrain 14 | 15 | 16 | 8 17 | 8 18 | UTF-8 19 | 20 | 21 | 22 | 23 | cn.sticki 24 | spel-validator-core 25 | 26 | 27 | 28 | cn.sticki 29 | spel-validator-test 30 | test 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/SpelConstraintValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core; 2 | 3 | import cn.sticki.spel.validator.core.result.FieldValidResult; 4 | 5 | import java.lang.annotation.Annotation; 6 | import java.lang.reflect.Field; 7 | import java.util.Collections; 8 | import java.util.Set; 9 | 10 | /** 11 | * Spel 约束校验器。 12 | * 13 | * @author 阿杆 14 | * @version 1.0 15 | * @since 2024/4/11 16 | */ 17 | public interface SpelConstraintValidator { 18 | 19 | Set> DEFAULT_SUPPORT_TYPE = Collections.singleton(Object.class); 20 | 21 | /** 22 | * 校验被标记的字段 23 | *

24 | * 限制: 25 | *

    26 | *
  • 该方法会被并发访问,实现时需要保证线程安全。
  • 27 | *
  • 校验时不能改变 obj 对象的值,这个对象与接口参数的对象是同一个。
  • 28 | *
29 | * 30 | * @param annotation 注解值 31 | * @param obj 被校验的对象 32 | * @param field 被校验的字段,该字段存在于 obj 中 33 | * @return 校验结果 34 | */ 35 | FieldValidResult isValid(A annotation, Object obj, Field field) throws IllegalAccessException; 36 | 37 | /** 38 | * 校验器支持的对象类型列表,默认为 Object 39 | * 40 | * @return 支持的对象类型列表 41 | */ 42 | default Set> supportType() { 43 | return DEFAULT_SUPPORT_TYPE; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /document/web-docs/docs/guide/changelog.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | ## v0.6.0-beta (2025-08-13) 4 | 5 | ### 🎉 新增功能 6 | 7 | - **新增 `@SpelDigits` 注解**:用于校验数字的整数部分和小数部分位数 8 | - 支持所有 `Number` 类型及它们的基本数据类型 9 | - 支持使用 `CharSequence` 表示的数字 10 | - 提供 `integer` 和 `fraction` 参数分别控制整数和小数部分的最大位数 11 | 12 | ### ✨ 功能增强 13 | 14 | - **`@SpelMin` 和 `@SpelMax` 支持 `CharSequence` 类型**:现在可以对字符串形式的数字进行范围校验 15 | - **`@SpelMin` 和 `@SpelMax` 新增 `inclusive` 参数**: 16 | - 支持配置边界值是否包含在内 17 | - `inclusive = true`(默认):`@SpelMin` 验证 `value >= min`,`@SpelMax` 验证 `value <= max` 18 | - `inclusive = false`:`@SpelMin` 验证 `value > min`,`@SpelMax` 验证 `value < max` 19 | 20 | > 本次增强 `@SpelMin` 和 `@SpelMax` 的目的,是为了对标 `@DecimalMax`、`@DecimalMin` 两个注解,参考 https://github.com/stick-i/spel-validator/issues/65 21 | 22 | ### 💥 破坏性变更 23 | 24 | - **`@SpelMin` 和 `@SpelMax` 的 `value` 参数改为必填**: 25 | - 移除了默认值,提高使用的明确性 26 | - 升级时需要为所有使用这两个注解的地方显式指定 `value` 值 27 | 28 | ### ⚠️ 升级注意事项 29 | 30 | 如果你从旧版本升级到 0.6.0-beta,请注意: 31 | 32 | 1. **必须为 `@SpelMin` 和 `@SpelMax` 指定 `value` 值**: 33 | ```java 34 | // 旧版本(不再支持) 35 | @SpelMin 36 | private Integer count; 37 | 38 | // 新版本(必须指定 value) 39 | @SpelMin(value = "0") 40 | private Integer count; 41 | ``` 42 | 43 | --- 44 | 45 | ## 历史版本 46 | 47 | 更多历史版本信息请查看 [GitHub Releases](https://github.com/stick-i/spel-validator/releases)。 -------------------------------------------------------------------------------- /document/web-docs/docs/.vuepress/public/images/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/release-docs.yml: -------------------------------------------------------------------------------- 1 | name: Release to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | # 确保这是你正在使用的分支名称 7 | - main 8 | # 手动触发部署 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | deploy-gh-pages: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | # 如果你文档需要 Git 子模块,取消注释下一行 24 | # submodules: true 25 | 26 | - name: 安装 pnpm 27 | uses: pnpm/action-setup@v2 28 | with: 29 | # 选择要使用的 pnpm 版本 30 | version: 8 31 | # 使用 pnpm 安装依赖 32 | run_install: true 33 | 34 | - name: 设置 Node.js 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: 20 38 | cache: pnpm 39 | cache-dependency-path: document/web-docs/package.json 40 | 41 | - name: 构建文档 42 | env: 43 | NODE_OPTIONS: --max_old_space_size=8192 44 | run: |- 45 | cd document/web-docs 46 | pnpm run docs:build 47 | > docs/.vuepress/dist/.nojekyll 48 | 49 | - name: 部署文档 50 | uses: JamesIves/github-pages-deploy-action@v4 51 | with: 52 | folder: document/web-docs/docs/.vuepress/dist 53 | repository-name: stick-i/spel-validator-gh-pages 54 | branch: gh-pages 55 | token: ${{ secrets.GH_PAGES_TOKEN }} 56 | -------------------------------------------------------------------------------- /spel-validator-javax/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cn.sticki 8 | spel-validator-root 9 | 0.6.1-beta 10 | 11 | 12 | Spel Validator Javax 13 | spel-validator-javax 14 | 15 | 16 | 8 17 | 8 18 | UTF-8 19 | 20 | 21 | 22 | 23 | cn.sticki 24 | spel-validator-constrain 25 | 26 | 27 | 28 | org.hibernate.validator 29 | hibernate-validator 30 | 6.2.5.Final 31 | 32 | 33 | 34 | cn.sticki 35 | spel-validator-test 36 | test 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_sk.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = mus\u00ED by\u0165 nie 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = mus\u00ED by\u0165 \u00E1no 3 | cn.sticki.spel.validator.constraint.Email.message = nespr\u00E1vny form\u00E1t emailovej adresy 4 | cn.sticki.spel.validator.constraint.Future.message = mus\u00ED by\u0165 v bud\u00FAcnosti 5 | cn.sticki.spel.validator.constraint.Max.message = mus\u00ED by\u0165 men\u0161ie alebo rovn\u00E9 {0} 6 | cn.sticki.spel.validator.constraint.Min.message = mus\u00ED by\u0165 v\u00E4\u010D\u0161ie alebo rovn\u00E9 {0} 7 | cn.sticki.spel.validator.constraint.NotBlank.message = nem\u00F4\u017Ee by\u0165 pr\u00E1zdne 8 | cn.sticki.spel.validator.constraint.NotEmpty.message = nem\u00F4\u017Ee by\u0165 pr\u00E1zdne 9 | cn.sticki.spel.validator.constraint.NotNull.message = nem\u00F4\u017Ee by\u0165 null 10 | cn.sticki.spel.validator.constraint.Null.message = mus\u00ED by\u0165 null 11 | cn.sticki.spel.validator.constraint.Past.message = mus\u00ED by\u0165 v minulosti 12 | cn.sticki.spel.validator.constraint.Pattern.message = sa mus\u00ED zhodova\u0165 s "{0}" 13 | cn.sticki.spel.validator.constraint.Size.message = ve\u013Ekos\u0165 mus\u00ED by\u0165 medzi {0} a {1} 14 | cn.sticki.spel.validator.constraint.Digits.message = \u010D\u00EDseln\u00E1 hodnota je mimo rozsahu (o\u010Dak\u00E1van\u00E9 <{0} \u010D\u00EDslic>.<{1} \u010D\u00EDslic>) 15 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_tr.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = teyit ba\u015Far\u0131s\u0131z 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = teyit ba\u015Far\u0131s\u0131z 3 | cn.sticki.spel.validator.constraint.Email.message = d\u00FCzg\u00FCn bi\u00E7imli bir e-posta adresi de\u011Fil! 4 | cn.sticki.spel.validator.constraint.Future.message = ileri bir tarih olmal\u0131 5 | cn.sticki.spel.validator.constraint.Max.message = '{0}' de\u011Ferinden k\u00FC\u00E7\u00FCk yada e\u015Fit olmal\u0131 6 | cn.sticki.spel.validator.constraint.Min.message = '{0}' de\u011Ferinden b\u00FCy\u00FCk yada e\u015Fit olmal\u0131 7 | cn.sticki.spel.validator.constraint.NotBlank.message = bo\u015F de\u011Fer olamaz 8 | cn.sticki.spel.validator.constraint.NotEmpty.message = bo\u015F de\u011Fer olamaz 9 | cn.sticki.spel.validator.constraint.NotNull.message = bo\u015F de\u011Fer olamaz 10 | cn.sticki.spel.validator.constraint.Null.message = bo\u015F de\u011Fer olmal\u0131 11 | cn.sticki.spel.validator.constraint.Past.message = ge\u00E7mi\u015F bir tarih olmal\u0131 12 | cn.sticki.spel.validator.constraint.Pattern.message = '{0}' ile e\u015Fle\u015Fmeli 13 | cn.sticki.spel.validator.constraint.Size.message = boyut '{0}' ile '{1}' aras\u0131nda olmal\u0131 14 | cn.sticki.spel.validator.constraint.Digits.message = s\u0131n\u0131rlar\u0131n d\u0131\u015F\u0131nda say\u0131sal de\u011Fer (beklenen <{0} basamak>.<{1} basamak>) 15 | -------------------------------------------------------------------------------- /spel-validator-jakarta/src/test/java/cn/sticki/spel/validator/jakarta/ConstrainTest.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.jakarta; 2 | 3 | import cn.sticki.spel.validator.jakarta.bean.*; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | /** 8 | * 约束类测试 9 | * 10 | * @author 阿杆 11 | * @version 1.0 12 | * @since 2024/6/15 13 | */ 14 | public class ConstrainTest { 15 | 16 | /** 17 | * 这是一个测试示例 18 | */ 19 | @Test 20 | void testExample() { 21 | boolean verified = JakartaSpelValidator.check(ExampleTestBean.testCase()); 22 | Assertions.assertTrue(verified); 23 | 24 | boolean innerTest = JakartaSpelValidator.check(ExampleTestBean.innerTestCase()); 25 | Assertions.assertTrue(innerTest); 26 | } 27 | 28 | @Test 29 | void testSpelValid() { 30 | boolean verified = JakartaSpelValidator.check(SpelValidTestBean.paramTestCase()); 31 | Assertions.assertTrue(verified); 32 | } 33 | 34 | @Test 35 | void testParentClass() { 36 | boolean verified = JakartaSpelValidator.check(ParentClassTestBean.paramTestCase()); 37 | Assertions.assertTrue(verified); 38 | } 39 | 40 | @Test 41 | void testI18n() { 42 | boolean testUs = JakartaSpelValidator.check(I18nTestBean.testUs()); 43 | Assertions.assertTrue(testUs, "I18nTestBean.testUs() failed"); 44 | 45 | boolean testZh = JakartaSpelValidator.check(I18nTestBean.testZh()); 46 | Assertions.assertTrue(testZh, "I18nTestBean.testZh() failed"); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/SpelConstraint.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 将注解标记为由 {@link cn.sticki.spel.validator.javax.SpelValid} 进行校验的Bean验证约束。 7 | *

8 | * 该注解的属性 {@link SpelConstraint#validatedBy()} 用于指定校验器的实现类,实现类需要实现 {@link SpelConstraintValidator} 接口。 9 | *

10 | * 每个约束注释必须包含以下属性: 11 | *

    12 | *
  • {@code String message() default [...];} 用于指定约束校验失败时的错误消息。 13 | *
  • 14 | *
  • {@code String condition() default "";} 用于指定约束开启条件的SpEL表达式。 15 | * 当 表达式为空 或 计算结果为true 时,才会对带注解的元素进行校验。 16 | *
  • 17 | *
  • {@code String[] group() default {};} 用于指定约束开启的分组条件,必须为合法的SpEL表达式。 18 | * 当分组信息不为空时,只有当 {@link cn.sticki.spel.validator.javax.SpelValid#spelGroups()} 中的分组信息与此处的分组信息有交集时,才会对带注解的元素进行校验。 19 | * 其计算结果可以是任何类型,但只有两个计算结果完全相等时,才被认为是相等的。 20 | *
  • 21 | *
22 | *

23 | * 这里有一些定义约束的例子,可以参考: 24 | *

    25 | *
  • {@link cn.sticki.spel.validator.constrain.SpelAssert}
  • 26 | *
  • {@link cn.sticki.spel.validator.constrain.SpelNotNull}
  • 27 | *
28 | * 29 | * @author 阿杆 30 | * @version 1.0 31 | * @see cn.sticki.spel.validator.javax.SpelValid 32 | * @see SpelConstraintValidator 33 | * @since 2024/4/11 34 | */ 35 | @Documented 36 | @Target(ElementType.ANNOTATION_TYPE) 37 | @Retention(RetentionPolicy.RUNTIME) 38 | public @interface SpelConstraint { 39 | 40 | /** 41 | * 校验器的实现类,用于校验被标记的注解。 42 | */ 43 | Class> validatedBy(); 44 | 45 | } 46 | -------------------------------------------------------------------------------- /spel-validator-jakarta/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cn.sticki 8 | spel-validator-root 9 | 0.6.1-beta 10 | 11 | 12 | Spel Validator Jakarta 13 | spel-validator-jakarta 14 | 15 | 16 | 11 17 | 11 18 | UTF-8 19 | 8.0.1.Final 20 | 21 | 22 | 23 | 24 | cn.sticki 25 | spel-validator-constrain 26 | 27 | 28 | 29 | org.hibernate.validator 30 | hibernate-validator 31 | 8.0.1.Final 32 | 33 | 34 | 35 | cn.sticki 36 | spel-validator-test 37 | test 38 | 39 | 40 | -------------------------------------------------------------------------------- /spel-validator-javax/src/test/java/cn/sticki/spel/validator/javax/bean/SpelValidTestBean.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.javax.bean; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelNotNull; 4 | import cn.sticki.spel.validator.javax.SpelValid; 5 | import cn.sticki.spel.validator.test.util.ID; 6 | import cn.sticki.spel.validator.test.util.VerifyFailedField; 7 | import cn.sticki.spel.validator.test.util.VerifyObject; 8 | import lombok.Builder; 9 | import lombok.Data; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | /** 15 | * SpelValid 测试用例 16 | * 17 | * @author 阿杆 18 | * @version 1.0 19 | * @since 2024/9/22 20 | */ 21 | public class SpelValidTestBean { 22 | 23 | public static List paramTestCase() { 24 | ArrayList result = new ArrayList<>(); 25 | 26 | // On 27 | result.add(VerifyObject.of( 28 | ParamTestBean.builder().id(1).condition(true).test(null).build(), 29 | VerifyFailedField.of(ParamTestBean::getTest) 30 | )); 31 | 32 | // off 33 | result.add(VerifyObject.of( 34 | ParamTestBean.builder().id(2).condition(false).test(null).build() 35 | )); 36 | 37 | return result; 38 | } 39 | 40 | /** 41 | * 参数测试 42 | */ 43 | @Data 44 | @Builder 45 | @SpelValid(condition = "#this.condition == true") 46 | public static class ParamTestBean implements ID { 47 | 48 | private int id; 49 | 50 | private boolean condition; 51 | 52 | @SpelNotNull 53 | private String test; 54 | 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /spel-validator-jakarta/src/test/java/cn/sticki/spel/validator/jakarta/bean/SpelValidTestBean.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.jakarta.bean; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelNotNull; 4 | import cn.sticki.spel.validator.jakarta.SpelValid; 5 | import cn.sticki.spel.validator.test.util.ID; 6 | import cn.sticki.spel.validator.test.util.VerifyFailedField; 7 | import cn.sticki.spel.validator.test.util.VerifyObject; 8 | import lombok.Builder; 9 | import lombok.Data; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | /** 15 | * SpelValid 测试用例 16 | * 17 | * @author 阿杆 18 | * @version 1.0 19 | * @since 2024/9/22 20 | */ 21 | public class SpelValidTestBean { 22 | 23 | public static List paramTestCase() { 24 | ArrayList result = new ArrayList<>(); 25 | 26 | // On 27 | result.add(VerifyObject.of( 28 | ParamTestBean.builder().id(1).condition(true).test(null).build(), 29 | VerifyFailedField.of(ParamTestBean::getTest) 30 | )); 31 | 32 | // off 33 | result.add(VerifyObject.of( 34 | ParamTestBean.builder().id(2).condition(false).test(null).build() 35 | )); 36 | 37 | return result; 38 | } 39 | 40 | /** 41 | * 参数测试 42 | */ 43 | @Data 44 | @Builder 45 | @SpelValid(condition = "#this.condition == true") 46 | public static class ParamTestBean implements ID { 47 | 48 | private int id; 49 | 50 | private boolean condition; 51 | 52 | @SpelNotNull 53 | private String test; 54 | 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /spel-validator-test/src/main/java/cn/sticki/spel/validator/test/util/BeanUtil.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.test.util; 2 | 3 | import java.lang.invoke.SerializedLambda; 4 | import java.lang.reflect.Method; 5 | 6 | /** 7 | * Bean工具类 8 | * 9 | * @author 阿杆 10 | * @version 1.0 11 | * @since 2024/6/13 12 | */ 13 | public class BeanUtil { 14 | 15 | /** 16 | * 获取字段名称 17 | */ 18 | public static String getFieldName(IGetter fn) { 19 | try { 20 | // 获取writeReplace方法 21 | Method writeReplace = fn.getClass().getDeclaredMethod("writeReplace"); 22 | writeReplace.setAccessible(true); 23 | // 调用writeReplace方法并获取SerializedLambda 24 | SerializedLambda serializedLambda = (SerializedLambda) writeReplace.invoke(fn); 25 | // 获取方法名 26 | String methodName = serializedLambda.getImplMethodName(); 27 | // 去掉方法名前的get或is,并将首字母转为小写 28 | if (methodName.startsWith("get")) { 29 | methodName = methodName.substring(3); 30 | } else if (methodName.startsWith("is")) { 31 | methodName = methodName.substring(2); 32 | } 33 | // 将首字母转为小写 34 | if (methodName.length() > 1) { 35 | methodName = Character.toLowerCase(methodName.charAt(0)) + methodName.substring(1); 36 | } else { 37 | methodName = methodName.toLowerCase(); 38 | } 39 | return methodName; 40 | } catch (Exception e) { 41 | throw new RuntimeException("Failed to get the field name", e); 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/util/CalcLengthUtil.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.util; 2 | 3 | import java.util.Collections; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | /** 8 | * 计算长度工具类 9 | *

10 | * 支持的类型有: 11 | *

    12 | *
  • {@link CharSequence}(评估字符序列的长度)
  • 13 | *
  • {@link java.util.Collection}(评估集合大小)
  • 14 | *
  • {@link java.util.Map}(评估Map大小)
  • 15 | *
  • 数组(计算数组长度)
  • 16 | *
17 | * 18 | * @author 阿杆 19 | * @version 1.0 20 | * @since 2024/5/5 21 | */ 22 | public class CalcLengthUtil { 23 | 24 | private CalcLengthUtil() { 25 | } 26 | 27 | public static final Set> SUPPORT_TYPE; 28 | 29 | static { 30 | HashSet> hashSet = new HashSet<>(); 31 | hashSet.add(CharSequence.class); 32 | hashSet.add(java.util.Collection.class); 33 | hashSet.add(java.util.Map.class); 34 | hashSet.add(Object[].class); 35 | SUPPORT_TYPE = Collections.unmodifiableSet(hashSet); 36 | } 37 | 38 | public static int calcFieldSize(Object object) { 39 | if (object instanceof CharSequence) { 40 | return ((CharSequence) object).length(); 41 | } else if (object instanceof java.util.Collection) { 42 | return ((java.util.Collection) object).size(); 43 | } else if (object instanceof java.util.Map) { 44 | return ((java.util.Map) object).size(); 45 | } else if (object instanceof Object[]) { 46 | return ((Object[]) object).length; 47 | } else { 48 | return 0; 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constraintvalidator/SpelSizeValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constraintvalidator; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelSize; 4 | import cn.sticki.spel.validator.core.SpelConstraintValidator; 5 | import cn.sticki.spel.validator.core.parse.SpelParser; 6 | import cn.sticki.spel.validator.core.result.FieldValidResult; 7 | import cn.sticki.spel.validator.core.util.CalcLengthUtil; 8 | 9 | import java.lang.reflect.Field; 10 | import java.util.Set; 11 | 12 | /** 13 | * {@link SpelSize} 注解校验器。 14 | * 15 | * @author 阿杆 16 | * @version 1.0 17 | * @since 2024/5/5 18 | */ 19 | public class SpelSizeValidator implements SpelConstraintValidator { 20 | 21 | @Override 22 | public FieldValidResult isValid(SpelSize annotation, Object obj, Field field) throws IllegalAccessException { 23 | Object fieldValue = field.get(obj); 24 | // 元素为null是被允许的 25 | if (fieldValue == null) { 26 | return FieldValidResult.success(); 27 | } 28 | 29 | // 计算字段内容的长度 30 | int size = CalcLengthUtil.calcFieldSize(fieldValue); 31 | 32 | // 计算表达式的值 33 | Integer min = SpelParser.parse(annotation.min(), obj, Integer.class); 34 | Integer max = SpelParser.parse(annotation.max(), obj, Integer.class); 35 | 36 | if (size < min || size > max) { 37 | return FieldValidResult.of(false, min, max); 38 | } 39 | 40 | return FieldValidResult.success(); 41 | } 42 | 43 | @Override 44 | public Set> supportType() { 45 | return CalcLengthUtil.SUPPORT_TYPE; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/result/FieldValidResult.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.result; 2 | 3 | import lombok.Data; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | /** 7 | * 字段校验结果 8 | * 9 | * @author 阿杆 10 | * @version 1.0 11 | * @since 2024/4/29 12 | */ 13 | @Data 14 | public class FieldValidResult { 15 | 16 | /** 17 | * 校验结果,true表示校验通过,false表示校验失败 18 | */ 19 | private boolean success; 20 | 21 | /** 22 | * 校验失败时的错误信息 23 | *

24 | * 当校验结果为false时,会将错误信息添加到最终的结果中,若此字段为null,则使用默认的错误信息 25 | */ 26 | @NotNull 27 | private String message = ""; 28 | 29 | /** 30 | * 验证的字段名称,用于校验失败时构建错误信息 31 | */ 32 | @NotNull 33 | private String fieldName = ""; 34 | 35 | /** 36 | * 用于错误信息的占位符替换参数 37 | */ 38 | private Object[] args; 39 | 40 | public static FieldValidResult of(boolean success) { 41 | return of(success, ""); 42 | } 43 | 44 | public static FieldValidResult of(boolean success, @NotNull String message) { 45 | return of(success, message, "", null); 46 | } 47 | 48 | public static FieldValidResult of(boolean success, Object... args) { 49 | return of(success, "", "", args); 50 | } 51 | 52 | public static FieldValidResult of(boolean success, @NotNull String message, String fieldName, Object[] args) { 53 | FieldValidResult result = new FieldValidResult(); 54 | result.success = success; 55 | result.fieldName = fieldName; 56 | result.message = message; 57 | result.args = args; 58 | return result; 59 | } 60 | 61 | public static FieldValidResult success() { 62 | return of(true); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constrain/SpelNull.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constrain; 2 | 3 | import cn.sticki.spel.validator.constraintvalidator.SpelNullValidator; 4 | import cn.sticki.spel.validator.core.SpelConstraint; 5 | import org.intellij.lang.annotations.Language; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Repeatable; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | import static java.lang.annotation.ElementType.FIELD; 13 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 14 | 15 | /** 16 | * 被标记的元素必须为 {@code null},支持任何类型。 17 | * 18 | * @author 阿杆 19 | * @version 1.0 20 | * @since 2024/5/5 21 | */ 22 | @Documented 23 | @Retention(RUNTIME) 24 | @Target(FIELD) 25 | @Repeatable(SpelNull.List.class) 26 | @SpelConstraint(validatedBy = SpelNullValidator.class) 27 | public @interface SpelNull { 28 | 29 | /** 30 | * 校验失败时的错误消息 31 | */ 32 | String message() default "{cn.sticki.spel.validator.constraint.Null.message}"; 33 | 34 | /** 35 | * 约束开启条件,必须为合法的SpEL表达式,计算结果必须为boolean类型。 36 | *

37 | * 当 表达式为空 或 计算结果为true 时,会对带注解的元素进行校验。 38 | *

39 | * 默认情况下,开启校验。 40 | */ 41 | @Language("SpEL") 42 | String condition() default ""; 43 | 44 | /** 45 | * 分组条件,必须为合法的SpEL表达式。 46 | *

47 | * 当分组信息不为空时,只有当 {@link SpelValid#spelGroups()} 中的分组信息与此处的分组信息有交集时,才会对带注解的元素进行校验。 48 | *

49 | * 其计算结果可以是任何类型,但只有两个计算结果完全相等时,才被认为是相等的。 50 | */ 51 | @Language("SpEL") 52 | String[] group() default {}; 53 | 54 | @Documented 55 | @Target(FIELD) 56 | @Retention(RUNTIME) 57 | @interface List { 58 | 59 | SpelNull[] value(); 60 | 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /document/web-docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spel-validator", 3 | "version": "0.6.1-beta", 4 | "description": "一个强大的 Java 参数校验包,基于 SpEL 实现,扩展自 jakarta.validation-api 包,几乎支持所有场景下的参数校验。", 5 | "license": "Apache-2.0", 6 | "type": "module", 7 | "scripts": { 8 | "docs:build": "vuepress build docs --clean-cache --clean-temp", 9 | "docs:dev": "vuepress dev docs --clean-cache --clean-temp", 10 | "docs:update-package": "pnpm dlx vp-update" 11 | }, 12 | "prettier": "prettier-config-vuepress", 13 | "dependencies": { 14 | "@vuepress/bundler-vite": "2.0.0-rc.14", 15 | "@vuepress/plugin-docsearch": "2.0.0-rc.40", 16 | "@vuepress/plugin-baidu-analytics": "2.0.0-rc.40", 17 | "@vuepress/plugin-register-components": "2.0.0-rc.37", 18 | "@vuepress/plugin-shiki": "2.0.0-rc.40", 19 | "@vuepress/plugin-sitemap": "2.0.0-rc.40", 20 | "@vuepress/theme-default": "2.0.0-rc.40", 21 | "http-server": "^14.1.1", 22 | "sass-loader": "^15.0.0", 23 | "vue": "^3.4.34", 24 | "vuepress": "2.0.0-rc.14" 25 | }, 26 | "devDependencies": { 27 | "@commitlint/cli": "^19.3.0", 28 | "@commitlint/config-conventional": "^19.2.2", 29 | "eslint": "^8.57.0", 30 | "eslint-config-vuepress": "^4.10.1", 31 | "eslint-config-vuepress-typescript": "^4.10.1", 32 | "husky": "^9.1.1", 33 | "lint-staged": "^15.2.7", 34 | "prettier": "^3.3.3", 35 | "prettier-config-vuepress": "^4.4.0", 36 | "rimraf": "^6.0.1", 37 | "sort-package-json": "^2.10.0", 38 | "tsconfig-vuepress": "^4.5.0", 39 | "typescript": "^5.5.4" 40 | }, 41 | "packageManager": "pnpm@9.5.0", 42 | "engines": { 43 | "node": ">=18.19.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/release-maven.yml: -------------------------------------------------------------------------------- 1 | name: Release to Maven Central 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # 仅发布以 v 开头的标签,比如 v1.0.0 7 | workflow_dispatch: 8 | 9 | jobs: 10 | deploy: 11 | name: Deploy to Maven Central 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout source code 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Java 19 | uses: actions/setup-java@v4 # Document: https://github.com/marketplace/actions/setup-java-jdk 20 | with: 21 | distribution: 'zulu' 22 | java-version: '11' 23 | gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} # GPG private key to import. Default is empty string. 24 | gpg-passphrase: GPG_PASSPHRASE # Environment variable name for the GPG private key passphrase. Default is GPG_PASSPHRASE. 25 | server-id: sonatype-sticki 26 | server-username: MAVEN_USERNAME # Environment variable name for the username for authentication to the Apache Maven repository. Default is GITHUB_ACTOR. 27 | server-password: MAVEN_CENTRAL_TOKEN # Environment variable name for password or token for authentication to the Apache Maven repository. Default is GITHUB_TOKEN. 28 | 29 | - name: Configure GPG for loopback mode 30 | run: | 31 | echo "allow-loopback-pinentry" >> ~/.gnupg/gpg-agent.conf 32 | echo "use-agent" >> ~/.gnupg/gpg.conf 33 | echo "pinentry-mode loopback" >> ~/.gnupg/gpg.conf 34 | gpgconf --kill gpg-agent 35 | 36 | - name: Build and Deploy 37 | run: mvn -B clean deploy -P release 38 | env: 39 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 40 | MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }} 41 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 42 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/result/ObjectValidResult.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.result; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | /** 8 | * 对象校验结果 9 | * 10 | * @author 阿杆 11 | * @version 1.1 12 | * @since 2024/4/29 13 | */ 14 | public class ObjectValidResult { 15 | 16 | private final List errors = new ArrayList<>(); 17 | 18 | public static final ObjectValidResult EMPTY = new ObjectValidResult(); 19 | 20 | public boolean hasError() { 21 | return !errors.isEmpty(); 22 | } 23 | 24 | public boolean noneError() { 25 | return errors.isEmpty(); 26 | } 27 | 28 | public List getErrors() { 29 | return Collections.unmodifiableList(errors); 30 | } 31 | 32 | public int getErrorSize() { 33 | return errors.size(); 34 | } 35 | 36 | /** 37 | * 添加校验结果 38 | *

39 | * 当校验结果为false时,会将错误信息添加到结果中 40 | * 41 | * @param results 字段校验结果列表 42 | */ 43 | public void addFieldResults(List results) { 44 | for (FieldValidResult result : results) { 45 | this.addFieldResult(result); 46 | } 47 | } 48 | 49 | /** 50 | * 添加校验结果 51 | *

52 | * 当校验结果为false时,会将错误信息添加到结果中 53 | * 54 | * @param result 字段校验结果 55 | */ 56 | public void addFieldResult(FieldValidResult result) { 57 | if (!result.isSuccess()) { 58 | errors.add(FieldError.of(result.getFieldName(), result.getMessage())); 59 | } 60 | } 61 | 62 | public void addFieldError(List fieldErrorList) { 63 | if (fieldErrorList != null && !fieldErrorList.isEmpty()) { 64 | errors.addAll(fieldErrorList); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constrain/SpelNotNull.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constrain; 2 | 3 | import cn.sticki.spel.validator.constraintvalidator.SpelNotNullValidator; 4 | import cn.sticki.spel.validator.core.SpelConstraint; 5 | import org.intellij.lang.annotations.Language; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Repeatable; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | import static java.lang.annotation.ElementType.FIELD; 13 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 14 | 15 | /** 16 | * 被标记的元素不能为 {@code null},支持任何类型。 17 | * 18 | * @author 阿杆 19 | * @version 1.0 20 | * @since 2024/5/1 21 | */ 22 | @Documented 23 | @Retention(RUNTIME) 24 | @Target(FIELD) 25 | @Repeatable(SpelNotNull.List.class) 26 | @SpelConstraint(validatedBy = SpelNotNullValidator.class) 27 | public @interface SpelNotNull { 28 | 29 | /** 30 | * 校验失败时的错误消息 31 | */ 32 | String message() default "{cn.sticki.spel.validator.constraint.NotNull.message}"; 33 | 34 | /** 35 | * 约束开启条件,必须为合法的SpEL表达式,计算结果必须为boolean类型。 36 | *

37 | * 当 表达式为空 或 计算结果为true 时,会对带注解的元素进行校验。 38 | *

39 | * 默认情况下,开启校验。 40 | */ 41 | @Language("SpEL") 42 | String condition() default ""; 43 | 44 | /** 45 | * 分组条件,必须为合法的SpEL表达式。 46 | *

47 | * 当分组信息不为空时,只有当 {@link SpelValid#spelGroups()} 中的分组信息与此处的分组信息有交集时,才会对带注解的元素进行校验。 48 | *

49 | * 其计算结果可以是任何类型,但只有两个计算结果完全相等时,才被认为是相等的。 50 | */ 51 | @Language("SpEL") 52 | String[] group() default {}; 53 | 54 | /** 55 | * 在同一元素上定义多个注解。 56 | * 57 | * @see SpelNotNull 58 | */ 59 | @Target(FIELD) 60 | @Retention(RUNTIME) 61 | @Documented 62 | @interface List { 63 | 64 | SpelNotNull[] value(); 65 | 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /spel-validator-constrain/src/test/java/cn/sticki/spel/validator/constrain/ResourceMessageTest.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constrain; 2 | 3 | import cn.sticki.spel.validator.constrain.bean.I18nTestBean; 4 | import cn.sticki.spel.validator.core.message.ResourceBundleMessageResolver; 5 | import cn.sticki.spel.validator.test.util.BaseSpelValidator; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.util.Locale; 11 | 12 | /** 13 | * 资源包消息测试 14 | * 15 | * @author 阿杆 16 | * @since 2025/4/10 17 | */ 18 | @Slf4j 19 | public class ResourceMessageTest { 20 | 21 | @Test 22 | void testRead() { 23 | String message = ResourceBundleMessageResolver.getMessage("cn.sticki.spel.validator.constraint.AssertFalse.message", Locale.CHINA); 24 | Assertions.assertEquals("只能为false", message); 25 | } 26 | 27 | @Test 28 | void testReadWithLocale() { 29 | String key = "cn.sticki.spel.validator.constraint.Size.message"; 30 | String message = ResourceBundleMessageResolver.getMessage(key, Locale.CHINA, 1, 2); 31 | Assertions.assertEquals("个数必须在1和2之间", message); 32 | 33 | message = ResourceBundleMessageResolver.getMessage(key, Locale.US, 1, 2); 34 | Assertions.assertEquals("size must be between 1 and 2", message); 35 | 36 | message = ResourceBundleMessageResolver.getMessage(key, Locale.JAPAN, 1, 2); 37 | Assertions.assertEquals("1 から 2 の間のサイズにしてください", message); 38 | } 39 | 40 | @Test 41 | void testI18n() { 42 | boolean test1 = BaseSpelValidator.check(I18nTestBean.testUs()); 43 | Assertions.assertTrue(test1, "I18nTestBean.test1() failed"); 44 | 45 | boolean test2 = BaseSpelValidator.check(I18nTestBean.testZh()); 46 | Assertions.assertTrue(test2, "I18nTestBean.test2() failed"); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /spel-validator-test/src/main/java/cn/sticki/spel/validator/test/util/VerifyFailedField.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.test.util; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | /** 10 | * 验证失败的字段 11 | * 12 | * @author 阿杆 13 | * @version 1.0 14 | * @since 2024/6/15 15 | */ 16 | @Data 17 | public class VerifyFailedField { 18 | 19 | /** 20 | * 字段名 21 | */ 22 | String name; 23 | 24 | /** 25 | * 异常信息 26 | */ 27 | String message; 28 | 29 | private VerifyFailedField() { 30 | } 31 | 32 | public static VerifyFailedField of(String fieldName) { 33 | return VerifyFailedField.of(fieldName, null); 34 | } 35 | 36 | public static VerifyFailedField of(IGetter field) { 37 | return VerifyFailedField.of(field, null); 38 | } 39 | 40 | @SafeVarargs 41 | public static List of(IGetter... fields) { 42 | return Arrays.stream(fields).map(VerifyFailedField::of).collect(Collectors.toList()); 43 | } 44 | 45 | /** 46 | * 创建一个验证失败的字段 47 | * 48 | * @param field 字段 49 | * @param errorMessage 期望的错误信息 50 | */ 51 | public static VerifyFailedField of(IGetter field, String errorMessage) { 52 | return of(BeanUtil.getFieldName(field), errorMessage); 53 | } 54 | 55 | /** 56 | * 创建一个验证失败的字段 57 | * 58 | * @param fieldName 字段名 59 | * @param errorMessage 期望的错误信息 60 | * @return 61 | */ 62 | public static VerifyFailedField of(String fieldName, String errorMessage) { 63 | VerifyFailedField verifyFailedField = new VerifyFailedField(); 64 | verifyFailedField.setName(fieldName); 65 | verifyFailedField.setMessage(errorMessage); 66 | return verifyFailedField; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constrain/SpelAssert.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constrain; 2 | 3 | import cn.sticki.spel.validator.constraintvalidator.SpelAssertValidator; 4 | import cn.sticki.spel.validator.core.SpelConstraint; 5 | import org.intellij.lang.annotations.Language; 6 | 7 | import java.lang.annotation.*; 8 | 9 | import static java.lang.annotation.ElementType.FIELD; 10 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 11 | 12 | /** 13 | * 被标记的元素需要满足指定的断言条件。 14 | *

15 | * 接受任何类型。 16 | * 17 | * @author 阿杆 18 | * @version 1.0 19 | * @since 2024/5/1 20 | */ 21 | @Documented 22 | @Retention(RUNTIME) 23 | @Target(FIELD) 24 | @Repeatable(SpelAssert.List.class) 25 | @SpelConstraint(validatedBy = SpelAssertValidator.class) 26 | public @interface SpelAssert { 27 | 28 | /** 29 | * 校验失败时的错误消息 30 | */ 31 | String message() default "{cn.sticki.spel.validator.constraint.AssertTrue.message}"; 32 | 33 | /** 34 | * 约束开启条件,必须为合法的SpEL表达式,计算结果必须为boolean类型。 35 | *

36 | * 当 表达式为空 或 计算结果为true 时,会对带注解的元素进行校验。 37 | *

38 | * 默认情况下,开启校验。 39 | */ 40 | @Language("SpEL") 41 | String condition() default ""; 42 | 43 | /** 44 | * 分组条件,必须为合法的SpEL表达式。 45 | *

46 | * 当分组信息不为空时,只有当 {@link SpelValid#spelGroups()} 中的分组信息与此处的分组信息有交集时,才会对带注解的元素进行校验。 47 | *

48 | * 其计算结果可以是任何类型,但只有两个计算结果完全相等时,才被认为是相等的。 49 | */ 50 | @Language("SpEL") 51 | String[] group() default {}; 52 | 53 | /** 54 | * 断言语句,必须为合法的SpEL表达式。 55 | *

56 | * 计算结果必须为boolean类型,true为校验成功,false为校验失败 57 | */ 58 | @Language("SpEL") 59 | String assertTrue() default "true"; 60 | 61 | @Retention(RetentionPolicy.RUNTIME) 62 | @Target(ElementType.FIELD) 63 | @Documented 64 | @interface List { 65 | 66 | SpelAssert[] value(); 67 | 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constrain/SpelNotBlank.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constrain; 2 | 3 | import cn.sticki.spel.validator.constraintvalidator.SpelNotBlankValidator; 4 | import cn.sticki.spel.validator.core.SpelConstraint; 5 | import org.intellij.lang.annotations.Language; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Repeatable; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | import static java.lang.annotation.ElementType.FIELD; 13 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 14 | 15 | /** 16 | * 被标记的元素必须为非空字符串。 17 | *

18 | * 不能为 {@code null},长度必须大于 0,不能全是空白字符。 19 | *

20 | * 接受 {@link CharSequence} 类型。 21 | * 22 | * @author 阿杆 23 | * @version 1.0 24 | * @since 2024/5/5 25 | */ 26 | @Documented 27 | @Retention(RUNTIME) 28 | @Target(FIELD) 29 | @Repeatable(SpelNotBlank.List.class) 30 | @SpelConstraint(validatedBy = SpelNotBlankValidator.class) 31 | public @interface SpelNotBlank { 32 | 33 | /** 34 | * 校验失败时的错误消息 35 | */ 36 | String message() default "{cn.sticki.spel.validator.constraint.NotBlank.message}"; 37 | 38 | /** 39 | * 约束开启条件,必须为合法的SpEL表达式,计算结果必须为boolean类型。 40 | *

41 | * 当 表达式为空 或 计算结果为true 时,会对带注解的元素进行校验。 42 | *

43 | * 默认情况下,开启校验。 44 | */ 45 | @Language("SpEL") 46 | String condition() default ""; 47 | 48 | /** 49 | * 分组条件,必须为合法的SpEL表达式。 50 | *

51 | * 当分组信息不为空时,只有当 {@link SpelValid#spelGroups()} 中的分组信息与此处的分组信息有交集时,才会对带注解的元素进行校验。 52 | *

53 | * 其计算结果可以是任何类型,但只有两个计算结果完全相等时,才被认为是相等的。 54 | */ 55 | @Language("SpEL") 56 | String[] group() default {}; 57 | 58 | @Documented 59 | @Target(FIELD) 60 | @Retention(RUNTIME) 61 | @interface List { 62 | 63 | SpelNotBlank[] value(); 64 | 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /spel-validator-test/src/main/java/cn/sticki/spel/validator/test/util/LogContext.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.test.util; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.slf4j.MDC; 5 | 6 | /** 7 | * 日志上下文 8 | * 9 | * @author 阿杆 10 | * @since 2024/10/29 11 | */ 12 | @Slf4j 13 | public class LogContext { 14 | 15 | /** 16 | * 设置验证对象日志上下文 17 | */ 18 | public static void setValidateObject(Object object) { 19 | String className = object.getClass().getSimpleName(); 20 | Class enclosingClass = object.getClass().getEnclosingClass(); 21 | if (enclosingClass != null) { 22 | className = enclosingClass.getSimpleName() + "." + className; 23 | } 24 | 25 | MDC.put("className", className); 26 | MDC.put("fullClassName", abbreviate(object.getClass().getName())); 27 | if (object instanceof ID) { 28 | MDC.put("id", String.valueOf(((ID) object).getId())); 29 | } 30 | } 31 | 32 | /** 33 | * 清除验证对象日志上下文 34 | */ 35 | public static void clearValidateObject() { 36 | MDC.remove("id"); 37 | MDC.remove("className"); 38 | MDC.remove("fieldName"); 39 | MDC.remove("fullClassName"); 40 | } 41 | 42 | public static void set(String key, String value) { 43 | MDC.put(key, value); 44 | } 45 | 46 | public static void remove(String key) { 47 | MDC.remove(key); 48 | } 49 | 50 | /** 51 | * 缩写类名 52 | */ 53 | private static String abbreviate(String className) { 54 | String[] parts = className.split("\\."); 55 | StringBuilder abbreviated = new StringBuilder(); 56 | for (int i = 0; i < parts.length - 1; i++) { 57 | abbreviated.append(parts[i].charAt(0)).append("."); 58 | } 59 | abbreviated.append(parts[parts.length - 1]); 60 | return abbreviated.toString(); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /document/web-docs/docs/guide/introduction.md: -------------------------------------------------------------------------------- 1 | # 组件介绍 2 | 3 | SpEL Validator 是基于 Spring Expression Language 的参数校验包,也是 jakarta.validation-api 的扩展增强包,用于简化参数校验,它几乎支持所有场景下的参数校验。 4 | 5 | 设计的初衷是为了解决一些需要判断另一个字段的值来决定当前字段是否校验的场景。 6 | 7 | ::: tip 8 | 9 | 本组件的目的不是代替 `jakarta.validation-api` 的校验注解,而是作为一个扩展,方便某些场景下的参数校验。 10 | 11 | ::: 12 | 13 | ## 它是如何工作的? 14 | 15 | 简单来说,它按照以下步骤工作: 16 | 17 | ```md 18 | @Valid/@Validated 19 | ↓ 20 | @SpelValid <- 激活 SpelValidator 21 | ↓ 22 | SpelValidator#isValid() <- 调用 SpelValidExecutor 23 | ↓ 24 | SpelValidExecutor <- 扫描字段、解析表达式、调用具体约束的校验器 25 | ↓ 26 | SpelConstraintValidator <- 执行约束校验 27 | ↓ 28 | FieldError <- 错误信息收集 29 | ↓ 30 | ConstraintViolation <- 转换为标准校验框架支持的结构 31 | ``` 32 | 33 | 详细的说明,可以参考章节 [工作原理](principle.md)。 34 | 35 | ## 它解决了什么问题? 36 | 37 | - 枚举值字段校验: 38 | ```java 39 | @SpelAssert(assertTrue = " T(cn.sticki.enums.UserStatusEnum).getByCode(#this.userStatus) != null ", message = "用户状态不合法") 40 | private Integer userStatus; 41 | ``` 42 | 43 | - 多字段联合校验: 44 | ```java 45 | @NotNull 46 | private Integer contentType; 47 | 48 | @SpelNotNull(condition = "#this.contentType == 1", message = "语音内容不能为空") 49 | private Object audioContent; 50 | 51 | @SpelNotNull(condition = "#this.contentType == 2", message = "视频内容不能为空") 52 | private Object videoContent; 53 | ``` 54 | 55 | - 复杂逻辑校验,调用静态方法: 56 | ```java 57 | // 中文算两个字符,英文算一个字符,要求总长度不超过 10 58 | // 调用外部静态方法进行校验 59 | @SpelAssert(assertTrue = "T(cn.sticki.util.StringUtil).getLength(#this.userName) <= 10", message = "用户名长度不能超过10") 60 | private String userName; 61 | ``` 62 | 63 | - 调用 Spring Bean(需要使用 @EnableSpelValidatorBeanRegistrar 开启Spring Bean支持): 64 | ```java 65 | // 这里只是简单举例,实际开发中不建议这样判断用户是否存在 66 | @SpelAssert(assertTrue = "@userService.getById(#this.userId) != null", message = "用户不存在") 67 | private Long userId; 68 | ``` 69 | 70 | - 更多使用场景,欢迎探索和补充! 71 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = must be false 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = must be true 3 | cn.sticki.spel.validator.constraint.Email.message = must be a well-formed email address 4 | cn.sticki.spel.validator.constraint.Future.message = must be a future date 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = must be a date in the present or in the future 6 | cn.sticki.spel.validator.constraint.Max.message = must be less than or equal to {0} 7 | cn.sticki.spel.validator.constraint.Min.message = must be greater than or equal to {0} 8 | cn.sticki.spel.validator.constraint.Negative.message = must be less than 0 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = must be less than or equal to 0 10 | cn.sticki.spel.validator.constraint.NotBlank.message = must not be blank 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = must not be empty 12 | cn.sticki.spel.validator.constraint.NotNull.message = must not be null 13 | cn.sticki.spel.validator.constraint.Null.message = must be null 14 | cn.sticki.spel.validator.constraint.Past.message = must be a past date 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = must be a date in the past or in the present 16 | cn.sticki.spel.validator.constraint.Pattern.message = must match "{0}" 17 | cn.sticki.spel.validator.constraint.Positive.message = must be greater than 0 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = must be greater than or equal to 0 19 | cn.sticki.spel.validator.constraint.Size.message = size must be between {0} and {1} 20 | cn.sticki.spel.validator.constraint.Digits.message = numeric value out of bounds (<{0} digits>.<{1} digits> expected) 21 | -------------------------------------------------------------------------------- /spel-validator-jakarta/src/test/java/cn/sticki/spel/validator/jakarta/bean/I18nTestBean.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.jakarta.bean; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelNotNull; 4 | import cn.sticki.spel.validator.constrain.SpelSize; 5 | import cn.sticki.spel.validator.jakarta.SpelValid; 6 | import cn.sticki.spel.validator.test.util.VerifyFailedField; 7 | import cn.sticki.spel.validator.test.util.VerifyObject; 8 | import lombok.Data; 9 | import org.springframework.context.i18n.LocaleContextHolder; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Locale; 14 | 15 | /** 16 | * 国际化消息测试 17 | * 18 | * @author 阿杆 19 | * @since 2025/4/11 20 | */ 21 | public class I18nTestBean { 22 | 23 | @Data 24 | @SpelValid 25 | public static class Test1 { 26 | 27 | @SpelNotNull 28 | String field1 = null; 29 | 30 | @SpelSize(min = "0", max = "1") 31 | String field8 = "12345678901"; 32 | 33 | } 34 | 35 | public static List testUs() { 36 | ArrayList list = new ArrayList<>(); 37 | 38 | LocaleContextHolder.setLocale(Locale.US); 39 | 40 | list.add(VerifyObject.of(new Test1(), 41 | VerifyFailedField.of(Test1::getField1, "must not be null"), 42 | VerifyFailedField.of(Test1::getField8, "size must be between 0 and 1") 43 | ) 44 | ); 45 | 46 | return list; 47 | } 48 | 49 | public static List testZh() { 50 | ArrayList list = new ArrayList<>(); 51 | 52 | LocaleContextHolder.setLocale(Locale.CHINESE); 53 | 54 | list.add(VerifyObject.of(new Test1(), 55 | VerifyFailedField.of(Test1::getField1, "不得为 null"), 56 | VerifyFailedField.of(Test1::getField8, "大小必须在 0 和 1 之间") 57 | ) 58 | ); 59 | return list; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_pt.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = deve ser falso 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = deve ser verdadeiro 3 | cn.sticki.spel.validator.constraint.Email.message = deve ser um endere\u00E7o de e-mail bem formado 4 | cn.sticki.spel.validator.constraint.Future.message = deve ser uma data futura 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = deve ser uma data no presente ou no futuro 6 | cn.sticki.spel.validator.constraint.Max.message = deve ser menor que ou igual a {0} 7 | cn.sticki.spel.validator.constraint.Min.message = deve ser maior que ou igual a {0} 8 | cn.sticki.spel.validator.constraint.Negative.message = deve ser menor que 0 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = deve ser menor ou igual a 0 10 | cn.sticki.spel.validator.constraint.NotBlank.message = n\u00E3o deve estar em branco 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = n\u00E3o deve estar vazio 12 | cn.sticki.spel.validator.constraint.NotNull.message = n\u00E3o deve ser nulo 13 | cn.sticki.spel.validator.constraint.Null.message = deve ser nulo 14 | cn.sticki.spel.validator.constraint.Past.message = deve ser uma data passada 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = deve ser uma data no passado ou no presente 16 | cn.sticki.spel.validator.constraint.Pattern.message = deve corresponder a "{0}" 17 | cn.sticki.spel.validator.constraint.Positive.message = deve ser maior que 0 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = deve ser maior ou igual a 0 19 | cn.sticki.spel.validator.constraint.Size.message = tamanho deve ser entre {0} e {1} 20 | cn.sticki.spel.validator.constraint.Digits.message = valor num\u00E9rico fora do limite (<{0} d\u00EDgitos>.<{1} d\u00EDgitos> esperado) 21 | -------------------------------------------------------------------------------- /spel-validator-core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cn.sticki 8 | spel-validator-root 9 | 0.6.1-beta 10 | 11 | 12 | Spel Validator Core 13 | spel-validator-core 14 | 15 | 16 | 8 17 | 8 18 | UTF-8 19 | 20 | 21 | 22 | 23 | org.springframework 24 | spring-context 25 | 26 | 27 | 28 | org.jetbrains 29 | annotations 30 | 31 | 32 | 33 | org.projectlombok 34 | lombok 35 | 36 | 37 | 38 | org.slf4j 39 | slf4j-api 40 | 41 | 42 | 43 | 44 | 45 | org.junit.jupiter 46 | junit-jupiter 47 | test 48 | 49 | 50 | 51 | ch.qos.logback 52 | logback-classic 53 | test 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/manager/ValidatorInstanceManager.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.manager; 2 | 3 | import cn.sticki.spel.validator.core.SpelConstraint; 4 | import cn.sticki.spel.validator.core.SpelConstraintValidator; 5 | import cn.sticki.spel.validator.core.exception.SpelValidatorException; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.lang.annotation.Annotation; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | 11 | /** 12 | * 验证器实例管理器 13 | * 14 | * @author 阿杆 15 | * @since 2024/11/4 16 | */ 17 | public class ValidatorInstanceManager { 18 | 19 | private ValidatorInstanceManager() { 20 | } 21 | 22 | /** 23 | * 校验器实例管理器,避免重复创建校验器实例。 24 | */ 25 | private static final ConcurrentHashMap> VALIDATOR_INSTANCE_CACHE = new ConcurrentHashMap<>(); 26 | 27 | /** 28 | * 获取校验器实例,当实例不存在时会创建一个新的实例。 29 | */ 30 | @NotNull 31 | public static SpelConstraintValidator getInstance(@NotNull Annotation annotation) { 32 | return VALIDATOR_INSTANCE_CACHE.computeIfAbsent(annotation, key -> { 33 | try { 34 | Class annoClazz = annotation.annotationType(); 35 | SpelConstraint constraint = annoClazz.getAnnotation(SpelConstraint.class); 36 | if (constraint == null) { 37 | throw new SpelValidatorException("Annotation [" + annoClazz.getName() + "] is not a Spel Constraint annotation"); 38 | } 39 | Class> validatorClass = constraint.validatedBy(); 40 | return validatorClass.getDeclaredConstructor().newInstance(); 41 | } catch (Exception e) { 42 | throw new SpelValidatorException("Failed to create validator instance, annotation [" + annotation.annotationType().getName() + "]", e); 43 | } 44 | }); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constrain/SpelNotEmpty.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constrain; 2 | 3 | import cn.sticki.spel.validator.constraintvalidator.SpelNotEmptyValidator; 4 | import cn.sticki.spel.validator.core.SpelConstraint; 5 | import org.intellij.lang.annotations.Language; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Repeatable; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | import static java.lang.annotation.ElementType.FIELD; 13 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 14 | 15 | /** 16 | * 被标记的元素不能为 {@code null} 或空。 17 | *

18 | * 支持的类型有: 19 | *

    20 | *
  • {@link CharSequence}(评估字符序列的长度)
  • 21 | *
  • {@link java.util.Collection}(评估集合大小)
  • 22 | *
  • {@link java.util.Map}(评估Map大小)
  • 23 | *
  • 数组(计算数组长度)
  • 24 | *
25 | * 26 | * @author 阿杆 27 | * @version 1.0 28 | * @since 2024/5/5 29 | */ 30 | @Documented 31 | @Retention(RUNTIME) 32 | @Target(FIELD) 33 | @Repeatable(SpelNotEmpty.List.class) 34 | @SpelConstraint(validatedBy = SpelNotEmptyValidator.class) 35 | public @interface SpelNotEmpty { 36 | 37 | /** 38 | * 校验失败时的错误消息 39 | */ 40 | String message() default "{cn.sticki.spel.validator.constraint.NotEmpty.message}"; 41 | 42 | /** 43 | * 约束开启条件,必须为合法的SpEL表达式,计算结果必须为boolean类型。 44 | *

45 | * 当 表达式为空 或 计算结果为true 时,会对带注解的元素进行校验。 46 | *

47 | * 默认情况下,开启校验。 48 | */ 49 | @Language("SpEL") 50 | String condition() default ""; 51 | 52 | /** 53 | * 分组条件,必须为合法的SpEL表达式。 54 | *

55 | * 当分组信息不为空时,只有当 {@link SpelValid#spelGroups()} 中的分组信息与此处的分组信息有交集时,才会对带注解的元素进行校验。 56 | *

57 | * 其计算结果可以是任何类型,但只有两个计算结果完全相等时,才被认为是相等的。 58 | */ 59 | @Language("SpEL") 60 | String[] group() default {}; 61 | 62 | @Documented 63 | @Target(FIELD) 64 | @Retention(RUNTIME) 65 | @interface List { 66 | 67 | SpelNotEmpty[] value(); 68 | 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_es.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = debe ser falso 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = debe ser verdadero 3 | cn.sticki.spel.validator.constraint.Email.message = debe ser una direcci\u00F3n de correo electr\u00F3nico con formato correcto 4 | cn.sticki.spel.validator.constraint.Future.message = debe ser una fecha futura 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = debe ser una fecha en el presente o en el futuro 6 | cn.sticki.spel.validator.constraint.Max.message = debe ser menor que o igual a {0} 7 | cn.sticki.spel.validator.constraint.Min.message = debe ser mayor que o igual a {0} 8 | cn.sticki.spel.validator.constraint.Negative.message = debe ser menor que 0 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = debe ser menor que o igual a 0 10 | cn.sticki.spel.validator.constraint.NotBlank.message = no debe estar vac\u00EDo 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = no debe estar vac\u00EDo 12 | cn.sticki.spel.validator.constraint.NotNull.message = no debe ser nulo 13 | cn.sticki.spel.validator.constraint.Null.message = debe ser nulo 14 | cn.sticki.spel.validator.constraint.Past.message = debe ser una fecha pasada 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = debe ser una fecha en el pasado o en el presente 16 | cn.sticki.spel.validator.constraint.Pattern.message = debe coincidir con "{0}" 17 | cn.sticki.spel.validator.constraint.Positive.message = debe ser mayor que 0 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = debe ser mayor que o igual a 0 19 | cn.sticki.spel.validator.constraint.Size.message = el tama\u00F1o debe estar entre {0} y {1} 20 | cn.sticki.spel.validator.constraint.Digits.message = valor num\u00E9rico fuera de l\u00EDmites (se esperaba <{0} d\u00EDgitos>.<{1} d\u00EDgitos>) 21 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_it.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = deve essere false 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = deve essere true 3 | cn.sticki.spel.validator.constraint.Email.message = deve essere un indirizzo email nel formato corretto 4 | cn.sticki.spel.validator.constraint.Future.message = deve essere una data nel futuro 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = deve essere una data nel presente o nel futuro 6 | cn.sticki.spel.validator.constraint.Max.message = deve essere inferiore o uguale a {0} 7 | cn.sticki.spel.validator.constraint.Min.message = deve essere superiore o uguale a {0} 8 | cn.sticki.spel.validator.constraint.Negative.message = deve essere inferiore a 0 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = deve essere inferiore o uguale a 0 10 | cn.sticki.spel.validator.constraint.NotBlank.message = non deve essere spazio 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = non deve essere vuoto 12 | cn.sticki.spel.validator.constraint.NotNull.message = non deve essere null 13 | cn.sticki.spel.validator.constraint.Null.message = deve essere null 14 | cn.sticki.spel.validator.constraint.Past.message = deve essere una data nel passato 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = deve essere una data nel passato o nel presente 16 | cn.sticki.spel.validator.constraint.Pattern.message = deve corrispondere a "{0}" 17 | cn.sticki.spel.validator.constraint.Positive.message = deve essere superiore a 0 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = deve essere superiore o uguale a 0 19 | cn.sticki.spel.validator.constraint.Size.message = la dimensione deve essere compresa tra {0} e {1} 20 | cn.sticki.spel.validator.constraint.Digits.message = valore numerico fuori dai limiti (previsto <{0} digits>.<{1} digits>) 21 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_nl.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = moet onwaar zijn 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = moet waar zijn 3 | cn.sticki.spel.validator.constraint.Email.message = het e-mailadres is ongeldig 4 | cn.sticki.spel.validator.constraint.Future.message = moet in de toekomst zijn 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = moet in het heden of in de toekomst zijn 6 | cn.sticki.spel.validator.constraint.Max.message = moet kleiner of gelijk aan {0} zijn 7 | cn.sticki.spel.validator.constraint.Min.message = moet groter of gelijk aan {0} zijn 8 | cn.sticki.spel.validator.constraint.Negative.message = moet kleiner dan 0 zijn 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = moet kleiner dan of gelijk aan 0 zijn 10 | cn.sticki.spel.validator.constraint.NotBlank.message = mag niet onbeschreven zijn 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = mag niet leeg zijn 12 | cn.sticki.spel.validator.constraint.NotNull.message = mag niet null zijn 13 | cn.sticki.spel.validator.constraint.Null.message = moet null zijn 14 | cn.sticki.spel.validator.constraint.Past.message = moet in het verleden zijn 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = moet in het heden of in het verleden zijn 16 | cn.sticki.spel.validator.constraint.Pattern.message = moet overeenkomen met "{0}" 17 | cn.sticki.spel.validator.constraint.Positive.message = moet groter dan 0 zijn 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = moet groter of gelijk zijn aan 0 19 | cn.sticki.spel.validator.constraint.Size.message = grootte moet tussen {0} en {1} liggen 20 | cn.sticki.spel.validator.constraint.Digits.message = numerieke waarde ligt buiten het toegestane bereik (<{0} cijfers><{1} cijfers> verwacht) 21 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/test/java/cn/sticki/spel/validator/constrain/bean/SpelNotBlankTestBean.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constrain.bean; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelNotBlank; 4 | import cn.sticki.spel.validator.test.util.VerifyFailedField; 5 | import cn.sticki.spel.validator.test.util.VerifyObject; 6 | import lombok.Data; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * SpelNotBlank 测试用例 13 | * 14 | * @author 阿杆 15 | * @version 1.0 16 | * @since 2024/6/17 17 | */ 18 | @Data 19 | public class SpelNotBlankTestBean { 20 | 21 | @SpelNotBlank 22 | private String typeTest1; 23 | 24 | @SpelNotBlank 25 | private StringBuilder typeTest2; 26 | 27 | @SpelNotBlank 28 | private StringBuffer typeTest3; 29 | 30 | @SpelNotBlank(condition = "false") 31 | private String test; 32 | 33 | @SpelNotBlank(condition = "true") 34 | private String test2; 35 | 36 | @SpelNotBlank(condition = "true", message = "test3") 37 | private String test3; 38 | 39 | public static List testCase() { 40 | ArrayList list = new ArrayList<>(); 41 | 42 | // 测试用例1:类型测试 43 | list.add(VerifyObject.of(new TestBean2(), true)); 44 | 45 | // 测试用例2:条件测试 46 | VerifyObject verifyObject2 = VerifyObject.of( 47 | new SpelNotBlankTestBean(), 48 | VerifyFailedField.of( 49 | SpelNotBlankTestBean::getTypeTest1, 50 | SpelNotBlankTestBean::getTypeTest2, 51 | SpelNotBlankTestBean::getTypeTest3, 52 | SpelNotBlankTestBean::getTest2 53 | )); 54 | verifyObject2.getVerifyFailedFields().add(VerifyFailedField.of(SpelNotBlankTestBean::getTest3, "test3")); 55 | list.add(verifyObject2); 56 | 57 | return list; 58 | } 59 | 60 | @Data 61 | public static class TestBean2 { 62 | 63 | @SpelNotBlank 64 | private Object objectTypeTest; 65 | 66 | } 67 | 68 | } 69 | 70 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_de.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = muss falsch sein 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = muss wahr sein 3 | cn.sticki.spel.validator.constraint.Email.message = muss eine korrekt formatierte E-Mail-Adresse sein 4 | cn.sticki.spel.validator.constraint.Future.message = muss ein Datum in der Zukunft sein 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = muss ein Datum in der Gegenwart oder in der Zukunft sein 6 | cn.sticki.spel.validator.constraint.Max.message = muss kleiner-gleich {0} sein 7 | cn.sticki.spel.validator.constraint.Min.message = muss gr\u00F6\u00DFer-gleich {0} sein 8 | cn.sticki.spel.validator.constraint.Negative.message = muss kleiner als 0 sein 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = muss kleiner-gleich 0 sein 10 | cn.sticki.spel.validator.constraint.NotBlank.message = darf nicht leer sein 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = darf nicht leer sein 12 | cn.sticki.spel.validator.constraint.NotNull.message = darf nicht null sein 13 | cn.sticki.spel.validator.constraint.Null.message = muss null sein 14 | cn.sticki.spel.validator.constraint.Past.message = muss ein Datum in der Vergangenheit sein 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = muss ein Datum in der Vergangenheit oder in der Gegenwart sein 16 | cn.sticki.spel.validator.constraint.Pattern.message = muss mit "{0}" \u00FCbereinstimmen 17 | cn.sticki.spel.validator.constraint.Positive.message = muss gr\u00F6\u00DFer als 0 sein 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = muss gr\u00F6\u00DFer-gleich 0 sein 19 | cn.sticki.spel.validator.constraint.Size.message = Gr\u00F6\u00DFe muss zwischen {0} und {1} sein 20 | cn.sticki.spel.validator.constraint.Digits.message = numerischer Wert au\u00dferhalb des g\u00fcltigen Bereichs (<{0} digits>.<{1} digits> erwartet) 21 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_da.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = skal v\u00E6re falsk 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = skal v\u00E6re sand 3 | cn.sticki.spel.validator.constraint.Email.message = skal v\u00E6re en korrekt formateret e-mail-adresse 4 | cn.sticki.spel.validator.constraint.Future.message = skal v\u00E6re en dato i fremtiden 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = skal v\u00E6re en dato i nutiden eller i fremtiden 6 | cn.sticki.spel.validator.constraint.Max.message = skal v\u00E6re mindre end eller lig med {0} 7 | cn.sticki.spel.validator.constraint.Min.message = skal v\u00E6re st\u00F8rre end eller lig med {0} 8 | cn.sticki.spel.validator.constraint.Negative.message = skal v\u00E6re mindre end 0 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = skal v\u00E6re mindre end eller lig med 0 10 | cn.sticki.spel.validator.constraint.NotBlank.message = m\u00E5 ikke v\u00E6re blank 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = m\u00E5 ikke v\u00E6re tom 12 | cn.sticki.spel.validator.constraint.NotNull.message = m\u00E5 ikke v\u00E6re null 13 | cn.sticki.spel.validator.constraint.Null.message = skal v\u00E6re null 14 | cn.sticki.spel.validator.constraint.Past.message = skal v\u00E6re en dato fortiden 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = skal v\u00E6re en dato i fortiden eller i nutiden 16 | cn.sticki.spel.validator.constraint.Pattern.message = skal matche "{0}" 17 | cn.sticki.spel.validator.constraint.Positive.message = skal v\u00E6re st\u00F8rre end 0 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = skal v\u00E6re st\u00F8rre end eller lig med 0 19 | cn.sticki.spel.validator.constraint.Size.message = st\u00F8rrelse skal v\u00E6re mellem {0} og {1} 20 | cn.sticki.spel.validator.constraint.Digits.message = tal v\u00E6rdierne er udenfor det gyldige omr\u00E5de (<{0} digits>.<{1} digits> forventet) 21 | -------------------------------------------------------------------------------- /spel-validator-jakarta/src/test/java/cn/sticki/spel/validator/jakarta/bean/ParentClassTestBean.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.jakarta.bean; 2 | 3 | import cn.sticki.spel.validator.constrain.SpelNotNull; 4 | import cn.sticki.spel.validator.jakarta.SpelValid; 5 | import cn.sticki.spel.validator.test.util.ID; 6 | import cn.sticki.spel.validator.test.util.VerifyFailedField; 7 | import cn.sticki.spel.validator.test.util.VerifyObject; 8 | import lombok.Data; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | /** 14 | * 测试继承类 15 | * 16 | * @author 阿杆 17 | * @since 2025/1/23 18 | */ 19 | public class ParentClassTestBean { 20 | 21 | @Data 22 | @SpelValid 23 | public static class Parent implements ID { 24 | 25 | private int id; 26 | 27 | @SpelNotNull(message = "parentField不能为空") 28 | private String parentField; 29 | 30 | } 31 | 32 | @Data 33 | @SpelValid 34 | public static class Child extends Parent implements ID { 35 | 36 | @SpelNotNull(message = "childField不能为空") 37 | private String childField; 38 | 39 | } 40 | 41 | public static List paramTestCase() { 42 | ArrayList result = new ArrayList<>(); 43 | 44 | // 父类测试 45 | Parent parent1 = new Parent(); 46 | parent1.setId(1); 47 | parent1.setParentField(null); 48 | result.add(VerifyObject.of(parent1, VerifyFailedField.of(Parent::getParentField))); 49 | 50 | Parent parent2 = new Parent(); 51 | parent2.setId(2); 52 | parent2.setParentField("123"); 53 | result.add(VerifyObject.of(parent2)); 54 | 55 | // 子类测试 56 | Child child1 = new Child(); 57 | child1.setId(1); 58 | child1.setParentField(null); 59 | child1.setChildField(null); 60 | result.add(VerifyObject.of(child1, VerifyFailedField.of(Child::getParentField, Child::getChildField))); 61 | 62 | Child child2 = new Child(); 63 | child2.setId(2); 64 | child2.setParentField("123"); 65 | child2.setChildField("123"); 66 | result.add(VerifyObject.of(child2)); 67 | 68 | return result; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /spel-validator-test-report/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cn.sticki 8 | spel-validator-root 9 | 0.6.1-beta 10 | 11 | 12 | Spel Validator Test Report 13 | spel-validator-test-report 14 | 15 | 16 | 8 17 | 8 18 | UTF-8 19 | 20 | 21 | 22 | 23 | cn.sticki 24 | spel-validator-core 25 | 26 | 27 | 28 | cn.sticki 29 | spel-validator-constrain 30 | 31 | 32 | 33 | cn.sticki 34 | spel-validator-jakarta 35 | 36 | 37 | 38 | cn.sticki 39 | spel-validator-javax 40 | 41 | 42 | 43 | 44 | 45 | 46 | org.jacoco 47 | jacoco-maven-plugin 48 | 0.8.12 49 | 50 | 51 | test 52 | 53 | report-aggregate 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /document/web-docs/docs/guide/FAQ.md: -------------------------------------------------------------------------------- 1 | # 常见问题 2 | 3 | ## 关于性能 4 | 5 | 我对[示例项目](https://github.com/stick-i/spel-validator-example)创建了一组简单的api测试用例,共15个请求,在后面的测试中,我会使用它们来进行测试。 6 | 7 | ![img_4.png](../image/faq-api-test.png) 8 | 9 | ::: tip 测试环境 10 | 11 | - Mac mini 2023款,M2芯片,16G内存 12 | - 示例项目的接口中无业务逻辑,只有参数校验 13 | - SpEL Validator 版本:v0.2.0-beta 14 | - JDK版本:8 15 | - SpringBoot版本:2.7.17 16 | 17 | ::: 18 | 19 | ### 执行耗时 20 | 21 | 测试条件: 22 | 23 | - 开启debug日志 24 | - 接口未预热 25 | - 使用Apifox进行2线程3循环的测试,共90次请求 26 | 27 | 测试结果: 28 | 29 | 除了前几次请求耗时会达到 10ms 以上, 30 | 后续请求耗时会稳定在 0~1ms 左右。 31 | 32 | ![img_2.png](../image/faq-execution-time1.png) 33 | ![img.png](../image/faq-execution-time2.png) 34 | 35 | 其中每条记录表示一次接口调用的完整校验耗时。 36 | 37 | ### 火焰图 38 | 39 | 测试条件: 40 | 41 | - 关闭debug日志 42 | - 接口充分预热 43 | - 使用 Apifox 进行10线程10循环的测试,共1500次请求 44 | - 使用 IDEA 自带的 IntelliJ Profiler 进行分析 45 | 46 | 得到如下的火焰图 47 | 48 | ![img_1.png](../image/faq-flame1.png) 49 | 50 | ![img_5.png](../image/faq-flame2.png) 51 | 52 | 可以看到,本组件的总耗时为170ms,平均每个请求耗时约0.11ms。 53 | 54 | 其中解析SpEL表达式的总耗时为110ms,占比约65%。 55 | 56 | 随后我在不同的数量的线程和循环次数后重新测试,得到的结果如下: 57 | 58 | - 5线程100循环,共7500次请求,总耗时约为 930ms,平均每个请求耗时约0.12ms,其中解析SpEL表达式的总耗时为 420ms,占比约45%。 59 | - 20线程,30循环,共9000次此请求,总耗时约为 1190ms,平均每个请求耗时约0.13ms,其中解析SpEL表达式的总耗时为 390ms,占比约33%。 60 | 61 | 这样看来,目前的性能表现还算可以接受,但还有优化空间,后续会继续优化。 62 | 63 | ## 如何对实体类单独进行校验 64 | 65 | 正常情况下,只需要触发 jakarta.validation-api 的校验,就会顺带触发 spel.validator 的校验。 66 | 这一点可以参考下源码的测试工具`cn.sticki.spel.validator.jakarta.JakartaSpelValidator.validate`的实现,大概是下面这个样子: 67 | 68 | ```java 69 | private static final Validator validator = Validation.byDefaultProvider() 70 | .configure() 71 | .messageInterpolator(new ParameterMessageInterpolator()) 72 | .buildValidatorFactory().getValidator(); 73 | 74 | /** 75 | * 参数校验 76 | *

77 | * 调用此方法会触发 jakarta.validation.constraints.* 的校验,类似于使用 @Valid 注解 78 | * 79 | * @return 校验结果,如果校验通过则返回空列表 80 | */ 81 | public static Set> validate(T obj) { 82 | return validator.validate(obj); 83 | } 84 | ``` 85 | 86 | 如果你的实体类中只有 spel.validator 的校验注解,或者你只想触发 spel.validator 的校验, 87 | 那更简单,你只需要调用 `cn.sticki.spel.validator.core.SpelValidExecutor#validateObject` 即可触发校验。 88 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_fr.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = doit avoir la valeur faux 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = doit avoir la valeur vrai 3 | cn.sticki.spel.validator.constraint.Email.message = doit \u00EAtre une adresse \u00E9lectronique syntaxiquement correcte 4 | cn.sticki.spel.validator.constraint.Future.message = doit \u00EAtre une date dans le futur 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = doit \u00EAtre une date dans le pr\u00E9sent ou le futur 6 | cn.sticki.spel.validator.constraint.Max.message = doit \u00EAtre inf\u00E9rieur ou \u00E9gal \u00E0 {0} 7 | cn.sticki.spel.validator.constraint.Min.message = doit \u00EAtre sup\u00E9rieur ou \u00E9gal \u00E0 {0} 8 | cn.sticki.spel.validator.constraint.Negative.message = doit \u00EAtre inf\u00E9rieur \u00E0 0 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = doit \u00EAtre inf\u00E9rieur ou \u00E9gal \u00E0 0 10 | cn.sticki.spel.validator.constraint.NotBlank.message = ne doit pas \u00EAtre vide 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = ne doit pas \u00EAtre vide 12 | cn.sticki.spel.validator.constraint.NotNull.message = ne doit pas \u00EAtre nul 13 | cn.sticki.spel.validator.constraint.Null.message = doit \u00EAtre nul 14 | cn.sticki.spel.validator.constraint.Past.message = doit \u00EAtre une date dans le pass\u00E9 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = doit \u00EAtre une date dans le pass\u00E9 ou le pr\u00E9sent 16 | cn.sticki.spel.validator.constraint.Pattern.message = doit correspondre \u00E0 "{0}" 17 | cn.sticki.spel.validator.constraint.Positive.message = doit \u00EAtre sup\u00E9rieur \u00E0 0 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = doit \u00EAtre sup\u00E9rieur ou \u00E9gal \u00E0 0 19 | cn.sticki.spel.validator.constraint.Size.message = la taille doit \u00EAtre comprise entre {0} et {1} 20 | cn.sticki.spel.validator.constraint.Digits.message = valeur num\u00E9rique hors limites (<{0} chiffres>.<{1} chiffres> attendu) 21 | -------------------------------------------------------------------------------- /document/web-docs/docs/guide/i18n.md: -------------------------------------------------------------------------------- 1 | # 国际化消息 2 | 3 | 自 v0.5.0-beta 版本起,SpEL Validator 提供了对校验消息的国际化支持。 4 | 通过配置资源文件和设置区域信息,您可以根据用户的语言环境返回相应的校验提示。 5 | 6 | 默认情况下,SpEL Validator 为所有内置约束注解提供了默认的国际化消息键, 7 | 这些消息键支持多语言,包括中文、英文、日文等。您可以通过添加自定义资源包来覆盖这些默认消息。 8 | 9 | 以下内容将指导您如何配置您自己的国际化消息。 10 | 11 | ## 配置国际化资源文件 12 | 13 | 您需要在 `resources` 目录下创建对应的国际化资源文件(不限制资源包名称),例如: 14 | 15 | - `ValidationMessages.properties`(默认语言) 16 | - `ValidationMessages_zh_CN.properties`(简体中文) 17 | - `ValidationMessages_en_US.properties`(美式英语) 18 | 19 | 每个文件中定义的键值对格式如下: 20 | 21 | ```properties 22 | you.message.key.SpelNotNull=must not be null 23 | ``` 24 | 25 | ## 添加自定义资源包 26 | 27 | 将您的资源包添加到 SpEL Validator 的资源包列表中: 28 | 29 | ```java 30 | ResourceBundleMessageResolver.addBasenames("ValidationMessages"); 31 | ``` 32 | 33 | 它会将 `ValidationMessages` 添加到原有资源包列表的最前面,这意味着如果存在相同的key,会覆盖掉原有的。 34 | 35 | ## 设置区域信息 36 | 37 | SpEL Validator 通过 Spring 提供的 `LocaleContextHolder` 来获取当前的区域设置。 38 | 默认情况下,它会根据当前 request headers 的 `Accept-Language` 字段来确定区域。 39 | 40 | 您也可以通过 `LocaleContextHolder.setLocale` 方法来手动更新区域。 41 | 42 | ## 使用国际化消息键 43 | 44 | 在使用 SpEL Validator 提供的注解时,您可以通过 `message` 属性指定国际化消息的键。 45 | 46 | ```java 47 | @SpelNotNull(assertTrue = "true", message = "{you.message.key.SpelNotNull}") 48 | private Integer age; 49 | ``` 50 | 51 | 在校验失败时,系统将根据当前的区域设置,从资源文件中获取对应的消息。 52 | 53 | ## 转义特殊字符 54 | 55 | 在资源文件中,如果需要使用花括号 `{}` 或反斜杠 `\`,请使用双反斜杠进行转义: 56 | 57 | ```properties 58 | cn.sticki.spel.validator.constraint.Custom.message=值必须在 \\{1, 2, 3\\} 之中 59 | ``` 60 | 61 | SpEL Validator 会自动处理这些转义字符,确保消息正确显示。 62 | 63 | ## 示例 64 | 65 | 假设您有以下资源文件: 66 | 67 | - `CustomValidationMessages.properties`: 68 | 69 | ```properties 70 | cn.sticki.spel.validator.constraint.AssertTrue.message=must be true 71 | ``` 72 | 73 | - `CustomValidationMessages_zh_CN.properties`: 74 | 75 | ```properties 76 | cn.sticki.spel.validator.constraint.AssertTrue.message=必须为 true 77 | ``` 78 | 79 | 在程序启动后的某个时机添加资源包: 80 | 81 | ```java 82 | ResourceBundleMessageResolver.addBasenames("CustomValidationMessages"); 83 | ``` 84 | 85 | 在代码中使用: 86 | 87 | ```java 88 | @SpelAssert(assertTrue = "#this.active", message = "{cn.sticki.spel.validator.constraint.AssertTrue.message}") 89 | private Boolean active; 90 | ``` 91 | 92 | 根据当前的区域设置,校验失败时将返回相应语言的提示信息。 -------------------------------------------------------------------------------- /document/web-docs/docs/guide/spel.md: -------------------------------------------------------------------------------- 1 | # SpEL 表达式 2 | 3 | 本章只介绍一些重要的 SpEL 表达式用法,更详细的使用说明请参考官方文档。 4 | 5 | 官方文档:[Spring Expression Language (SpEL)](https://docs.spring.io/spring-framework/reference/core/expressions/language-ref.html) 6 | 7 | ::: tip 8 | 9 | 如果你的 IDEA 版本比较新,理论上来说,IDEA 应该能够识别到表达式,并且会给出提示,也具备引用的功能。 10 | 11 | ::: 12 | 13 | ## 基本操作符 14 | 15 | > 此部分由GPT生成,进行了部分删减。 16 | 17 | 1. **算术操作符** 18 | - `+` 加法 19 | - `-` 减法 20 | - `*` 乘法 21 | - `/` 除法 22 | - `%` 取模 23 | 24 | 2. **关系操作符** 25 | - `==` 等于 26 | - `!=` 不等于 27 | - `<` 小于 28 | - `<=` 小于等于 29 | - `>` 大于 30 | - `>=` 大于等于 31 | 32 | 3. **逻辑操作符** 33 | - `&&` 逻辑与 34 | - `||` 逻辑或 35 | - `!` 逻辑非 36 | 37 | 4. **条件操作符(三元操作符)** 38 | - `? :` 条件表达式,类似于 Java 中的 `? :` 39 | 40 | 5. **成员访问** 41 | - `.` 属性访问 42 | - `[]` 属性访问(使用字符串键) 43 | 44 | 6. **集合操作符** 45 | - `in` 判断元素是否在集合中 46 | - `!in` 判断元素是否不在集合中 47 | 48 | 7. **空安全操作符** 49 | - `?.` 空安全属性访问 50 | - `:?` 空安全方法调用 51 | 52 | 8. **空合并操作符** 53 | - `?:` 当左侧表达式为 null 时,返回右侧表达式的值 54 | 55 | 56 | ## 调用静态方法 57 | 58 | 调用静态方法的语法为:`T(全类名).方法名(参数)`。 59 | 60 | ```java 61 | @Data 62 | @SpelValid 63 | public class SimpleExampleParamVo { 64 | 65 | /** 66 | * 枚举值字段校验 67 | */ 68 | @SpelAssert(assertTrue = " T(cn.sticki.enums.UserStatusEnum).getByCode(#this.userStatus) != null ", message = "用户状态不合法") 69 | private Integer userStatus; 70 | 71 | // 中文算两个字符,英文算一个字符,要求总长度不超过 10 72 | // 调用外部静态方法进行校验 73 | @SpelAssert(assertTrue = "T(cn.sticki.util.StringUtil).getLength(#this.userName) <= 10", message = "用户名长度不能超过10") 74 | private String userName; 75 | 76 | } 77 | ``` 78 | 79 | ## 调用 Spring Bean 80 | 81 | [开启 Spring Bean 支持](user-guide.md#开启对-spring-bean-的支持)后,即可在 SpEL 表达式中调用 Spring Bean。 82 | 83 | 调用 Bean 的语法为:`@beanName.methodName(参数)`。 84 | 85 | ```java 86 | @Data 87 | @SpelValid 88 | public class SimpleExampleParamVo { 89 | 90 | /** 91 | * 调用 userService 的 getById 方法,判断用户是否存在 92 | * 校验失败时,提示信息为:用户不存在 93 | * 这里只是简单举例,实际开发中不建议这样判断用户是否存在 94 | */ 95 | @SpelAssert(assertTrue = "@userService.getById(#this.userId) != null", message = "用户不存在") 96 | private Long userId; 97 | 98 | } 99 | ``` 100 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_zh.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = \u5FC5\u987B\u4E3A false 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = \u5FC5\u987B\u4E3A true 3 | cn.sticki.spel.validator.constraint.Email.message = \u5FC5\u987B\u4E3A\u683C\u5F0F\u89C4\u8303\u7684\u7535\u5B50\u90AE\u4EF6\u5730\u5740 4 | cn.sticki.spel.validator.constraint.Future.message = \u5FC5\u987B\u662F\u672A\u6765\u7684\u65E5\u671F 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = \u5FC5\u987B\u662F\u73B0\u5728\u6216\u5C06\u6765\u7684\u65E5\u671F 6 | cn.sticki.spel.validator.constraint.Max.message = \u5FC5\u987B\u5C0F\u4E8E\u6216\u7B49\u4E8E {0} 7 | cn.sticki.spel.validator.constraint.Min.message = \u5FC5\u987B\u5927\u4E8E\u6216\u7B49\u4E8E {0} 8 | cn.sticki.spel.validator.constraint.Negative.message = \u5FC5\u987B\u5C0F\u4E8E 0 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = \u5FC5\u987B\u5C0F\u4E8E\u6216\u7B49\u4E8E 0 10 | cn.sticki.spel.validator.constraint.NotBlank.message = \u4E0D\u5F97\u4E3A\u7A7A\u767D 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = \u4E0D\u5F97\u4E3A\u7A7A 12 | cn.sticki.spel.validator.constraint.NotNull.message = \u4E0D\u5F97\u4E3A null 13 | cn.sticki.spel.validator.constraint.Null.message = \u5FC5\u987B\u4E3A null 14 | cn.sticki.spel.validator.constraint.Past.message = \u5FC5\u987B\u662F\u8FC7\u53BB\u7684\u65E5\u671F 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = \u5FC5\u987B\u662F\u8FC7\u53BB\u6216\u73B0\u5728\u7684\u65E5\u671F 16 | cn.sticki.spel.validator.constraint.Pattern.message = \u5FC5\u987B\u4E0E "{0}" \u5339\u914D 17 | cn.sticki.spel.validator.constraint.Positive.message = \u5FC5\u987B\u5927\u4E8E 0 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = \u5FC5\u987B\u5927\u4E8E\u6216\u7B49\u4E8E 0 19 | cn.sticki.spel.validator.constraint.Size.message = \u5927\u5C0F\u5FC5\u987B\u5728 {0} \u548C {1} \u4E4B\u95F4 20 | cn.sticki.spel.validator.constraint.Digits.message = \u6570\u5b57\u503c\u8d85\u51fa\u4e86\u8fb9\u754c\uff08\u671f\u671b <{0} digits>.<{1} digits>\uff09 21 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_zh_TW.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = \u5FC5\u9808\u662F false 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = \u5FC5\u9808\u662F true 3 | cn.sticki.spel.validator.constraint.Email.message = \u5FC5\u9808\u662F\u5F62\u5F0F\u5B8C\u6574\u7684\u96FB\u5B50\u90F5\u4EF6\u4F4D\u5740 4 | cn.sticki.spel.validator.constraint.Future.message = \u5FC5\u9808\u662F\u672A\u4F86\u7684\u65E5\u671F 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = \u5FC5\u9808\u662F\u7576\u5929\u6216\u672A\u4F86\u7684\u65E5\u671F 6 | cn.sticki.spel.validator.constraint.Max.message = \u5FC5\u9808\u5C0F\u65BC\u6216\u7B49\u65BC {0} 7 | cn.sticki.spel.validator.constraint.Min.message = \u5FC5\u9808\u5927\u65BC\u6216\u7B49\u65BC {0} 8 | cn.sticki.spel.validator.constraint.Negative.message = \u5FC5\u9808\u5C0F\u65BC 0 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = \u5FC5\u9808\u5C0F\u65BC\u6216\u7B49\u65BC 0 10 | cn.sticki.spel.validator.constraint.NotBlank.message = \u4E0D\u5F97\u7A7A\u767D 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = \u4E0D\u5F97\u662F\u7A7A\u7684 12 | cn.sticki.spel.validator.constraint.NotNull.message = \u4E0D\u5F97\u662F\u7A7A\u503C 13 | cn.sticki.spel.validator.constraint.Null.message = \u5FC5\u9808\u662F\u7A7A\u503C 14 | cn.sticki.spel.validator.constraint.Past.message = \u5FC5\u9808\u662F\u904E\u53BB\u7684\u65E5\u671F 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = \u5FC5\u9808\u662F\u904E\u53BB\u6216\u7576\u5929\u7684\u65E5\u671F 16 | cn.sticki.spel.validator.constraint.Pattern.message = \u5FC5\u9808\u7B26\u5408 "{0}" 17 | cn.sticki.spel.validator.constraint.Positive.message = \u5FC5\u9808\u5927\u65BC 0 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = \u5FC5\u9808\u5927\u65BC\u6216\u7B49\u65BC 0 19 | cn.sticki.spel.validator.constraint.Size.message = \u5927\u5C0F\u5FC5\u9808\u5728 {0} \u548C {1} \u4E4B\u9593 20 | cn.sticki.spel.validator.constraint.Digits.message = \u6578\u503c\u8d85\u51fa\u7bc4\u570d\uff08\u9810\u671f\u70ba <{0} digits>.<{1} digits>\uff09 21 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_ro.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = trebuie s\u0103 fie false 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = trebuie s\u0103 fie true 3 | cn.sticki.spel.validator.constraint.Email.message = trebuie s\u0103 fie o adres\u0103 de e-mail cu format corect 4 | cn.sticki.spel.validator.constraint.Future.message = trebuie s\u0103 fie o dat\u0103 viitoare 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = trebuie s\u0103 fie o dat\u0103 \u00EEn prezent sau \u00EEn viitor 6 | cn.sticki.spel.validator.constraint.Max.message = trebuie s\u0103 fie mai mic sau egal dec\u00E2t {0} 7 | cn.sticki.spel.validator.constraint.Min.message = trebuie s\u0103 fie mai mare sau egal dec\u00E2t {0} 8 | cn.sticki.spel.validator.constraint.Negative.message = trebuie s\u0103 fie mai mic dec\u00E2t 0 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = trebuie s\u0103 fie mai mic sau egal dec\u00E2t 0 10 | cn.sticki.spel.validator.constraint.NotBlank.message = nu trebuie s\u0103 fie blanc 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = nu trebuie s\u0103 fie gol 12 | cn.sticki.spel.validator.constraint.NotNull.message = nu trebuie s\u0103 fie null 13 | cn.sticki.spel.validator.constraint.Null.message = trebuie s\u0103 fie null 14 | cn.sticki.spel.validator.constraint.Past.message = trebuie s\u0103 fie o dat\u0103 anterioar\u0103 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = trebuie s\u0103 fie o dat\u0103 anterioar\u0103 sau din prezent 16 | cn.sticki.spel.validator.constraint.Pattern.message = trebuie s\u0103 se potriveasc\u0103 "{0}" 17 | cn.sticki.spel.validator.constraint.Positive.message = trebuie s\u0103 fie mai mare dec\u00E2t 0 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = trebuie s\u0103 fie mai mare sau egal dec\u00E2t 0 19 | cn.sticki.spel.validator.constraint.Size.message = dimensiunea trebuie s\u0103 fie \u00EEntre {0} \u015Fi {1} 20 | cn.sticki.spel.validator.constraint.Digits.message = valoare numeric\u0103 \u00een afara limitelor (<{0} digi\u0163i>.<{1} digi\u0163i> a\u015fteptat) 21 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/message/ResourceBundleMessageResolver.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.message; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.context.support.ResourceBundleMessageSource; 5 | 6 | import java.util.Locale; 7 | 8 | /** 9 | * 资源包消息解析器 10 | * 11 | * @author 阿杆 12 | * @since 2025/2/25 13 | */ 14 | @Slf4j 15 | public class ResourceBundleMessageResolver { 16 | 17 | private ResourceBundleMessageResolver() {} 18 | 19 | /** 20 | * The name of the default message bundle. 21 | */ 22 | public static final String DEFAULT_VALIDATION_MESSAGES = "cn.sticki.spel.validator.ValidationMessages"; 23 | 24 | private static final ResourceBundleMessageSource MESSAGE_SOURCE = initMessageSource(); 25 | 26 | private static ResourceBundleMessageSource initMessageSource() { 27 | ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); 28 | messageSource.setBasenames( 29 | DEFAULT_VALIDATION_MESSAGES 30 | ); 31 | messageSource.setDefaultEncoding("UTF-8"); 32 | return messageSource; 33 | } 34 | 35 | /** 36 | * 重置资源包 37 | */ 38 | public static void resetBasenames() { 39 | MESSAGE_SOURCE.setBasenames( 40 | DEFAULT_VALIDATION_MESSAGES 41 | ); 42 | } 43 | 44 | /** 45 | * 添加资源包 46 | * 47 | * @param basename 资源包名称 48 | */ 49 | public static void addBasenames(String... basename) { 50 | String[] existingBasename = MESSAGE_SOURCE.getBasenameSet().toArray(new String[0]); 51 | 52 | // 创建一个新的 basename 数组,将新添加的放在前面 53 | String[] combinedBasename = new String[basename.length + existingBasename.length]; 54 | System.arraycopy(basename, 0, combinedBasename, 0, basename.length); 55 | System.arraycopy(existingBasename, 0, combinedBasename, basename.length, existingBasename.length); 56 | log.debug("Combined basename: {}", (Object) combinedBasename); 57 | 58 | // 重新设置 basename 59 | MESSAGE_SOURCE.setBasenames(combinedBasename); 60 | } 61 | 62 | public static String getMessage(String key, Locale locale, Object... args) { 63 | return MESSAGE_SOURCE.getMessage(key, args, locale); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constrain/SpelDigits.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constrain; 2 | 3 | import cn.sticki.spel.validator.constraintvalidator.SpelDigitsValidator; 4 | import cn.sticki.spel.validator.core.SpelConstraint; 5 | import org.intellij.lang.annotations.Language; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Repeatable; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | import static java.lang.annotation.ElementType.FIELD; 13 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 14 | 15 | /** 16 | * 被标记的元素值必须是指定范围内的数字。 17 | *

18 | * 支持的类型有: 19 | *

    20 | *
  • 所有 {@link Number} 类型及它们的基本数据类型
  • 21 | *
  • 使用 {@link CharSequence} 表示的数字,支持科学计数法
  • 22 | *
23 | *

24 | * {@code null} 元素被认为是有效的。 25 | * 26 | * @author 阿杆 27 | * @version 1.0 28 | * @since 2025/8/10 29 | */ 30 | @Documented 31 | @Retention(RUNTIME) 32 | @Target(FIELD) 33 | @Repeatable(SpelDigits.List.class) 34 | @SpelConstraint(validatedBy = SpelDigitsValidator.class) 35 | public @interface SpelDigits { 36 | 37 | /** 38 | * 校验失败时的错误消息。 39 | */ 40 | String message() default "{cn.sticki.spel.validator.constraint.Digits.message}"; 41 | 42 | /** 43 | * 约束开启条件,必须为合法的SpEL表达式,计算结果必须为boolean类型。 44 | *

45 | * 当 表达式为空 或 计算结果为true 时,会对带注解的元素进行校验。 46 | *

47 | * 默认情况下,开启校验。 48 | */ 49 | @Language("SpEL") 50 | String condition() default ""; 51 | 52 | /** 53 | * 分组条件,必须为合法的SpEL表达式。 54 | *

55 | * 当分组信息不为空时,只有当 {@link SpelValid#spelGroups()} 中的分组信息与此处的分组信息有交集时,才会对带注解的元素进行校验。 56 | *

57 | * 其计算结果可以是任何类型,但只有两个计算结果完全相等时,才被认为是相等的。 58 | */ 59 | @Language("SpEL") 60 | String[] group() default {}; 61 | 62 | /** 63 | * 整数部分的最大位数。必须为合法的SpEL表达式, 64 | *

65 | * 表达式的计算结果必须为非负整数。 66 | */ 67 | @Language("SpEL") 68 | String integer(); 69 | 70 | /** 71 | * 小数部分的最大位数。必须为合法的SpEL表达式, 72 | *

73 | * 表达式的计算结果必须为非负整数。 74 | */ 75 | @Language("SpEL") 76 | String fraction(); 77 | 78 | @Documented 79 | @Target(FIELD) 80 | @Retention(RUNTIME) 81 | @interface List { 82 | 83 | SpelDigits[] value(); 84 | 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_pl.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = musi mie\u0107 warto\u015B\u0107 false 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = musi mie\u0107 warto\u015B\u0107 true 3 | cn.sticki.spel.validator.constraint.Email.message = musi by\u0107 poprawnie sformatowanym adresem e-mail 4 | cn.sticki.spel.validator.constraint.Future.message = musi by\u0107 dat\u0105 w przysz\u0142o\u015Bci 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = musi by\u0107 dat\u0105 bie\u017C\u0105c\u0105 lub w przysz\u0142o\u015Bci 6 | cn.sticki.spel.validator.constraint.Max.message = musi by\u0107 r\u00F3wne lub mniejsze od {0} 7 | cn.sticki.spel.validator.constraint.Min.message = musi by\u0107 r\u00F3wne lub wi\u0119ksze od {0} 8 | cn.sticki.spel.validator.constraint.Negative.message = musi by\u0107 mniejsze od 0 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = musi by\u0107 r\u00F3wne lub mniejsze od 0 10 | cn.sticki.spel.validator.constraint.NotBlank.message = nie mo\u017Ce by\u0107 odst\u0119pem 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = nie mo\u017Ce by\u0107 puste 12 | cn.sticki.spel.validator.constraint.NotNull.message = nie mo\u017Ce mie\u0107 warto\u015Bci null 13 | cn.sticki.spel.validator.constraint.Null.message = musi mie\u0107 warto\u015B\u0107 null 14 | cn.sticki.spel.validator.constraint.Past.message = musi by\u0107 dat\u0105 w przesz\u0142o\u015Bci 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = musi by\u0107 dat\u0105 bie\u017C\u0105c\u0105 lub w przesz\u0142o\u015Bci 16 | cn.sticki.spel.validator.constraint.Pattern.message = musi pasowa\u0107 do wyra\u017Cenia {0} 17 | cn.sticki.spel.validator.constraint.Positive.message = musi by\u0107 wi\u0119ksze od 0 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = musi by\u0107 r\u00F3wne lub wi\u0119ksze od 0 19 | cn.sticki.spel.validator.constraint.Size.message = wielko\u015B\u0107 musi nale\u017Ce\u0107 do zakresu od {0} do {1} 20 | cn.sticki.spel.validator.constraint.Digits.message = warto\u015B\u0107 liczbowa spoza zakresu (oczekiwano <{0} cyfr>.<{1} cyfr>) 21 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_hu.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = hamis \u00E9rt\u00E9k\u0171nek kell lennie 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = igaz \u00E9rt\u00E9k\u0171nek kell lennie 3 | cn.sticki.spel.validator.constraint.Email.message = helyes form\u00E1tum\u00FA e-mail c\u00EDmnek kell lennie 4 | cn.sticki.spel.validator.constraint.Future.message = j\u00F6v\u0151beli d\u00E1tumnak kell lennie 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = jelen vagy j\u00F6v\u0151beli d\u00E1tumnak kell lennie 6 | cn.sticki.spel.validator.constraint.Max.message = kisebbnek, vagy egyenl\u0151nek kell lennie, mint {0} 7 | cn.sticki.spel.validator.constraint.Min.message = nagyobbnak, vagy egyenl\u0151nek kell lennie, mint {0} 8 | cn.sticki.spel.validator.constraint.Negative.message = kisebbnek kell lennie, mint 0 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = kisebbnek, vagy egyenl\u0151nek kell lennie, mint 0 10 | cn.sticki.spel.validator.constraint.NotBlank.message = nem lehet \u00FCres 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = nem lehet \u00FCres 12 | cn.sticki.spel.validator.constraint.NotNull.message = nem lehet null 13 | cn.sticki.spel.validator.constraint.Null.message = null\u00E9rt\u00E9k\u0171nek kell lennie 14 | cn.sticki.spel.validator.constraint.Past.message = m\u00FAltbeli d\u00E1tumnak kell lennie 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = m\u00FAltbeli vagy jelen d\u00E1tumnak kell lennie 16 | cn.sticki.spel.validator.constraint.Pattern.message = meg kell felelnie a(z) "{0}" kifejez\u00E9snek 17 | cn.sticki.spel.validator.constraint.Positive.message = nagyobbnak kell lennie, mint 0 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = nagyobbnak vagy egyenl\u0151nek kell lennie, mint 0 19 | cn.sticki.spel.validator.constraint.Size.message = a m\u00E9retnek a(z) {0} \u00E9s {1} \u00E9rt\u00E9kek k\u00F6z\u00F6tt kell lennie 20 | cn.sticki.spel.validator.constraint.Digits.message = a numerikus \u00E9rt\u00E9k a korl\u00E1tokon k\u00EDv\u00FCl esik (<{0} számjegy>.<{1} számjegy> sz\u00E1mot v\u00E1rt a rendszer) 21 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constrain/SpelMax.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constrain; 2 | 3 | import cn.sticki.spel.validator.constraintvalidator.SpelMaxValidator; 4 | import cn.sticki.spel.validator.core.SpelConstraint; 5 | import org.intellij.lang.annotations.Language; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Repeatable; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | import static java.lang.annotation.ElementType.FIELD; 13 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 14 | 15 | /** 16 | * 被标记的元素值必须小于或等于指定的最大值。{@code null} 元素被认为是有效的。 17 | *

18 | * 支持的类型有: 19 | *

    20 | *
  • 所有 {@link Number} 类型及它们的基本数据类型
  • 21 | *
  • 使用 {@link CharSequence} 表示的数字,支持科学计数法
  • 22 | *
23 | * 24 | * @author 阿杆 25 | * @version 1.0 26 | * @since 2024/9/29 27 | */ 28 | @Documented 29 | @Retention(RUNTIME) 30 | @Target(FIELD) 31 | @Repeatable(SpelMax.List.class) 32 | @SpelConstraint(validatedBy = SpelMaxValidator.class) 33 | public @interface SpelMax { 34 | 35 | /** 36 | * 校验失败时的错误消息。 37 | */ 38 | String message() default "{cn.sticki.spel.validator.constraint.Max.message}"; 39 | 40 | /** 41 | * 约束开启条件,必须为合法的SpEL表达式,计算结果必须为boolean类型。 42 | *

43 | * 当 表达式为空 或 计算结果为true 时,会对带注解的元素进行校验。 44 | *

45 | * 默认情况下,开启校验。 46 | */ 47 | @Language("SpEL") 48 | String condition() default ""; 49 | 50 | /** 51 | * 分组条件,必须为合法的SpEL表达式。 52 | *

53 | * 当分组信息不为空时,只有当 {@link SpelValid#spelGroups()} 中的分组信息与此处的分组信息有交集时,才会对带注解的元素进行校验。 54 | *

55 | * 其计算结果可以是任何类型,但只有两个计算结果完全相等时,才被认为是相等的。 56 | */ 57 | @Language("SpEL") 58 | String[] group() default {}; 59 | 60 | /** 61 | * 指定元素最大值。必须为合法的SpEL表达式, 62 | *

63 | * 表达式的计算结果必须为 {@link Number} 类型。 64 | */ 65 | @Language("SpEL") 66 | String value(); 67 | 68 | /** 69 | * 指定边界值是否被包含在内。 70 | *

71 | * 当为 true 时,验证 value <= max;当为 false 时,验证 value < max。 72 | *

73 | * 默认为 true。 74 | */ 75 | boolean inclusive() default true; 76 | 77 | @Documented 78 | @Target(FIELD) 79 | @Retention(RUNTIME) 80 | @interface List { 81 | 82 | SpelMax[] value(); 83 | 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constrain/SpelMin.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constrain; 2 | 3 | import cn.sticki.spel.validator.constraintvalidator.SpelMinValidator; 4 | import cn.sticki.spel.validator.core.SpelConstraint; 5 | import org.intellij.lang.annotations.Language; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Repeatable; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | import static java.lang.annotation.ElementType.FIELD; 13 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 14 | 15 | /** 16 | * 被标记的元素值必须大于或等于指定的最小值。{@code null} 元素被认为是有效的。 17 | *

18 | * 支持的类型有: 19 | *

    20 | *
  • 所有 {@link Number} 类型及它们的基本数据类型
  • 21 | *
  • 使用 {@link CharSequence} 表示的数字,支持科学计数法
  • 22 | *
23 | * 24 | * @author oddfar、阿杆 25 | * @version 1.0 26 | * @since 2024/8/25 27 | */ 28 | @Documented 29 | @Retention(RUNTIME) 30 | @Target(FIELD) 31 | @Repeatable(SpelMin.List.class) 32 | @SpelConstraint(validatedBy = SpelMinValidator.class) 33 | public @interface SpelMin { 34 | 35 | /** 36 | * 校验失败时的错误消息。 37 | */ 38 | String message() default "{cn.sticki.spel.validator.constraint.Min.message}"; 39 | 40 | /** 41 | * 约束开启条件,必须为合法的SpEL表达式,计算结果必须为boolean类型。 42 | *

43 | * 当 表达式为空 或 计算结果为true 时,会对带注解的元素进行校验。 44 | *

45 | * 默认情况下,开启校验。 46 | */ 47 | @Language("SpEL") 48 | String condition() default ""; 49 | 50 | /** 51 | * 分组条件,必须为合法的SpEL表达式。 52 | *

53 | * 当分组信息不为空时,只有当 {@link SpelValid#spelGroups()} 中的分组信息与此处的分组信息有交集时,才会对带注解的元素进行校验。 54 | *

55 | * 其计算结果可以是任何类型,但只有两个计算结果完全相等时,才被认为是相等的。 56 | */ 57 | @Language("SpEL") 58 | String[] group() default {}; 59 | 60 | /** 61 | * 指定元素最小值。必须为合法的SpEL表达式, 62 | *

63 | * 表达式的计算结果必须为 {@link Number} 类型。 64 | */ 65 | @Language("SpEL") 66 | String value(); 67 | 68 | /** 69 | * 指定边界值是否被包含在内。 70 | *

71 | * 当为 true 时,验证 value >= min;当为 false 时,验证 value > min。 72 | *

73 | * 默认为 true。 74 | */ 75 | boolean inclusive() default true; 76 | 77 | @Documented 78 | @Target(FIELD) 79 | @Retention(RUNTIME) 80 | @interface List { 81 | 82 | SpelMin[] value(); 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /spel-validator-test/src/main/java/cn/sticki/spel/validator/test/util/ConstraintViolationSet.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.test.util; 2 | 3 | import cn.sticki.spel.validator.core.result.FieldError; 4 | 5 | import java.util.*; 6 | import java.util.stream.Collectors; 7 | 8 | /** 9 | * 约束违反集合 10 | *

11 | * 用于存储校验结果,根据字段名和期望的错误信息来获取字段约束结果 12 | * 13 | * @author 阿杆 14 | * @since 2024/10/29 15 | */ 16 | public class ConstraintViolationSet { 17 | 18 | private final Map> verifyMap; 19 | 20 | public ConstraintViolationSet(Collection fieldErrors) { 21 | if (fieldErrors == null || fieldErrors.isEmpty()) { 22 | verifyMap = Collections.emptyMap(); 23 | return; 24 | } 25 | 26 | this.verifyMap = fieldErrors.stream().collect(Collectors.groupingBy(FieldError::getFieldName)); 27 | } 28 | 29 | public static ConstraintViolationSet of(List fieldErrors) { 30 | return new ConstraintViolationSet(fieldErrors); 31 | } 32 | 33 | /** 34 | * 根据字段和期望的错误信息来获取字段约束结果 35 | * 36 | * @param fieldName 字段名 37 | * @param expectMessage 期望的错误信息 38 | * @return 字段约束结果,当 expectMessage 不为null时,会优先匹配具有相同message的数据 39 | */ 40 | public FieldError getAndRemove(String fieldName, String expectMessage) { 41 | List violationList = verifyMap.get(fieldName); 42 | if (violationList == null || violationList.isEmpty()) { 43 | return null; 44 | } 45 | if (violationList.size() == 1 || expectMessage == null) { 46 | FieldError violation = violationList.get(0); 47 | verifyMap.remove(fieldName); 48 | return violation; 49 | } 50 | // 当存在多个约束时,优先匹配具有相同message的数据。 51 | // 否则当一个字段有多个约束条件时,无法匹配到期望的约束。 52 | for (FieldError violation : violationList) { 53 | if (expectMessage.equals(violation.getErrorMessage())) { 54 | violationList.remove(violation); 55 | return violation; 56 | } 57 | } 58 | 59 | return violationList.remove(0); 60 | } 61 | 62 | /** 63 | * 获取所有的约束违反字段 64 | */ 65 | public Set getAll() { 66 | return verifyMap.values().stream().flatMap(List::stream).collect(Collectors.toSet()); 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_zh_CN.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = \u53EA\u80FD\u4E3Afalse 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = \u53EA\u80FD\u4E3Atrue 3 | cn.sticki.spel.validator.constraint.Email.message = \u4E0D\u662F\u4E00\u4E2A\u5408\u6CD5\u7684\u7535\u5B50\u90AE\u4EF6\u5730\u5740 4 | cn.sticki.spel.validator.constraint.Future.message = \u9700\u8981\u662F\u4E00\u4E2A\u5C06\u6765\u7684\u65F6\u95F4 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = \u9700\u8981\u662F\u4E00\u4E2A\u5C06\u6765\u6216\u73B0\u5728\u7684\u65F6\u95F4 6 | cn.sticki.spel.validator.constraint.Max.message = \u6700\u5927\u4E0D\u80FD\u8D85\u8FC7{0} 7 | cn.sticki.spel.validator.constraint.Min.message = \u6700\u5C0F\u4E0D\u80FD\u5C0F\u4E8E{0} 8 | cn.sticki.spel.validator.constraint.Negative.message = \u5FC5\u987B\u662F\u8D1F\u6570 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = \u5FC5\u987B\u662F\u8D1F\u6570\u6216\u96F6 10 | cn.sticki.spel.validator.constraint.NotBlank.message = \u4E0D\u80FD\u4E3A\u7A7A 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = \u4E0D\u80FD\u4E3A\u7A7A 12 | cn.sticki.spel.validator.constraint.NotNull.message = \u4E0D\u80FD\u4E3Anull 13 | cn.sticki.spel.validator.constraint.Null.message = \u5FC5\u987B\u4E3Anull 14 | cn.sticki.spel.validator.constraint.Past.message = \u9700\u8981\u662F\u4E00\u4E2A\u8FC7\u53BB\u7684\u65F6\u95F4 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = \u9700\u8981\u662F\u4E00\u4E2A\u8FC7\u53BB\u6216\u73B0\u5728\u7684\u65F6\u95F4 16 | cn.sticki.spel.validator.constraint.Pattern.message = \u9700\u8981\u5339\u914D\u6B63\u5219\u8868\u8FBE\u5F0F"{0}" 17 | cn.sticki.spel.validator.constraint.Positive.message = \u5FC5\u987B\u662F\u6B63\u6570 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = \u5FC5\u987B\u662F\u6B63\u6570\u6216\u96F6 19 | cn.sticki.spel.validator.constraint.Size.message = \u4E2A\u6570\u5FC5\u987B\u5728{0}\u548C{1}\u4E4B\u95F4 20 | cn.sticki.spel.validator.constraint.Digits.message = \u6570\u5b57\u7684\u503c\u8d85\u51fa\u4e86\u5141\u8bb8\u8303\u56f4(\u53ea\u5141\u8bb8\u5728{0}\u4f4d\u6574\u6570\u548c{1}\u4f4d\u5c0f\u6570\u8303\u56f4\u5185) 21 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constrain/SpelSize.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constrain; 2 | 3 | import cn.sticki.spel.validator.constraintvalidator.SpelSizeValidator; 4 | import cn.sticki.spel.validator.core.SpelConstraint; 5 | import org.intellij.lang.annotations.Language; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Repeatable; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | import static java.lang.annotation.ElementType.FIELD; 13 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 14 | 15 | /** 16 | * 被标记的元素大小必须在指定边界(包含)之间。{@code null} 元素被认为是有效的。 17 | *

18 | * 支持的类型有: 19 | *

    20 | *
  • {@link CharSequence}(评估字符序列的长度)
  • 21 | *
  • {@link java.util.Collection}(评估集合大小)
  • 22 | *
  • {@link java.util.Map}(评估Map大小)
  • 23 | *
  • 数组(计算数组长度)
  • 24 | *
25 | *

26 | * 27 | * @author 阿杆 28 | * @version 1.0 29 | * @since 2024/5/5 30 | */ 31 | @Documented 32 | @Retention(RUNTIME) 33 | @Target(FIELD) 34 | @Repeatable(SpelSize.List.class) 35 | @SpelConstraint(validatedBy = SpelSizeValidator.class) 36 | public @interface SpelSize { 37 | 38 | /** 39 | * 校验失败时的错误消息。 40 | */ 41 | String message() default "{cn.sticki.spel.validator.constraint.Size.message}"; 42 | 43 | /** 44 | * 约束开启条件,必须为合法的SpEL表达式,计算结果必须为boolean类型。 45 | *

46 | * 当 表达式为空 或 计算结果为true 时,会对带注解的元素进行校验。 47 | *

48 | * 默认情况下,开启校验。 49 | */ 50 | @Language("SpEL") 51 | String condition() default ""; 52 | 53 | /** 54 | * 分组条件,必须为合法的SpEL表达式。 55 | *

56 | * 当分组信息不为空时,只有当 {@link SpelValid#spelGroups()} 中的分组信息与此处的分组信息有交集时,才会对带注解的元素进行校验。 57 | *

58 | * 其计算结果可以是任何类型,但只有两个计算结果完全相等时,才被认为是相等的。 59 | */ 60 | @Language("SpEL") 61 | String[] group() default {}; 62 | 63 | /** 64 | * @return 指定元素最小值。必须为合法的SpEL表达式,计算结果必须为数字类型。默认值为 0。 65 | */ 66 | @Language("SpEL") 67 | String min() default "0"; 68 | 69 | /** 70 | * @return 指定元素最大值。必须为合法的SpEL表达式,计算结果必须为数字类型。默认值为 {@link Integer#MAX_VALUE}。 71 | */ 72 | @Language("SpEL") 73 | String max() default "T(Integer).MAX_VALUE"; 74 | 75 | @Documented 76 | @Target(FIELD) 77 | @Retention(RUNTIME) 78 | @interface List { 79 | 80 | SpelSize[] value(); 81 | 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_cs.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = mus\u00ED m\u00EDt hodnotu ne 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = mus\u00ED m\u00EDt hodnotu ano 3 | cn.sticki.spel.validator.constraint.Email.message = mus\u00ED m\u00EDt spr\u00E1vn\u011B utvo\u0159enou e-mailovou adresu 4 | cn.sticki.spel.validator.constraint.Future.message = mus\u00ED se jednat o datum v budoucnu 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = mus\u00ED m\u00EDt aktu\u00E1ln\u00ED nebo budouc\u00ED datum 6 | cn.sticki.spel.validator.constraint.Max.message = mus\u00ED b\u00FDt men\u0161\u00ED nebo rovna hodnot\u011B {0} 7 | cn.sticki.spel.validator.constraint.Min.message = mus\u00ED b\u00FDt v\u011Bt\u0161\u00ED nebo rovna hodnot\u011B {0} 8 | cn.sticki.spel.validator.constraint.Negative.message = mus\u00ED b\u00FDt men\u0161\u00ED ne\u017E 0 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = mus\u00ED b\u00FDt men\u0161\u00ED ne\u017E nebo rovna hodnot\u011B 0 10 | cn.sticki.spel.validator.constraint.NotBlank.message = nesm\u00ED b\u00FDt pr\u00E1zdn\u00E1 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = nesm\u00ED b\u00FDt pr\u00E1zdn\u00E1 12 | cn.sticki.spel.validator.constraint.NotNull.message = nesm\u00ED m\u00EDt hodnotu Null 13 | cn.sticki.spel.validator.constraint.Null.message = mus\u00ED m\u00EDt hodnotu Null 14 | cn.sticki.spel.validator.constraint.Past.message = mus\u00ED se jednat o datum v minulosti 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = mus\u00ED b\u00FDt datum v minulosti nebo aktu\u00E1ln\u00ED 16 | cn.sticki.spel.validator.constraint.Pattern.message = mus\u00ED odpov\u00EDdat "{0}" 17 | cn.sticki.spel.validator.constraint.Positive.message = mus\u00ED b\u00FDt v\u011Bt\u0161\u00ED ne\u017E 0 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = mus\u00ED b\u00FDt v\u011Bt\u0161\u00ED ne\u017E nebo rovna hodnot\u011B 0 19 | cn.sticki.spel.validator.constraint.Size.message = velikost mus\u00ED le\u017Eet v rozsahu {0} a\u017E {1} 20 | cn.sticki.spel.validator.constraint.Digits.message = \u010d\u00edseln\u00e1 hodnota mimo rozsah (o\u010dek\u00e1v\u00e1no: <{0} \u010d\u00edslic>.<{1} \u010d\u00edslic>) 21 | -------------------------------------------------------------------------------- /spel-validator-jakarta/src/main/java/cn/sticki/spel/validator/jakarta/SpelValid.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.jakarta; 2 | 3 | import jakarta.validation.Constraint; 4 | import org.intellij.lang.annotations.Language; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | import static java.lang.annotation.ElementType.*; 11 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 12 | 13 | /** 14 | * 标记开启spel约束注解的验证,其功能类似于 {@link jakarta.validation.Valid},用于开启 {@link cn.sticki.spel.validator.core.constrain} 包下定义的spel约束。 15 | *

16 | * 注意:该注解需要配合 {@link jakarta.validation.Valid} 或 {@link org.springframework.validation.annotation.Validated} 注解一起使用。 17 | *

18 | * 这种行为是非递归应用的,只对当前标记对象的属性生效,不会对其属性的下层属性进行验证。 19 | *

20 | * 以下是一个简单的例子: 21 | *

{@code
22 |  *   @PostMapping("/ test")
23 |  *   public void test(@RequestBody @Valid TestParamVo testParamVo) {
24 |  *      ...
25 |  *   }
26 |  *
27 |  *   @Data
28 |  *   @SpelValid
29 |  *   public class TestParamVo {
30 |  *
31 |  *       private Boolean switchVoice;
32 |  *
33 |  *       @SpelNotNull(condition = "#this. switchVoice == true")
34 |  *       private Object voiceContent;
35 |  *
36 |  *       @Valid
37 |  *       @SpelValid
38 |  *       private TestParamVo2 testParamVo2;
39 |  *
40 |  *   }
41 |  *
42 |  *   @Data
43 |  *   public class TestParamVo2 {
44 |  *
45 |  *      @SpelNotNull(condition = "true")
46 |  *      private Object object;
47 |  *
48 |  *   }
49 |  *
50 |  * }
51 | *

52 | * 在上面的例子中,{@code TestParamVo} 和 {@code TestParamVo2} 都成功开启了spel校验。 53 | * 54 | * @author 阿杆 55 | * @version 1.0 56 | * @since 2024/4/11 57 | */ 58 | @Documented 59 | @Retention(RUNTIME) 60 | @Target({METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE, TYPE}) 61 | @Constraint(validatedBy = {SpelValidator.class}) 62 | public @interface SpelValid { 63 | 64 | /** 65 | * 开启校验的前置条件,值必须为合法的 spel 表达式 66 | *

67 | * 当 表达式为空 或 计算结果为true 时,表示开启校验 68 | */ 69 | @Language("SpEL") 70 | String condition() default ""; 71 | 72 | /** 73 | * 分组功能,值必须为合法的 spel 表达式 74 | *

75 | * 当分组信息为空时,表示不开启分组校验 76 | */ 77 | @Language("SpEL") 78 | String[] spelGroups() default {}; 79 | 80 | String message() default ""; 81 | 82 | Class[] groups() default {}; 83 | 84 | Class[] payload() default {}; 85 | 86 | } 87 | -------------------------------------------------------------------------------- /spel-validator-javax/src/main/java/cn/sticki/spel/validator/javax/SpelValid.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.javax; 2 | 3 | import org.intellij.lang.annotations.Language; 4 | 5 | import javax.validation.Constraint; 6 | import javax.validation.constraints.NotNull; 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.Target; 10 | 11 | import static java.lang.annotation.ElementType.*; 12 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 13 | 14 | /** 15 | * 标记开启spel约束注解的验证,其功能类似于 {@link javax.validation.Valid},用于开启 {@link cn.sticki.spel.validator.core.constrain} 包下定义的spel约束。 16 | *

17 | * 注意:该注解需要配合 {@link javax.validation.Valid} 或 {@link org.springframework.validation.annotation.Validated} 注解一起使用。 18 | *

19 | * 这种行为是非递归应用的,只对当前标记对象的属性生效,不会对其属性的下层属性进行验证。 20 | *

21 | * 以下是一个简单的例子: 22 | *

{@code
23 |  *   @PostMapping("/ test")
24 |  *   public void test(@RequestBody @Valid TestParamVo testParamVo) {
25 |  *      ...
26 |  *   }
27 |  *
28 |  *   @Data
29 |  *   @SpelValid
30 |  *   public class TestParamVo {
31 |  *
32 |  *       private Boolean switchVoice;
33 |  *
34 |  *       @SpelNotNull(condition = "#this. switchVoice == true")
35 |  *       private Object voiceContent;
36 |  *
37 |  *       @Valid
38 |  *       @SpelValid
39 |  *       private TestParamVo2 testParamVo2;
40 |  *
41 |  *   }
42 |  *
43 |  *   @Data
44 |  *   public class TestParamVo2 {
45 |  *
46 |  *      @SpelNotNull(condition = "true")
47 |  *      private Object object;
48 |  *
49 |  *   }
50 |  *
51 |  * }
52 | *

53 | * 在上面的例子中,{@code TestParamVo} 和 {@code TestParamVo2} 都成功开启了spel校验。 54 | * 55 | * @author 阿杆 56 | * @version 1.0 57 | * @since 2024/4/11 58 | */ 59 | @Documented 60 | @Retention(RUNTIME) 61 | @Target({METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE}) 62 | @Constraint(validatedBy = {SpelValidator.class}) 63 | public @interface SpelValid { 64 | 65 | /** 66 | * 开启校验的前置条件,值必须为合法的 spel 表达式 67 | *

68 | * 当 表达式为空 或 计算结果为true 时,表示开启校验 69 | */ 70 | @Language("SpEL") 71 | String condition() default ""; 72 | 73 | /** 74 | * 分组功能,值必须为合法的 spel 表达式 75 | *

76 | * 当分组信息为空时,表示不开启分组校验 77 | */ 78 | @Language("SpEL") 79 | String[] spelGroups() default {}; 80 | 81 | @NotNull 82 | String message() default ""; 83 | 84 | Class[] groups() default {}; 85 | 86 | Class[] payload() default {}; 87 | 88 | } 89 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/test/java/cn/sticki/spel/validator/constrain/MessageInterpolatorTest.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constrain; 2 | 3 | import cn.sticki.spel.validator.core.message.ResourceBundleMessageResolver; 4 | import cn.sticki.spel.validator.core.message.ValidatorMessageInterpolator; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.Locale; 9 | 10 | /** 11 | * 消息插值器测试 12 | * 13 | * @author 阿杆 14 | * @since 2025/4/10 15 | */ 16 | public class MessageInterpolatorTest { 17 | 18 | private final ValidatorMessageInterpolator messageInterpolator = new ValidatorMessageInterpolator(); 19 | 20 | @Test 21 | void testEscapedLiterals() { 22 | ResourceBundleMessageResolver.addBasenames("testMessages"); 23 | 24 | String message = "{test.info}"; 25 | String interpolate = messageInterpolator.interpolate(message, Locale.getDefault()); 26 | Assertions.assertEquals("Test info", interpolate); 27 | 28 | String originalMessage = "{test.info}"; 29 | message = "\\{test.info}"; 30 | interpolate = messageInterpolator.interpolate(message, Locale.getDefault()); 31 | Assertions.assertEquals(originalMessage, interpolate); 32 | 33 | message = "\\{test.info\\}"; 34 | interpolate = messageInterpolator.interpolate(message, Locale.getDefault()); 35 | Assertions.assertEquals(originalMessage, interpolate); 36 | 37 | message = "{test.info}\\\\"; 38 | interpolate = messageInterpolator.interpolate(message, Locale.getDefault()); 39 | Assertions.assertEquals(originalMessage + "\\", interpolate); 40 | 41 | ResourceBundleMessageResolver.resetBasenames(); 42 | } 43 | 44 | @Test 45 | void testSequence() { 46 | ResourceBundleMessageResolver.addBasenames("testMessages"); 47 | String message = "{cn.sticki.spel.validator.constraint.Size.message}"; 48 | String interpolate = messageInterpolator.interpolate(message, Locale.getDefault(), 1, 2); 49 | Assertions.assertEquals("size must be between 1 and 2 (test)", interpolate); 50 | 51 | ResourceBundleMessageResolver.addBasenames("testMessages2"); 52 | interpolate = messageInterpolator.interpolate(message, Locale.getDefault(), 1, 2); 53 | Assertions.assertEquals("size must be between 1 and 2 (test2)", interpolate); 54 | 55 | ResourceBundleMessageResolver.resetBasenames(); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_mn_MN.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = \u0425\u0443\u0434\u0430\u043B \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = \u04AE\u043D\u044D\u043D \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439 3 | cn.sticki.spel.validator.constraint.Email.message = \u0411\u0443\u0440\u0443\u0443 \u0438-\u043C\u044D\u0439\u043B \u0445\u0430\u044F\u0433 \u0431\u0430\u0439\u043D\u0430 4 | cn.sticki.spel.validator.constraint.Future.message = \u0418\u0440\u044D\u044D\u0434\u04AF\u0439\u0434 \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439 5 | cn.sticki.spel.validator.constraint.Max.message = {0}-\u0430\u0430\u0441 \u0431\u0430\u0433\u0430 \u0431\u0443\u044E\u0443 \u0442\u044D\u043D\u0446\u04AF\u04AF \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439 6 | cn.sticki.spel.validator.constraint.Min.message = {0}-\u0430\u0430\u0441 \u0438\u0445 \u0431\u0443\u044E\u0443 \u0442\u044D\u043D\u0446\u04AF\u04AF \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439 7 | cn.sticki.spel.validator.constraint.NotBlank.message = \u0425\u043E\u043E\u0441\u043E\u043D \u0431\u0430\u0439\u0436 \u0431\u043E\u043B\u043E\u0445\u0433\u04AF\u0439 8 | cn.sticki.spel.validator.constraint.NotEmpty.message = \u0425\u043E\u043E\u0441\u043E\u043D \u0431\u0430\u0439\u0436 \u0431\u043E\u043B\u043E\u0445\u0433\u04AF\u0439 9 | cn.sticki.spel.validator.constraint.NotNull.message = null \u0431\u0430\u0439\u0436 \u0431\u043E\u043B\u043E\u0445\u0433\u04AF\u0439 10 | cn.sticki.spel.validator.constraint.Null.message = null \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439 11 | cn.sticki.spel.validator.constraint.Past.message = \u04E8\u043D\u0433\u04E9\u0440\u0441\u04E9\u043D\u0434 \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439 12 | cn.sticki.spel.validator.constraint.Pattern.message = "{0}"-\u0434 \u0442\u0430\u0430\u0440\u0430\u0445 \u0451\u0441\u0442\u043E\u0439 13 | cn.sticki.spel.validator.constraint.Size.message = \u0425\u044D\u043C\u0436\u044D\u044D {0}-\u0441 {1} \u0445\u043E\u043E\u0440\u043E\u043D\u0434 \u0431\u0430\u0439\u043D\u0430 14 | cn.sticki.spel.validator.constraint.Digits.message = \u0422\u043E\u043E\u043D \u0445\u044F\u0437\u0433\u0430\u0430\u0440\u0430\u0430\u0441 \u0445\u044D\u0442\u044D\u0440\u0441\u044D\u043D \u0431\u0430\u0439\u043D\u0430 (<{0} digits>.<{1} digits> \u0445\u043E\u043E\u0440\u043E\u043D\u0434 \u0431\u0430\u0439\u043D\u0430) 15 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_ko.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = false\uC5EC\uC57C \uD569\uB2C8\uB2E4 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = true\uC5EC\uC57C \uD569\uB2C8\uB2E4 3 | cn.sticki.spel.validator.constraint.Email.message = \uC62C\uBC14\uB978 \uD615\uC2DD\uC758 \uC774\uBA54\uC77C \uC8FC\uC18C\uC5EC\uC57C \uD569\uB2C8\uB2E4 4 | cn.sticki.spel.validator.constraint.Future.message = \uBBF8\uB798 \uB0A0\uC9DC\uC5EC\uC57C \uD569\uB2C8\uB2E4 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = \uD604\uC7AC \uB610\uB294 \uBBF8\uB798\uC758 \uB0A0\uC9DC\uC5EC\uC57C \uD569\uB2C8\uB2E4 6 | cn.sticki.spel.validator.constraint.Max.message = {0} \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4 7 | cn.sticki.spel.validator.constraint.Min.message = {0} \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4 8 | cn.sticki.spel.validator.constraint.Negative.message = 0 \uBBF8\uB9CC\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = 0 \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4 10 | cn.sticki.spel.validator.constraint.NotBlank.message = \uACF5\uBC31\uC77C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = \uBE44\uC5B4 \uC788\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 12 | cn.sticki.spel.validator.constraint.NotNull.message = \uB110\uC774\uC5B4\uC11C\uB294 \uC548\uB429\uB2C8\uB2E4 13 | cn.sticki.spel.validator.constraint.Null.message = \uB110\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4 14 | cn.sticki.spel.validator.constraint.Past.message = \uACFC\uAC70 \uB0A0\uC9DC\uC5EC\uC57C \uD569\uB2C8\uB2E4 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = \uACFC\uAC70 \uB610\uB294 \uD604\uC7AC\uC758 \uB0A0\uC9DC\uC5EC\uC57C \uD569\uB2C8\uB2E4 16 | cn.sticki.spel.validator.constraint.Pattern.message = "{0}"\uC640 \uC77C\uCE58\uD574\uC57C \uD569\uB2C8\uB2E4 17 | cn.sticki.spel.validator.constraint.Positive.message = 0\uBCF4\uB2E4 \uCEE4\uC57C \uD569\uB2C8\uB2E4 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = 0 \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4 19 | cn.sticki.spel.validator.constraint.Size.message = \uD06C\uAE30\uAC00 {0}\uC5D0\uC11C {1} \uC0AC\uC774\uC5EC\uC57C \uD569\uB2C8\uB2E4 20 | cn.sticki.spel.validator.constraint.Digits.message = \uc22b\uc790 \uac12\uc774 \ud55c\uacc4\ub97c \ucd08\uacfc\ud569\ub2c8\ub2e4(<{0} \uc790\ub9ac>.<{1} \uc790\ub9ac> \uc608\uc0c1) 21 | -------------------------------------------------------------------------------- /spel-validator-core/src/test/java/cn/sticki/spel/validator/core/SpelValidExecutorTest.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core; 2 | 3 | import cn.sticki.spel.validator.core.constraint.SpelNotNullTest; 4 | import cn.sticki.spel.validator.core.result.FieldValidResult; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.lang.annotation.Documented; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | import java.lang.reflect.Field; 12 | 13 | import static java.lang.annotation.ElementType.FIELD; 14 | import static java.lang.annotation.ElementType.TYPE; 15 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 16 | 17 | /** 18 | * SpelValidExecutor 测试 19 | * 20 | * @author 阿杆 21 | * @since 2024/11/4 22 | */ 23 | public class SpelValidExecutorTest { 24 | 25 | @Documented 26 | @Retention(RUNTIME) 27 | @Target({FIELD, TYPE}) 28 | @SpelConstraint(validatedBy = TestValidator.class) 29 | @interface TestAnno1 { 30 | } 31 | 32 | @Documented 33 | @Retention(RUNTIME) 34 | @Target({FIELD, TYPE}) 35 | @SpelConstraint(validatedBy = TestValidator.class) 36 | @interface TestAnno2 { 37 | 38 | String message() default "必须小于或等于 {value}"; 39 | 40 | } 41 | 42 | @Documented 43 | @Retention(RUNTIME) 44 | @Target({FIELD, TYPE}) 45 | @SpelConstraint(validatedBy = TestValidator.class) 46 | @interface TestAnno3 { 47 | 48 | String message() default "必须小于或等于 {value}"; 49 | 50 | String condition() default ""; 51 | 52 | } 53 | 54 | static class TestValidator implements SpelConstraintValidator { 55 | 56 | @Override 57 | public FieldValidResult isValid(SpelNotNullTest annotation, Object obj, Field field) throws IllegalAccessException { 58 | return null; 59 | } 60 | 61 | } 62 | 63 | static class TestClass1 { 64 | 65 | @TestAnno1 66 | private String name; 67 | 68 | } 69 | 70 | static class TestClass2 { 71 | 72 | @TestAnno2 73 | private String name; 74 | 75 | } 76 | 77 | static class TestClass3 { 78 | 79 | @TestAnno3 80 | private String name; 81 | 82 | } 83 | 84 | @Test 85 | void test() { 86 | Assertions.assertTrue(SpelValidExecutor.validateObject(new TestClass1()).noneError()); 87 | Assertions.assertTrue(SpelValidExecutor.validateObject(new TestClass2()).noneError()); 88 | Assertions.assertTrue(SpelValidExecutor.validateObject(new TestClass3()).noneError()); 89 | } 90 | 91 | } 92 | 93 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/message/ValidatorMessageInterpolator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.message; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | import java.util.Locale; 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | /** 10 | * 验证器消息解析器 11 | * 12 | * @author 阿杆 13 | * @since 2025/4/9 14 | */ 15 | @Slf4j 16 | public class ValidatorMessageInterpolator { 17 | 18 | private static final Pattern LEFT_BRACE = Pattern.compile("\\{", Pattern.LITERAL); 19 | 20 | private static final Pattern RIGHT_BRACE = Pattern.compile("\\}", Pattern.LITERAL); 21 | 22 | private static final Pattern SLASH = Pattern.compile("\\\\", Pattern.LITERAL); 23 | 24 | // private static final Pattern DOLLAR = Pattern.compile("\\$", Pattern.LITERAL); 25 | 26 | /** 27 | * 解析消息中的key,并从资源包中获取对应的多语言消息 28 | * 29 | * @return the interpolated message. 30 | */ 31 | public String interpolate(String message, Locale locale, Object... args) { 32 | return interpolateMessage(message, locale, args); 33 | } 34 | 35 | private String interpolateMessage(String message, Locale locale, Object... args) { 36 | if (message.indexOf('{') < 0) { 37 | return replaceEscapedLiterals(message); 38 | } 39 | 40 | String resolvedMessage = resolveMessage(message, locale, args); 41 | resolvedMessage = replaceEscapedLiterals(resolvedMessage); 42 | 43 | return resolvedMessage; 44 | } 45 | 46 | private String resolveMessage(String message, Locale locale, Object... args) { 47 | if (message.charAt(0) != '{' || message.charAt(message.length() - 1) != '}') { 48 | return message; 49 | } else { 50 | // get key from message 51 | String key = message.substring(1, message.length() - 1); 52 | // get message from resource bundle 53 | return ResourceBundleMessageResolver.getMessage(key, locale, args); 54 | } 55 | } 56 | 57 | private String replaceEscapedLiterals(String resolvedMessage) { 58 | if (resolvedMessage.indexOf('\\') > -1) { 59 | resolvedMessage = LEFT_BRACE.matcher(resolvedMessage).replaceAll("{"); 60 | resolvedMessage = RIGHT_BRACE.matcher(resolvedMessage).replaceAll("}"); 61 | resolvedMessage = SLASH.matcher(resolvedMessage).replaceAll(Matcher.quoteReplacement("\\")); 62 | // resolvedMessage = DOLLAR.matcher(resolvedMessage).replaceAll(Matcher.quoteReplacement("$")); 63 | } 64 | return resolvedMessage; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /spel-validator-javax/src/main/java/cn/sticki/spel/validator/javax/SpelValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.javax; 2 | 3 | import cn.sticki.spel.validator.core.SpelValidContext; 4 | import cn.sticki.spel.validator.core.SpelValidExecutor; 5 | import cn.sticki.spel.validator.core.parse.SpelParser; 6 | import cn.sticki.spel.validator.core.result.FieldError; 7 | import cn.sticki.spel.validator.core.result.ObjectValidResult; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.context.i18n.LocaleContextHolder; 10 | 11 | import javax.validation.ConstraintValidator; 12 | import javax.validation.ConstraintValidatorContext; 13 | 14 | /** 15 | * {@link SpelValid} 的实际校验器 16 | * 17 | * @author 阿杆 18 | * @version 1.0 19 | * @since 2024/4/11 20 | */ 21 | @Slf4j 22 | public class SpelValidator implements ConstraintValidator { 23 | 24 | private SpelValid spelValid; 25 | 26 | @Override 27 | public void initialize(SpelValid constraintAnnotation) { 28 | this.spelValid = constraintAnnotation; 29 | } 30 | 31 | @Override 32 | public boolean isValid(Object value, ConstraintValidatorContext context) { 33 | if (value == null) { 34 | return true; 35 | } 36 | 37 | // 表达式不为空且计算结果为 false,跳过校验 38 | if (!spelValid.condition().isEmpty() && !SpelParser.parse(spelValid.condition(), value, Boolean.class)) { 39 | log.debug("SpelValid condition is not satisfied, skip validation, condition: {}", spelValid.condition()); 40 | return true; 41 | } 42 | 43 | // 构建上下文 44 | SpelValidContext spelValidContext = SpelValidContext.builder() 45 | .locale(LocaleContextHolder.getLocale()) 46 | .build(); 47 | 48 | // 校验对象 49 | ObjectValidResult validateObjectResult = SpelValidExecutor.validateObject(value, spelValid.spelGroups(), spelValidContext); 50 | 51 | // 构建错误信息 52 | buildConstraintViolation(validateObjectResult, context); 53 | return validateObjectResult.noneError(); 54 | } 55 | 56 | /** 57 | * 生成错误信息并将其添加到验证上下文 58 | */ 59 | private void buildConstraintViolation(ObjectValidResult validateObjectResult, ConstraintValidatorContext context) { 60 | if (validateObjectResult.noneError()) { 61 | return; 62 | } 63 | context.disableDefaultConstraintViolation(); 64 | for (FieldError error : validateObjectResult.getErrors()) { 65 | context.buildConstraintViolationWithTemplate(error.getErrorMessage()).addPropertyNode(error.getFieldName()).addConstraintViolation(); 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /spel-validator-jakarta/src/main/java/cn/sticki/spel/validator/jakarta/SpelValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.jakarta; 2 | 3 | import cn.sticki.spel.validator.core.SpelValidContext; 4 | import cn.sticki.spel.validator.core.SpelValidExecutor; 5 | import cn.sticki.spel.validator.core.parse.SpelParser; 6 | import cn.sticki.spel.validator.core.result.FieldError; 7 | import cn.sticki.spel.validator.core.result.ObjectValidResult; 8 | import jakarta.validation.ConstraintValidator; 9 | import jakarta.validation.ConstraintValidatorContext; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.context.i18n.LocaleContextHolder; 12 | 13 | /** 14 | * {@link SpelValid} 的实际校验器 15 | * 16 | * @author 阿杆 17 | * @version 1.0 18 | * @since 2024/4/11 19 | */ 20 | @Slf4j 21 | public class SpelValidator implements ConstraintValidator { 22 | 23 | private SpelValid spelValid; 24 | 25 | @Override 26 | public void initialize(SpelValid constraintAnnotation) { 27 | this.spelValid = constraintAnnotation; 28 | } 29 | 30 | @Override 31 | public boolean isValid(Object value, ConstraintValidatorContext context) { 32 | if (value == null) { 33 | return true; 34 | } 35 | 36 | // 表达式不为空且计算结果为 false,跳过校验 37 | if (!spelValid.condition().isEmpty() && !SpelParser.parse(spelValid.condition(), value, Boolean.class)) { 38 | log.debug("SpelValid condition is not satisfied, skip validation, condition: {}", spelValid.condition()); 39 | return true; 40 | } 41 | 42 | // 构建上下文 43 | SpelValidContext spelValidContext = SpelValidContext.builder() 44 | .locale(LocaleContextHolder.getLocale()) 45 | .build(); 46 | 47 | // 校验对象 48 | ObjectValidResult validateObjectResult = SpelValidExecutor.validateObject(value, spelValid.spelGroups(), spelValidContext); 49 | 50 | // 构建错误信息 51 | buildConstraintViolation(validateObjectResult, context); 52 | return validateObjectResult.noneError(); 53 | } 54 | 55 | /** 56 | * 生成错误信息并将其添加到验证上下文 57 | */ 58 | private void buildConstraintViolation(ObjectValidResult validateObjectResult, ConstraintValidatorContext context) { 59 | if (validateObjectResult.noneError()) { 60 | return; 61 | } 62 | context.disableDefaultConstraintViolation(); 63 | for (FieldError error : validateObjectResult.getErrors()) { 64 | context.buildConstraintViolationWithTemplate(error.getErrorMessage()).addPropertyNode(error.getFieldName()).addConstraintViolation(); 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_ar.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = \u064A\u062C\u0628 \u0623\u0646 \u062A\u0643\u0648\u0646 \u062E\u0627\u0637\u0626\u0629 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = \u064A\u062C\u0628 \u0623\u0646 \u062A\u0643\u0648\u0646 \u0635\u062D\u064A\u062D\u0629 3 | cn.sticki.spel.validator.constraint.Email.message = \u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0628\u0631\u064A\u062F \u0627\u0644\u0625\u0644\u0643\u062A\u0631\u0648\u0646\u064A \u063A\u064A\u0631 \u0645\u0631\u0643\u0628 \u0628\u0634\u0643\u0644 \u062C\u064A\u062F 4 | cn.sticki.spel.validator.constraint.Future.message = \u064A\u062C\u0628 \u0623\u0646 \u062A\u0643\u0648\u0646 \u0641\u064A \u0627\u0644\u0645\u0633\u062A\u0642\u0628\u0644 5 | cn.sticki.spel.validator.constraint.Max.message = \u064A\u062C\u0628 \u0623\u0646 \u062A\u0643\u0648\u0646 \u0623\u0642\u0644 \u0623\u0648 \u0645\u0633\u0627\u0648\u064A\u0629 \u0644 {0} 6 | cn.sticki.spel.validator.constraint.Min.message = \u064A\u062C\u0628 \u0623\u0646 \u062A\u0643\u0648\u0646 \u0623\u0643\u0628\u0631 \u0645\u0646 \u0623\u0648 \u0645\u0633\u0627\u0648\u064A\u0629 \u0644 {0} 7 | cn.sticki.spel.validator.constraint.NotBlank.message = \u0644\u0627 \u064A\u0645\u0643\u0646 \u0623\u0646 \u062A\u0643\u0648\u0646 \u0641\u0627\u0631\u063A\u0629 8 | cn.sticki.spel.validator.constraint.NotEmpty.message = \u0644\u0627 \u064A\u0645\u0643\u0646 \u0623\u0646 \u062A\u0643\u0648\u0646 \u0641\u0627\u0631\u063A\u0629 9 | cn.sticki.spel.validator.constraint.NotNull.message = \u0644\u0627 \u064A\u0645\u0643\u0646 \u0623\u0646 \u064A\u0643\u0648\u0646 \u0645\u0646\u0639\u062F\u0645 10 | cn.sticki.spel.validator.constraint.Null.message = \u064A\u062C\u0628 \u0623\u0646 \u064A\u0643\u0648\u0646 \u0645\u0646\u0639\u062F\u0645 11 | cn.sticki.spel.validator.constraint.Past.message = \u064A\u062C\u0628 \u0623\u0646 \u064A\u0643\u0648\u0646 \u0641\u064A \u0627\u0644\u0645\u0627\u0636\u064A 12 | cn.sticki.spel.validator.constraint.Pattern.message = \u064A\u062C\u0628 \u0623\u0646 \u064A\u062A\u0637\u0627\u0628\u0642 \u0645\u0639 "{0}" 13 | cn.sticki.spel.validator.constraint.Size.message = \u064A\u062C\u0628 \u0623\u0646 \u064A\u0643\u0648\u0646 \u0627\u0644\u062D\u062C\u0645 \u0628\u064A\u0646 {0} \u0648{1} 14 | cn.sticki.spel.validator.constraint.Digits.message = \u0627\u0644\u0642\u064A\u0645\u0629 \u0627\u0644\u0631\u0642\u0645\u064A\u0629 \u062E\u0627\u0631\u062C \u0627\u0644\u062D\u062F\u0648\u062F (<{0} \u0623\u0631\u0642\u0627\u0645>. <{1} \u0623\u0631\u0642\u0627\u0645> \u0645\u062A\u0648\u0642\u0639\u0629) 15 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constrain/SpelPast.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constrain; 2 | 3 | import cn.sticki.spel.validator.constraintvalidator.SpelPastValidator; 4 | import cn.sticki.spel.validator.core.SpelConstraint; 5 | import org.intellij.lang.annotations.Language; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Repeatable; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | import static java.lang.annotation.ElementType.FIELD; 13 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 14 | 15 | /** 16 | * 被标记的元素必须是一个过去的时间。{@code null} 元素被认为是有效的。 17 | *

18 | * 支持的类型有: 19 | *

    20 | *
  • {@link java.util.Date}
  • 21 | *
  • {@link java.util.Calendar}
  • 22 | *
  • {@link java.time.Instant}
  • 23 | *
  • {@link java.time.LocalDate}
  • 24 | *
  • {@link java.time.LocalDateTime}
  • 25 | *
  • {@link java.time.LocalTime}
  • 26 | *
  • {@link java.time.MonthDay}
  • 27 | *
  • {@link java.time.OffsetDateTime}
  • 28 | *
  • {@link java.time.OffsetTime}
  • 29 | *
  • {@link java.time.Year}
  • 30 | *
  • {@link java.time.YearMonth}
  • 31 | *
  • {@link java.time.ZonedDateTime}
  • 32 | *
  • {@link java.time.chrono.HijrahDate}
  • 33 | *
  • {@link java.time.chrono.JapaneseDate}
  • 34 | *
  • {@link java.time.chrono.MinguoDate}
  • 35 | *
  • {@link java.time.chrono.ThaiBuddhistDate}
  • 36 | *
37 | * 38 | * @author 阿杆 39 | * @version 1.0 40 | * @since 2025/07/20 41 | */ 42 | @Documented 43 | @Retention(RUNTIME) 44 | @Target(FIELD) 45 | @Repeatable(SpelPast.List.class) 46 | @SpelConstraint(validatedBy = SpelPastValidator.class) 47 | public @interface SpelPast { 48 | 49 | /** 50 | * 校验失败时的错误消息 51 | */ 52 | String message() default "{cn.sticki.spel.validator.constraint.Past.message}"; 53 | 54 | /** 55 | * 约束开启条件,必须为合法的SpEL表达式,计算结果必须为boolean类型。 56 | *

57 | * 当 表达式为空 或 计算结果为true 时,会对带注解的元素进行校验。 58 | *

59 | * 默认情况下,开启校验。 60 | */ 61 | @Language("SpEL") 62 | String condition() default ""; 63 | 64 | /** 65 | * 分组条件,必须为合法的SpEL表达式。 66 | *

67 | * 当分组信息不为空时,只有当 {@link SpelValid#spelGroups()} 中的分组信息与此处的分组信息有交集时,才会对带注解的元素进行校验。 68 | *

69 | * 其计算结果可以是任何类型,但只有两个计算结果完全相等时,才被认为是相等的。 70 | */ 71 | @Language("SpEL") 72 | String[] group() default {}; 73 | 74 | /** 75 | * 在同一元素上定义多个注解。 76 | * 77 | * @see SpelPast 78 | */ 79 | @Target(FIELD) 80 | @Retention(RUNTIME) 81 | @Documented 82 | @interface List { 83 | 84 | SpelPast[] value(); 85 | 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constrain/SpelFuture.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constrain; 2 | 3 | import cn.sticki.spel.validator.constraintvalidator.SpelFutureValidator; 4 | import cn.sticki.spel.validator.core.SpelConstraint; 5 | import org.intellij.lang.annotations.Language; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Repeatable; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | import static java.lang.annotation.ElementType.FIELD; 13 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 14 | 15 | /** 16 | * 被标记的元素必须是一个将来的时间。{@code null} 元素被认为是有效的。 17 | *

18 | * 支持的类型有: 19 | *

    20 | *
  • {@link java.util.Date}
  • 21 | *
  • {@link java.util.Calendar}
  • 22 | *
  • {@link java.time.Instant}
  • 23 | *
  • {@link java.time.LocalDate}
  • 24 | *
  • {@link java.time.LocalDateTime}
  • 25 | *
  • {@link java.time.LocalTime}
  • 26 | *
  • {@link java.time.MonthDay}
  • 27 | *
  • {@link java.time.OffsetDateTime}
  • 28 | *
  • {@link java.time.OffsetTime}
  • 29 | *
  • {@link java.time.Year}
  • 30 | *
  • {@link java.time.YearMonth}
  • 31 | *
  • {@link java.time.ZonedDateTime}
  • 32 | *
  • {@link java.time.chrono.HijrahDate}
  • 33 | *
  • {@link java.time.chrono.JapaneseDate}
  • 34 | *
  • {@link java.time.chrono.MinguoDate}
  • 35 | *
  • {@link java.time.chrono.ThaiBuddhistDate}
  • 36 | *
37 | * 38 | * @author 阿杆 39 | * @version 1.0 40 | * @since 2025/07/20 41 | */ 42 | @Documented 43 | @Retention(RUNTIME) 44 | @Target(FIELD) 45 | @Repeatable(SpelFuture.List.class) 46 | @SpelConstraint(validatedBy = SpelFutureValidator.class) 47 | public @interface SpelFuture { 48 | 49 | /** 50 | * 校验失败时的错误消息 51 | */ 52 | String message() default "{cn.sticki.spel.validator.constraint.Future.message}"; 53 | 54 | /** 55 | * 约束开启条件,必须为合法的SpEL表达式,计算结果必须为boolean类型。 56 | *

57 | * 当 表达式为空 或 计算结果为true 时,会对带注解的元素进行校验。 58 | *

59 | * 默认情况下,开启校验。 60 | */ 61 | @Language("SpEL") 62 | String condition() default ""; 63 | 64 | /** 65 | * 分组条件,必须为合法的SpEL表达式。 66 | *

67 | * 当分组信息不为空时,只有当 {@link SpelValid#spelGroups()} 中的分组信息与此处的分组信息有交集时,才会对带注解的元素进行校验。 68 | *

69 | * 其计算结果可以是任何类型,但只有两个计算结果完全相等时,才被认为是相等的。 70 | */ 71 | @Language("SpEL") 72 | String[] group() default {}; 73 | 74 | /** 75 | * 在同一元素上定义多个注解。 76 | * 77 | * @see SpelFuture 78 | */ 79 | @Target(FIELD) 80 | @Retention(RUNTIME) 81 | @Documented 82 | @interface List { 83 | 84 | SpelFuture[] value(); 85 | 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constrain/SpelPastOrPresent.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constrain; 2 | 3 | import cn.sticki.spel.validator.constraintvalidator.SpelPastOrPresentValidator; 4 | import cn.sticki.spel.validator.core.SpelConstraint; 5 | import org.intellij.lang.annotations.Language; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Repeatable; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | import static java.lang.annotation.ElementType.FIELD; 13 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 14 | 15 | /** 16 | * 被标记的元素必须是一个过去或现在的时间。{@code null} 元素被认为是有效的。 17 | *

18 | * 支持的类型有: 19 | *

    20 | *
  • {@link java.util.Date}
  • 21 | *
  • {@link java.util.Calendar}
  • 22 | *
  • {@link java.time.Instant}
  • 23 | *
  • {@link java.time.LocalDate}
  • 24 | *
  • {@link java.time.LocalDateTime}
  • 25 | *
  • {@link java.time.LocalTime}
  • 26 | *
  • {@link java.time.MonthDay}
  • 27 | *
  • {@link java.time.OffsetDateTime}
  • 28 | *
  • {@link java.time.OffsetTime}
  • 29 | *
  • {@link java.time.Year}
  • 30 | *
  • {@link java.time.YearMonth}
  • 31 | *
  • {@link java.time.ZonedDateTime}
  • 32 | *
  • {@link java.time.chrono.HijrahDate}
  • 33 | *
  • {@link java.time.chrono.JapaneseDate}
  • 34 | *
  • {@link java.time.chrono.MinguoDate}
  • 35 | *
  • {@link java.time.chrono.ThaiBuddhistDate}
  • 36 | *
37 | * 38 | * @author 阿杆 39 | * @version 1.0 40 | * @since 2025/07/20 41 | */ 42 | @Documented 43 | @Retention(RUNTIME) 44 | @Target(FIELD) 45 | @Repeatable(SpelPastOrPresent.List.class) 46 | @SpelConstraint(validatedBy = SpelPastOrPresentValidator.class) 47 | public @interface SpelPastOrPresent { 48 | 49 | /** 50 | * 校验失败时的错误消息 51 | */ 52 | String message() default "{cn.sticki.spel.validator.constraint.PastOrPresent.message}"; 53 | 54 | /** 55 | * 约束开启条件,必须为合法的SpEL表达式,计算结果必须为boolean类型。 56 | *

57 | * 当 表达式为空 或 计算结果为true 时,会对带注解的元素进行校验。 58 | *

59 | * 默认情况下,开启校验。 60 | */ 61 | @Language("SpEL") 62 | String condition() default ""; 63 | 64 | /** 65 | * 分组条件,必须为合法的SpEL表达式。 66 | *

67 | * 当分组信息不为空时,只有当 {@link SpelValid#spelGroups()} 中的分组信息与此处的分组信息有交集时,才会对带注解的元素进行校验。 68 | *

69 | * 其计算结果可以是任何类型,但只有两个计算结果完全相等时,才被认为是相等的。 70 | */ 71 | @Language("SpEL") 72 | String[] group() default {}; 73 | 74 | /** 75 | * 在同一元素上定义多个注解。 76 | * 77 | * @see SpelPastOrPresent 78 | */ 79 | @Target(FIELD) 80 | @Retention(RUNTIME) 81 | @Documented 82 | @interface List { 83 | 84 | SpelPastOrPresent[] value(); 85 | 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/java/cn/sticki/spel/validator/constrain/SpelFutureOrPresent.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.constrain; 2 | 3 | import cn.sticki.spel.validator.constraintvalidator.SpelFutureOrPresentValidator; 4 | import cn.sticki.spel.validator.core.SpelConstraint; 5 | import org.intellij.lang.annotations.Language; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Repeatable; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | import static java.lang.annotation.ElementType.FIELD; 13 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 14 | 15 | /** 16 | * 被标记的元素必须是一个将来或现在的时间。{@code null} 元素被认为是有效的。 17 | *

18 | * 支持的类型有: 19 | *

    20 | *
  • {@link java.util.Date}
  • 21 | *
  • {@link java.util.Calendar}
  • 22 | *
  • {@link java.time.Instant}
  • 23 | *
  • {@link java.time.LocalDate}
  • 24 | *
  • {@link java.time.LocalDateTime}
  • 25 | *
  • {@link java.time.LocalTime}
  • 26 | *
  • {@link java.time.MonthDay}
  • 27 | *
  • {@link java.time.OffsetDateTime}
  • 28 | *
  • {@link java.time.OffsetTime}
  • 29 | *
  • {@link java.time.Year}
  • 30 | *
  • {@link java.time.YearMonth}
  • 31 | *
  • {@link java.time.ZonedDateTime}
  • 32 | *
  • {@link java.time.chrono.HijrahDate}
  • 33 | *
  • {@link java.time.chrono.JapaneseDate}
  • 34 | *
  • {@link java.time.chrono.MinguoDate}
  • 35 | *
  • {@link java.time.chrono.ThaiBuddhistDate}
  • 36 | *
37 | * 38 | * @author 阿杆 39 | * @version 1.0 40 | * @since 2025/07/20 41 | */ 42 | @Documented 43 | @Retention(RUNTIME) 44 | @Target(FIELD) 45 | @Repeatable(SpelFutureOrPresent.List.class) 46 | @SpelConstraint(validatedBy = SpelFutureOrPresentValidator.class) 47 | public @interface SpelFutureOrPresent { 48 | 49 | /** 50 | * 校验失败时的错误消息 51 | */ 52 | String message() default "{cn.sticki.spel.validator.constraint.FutureOrPresent.message}"; 53 | 54 | /** 55 | * 约束开启条件,必须为合法的SpEL表达式,计算结果必须为boolean类型。 56 | *

57 | * 当 表达式为空 或 计算结果为true 时,会对带注解的元素进行校验。 58 | *

59 | * 默认情况下,开启校验。 60 | */ 61 | @Language("SpEL") 62 | String condition() default ""; 63 | 64 | /** 65 | * 分组条件,必须为合法的SpEL表达式。 66 | *

67 | * 当分组信息不为空时,只有当 {@link SpelValid#spelGroups()} 中的分组信息与此处的分组信息有交集时,才会对带注解的元素进行校验。 68 | *

69 | * 其计算结果可以是任何类型,但只有两个计算结果完全相等时,才被认为是相等的。 70 | */ 71 | @Language("SpEL") 72 | String[] group() default {}; 73 | 74 | /** 75 | * 在同一元素上定义多个注解。 76 | * 77 | * @see SpelFutureOrPresent 78 | */ 79 | @Target(FIELD) 80 | @Retention(RUNTIME) 81 | @Documented 82 | @interface List { 83 | 84 | SpelFutureOrPresent[] value(); 85 | 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /spel-validator-jakarta/src/test/java/cn/sticki/spel/validator/jakarta/JakartaSpelValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.jakarta; 2 | 3 | import cn.sticki.spel.validator.core.SpelValidContext; 4 | import cn.sticki.spel.validator.core.SpelValidExecutor; 5 | import cn.sticki.spel.validator.core.result.FieldError; 6 | import cn.sticki.spel.validator.core.result.ObjectValidResult; 7 | import cn.sticki.spel.validator.test.util.AbstractSpelValidator; 8 | import cn.sticki.spel.validator.test.util.VerifyObject; 9 | import jakarta.validation.ConstraintViolation; 10 | import jakarta.validation.Validation; 11 | import jakarta.validation.Validator; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator; 14 | 15 | import java.util.List; 16 | import java.util.Set; 17 | import java.util.stream.Collectors; 18 | 19 | /** 20 | * 测试验证工具类 21 | * 22 | * @author 阿杆 23 | * @version 1.0 24 | * @since 2024/6/13 25 | */ 26 | @Slf4j 27 | public class JakartaSpelValidator extends AbstractSpelValidator { 28 | 29 | private static final JakartaSpelValidator INSTANCE = new JakartaSpelValidator(); 30 | 31 | @SuppressWarnings("resource") 32 | private static final Validator validator = Validation.byDefaultProvider() 33 | .configure() 34 | .messageInterpolator(new ParameterMessageInterpolator()) 35 | .buildValidatorFactory().getValidator(); 36 | 37 | /** 38 | * 验证约束结果是否符合预期 39 | */ 40 | public static boolean check(List verifyObjectList) { 41 | return INSTANCE.checkConstraintResult(verifyObjectList); 42 | } 43 | 44 | /** 45 | * 参数校验 46 | *

47 | * 调用此方法会触发约束校验 48 | * 49 | * @return 校验结果 50 | */ 51 | @Override 52 | public ObjectValidResult validate(Object obj, String[] spelGroups, SpelValidContext context) { 53 | // 如果对象没有使用 SpelValid 注解,则直接调用验证执行器进行验证 54 | // 这种情况下,只会验证本框架提供的约束注解 55 | if (!obj.getClass().isAnnotationPresent(SpelValid.class)) { 56 | return SpelValidExecutor.validateObject(obj, spelGroups, context); 57 | } 58 | 59 | // 通过 @Valid 的方式进行验证 60 | Set> validate = validator.validate(obj); 61 | if (validate == null || validate.isEmpty()) { 62 | return ObjectValidResult.EMPTY; 63 | } 64 | ObjectValidResult validResult = new ObjectValidResult(); 65 | List list = validate.stream().map(JakartaSpelValidator::convert).collect(Collectors.toList()); 66 | validResult.addFieldError(list); 67 | return validResult; 68 | } 69 | 70 | private static FieldError convert(ConstraintViolation violation) { 71 | return FieldError.of(violation.getPropertyPath().toString(), violation.getMessage()); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /spel-validator-javax/src/test/java/cn/sticki/spel/validator/javax/util/JavaxSpelValidator.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.javax.util; 2 | 3 | import cn.sticki.spel.validator.core.SpelValidContext; 4 | import cn.sticki.spel.validator.core.SpelValidExecutor; 5 | import cn.sticki.spel.validator.core.result.FieldError; 6 | import cn.sticki.spel.validator.core.result.ObjectValidResult; 7 | import cn.sticki.spel.validator.javax.SpelValid; 8 | import cn.sticki.spel.validator.test.util.AbstractSpelValidator; 9 | import cn.sticki.spel.validator.test.util.VerifyObject; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator; 12 | 13 | import javax.validation.ConstraintViolation; 14 | import javax.validation.Validation; 15 | import javax.validation.Validator; 16 | import java.util.List; 17 | import java.util.Set; 18 | import java.util.stream.Collectors; 19 | 20 | /** 21 | * 测试验证工具类 22 | * 23 | * @author 阿杆 24 | * @version 1.0 25 | * @since 2024/6/13 26 | */ 27 | @Slf4j 28 | public class JavaxSpelValidator extends AbstractSpelValidator { 29 | 30 | private static final JavaxSpelValidator INSTANCE = new JavaxSpelValidator(); 31 | 32 | @SuppressWarnings("resource") 33 | private static final Validator validator = Validation.byDefaultProvider() 34 | .configure() 35 | .messageInterpolator(new ParameterMessageInterpolator()) 36 | .buildValidatorFactory().getValidator(); 37 | 38 | /** 39 | * 验证约束结果是否符合预期 40 | */ 41 | public static boolean check(List verifyObjectList) { 42 | return INSTANCE.checkConstraintResult(verifyObjectList); 43 | } 44 | 45 | /** 46 | * 参数校验 47 | *

48 | * 调用此方法会触发约束校验 49 | * 50 | * @return 校验结果 51 | */ 52 | @Override 53 | public ObjectValidResult validate(Object obj, String[] spelGroups, SpelValidContext context) { 54 | // 如果对象没有使用 SpelValid 注解,则直接调用验证执行器进行验证 55 | // 这种情况下,只会验证本框架提供的约束注解 56 | if (!obj.getClass().isAnnotationPresent(SpelValid.class)) { 57 | return SpelValidExecutor.validateObject(obj, spelGroups, context); 58 | } 59 | 60 | // 通过 @Valid 的方式进行验证 61 | Set> validate = validator.validate(obj); 62 | if (validate == null || validate.isEmpty()) { 63 | return ObjectValidResult.EMPTY; 64 | } 65 | ObjectValidResult validResult = new ObjectValidResult(); 66 | List list = validate.stream().map(JavaxSpelValidator::convert).collect(Collectors.toList()); 67 | validResult.addFieldError(list); 68 | return validResult; 69 | } 70 | 71 | private static FieldError convert(ConstraintViolation violation) { 72 | return FieldError.of(violation.getPropertyPath().toString(), violation.getMessage()); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /document/web-docs/docs/guide/custom.md: -------------------------------------------------------------------------------- 1 | # 自定义约束注解 2 | 3 | ::: tip 4 | 如果你使用过 `jakarta.validation-api` 的自定义约束注解,那么你会发现 `SpEL Validator` 的自定义约束注解几乎与 `jakarta.validation-api` 一致。 5 | ::: 6 | 7 | 下面以 `@SpelNotBlank` 为例,展示如何实现自定义约束注解。 8 | 9 | ## 创建约束注解类 10 | 11 | 每个约束注释必须包含以下属性: 12 | - `String message() default "";` 用于指定约束校验失败时的错误消息。 13 | - `String condition() default "";` 用于指定约束开启条件的SpEL表达式。 14 | - `String[] group() default {};` 用于指定分组条件的SpEL表达式。 15 | 16 | ```java 17 | @Documented 18 | @Retention(RetentionPolicy.RUNTIME) 19 | @Target(ElementType.FIELD) 20 | @Repeatable(SpelNotBlank.List.class) 21 | public @interface SpelNotBlank { 22 | 23 | String message() default "不能为空字符串"; 24 | 25 | String condition() default ""; 26 | 27 | String[] group() default {}; 28 | 29 | @Target(FIELD) 30 | @Retention(RUNTIME) 31 | @Documented 32 | @interface List { 33 | 34 | SpelNotBlank[] value(); 35 | 36 | } 37 | 38 | } 39 | ``` 40 | 41 | ## 创建约束验证器 42 | 43 | 创建类 `SpelNotBlankValidator`,实现 `SpelConstraintValidator` 接口,其中泛型 `T` 为要校验的约束注解类,在这里是 `SpelNotBlank`。 44 | 45 | 实现 `isValid` 方法,校验逻辑在该方法中实现。 46 | 47 | `isValid` 方法的参数说明如下: 48 | - `annotation`:当前约束注解的实例。 49 | - `obj`:当前校验的根对象。 50 | - `field`:当前校验的字段。 51 | 52 | ```java 53 | public class SpelNotBlankValidator implements SpelConstraintValidator { 54 | 55 | @Override 56 | public FieldValidResult isValid(SpelNotBlank annotation, Object obj, Field field) throws IllegalAccessException { 57 | CharSequence fieldValue = (CharSequence) field.get(obj); 58 | return FieldValidResult.of(StringUtils.hasText(fieldValue)); 59 | } 60 | 61 | } 62 | ``` 63 | 64 | 一般情况下,只需要校验当前字段的值,通过 `field.get(obj)` 即可获取。 65 | 66 | 有些约束注解可能仅支持特定类型的字段,可以通过重写 `supportType()` 方法来指定支持的类型。默认情况下,支持所有类型。 67 | 68 | ```java 69 | public class SpelNotBlankValidator implements SpelConstraintValidator { 70 | 71 | @Override 72 | public FieldValidResult isValid(SpelNotBlank annotation, Object obj, Field field) throws IllegalAccessException { 73 | CharSequence fieldValue = (CharSequence) field.get(obj); 74 | return FieldValidResult.of(StringUtils.hasText(fieldValue)); 75 | } 76 | 77 | private static final Set> SUPPORT_TYPE = Collections.singleton(CharSequence.class); 78 | 79 | @Override 80 | public Set> supportType() { 81 | return SUPPORT_TYPE; 82 | } 83 | 84 | } 85 | ``` 86 | 87 | ## 关联注解和验证器 88 | 89 | 在 `SpelNotBlank` 注解上添加 `@SpelConstraint` 注解,指定该注解的验证器为 `SpelNotBlankValidator`。 90 | 91 | ```java 92 | @Documented 93 | @Retention(RUNTIME) 94 | @Target(FIELD) 95 | @Repeatable(SpelNotBlank.List.class) 96 | @SpelConstraint(validatedBy = SpelNotBlankValidator.class) // 关联验证器 97 | public @interface SpelNotBlank { 98 | // ... 99 | } 100 | ``` 101 | 102 | ## 使用自定义约束注解 103 | 104 | 完成上面的步骤,就可以在需要校验的字段上使用 `@SpelNotBlank` 注解了,使用方法就和内置的约束注解一样,[使用指南](user-guide.md)。 105 | 106 | 已经大功告成了,这里我就不举例了。 107 | -------------------------------------------------------------------------------- /spel-validator-constrain/src/main/resources/cn/sticki/spel/validator/ValidationMessages_ja.properties: -------------------------------------------------------------------------------- 1 | cn.sticki.spel.validator.constraint.AssertFalse.message = false \u306B\u3057\u3066\u304F\u3060\u3055\u3044 2 | cn.sticki.spel.validator.constraint.AssertTrue.message = true \u306B\u3057\u3066\u304F\u3060\u3055\u3044 3 | cn.sticki.spel.validator.constraint.Email.message = \u96FB\u5B50\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3068\u3057\u3066\u6B63\u3057\u3044\u5F62\u5F0F\u306B\u3057\u3066\u304F\u3060\u3055\u3044 4 | cn.sticki.spel.validator.constraint.Future.message = \u672A\u6765\u306E\u65E5\u4ED8\u306B\u3057\u3066\u304F\u3060\u3055\u3044 5 | cn.sticki.spel.validator.constraint.FutureOrPresent.message = \u73FE\u5728\u3082\u3057\u304F\u306F\u672A\u6765\u306E\u65E5\u4ED8\u306B\u3057\u3066\u304F\u3060\u3055\u3044 6 | cn.sticki.spel.validator.constraint.Max.message = {0} \u4EE5\u4E0B\u306E\u5024\u306B\u3057\u3066\u304F\u3060\u3055\u3044 7 | cn.sticki.spel.validator.constraint.Min.message = {0} \u4EE5\u4E0A\u306E\u5024\u306B\u3057\u3066\u304F\u3060\u3055\u3044 8 | cn.sticki.spel.validator.constraint.Negative.message = 0 \u3088\u308A\u5C0F\u3055\u306A\u5024\u306B\u3057\u3066\u304F\u3060\u3055\u3044 9 | cn.sticki.spel.validator.constraint.NegativeOrZero.message = 0 \u4EE5\u4E0B\u306E\u5024\u306B\u3057\u3066\u304F\u3060\u3055\u3044 10 | cn.sticki.spel.validator.constraint.NotBlank.message = \u7A7A\u767D\u306F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u305B\u3093 11 | cn.sticki.spel.validator.constraint.NotEmpty.message = \u7A7A\u8981\u7D20\u306F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u305B\u3093 12 | cn.sticki.spel.validator.constraint.NotNull.message = null \u306F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u305B\u3093 13 | cn.sticki.spel.validator.constraint.Null.message = null \u306B\u3057\u3066\u304F\u3060\u3055\u3044 14 | cn.sticki.spel.validator.constraint.Past.message = \u904E\u53BB\u306E\u65E5\u4ED8\u306B\u3057\u3066\u304F\u3060\u3055\u3044 15 | cn.sticki.spel.validator.constraint.PastOrPresent.message = \u73FE\u5728\u3082\u3057\u304F\u306F\u904E\u53BB\u306E\u65E5\u4ED8\u306B\u3057\u3066\u304F\u3060\u3055\u3044 16 | cn.sticki.spel.validator.constraint.Pattern.message = \u6B63\u898F\u8868\u73FE "{0}" \u306B\u30DE\u30C3\u30C1\u3055\u305B\u3066\u304F\u3060\u3055\u3044 17 | cn.sticki.spel.validator.constraint.Positive.message = 0 \u3088\u308A\u5927\u304D\u306A\u5024\u306B\u3057\u3066\u304F\u3060\u3055\u3044 18 | cn.sticki.spel.validator.constraint.PositiveOrZero.message = 0 \u4EE5\u4E0A\u306E\u5024\u306B\u3057\u3066\u304F\u3060\u3055\u3044 19 | cn.sticki.spel.validator.constraint.Size.message = {0} \u304B\u3089 {1} \u306E\u9593\u306E\u30B5\u30A4\u30BA\u306B\u3057\u3066\u304F\u3060\u3055\u3044 20 | cn.sticki.spel.validator.constraint.Digits.message = \u5024\u306F\u6B21\u306E\u7BC4\u56F2\u306B\u3057\u3066\u304F\u3060\u3055\u3044 (<\u6574\u6570 {0} \u6841>.<\u5C0F\u6570\u70B9 {1} \u6841>) 21 | -------------------------------------------------------------------------------- /document/web-docs/docs/guide/principle.md: -------------------------------------------------------------------------------- 1 | # 工作原理 2 | 3 | ## 一、调用入口来自 `@Valid` / `@Validated` 4 | 5 | 这是 Java 的标准约束校验机制:只要方法参数上使用了 `@Valid` 或 `@Validated`,Spring 或 Jakarta Validation 会自动识别参数上有哪些字段或类需要校验。 6 | 7 | ```java 8 | 9 | @PostMapping("/submit") 10 | public Resp doSomething(@RequestBody @Valid MyParam param) { 11 | ... 12 | } 13 | ``` 14 | 15 | ## 二、识别到类上标注了 `@SpelValid` 16 | 17 | 这是 **SpEL Validator 的启动注解**,本质上是个 `@Constraint` 注解,会被绑定到一个实现了 `ConstraintValidator` 接口的类: 18 | 19 | ```java 20 | @Constraint(validatedBy = SpelValidator.class) 21 | @Target({TYPE}) 22 | @Retention(RUNTIME) 23 | public @interface SpelValid { 24 | ... 25 | } 26 | ``` 27 | 28 | 所以当 Validator 框架(如 `hibernate-validator`)发现你的类上有 `@SpelValid` 时,就会调用绑定的校验器:`SpelValidator`。 29 | 30 | ## 三、SpelValidator 实际工作流程 31 | 32 | 在 `SpelValidator` 的逻辑中: 33 | 34 | ### 1. 校验条件成立才执行(支持 condition) 35 | 36 | ```java 37 | if(!spelValid.condition().isEmpty() &&SpelParser.parse(spelValid.condition()))return true; 38 | ``` 39 | 40 | - 支持通过 `@SpelValid(condition = "...")` 控制是否触发校验逻辑 41 | - 允许动态场景下跳过校验,例如某些参数为空时跳过 42 | 43 | ### 2. 构建上下文并调用执行器 `SpelValidExecutor` 44 | 45 | ```java 46 | ObjectValidResult result = SpelValidExecutor.validateObject(obj, spelGroups, context); 47 | ``` 48 | 49 | - `spelGroups` 指定了这次校验使用哪些分组 50 | - `context` 中包含语言环境等信息,便于做国际化 51 | 52 | ## 四、SpelValidExecutor 启动字段校验流程 53 | 54 | 执行器 `SpelValidExecutor` 是 SpEL Validator 的核心引擎,它会: 55 | 56 | 1. 找出被校验类的所有字段 57 | 2. 找出字段上有哪些 `@SpelConstraint` 标注的注解(如 `@SpelSize`, `@SpelNotNull`, `@SpelFuture` 等) 58 | 3. 根据分组和 `condition` 决定是否执行注解 59 | 4. 执行注解对应的 `SpelConstraintValidator` 60 | 5. 收集错误结果,支持国际化 + 参数插值 61 | 62 | 每一个字段的错误,都会包装成 `FieldError`,包含了一些必要的信息。 63 | 64 | ## 五、回传错误信息给上层框架 65 | 66 | ```java 67 | context.buildConstraintViolationWithTemplate(error.getErrorMessage()) 68 | .addPropertyNode(error.getFieldName()) 69 | .addConstraintViolation(); 70 | ``` 71 | 72 | 这一步是把内部的校验错误结果,转换为标准的 Jakarta Validation 机制可感知的 ConstraintViolation: 73 | 74 | - 自动在响应中显示字段名、错误信息 75 | - 可以统一封装为 `400 Bad Request`、`ErrorResult` 等结构返回前端 76 | 77 | 这样可以与 BindingResult 等常见的 Spring 错误处理机制无缝集成。 78 | 79 | ## 六、整体执行流程如下 80 | 81 | ```md 82 | @Valid/@Validated 83 | ↓ 84 | @SpelValid <- 激活 SpelValidator 85 | ↓ 86 | SpelValidator#isValid() <- 调用 SpelValidExecutor 87 | ↓ 88 | SpelValidExecutor <- 扫描字段、解析表达式、调用具体约束的校验器 89 | ↓ 90 | SpelConstraintValidator <- 执行约束校验 91 | ↓ 92 | FieldError <- 错误信息收集 93 | ↓ 94 | ConstraintViolation <- 转换为标准校验框架支持的结构 95 | ``` 96 | 97 | # 小结 98 | 99 | SpEL Validator 是一个基于 Jakarta Validation 扩展的表达式校验框架,其核心特性包括: 100 | 101 | - 支持基于 SpEL 表达式的灵活字段校验 102 | - 支持注解级 condition 控制 103 | - 支持分组校验与国际化提示 104 | - 与 Spring MVC 原生校验机制无缝集成 105 | 106 | 使用时必须注意: 107 | 108 | | 场景 | 是否会触发 SpEL 校验 | 109 | |----------------------------|-----------------| 110 | | 只有 `@Valid` / `@Validated` | ❌ 不会触发 SpEL 逻辑 | 111 | | 只有 `@SpelValid` | ❌ 不会被 Spring 调用 | 112 | | 同时加了 `@Valid + @SpelValid` | ✅ 生效 | 113 | 114 | -------------------------------------------------------------------------------- /document/web-docs/docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | :::tip 4 | 本章仅介绍如何快速上手 SpEL Validator 的基本使用,更详细的使用说明请参考 [使用指南](user-guide.md)。 5 | ::: 6 | 7 | ## 添加依赖 8 | 9 | Latest Version: 10 | [![Maven Central](https://img.shields.io/maven-central/v/cn.sticki/spel-validator-root.svg)](https://central.sonatype.com/search?q=g:cn.sticki%20a:spel-validator-root) 11 | 12 | ### SpringBoot 2.x 13 | 14 | ```xml 15 | 16 | cn.sticki 17 | spel-validator-javax 18 | Latest Version 19 | 20 | ``` 21 | 22 | ### SpringBoot 3.x 23 | 24 | ```xml 25 | 26 | cn.sticki 27 | spel-validator-jakarta 28 | Latest Version 29 | 30 | ``` 31 | 32 | ## 添加启动注解 33 | 34 | 在接口参数上对需要进行校验的类使用 `@Valid` 或 `@Validated` 注解 35 | 36 | ```java 37 | @RestController 38 | @RequestMapping("/example") 39 | public class ExampleController { 40 | 41 | /** 42 | * 简单校验示例,这里使用了 @Valid 注解 43 | */ 44 | @PostMapping("/simple") 45 | public Resp simple(@RequestBody @Valid SimpleExampleParamVo simpleExampleParamVo) { 46 | return Resp.ok(null); 47 | } 48 | 49 | } 50 | ``` 51 | 52 | ## 添加SpEL约束注解 53 | 54 | 在实体类上使用 `@SpelValid` 注解,表示开启校验,同时在需要校验的字段上使用 `@SpelNotNull` 等约束注解 55 | 56 | ```java 57 | @Data 58 | @SpelValid // 添加启动注解 59 | public class SimpleExampleParamVo { 60 | 61 | @NotNull 62 | private Boolean switchAudio; 63 | 64 | /** 65 | * 此处开启了注解校验 66 | * 当 switchAudio 字段为 true 时,校验 audioContent,audioContent 不能为null 67 | */ 68 | @SpelNotNull(condition = "#this.switchAudio == true", message = "语音内容不能为空") 69 | private Object audioContent; 70 | 71 | } 72 | ``` 73 | 74 | ## 处理异常 75 | 76 | 添加全局异常处理器,处理校验不通过的异常信息 77 | 78 | ```java 79 | @RestControllerAdvice 80 | public class ControllerExceptionAdvice { 81 | 82 | @ExceptionHandler({BindException.class, MethodArgumentNotValidException.class}) 83 | public Resp handleBindException(BindException ex) { 84 | String msg = ex.getFieldErrors().stream() 85 | .map(error -> error.getField() + " " + error.getDefaultMessage()) 86 | .reduce((s1, s2) -> s1 + "," + s2) 87 | .orElse(""); 88 | return new Resp<>(400, msg); 89 | } 90 | 91 | } 92 | ``` 93 | 94 | ## 请求示例 95 | 96 | 发起请求,即可看到校验结果 97 | 98 | 示例一:@SpelNotNull 校验不通过 99 | 100 | - 请求体: 101 | 102 | ```json 103 | { 104 | "switchAudio": true, 105 | "audioContent": null 106 | } 107 | ``` 108 | 109 | - 响应体 110 | ```json 111 | { 112 | "code": 400, 113 | "message": "audioContent 语音内容不能为空", 114 | "data": null 115 | } 116 | ``` 117 | 118 | 示例二:校验通过 119 | 120 | - 请求体 121 | ```json 122 | { 123 | "switchAudio": false, 124 | "audioContent": null 125 | } 126 | ``` 127 | 128 | - 响应体 129 | ```json 130 | { 131 | "code": 200, 132 | "message": "成功", 133 | "data": null 134 | } 135 | ``` 136 | 137 | 示例三:@NotNull 校验不通过 138 | 139 | - 请求体 140 | ```json 141 | { 142 | "switchAudio": null, 143 | "audioContent": null 144 | } 145 | ``` 146 | 147 | - 响应体 148 | ```json 149 | { 150 | "code": 400, 151 | "message": "switchAudio 不能为null", 152 | "data": null 153 | } 154 | ``` 155 | 156 | -------------------------------------------------------------------------------- /spel-validator-core/src/main/java/cn/sticki/spel/validator/core/util/NumberComparatorUtil.java: -------------------------------------------------------------------------------- 1 | package cn.sticki.spel.validator.core.util; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.OptionalInt; 5 | 6 | /** 7 | * 数值类比较工具 8 | * 参考 {@link org.hibernate.validator.internal.constraintvalidators.bv.number.bound.NumberComparatorHelper} 9 | * 在此基础上拓展比较 10 | * 11 | * @author oddfar 12 | */ 13 | @SuppressWarnings({"OptionalUsedAsFieldOrParameterType", "JavadocReference"}) 14 | public class NumberComparatorUtil { 15 | 16 | private NumberComparatorUtil() { 17 | } 18 | 19 | public static final OptionalInt LESS_THAN = OptionalInt.of(-1); 20 | 21 | public static final OptionalInt FINITE_VALUE = OptionalInt.empty(); 22 | 23 | public static final OptionalInt GREATER_THAN = OptionalInt.of(1); 24 | 25 | public static int compare(Number number, Number value, OptionalInt treatNanAs) { 26 | if (number == null || value == null || treatNanAs == null || !treatNanAs.isPresent()) { 27 | throw new IllegalArgumentException("[Number], [Value] and [TreatNanAs] must not be null."); 28 | } 29 | if (number.equals(value)) { 30 | return 0; 31 | } 32 | boolean numberIsDouble = number instanceof Double || number instanceof Float; 33 | boolean valueIsDouble = value instanceof Double || value instanceof Float; 34 | 35 | if (numberIsDouble && valueIsDouble) { 36 | return compare(number.doubleValue(), value.doubleValue()); 37 | } 38 | if (numberIsDouble) { 39 | return compare(number.doubleValue(), value, treatNanAs); 40 | } 41 | if (valueIsDouble) { 42 | return compare(number, value.doubleValue(), treatNanAs); 43 | } 44 | 45 | return BigDecimalUtil.valueOf(number).compareTo(BigDecimalUtil.valueOf(value)); 46 | } 47 | 48 | private static int compare(Double number, double value) { 49 | return number.compareTo(value); 50 | } 51 | 52 | private static int compare(Number number, Double value, OptionalInt treatNanAs) { 53 | // 检查的是 value,所以需要反转 54 | //noinspection OptionalGetWithoutIsPresent 55 | treatNanAs = OptionalInt.of(-treatNanAs.getAsInt()); 56 | OptionalInt infinity = infinityCheck(value, treatNanAs); 57 | if (infinity.isPresent()) { 58 | // 这里也要反转 59 | return -infinity.getAsInt(); 60 | } 61 | return BigDecimalUtil.valueOf(number).compareTo(BigDecimal.valueOf(value)); 62 | } 63 | 64 | private static int compare(Double number, Number value, OptionalInt treatNanAs) { 65 | OptionalInt infinity = infinityCheck(number, treatNanAs); 66 | if (infinity.isPresent()) { 67 | return infinity.getAsInt(); 68 | } 69 | return BigDecimalUtil.valueOf(number).compareTo(BigDecimalUtil.valueOf(value)); 70 | } 71 | 72 | private static OptionalInt infinityCheck(Double number, OptionalInt treatNanAs) { 73 | OptionalInt result = FINITE_VALUE; 74 | if (number == Double.NEGATIVE_INFINITY) { 75 | result = LESS_THAN; 76 | } else if (number.isNaN()) { 77 | result = treatNanAs; 78 | } else if (number == Double.POSITIVE_INFINITY) { 79 | result = GREATER_THAN; 80 | } 81 | return result; 82 | } 83 | 84 | } 85 | --------------------------------------------------------------------------------