掘金 后端 ( ) • 2024-04-20 15:35

1. 认识注解

注解是 Java 5 新增的功能,它是 Java 代码里的特殊标记(本质上是一种特殊的接口),可以用来为程序元素(包、类、构造器、方法、成员变量、参数、局部变量)设置元数据,使得开发人员在不改变代码逻辑、不影响程序运行逻辑的情况下,为源文件嵌入一些补充的信息
注解只有通过某种配套工具对其中的信息进行访问和处理方能起作用。在编译阶段,可以通过编写注解处理器动态处理相关逻辑,主要是用来自动生成代码,例如 Lombok 中的@Data注解。在运行阶段,可以通过反射动态处理相关处理。
以下提供了几个示例先对注解有个基本的印象。

示例1:@Override注解

public class OverrideAnnotationExample extends AbstractClass {
    @Override
    void method() {
        System.out.println("Override 注解示例");
    }
}

abstract class AbstractClass {
    abstract void method();
}

示例2:自定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CustomAnnotation {
}

2. 常见的注解

2.1 常用的Java 内置注解

Java 内置注解均在 java.lang 包中定义。

从 Java 5 开始,Java 增加了注解功能,以下介绍 Java 中常用的几个注解:

  • @Override
  • @Deprecated
  • @SuppressWarnings
  • @SafeVarargs
  • @FunctionalInterface

@Override

@Override注解在类继承中经常使用到,常被用来指定方法覆载,它强制一个子类必须实现父类的方法。例如:

public class Parent {
    public void example() {
        System.out.println("这里是父类方法");
    }
}

public class Child extends Parent {
    @Override
    public void example() {
        System.out.println("这里是子类方法");
    }
}

在上面的示例中,@Override注解被用来修饰类成员方法,且没有改变子类的方法运行逻辑,在代码编译阶段,编译器会自动读取该注解中的元数据信息,保证父类中存在一个被该方法重写的方法,否则会提示编译错误。

@Deprecated

@Deprecated注解用来表示某个程序元素(类、方法等)已过时或者已有更好的实现方式,当其他程序使用该注解标记的元素时,编译器将会给出警告。从 Java 9 版本开始,该注解增加了 2 个属性:

  • forRemoval:Boolean 类型,指定该元素是否在将来被删除;
  • since:Stirng 类型,指定该元素从哪个版本被标记为过时。

@SupressWarnings

@SupressWarnings注解被用来取消显示该注解修饰的元素(包含其子元素)的编译器警告。

@SafeVarargs

使用泛型作为可变参数时可能会引起“堆污染”警告,Java 编译器在编译阶段会提示相关的警告,并且编译器以及调用方也会出现类似的警告。使用 @SafeVarargs 可以取消这些警告。

@SafeVarargs注解被用来在使用泛型作为可变参数的方法或构造器中关闭对调用者的警告。该注解在 Java 9 版本中得到增强,允许用来修饰私有方法。

示例文件:SafeVarargsAnnotationExample.java

public class SafeVarargsAnnotationExample {
    public static void main(String[] args) throws NoSuchAlgorithmException {
        SafeVarargsExample.faultyMethod(Collections.singletonList("Hello!"), Collections.singletonList("World!"));
    }
}

class SafeVarargsExample {
    private SafeVarargsExample() {}
    
    public static void faultyMethod(List<String>... listStrArray) throws NoSuchAlgorithmException {
        List<Integer> myList = new ArrayList<>();
        Random random = SecureRandom.getInstanceStrong();
        myList.add(random.nextInt(100));

        List[] listArray = listStrArray;
        listArray[0] = myList;
        String s = listStrArray[0].get(0);
        System.out.println(s);
    }
}

对于上面的示例而言,如果未使用该注解,那么在编译器中faultMethod方法和测试类中main方法调用处都会提示警告信息。在faultMehod方法中使用该注解后,这些警告信息则会消失。

@FunctionalInterface

函数式接口会在另外一篇文章进行描述

@FunctionalInterface注解用来声明接口是函数式接口,即保证该接口必须满足函数式接口的定义,只能包含一个抽象方法,否则就会编译出错。

2.2 元注解

元注解都在 java.lang.annotation 包中定义

