导读:本期聚焦于印尼程序员创作的《如何利用反射获取方法参数上的注解信息实现参数校验》,敬请观看详情。为什么自定义注解明明标在了方法参数上,运行时却总是拿不到注解信息?答案往往藏在注解的RetentionPolicy策略和反射API的调用方式上。本文从注解的生命周期讲起,逐步演示如何通过Method对象获取参数数组,再借助getParameterAnnotations拿到每个参数上的注解,最后手写一个轻量级的参数校验工具,支持非空校验、长度校验和正则校验,并给出AOP结合反射的完整实战代码。文中还对比了反射获取注解的几种常见写法差异,分析了getDeclaredAnnotations与getParameterAnnotations的使用场景,帮助你理解Spring等框架参数校验背后的实现原理,避开性能与缓存方面的坑。

参数校验是业务开发中绕不开的环节。虽然Bean Validation规范配合Spring已经能覆盖大部分场景,但当校验对象不是标准的JavaBean,而是普通方法参数时,很多同学就会卡在“注解写了却读不到”的问题上。这篇文章从底层原理出发,完整讲解如何用反射拿到方法参数上的注解,并基于此实现一个可以投入使用的参数校验工具。

如何利用反射获取方法参数上的注解信息实现参数校验

先搞懂注解为什么有时拿不到

在动手写代码之前,必须先理解注解的保留策略。@Retention注解决定了一个注解能“存活”到哪个阶段,它有三个可选值:RetentionPolicy.SOURCE表示只在源码阶段存在,编译后就被丢弃;RetentionPolicy.CLASS表示会写入class文件但运行时不可见;只有RetentionPolicy.RUNTIME才允许通过反射API在运行时读取。如果你定义的注解没有显式指定RUNTIME,反射拿到的永远是空数组,这是新手最常踩的坑。

除了保留策略,@Target也决定了注解能标注的位置。要想把注解放在方法参数上,必须在定义时声明ElementType.PARAMETER,否则编译器会直接报错。下面是一个规范的自定义校验注解定义,后面的例子都基于它展开:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface NotBlank {
    String message() default "参数不能为空";
}

这里有个容易被忽略的细节:注解的属性如果只有一个,且名字为value,使用时可以省略属性名。属性必须有无默认值或者在标注时显式赋值,否则编译不通过。把这两个元注解理解清楚之后,反射读取注解才有意义。

用反射逐层拿到参数上的注解

反射的入口是Class对象。拿到Class之后可以通过getMethodgetDeclaredMethod定位到具体方法,前者只能取public方法(包括继承的),后者能取本类声明的所有方法但不含继承的。拿到Method对象后,调用getParameters可以得到参数数组,再对每个参数调用getParameterAnnotations方式并不存在——正确的方式是直接在Method上调用getParameterAnnotations(),它返回一个二维数组,第一维对应参数下标,第二维对应该参数上的所有注解。这一点和很多人的直觉不同:注解不是从Parameter对象上获取的,而是整体从Method上一次性取出的。

完整的读取示例如下:

public class DemoService {
    public void register(@NotBlank String username, @NotBlank String email) {
        // 业务逻辑
    }
}

public static void main(String[] args) throws Exception {
    Method method = DemoService.class
            .getDeclaredMethod("register", String.class, String.class);
    Annotation[][] annotations = method.getParameterAnnotations();
    Parameter[] params = method.getParameters();
    for (int i = 0; i < params.length; i++) {
        System.out.println("参数名: " + params[i].getName());
        for (Annotation anno : annotations[i]) {
            if (anno instanceof NotBlank) {
                NotBlank nb = (NotBlank) anno;
                System.out.println("发现NotBlack注解, 提示语: " + nb.message());
            }
        }
    }
}

需要特别注意的是,getParameterAnnotations返回的二维数组长度等于参数个数,即使某个参数上没有任何注解,对应的内层数组也只是长度为零的空数组,而不是null。遍历时不用担心空指针,但要记得编译后的参数名可能变成arg0、arg1,除非编译时加上-parameters选项,Maven项目中可以在maven-compiler-plugin的configuration里加上这个compilerArgument来保留真实参数名。

另外,Annotation instanceof NotBlank这种判断方式在Spring环境中要小心。Spring为了支持组合注解,使用了代理对象实现注解接口,此时instanceof判断可能失效,更稳妥的写法是借助AnnotationUtils.findAnnotation去做查找。不过在纯JDK环境下,instanceof和强转都是安全的。