在定义注解时,不管是内置注解还是自定义注解,都需要用到元注解。Java 中提供了 5 个元注解是用来修饰其他注解定义的,具体如下。

@Retention

Retention 中文意思是保持的意思,@Retention元注解用于指定被修饰的注解可以保留多长时间,只能用来修饰注解定义并保留至 RUNTIME 阶段。
该注解的值仅支持RetentionPolicy枚举类型中的变量值,其中:

  • RetentionPolicy.SOURCE在源代码中保留注解,在编译时丢失,也就说,在编译阶段,编译器会忽略该注解,并不会将其包含在编译得到的 class 文件中。这种策略常用于编写工具,例如代码分析工具等。
  • RetentionPolicy.CLASS在编译阶段保留,在运行时丢弃。编译器将注解记录保存在 class 文件中,但当运行 Java 程序时,JVM 不能获取该注解的信息。这种保留策略通常用于注解处理器,在编译期间处理注记并生成额外的代码。当注解不使用@Retention元注解时,默认使用该值。
  • RetentionPolicy.RUNTIME在运行时保留,可以通过反射机制获取。编译器将注解记录在 class 文件中,并且当运行 Java 程序时, JVM 可以获取注解的信息,开发人员可以通过反射来获取这些信息。这种保留策略通常用于在运行时动态地检查、配置或处理程序。

@Retention元注解使用示例如下。

@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationExample {}

@Target

@Target元注解用于指定被修饰的注解能用于修饰哪些程序元素,可以用来修饰注解定义并保留至 RUNTIME 阶段。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationExample {}

该注解的值仅支持ElementType类型的值,具体取值可为:

  • ElementType.ANNOTATION_TYPE:指定被修饰的注解只能修饰注解
  • ElementType.CONSTRUCTOR:指定被修饰的注解只能修饰构造器
  • ElementType.FIELD:指定被修饰的注解只能修饰类成员变量
  • ElementType.LOCAL_VARIABLE:指定被修饰的注解只能修饰局部变量
  • ElementType.METHOD:指定被修饰的注解只能修饰方法定义
  • ElementType.PACKAGE:指定被修饰的注解只能修饰包定义
  • ElementType.PARAMETER:指定被修饰的注解只能修饰参数
  • ElementType.TYPE:指定被修饰的注解可以用来修饰类、接口(包括注解)以及枚举定义
  • ElementType.TYPE_PARAMETER:指定注解可以应用于类型参数上,具体来讲,就是可以在类、接口、方法等中定义类型参数。
public <T> void process(@NotNull T name) {}
  • ElementType.TYPE_USE:指定注解可以应用于任何使用类型的地方,包括:类、接口、枚举、注解、类型参数、方法的返回类型、字段的类型、局部变量的类型、方法的参数类型等等。
@NonNull List<@NonNull String> names = new ArrayList<>();

@Documented

@Documented元注解用来指定被该注解修饰的注解类将被 javadoc 工具提取成文档,可以用来修饰注解定义并保留至 RUNTIME 阶段。使用@Documented注解可以让注解的信息在生成的文档中可见,有助于使用者了解如何正确地使用注解以及注解的含义。
示例:在定义 AnnotationExample 注解时,使用了@Documented元注解,则其他代码使用 AnnotationExample 注解时,生成的 API 文档中会包含该注解说明。

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationExample {}
@AnnotationExample
public class DocumentedMetaAnnotationExample {}

@Inherited

@Inherited元注解用来指定被它修饰的注解将具备继承性,可用来修饰注解定义并保留至 RUNTIME 阶段。如果某个类被使用@Inherited元注解修饰的注解修饰,则其子类会自动被该注解修饰。

@Repeatable

@Repeatable元注解用来指定被它修饰的注解可以在一个类上重复使用,而不需要使用数组形式的语法,可以用来修饰注解定义并保留至 RUNTIME 阶段。使用@Repeatable注解时,必须为 value 成员变量指定值,该成员变量的值应该是一个“容器”注解(可以理解为包含多个特定容器的数组)。
在 Java 8 版本之前,如果需要在同一个程序元素上使用多个同类型的注解,则必须使用注解“容器”,具体示例如下。其中,RepeatableAnnotations 表示为注解“容器”。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RepeatableAnnotation {
    String name();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RepeatableAnnotations {
    RepeatableAnnotation[] value();
}

public class RepeatableAnnotationExample {
    @RepeatableAnnotations({
            @RepeatableAnnotation(name = "张三"),
            @RepeatableAnnotation(name = "李四")
    })
    public void printNames() {}
}

但从 Java 8 开始,可以使用@Repeatable元注解来修饰特定的注解,从而允许在同一个程序元素上多次使用同类型的注解。使用@Repeatable元注解后需要对以上代码进行修改,具体为:

第1步:RepeatableAnnotation 添加 @Repeatable 元注解

@Repeatable(RepeatableAnnotations.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RepeatableAnnotation {
    String name();
}

第2步:修改 RepeatableAnnotation 的使用方式

public class RepeatableAnnotationExample {
    @RepeatableAnnotation(name = "张三")
    @RepeatableAnnotation(name = "李四")
    public void printNames() {}
}

3. 自定义注解

3.1 定义注解

正如在第一章示例2中介绍的那样,可以使用@interface关键字定义新的注解类型,并使用多种元注解修饰这个新的注解类型。定义了注解后,可以在对应的程序元素上使用该注解。

<元注解>
public @interface AnnotationName {
}

注解不仅可以是这种简单的注解,还可以携带成员变量。成员变量在注解定义中以_无形参的方法形式_来声明,其方法名和返回值定义了该成员变量的名字和类型,例如下面代码中的String name()定义了成员变量名称为 name、返回值类型为 String。定义成员变量时,可以使用 default 关键字为其指定默认值。

public @interface Test {
    String name() default "张三";
    int age();
}

一旦定义注解时携带了成员变量,那么在使用时就应该为它的成员变量指定值。

@Test(name = "test", age = 23)
public class TestExample {
}

3.2 提取注解信息

在定义及使用注解后,这些注解并不会自己生效,还需要开发人员提供对应的工具用来提取和处理注解信息,通常使用反射来实现。
自 Java 5 版本开始,在java.lang.reflect包下面增加了 AnnotatedElement 接口,该接口是所有程序元素的父接口,程序通过反射获取某个类的 AnnotatedElement 对象之后,就可以调用该对象的方法来访问注解信息。具体的方法列表如下:

  • <A extends Annotation> A getAnnotation(Class<A> annotationClass):返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回 null。
  • <A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass):尝试获取直接修饰该程序元素、指定类型的注解。如果该类型的注解不存在,则返回 null。
  • Annotation[] getAnnotations():返回该程序元素上存在的所有注解。
  • Annotation[] getDeclaredAnnotations():返回直接修饰该程序元素的所有注解。
  • boolean isAnnotationPresent(Class<? extends Annotation> annotationClass):判断该程序元素上是否存在指定类型的注解,如果存在则返回 true,否则为 false。
  • <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationClass):与 getAnnotation 方法基本类似,但该方法可以获取修饰该程序元素、指定类型的多个注解(例如:重复注解场景)。
  • <A extends Annotation> A[] getDeclaredAnnotationsByType(Class<A> annotationClass):与 getDeclaredAnnotation 方法基本类似,但该方法可以获取修饰该程序元素、指定类型的多个注解(例如:重复注解场景)。

示例代码

public class RepeatableAnnotationExample {
    @RepeatableAnnotation(name = "张三")
    @RepeatableAnnotation(name = "李四")
    public void printNames() {
        try {
            Annotation[] annotations =
                    Class.forName(RepeatableAnnotationExample.class.getName())
                            .getMethod("printNames")
                            .getAnnotationsByType(RepeatableAnnotation.class);
            for (Annotation annotation : annotations) {
                if (annotation instanceof RepeatableAnnotation) {
                    System.out.println(((RepeatableAnnotation) annotation).name());
                }
            }
        } catch (ClassNotFoundException | NoSuchMethodException exception) {
            exception.printStackTrace();
        }
    }

    public static void main(String[] args) {
        RepeatableAnnotationExample example = new RepeatableAnnotationExample();
        example.printNames();
    }
}

运行结果

张三
李四

3.3 编译时处理注解