实现一个支持多种规则的校验工具

有了读取注解的能力,就可以设计一套完整的校验方案。先定义几个不同粒度的校验注解:@NotNull负责判空,@Length负责长度范围,@Pattern负责正则匹配。然后写一个校验器,输入方法对象和实参数组,输出校验失败的提示信息。核心思路是:遍历参数下标,取对应注解,逐个执行校验规则,收集所有错误一次性返回,而不是遇到第一个错误就中断,这样用户体验更好。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Length {
    int min() default 0;
    int max() default Integer.MAX_VALUE;
    String message() default "长度不合法";
}

public class ParamValidator {
    public static List<String> validate(Method method, Object[] args) {
        List<String> errors = new ArrayList<<>>();
        Annotation[][] annos = method.getParameterAnnotations();
        for (int i = 0; i < args.length; i++) {
            Object value = args[i];
            for (Annotation anno : annos[i]) {
                if (anno instanceof NotBlank && !isValidNotBlank(value)) {
                    errors.add(((NotBlank) anno).message());
                }
                if (anno instanceof Length && !isValidLength(value, (Length) anno)) {
                    errors.add(((Length) anno).message());
                }
            }
        }
        return errors;
    }

    private static boolean isValidNotBlank(Object value) {
        return value != null && !value.toString().trim().isEmpty();
    }

    private static boolean isValidLength(Object value, Length anno) {
        if (value == null) return true; // 空值交给NotNull处理
        int len = value.toString().length();
        return len >= anno.min() && len <= anno.max();
    }
}

这套设计的好处是校验规则与业务代码完全解耦。校验器本身不知道任何业务语义,它只关心注解元数据和实参值,新增一种校验规则只需要增加一个注解和一段判断逻辑。如果想做得更优雅,可以参考Bean Validation的做法,把每个注解对应的校验逻辑抽象成独立的Validator类,注册到Map中,通过注解类型作为key来查找执行,这样符合开闭原则。

结合AOP让校验对业务代码零侵入

手动调用校验器虽然可行,但每个方法都要写一行校验代码,侵入性太强。更成熟的方案是配合动态代理或AOP,在方法执行前自动完成校验。以Spring AOP为例,定义一个切面拦截标注了自定义@Check注解的方法,在环绕通知里通过joinPoint.getSignature()拿到MethodSignature,进而获取Method对象和实参数组,调用前面的校验器即可。

@Aspect
@Component
public class ValidationAspect {

    @Around("@annotation(check)")
    public Object around(ProceedingJoinPoint pjp, Check check) throws Throwable {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        List<String> errors = ParamValidator.validate(method, pjp.getArgs());
        if (!errors.isEmpty()) {
            throw new IllegalArgumentException(String.join("; ", errors));
        }
        return pjp.proceed();
    }
}

使用时只需在方法上标注@Check,参数照常标注校验注解,业务方法内部不写任何校验代码。Spring框架内部的校验本质上也是这条路:HandlerMethod在ArgumentResolver阶段解析参数注解,调用Validator完成校验,理解了反射读取参数注解的原理,再看Spring Validation的源码就通透了。

性能与工程上的注意事项

反射调用本身有一定开销,getParameterAnnotations每次调用都会重新解析并创建新的注解代理对象,绝不能在高频路径上反复调用。正确的做法是在启动阶段或首次调用时把Method的参数注解解析结果缓存起来,用方法签名做key存入ConcurrentHashMap,后续直接命中缓存。Spring中AnnotationUtils内部就大量使用了类似的缓存设计。

其次要注意代理场景下的方法归属问题。如果目标类被CGLIB代理,通过切面拿到的Method可能是代理类的方法,其参数注解与目标类一致,问题不大;但如果接口与实现类的注解不一致,要通过AopUtils.getMostSpecificMethod找到最终要执行的方法再解析注解,否则可能漏掉注解。最后,对于简单场景优先考虑现成的Bean Validation标准实现,自定义注解加反射的方案更适合无法引入框架或校验逻辑高度定制化的场景,两者并不冲突,理解原理才能在技术选型时做出准确判断。

Java反射注解参数校验修改时间:2026-09-06 14:44:46

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260906/51608.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。