APT(Annotation Processing Tool,注解处理工具),它对源代码进行检测,并找出源代码中包含的注解信息,然后针对注解信息进行额外的处理。APT 工具的主要目的是帮助开发者减轻工作量。
使用 APT 工具处理注解时可以根据源文件的注解生成额外的源文件和其他的文件(文件的具体内容可以由 APT 的编写者决定),APT 还会编译生成源代码文件和原来的源文件,并将它们一起生成 class 文件。
Java 提供的 javac.exe 工具提供了-processor选项,该选项指定一个注解处理器,用于在编译阶段提取并处理 Java 源文件中的注解。
注解处理器最关键的程序要素就是 Processor 接口,如果编写自定义注解处理器实现该接口,需要实现接口中定义的所有方法。Java 提供了 AbstractProcessor 抽象类,该类实现了 Processor 接口并实现了大部分接口,因此,大多数注解处理器都是继承 AbstractProcessor 抽象类的方式来实现。一个注解处理器可以处理一种或者多种注解类型。
AbstractProcessor 抽象类方法列表:

  1. **void init(ProcessingEnvironment processingEnv)**

初始化方法,方法参数为 ProcessingEnvironment 接口实例。通常在自定义注解处理器时,可以不用覆写该方法,也可以覆写该方法实现其他的功能。
ProcessingEnvironment 接口关键的方法如下:

  • Map<String, String> getOptions():返回传递给注解处理器的指定参数选项。
  • Messager getMessager():返回实现 Messager 接口的对象,用于报告错误、警告等信息。
  • Filter getFilter():返回实现 Filter 接口的对象,用来创建类和其他辅助文件。
  • Elements getElementUtils():返回实现 Elements 接口的对象,是用于操作元素的工具类。
  • Types getTypeUtils():返回实现Types接口的对象,用于操作类型的工具类。
  1. **Set<String> getSupportedOptions()**

获取@SupportedOptions注解配置的值,在使用注解处理器时,可以使用对应的命令行参数传入对应的值。例如,对于下面代码中定义的MyDataProcessor注解处理器,在使用时,可以通过-Aname=xxx -Aage=21方式传递参数。

@SupportedOptions({"name", "age"})
public class MyDataProcessor extends AbstractProcessor {}
javac -processor MyAnnotationProcessor -Aname=jiaoxn -Aage=21 ExampleForAnnotationProcessor.java

通常该方法和前面介绍的getOptions()配合使用,前者用于获取受支持的参数类型,后者用于获取传递给注解处理器的所有参数。

  1. **Set<String> getSupportedAnnotationTypes()**

获取这个注解处理器可以处理的注解,方法的返回值是一个字符串集合,包含处理器想要处理的注解类型的合法全称。

  1. **SourceVersion getSupportedSourceVersion()**

获取指定的 Java 版本,默认情况下,该方法返回 SourceVersion.latestSupported()。

  1. **abstract boolean process(Set<? extends TypeElement> annotations,RoundEnvironment roundEnv)**

抽象方法,需要每个子类来实现。该方法相当于每个处理器的主函数 main(),实现扫描、解析和处理注解的逻辑,以及生成 Java 文件。方法会传入参数 RoundEnviroment,可以让开发者查询出包含特定注解的被注解元素。如果结果返回了 true,则表示该注解处理器处理了注解,其他注解处理器不会再运行。如要要其他处理器能够继续运行,此方法应返回 false。

  1. **Iterable<? extends Completion> getCompletions(Element element,AnnotationMirror annotation, ExecutableElement member,String userText)**

该方法主要供 IDE 使用,用于在使用注解时向用户提供关于注解的建议。

4. 示例

4.1 示例1:模拟 Lombok 中的 @Data

相信大家很多人使用过 Lombok,它能够让开发人员编写更少的代码,提高开发效率。为了更好地理解如何在编译阶段处理注解,本小节模拟了 Lombok 中的@Data注解的实现。 绘图1 (1).jpg

4.1.1 Lombok @Data 原理解析

一个 Java 源代码文件编译成二进制文件(.class)大致需要经历3个阶段:解析与填充符号表、注解处理、分析与生成字节码。
解析与填充符号过程: 经过词法、语法分析,然后将源代码转为一颗抽象语法树(AST)。
注解处理过程: 主要使用注解处理器来处理源代码中的相关注解,这个过程可能会修改源代码文件或者生成其他文件,这些文件将再次进入第一个阶段。
分析与生成字节码过程: 分析 AST 并生成 CLASS 文件。
Lombok 在注解处理阶段,首先找到@Data注解对应的语法树,然后修改语法树,增加对应的 getter、setter 方法到指定的树节点。

4.1.2 模拟实现

第1步:定义注解 MyData

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface MyData {
}

第2步:定义 APT 工具 MyDataProcessor

// 注解处理器支持的Java源代码版本
@SupportedSourceVersion(SourceVersion.RELEASE_8)
// 注解处理器支持的注解类型
@SupportedAnnotationTypes("com.funnymap.mydata.MyData")
public class MyDataProcessor extends AbstractProcessor {
    // 待处理的 AST
    private JavacTrees javacTrees;
    // 封装创建 AST 节点的方法
    private TreeMaker treeMaker;
    // 提供了创建标识符的方法
    private Names names;
    private Context context;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        this.javacTrees = JavacTrees.instance(processingEnv);

        context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(MyData.class);
        for (Element element : elements) {
            // JCTree表示AST中的节点
            JCTree tree = javacTrees.getTree(element);

            // accept可以遍历和操作AST节点
            // TreeTranslator 用于遍历和转换抽象语法树(AST)节点的类
            tree.accept(new TreeTranslator() {
                // visitClassDef用于定义AST中的节点
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    List<JCTree.JCVariableDecl> jcVariableDecls = List.nil();

                    for (JCTree item : jcClassDecl.defs) {
                        if (item.getKind().equals(Tree.Kind.VARIABLE)) {
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) item;
                            jcVariableDecls = jcVariableDecls.append(jcVariableDecl);
                        }
                    }

                    for (JCTree.JCVariableDecl jcVariableDecl : jcVariableDecls) {
                        jcClassDecl.defs = jcClassDecl.defs
                                .prepend(makeSetterMethodDecl(jcVariableDecl))
                                .prepend(makeGetterMethodDecl(jcVariableDecl));
                    }

                    super.visitClassDef(jcClassDecl);
                }
            });
        }
        return false;
    }

    private JCTree.JCMethodDecl makeSetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
        // 函数标识
        JCTree.JCModifiers funcModifier = treeMaker.Modifiers(Flags.PUBLIC);
        // 函数名称
        Name funcName = setterMethodName(jcVariableDecl.getName());
        // 返回值类型
        JCTree.JCExpression funcRestype = treeMaker.Type(new Type.JCVoidType());
        // 函数参数入参
        JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), jcVariableDecl.getName()
                , jcVariableDecl.vartype, null);
        List<JCTree.JCVariableDecl> parameters = List.of(param);
        // 函数方法体
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        JCTree.JCExpressionStatement expressionStatement =
                makeStatement(
                        treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()),
                        treeMaker.Ident(jcVariableDecl.getName()));
        statements.add(expressionStatement);
        JCTree.JCBlock block = treeMaker.Block(0, statements.toList());

        // 第1个参数:Java程序中的修饰符集合,用于描述类、接口、字段、方法等的特性,例如public、static等
        // 第2个参数:函数名称,例如:getName
        // 第3个参数:函数返回值类型
        // 第4个参数:泛型参数列表
        // 第5个参数:参数列表
        // 第6个参数:异常声明列表
        // 第7个参数:方法体
        // 第8个参数:默认方法
        return treeMaker.MethodDef(
                funcModifier,
                funcName,
                funcRestype,
                List.nil(),
                parameters,
                List.nil(),
                block,
                null);
    }
    private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
        // 函数标识
        JCTree.JCModifiers funcModifier = treeMaker.Modifiers(Flags.PUBLIC);
        // 函数名称
        Name funcName = getterMethodName(jcVariableDecl.getName());
        // 函数方法体
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        JCTree.JCReturn returnStatement = makeReturnStatement(jcVariableDecl);
        statements.add(returnStatement);
        JCTree.JCBlock block = treeMaker.Block(0, statements.toList());

        // 第1个参数:Java程序中的修饰符集合,用于描述类、接口、字段、方法等的特性,例如public、static等
        // 第2个参数:函数名称,例如:getName
        // 第3个参数:函数返回值类型
        // 第4个参数:泛型参数列表
        // 第5个参数:参数列表
        // 第6个参数:异常声明列表
        // 第7个参数:方法体
        // 第8个参数:默认方法
        return treeMaker.MethodDef(
                funcModifier,
                funcName,
                jcVariableDecl.vartype,
                List.nil(),
                List.nil(),
                List.nil(),
                block,
                null);
    }
    private Name getterMethodName(Name name) {
        String s = name.toString();
        return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
    }
    private Name setterMethodName(Name name) {
        String s = name.toString();
        return names.fromString("set" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
    }
    private JCTree.JCExpressionStatement makeStatement(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
        return treeMaker.Exec(treeMaker.Assign(lhs, rhs));
    }
    private JCTree.JCReturn makeReturnStatement(JCTree.JCVariableDecl jcVariableDecl) {
        return treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()));
    }
}

4.2 示例2:自定义注解校验文件类型

4.2.1 需求描述

对前端传递的文件参数进行格式校验,如果格式不满足需要就提示错误。

4.2.2 具体实现

大致实现思路:

  1. 添加依赖:spring-boot-starter-validation
  2. 创建增强的文件格式校验注解
  3. 创建执行校验的验证器
  1. 添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  1. 创建文件格式校验注解

与普通的注解不同的是,下面代码增加了第1行的@Constraint配置,用于标记自定义约束注解,并指定用于执行验证的验证器实现类。

@Constraint(validatedBy = {MultipartFileTypeValidator.class})
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.FIELD})
public @interface MultipartFileTypeValidationAnno {
    String[] value();
    String message() default "请上传指定类型的文件";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
  1. 创建验证器
public class MultipartFileTypeValidator implements ConstraintValidator<MultipartFileTypeValidationAnno, MultipartFile> {
    private final Set<String> supportFileTypes = new HashSet<>();


    /**
     * 自定义校验器初始化,获取期望的MultipartFile的文件类型
     *
     * @param constraintAnnotation 自定义注解
     */
    @Override
    public void initialize(MultipartFileTypeValidationAnno constraintAnnotation) {
        String[] vals = constraintAnnotation.value();
        for (String val : vals) {
            this.supportFileTypes.add(val.toLowerCase());
        }
    }

    /**
     * 校验方法
     *
     * @param file 注解在方法上时,该值为方法返回值;注解在字段上时,该值是字段值
     * @param constraintValidatorContext 上下文
     * @return true表示校验通过,反之校验不通过
     */
    @Override
    public boolean isValid(MultipartFile file, ConstraintValidatorContext constraintValidatorContext) {
        // 如果文件为空,默认校验通过
        if (file == null || file.isEmpty()) {
            return true;
        }

        String fileName = file.getOriginalFilename();

        // 如果无法获取文件名,则表示校验失败
        if (fileName == null) {
            return false;
        }

        String fileExtension = this.getFileExtension(fileName);
        return this.supportFileTypes.contains(fileExtension.toLowerCase());
    }
    private String getFileExtension(String fileName) {
        int lastDotIndex = fileName.lastIndexOf('.');
        if (lastDotIndex == -1) {
            // 如果没有找到点号,返回空字符串
            return "";
        }

        // 获取文件后缀名并转换为小写
        return fileName.substring(lastDotIndex + 1).toLowerCase();
    }
}
  1. 使用示例

在使用时需要注意第1行的@Validated。如果校验注解不是应用在 RequestBody 上,则需要在 Controller 上添加@Validated方可使得参数注解生效。

@Validated
@RequestMapping("/")
@RestController
public class ACPMController {
    @PostMapping("/json")
    public String uploadExample(@MultipartFileTypeValidationAnno("json") @RequestParam MultipartFile multipartFile) {
        return multipartFile.getOriginalFilename();
    }
}
  1. 异常示例

对于上面的示例而言,如果传入一个不是 JSON 格式的文件,将得到如下的错误。

javax.validation.ConstraintViolationException: uploadExample.multipartFile: 请上传指定类型的文件
	at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120)
	...

5. 参考文档

注解处理器 2:java 注解处理器 - 尛惢刕孨 - 博客园
99%的程序员都在用Lombok,原理竟然这么简单?我也手撸了一个!|建议收藏!!! - 磊哥|www.javacn.site - 博客园
5. JCTree相关知识学习-CSDN博客
Java中的屠龙之术——如何修改语法树 - Mr.墨斗的博客 | MoDou Blog