一行注解优化服务层百行@Autowire代码


思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜


@Autowire可以说是我们日常开发中使用最频繁的一个注解了, 相信你在日常开发中一定看到过如下类似的代码:

java 复制代码
public class UserService {


    @Autowire
    private xxxMapper  xxxMapper;
    
    @Autowire
    private xxx1Mapper xxx1Mapper;
    
    @Autowire
    private xxxService xxxService;

   // ... 此处省略更多的@Autowire注解的使用
}

事实上,在实际的业务中有时需要注入的属性能达到十几个之多处,也就是说@Autowire在一个类中会频繁的重复出现。

那有没一种方式能让我们不用每次都重复写@Autowire的方式呢?肯定是有的,答案就是今天我们讨论的@RequiredArgsConstructor

什么是@RequiredArgsConstructor

@RequiredArgsConstructorLombok 提供的注解之一,其主要用于在类上,并为类中标有 final 字段的生成相应的构造方法。具体使用如下所示:

java 复制代码
@RequiredArgsConstructor
public class ExamplePo {

    private final String name;
    private final int age ;
    private String nameCode;

   public static void main(String[] args) {
   
      ExamplePo examplePo = new ExamplePo("name",12);
   }

}

在上述代码中,在Main方法中使用@RequiredArgsConstructor生成了一个构造方法以供我们构建实体ExamplePo。细心的读者可能已经注意到了在ExamplePo中定义了name、age、nameCode三个字段,但是当我们在Main方法中构建ExamplePo实体对象时,由于nameCode并未使用final修饰,所以仅能根据nameage两个属性来完成构建。

众所周知,final 修饰的字段在声明时必须进行初始化,且一旦被初始化后其值就不能再被修改。因此为了保证字段的不变性,@RequiredArgsConstructor 会自动生成构造方法时,会为检查类内部被 final 字段修饰的全部变量,并在构造方法中进行初始化。@RequiredArgsConstructor这样做不仅确保了对象在创建时所有 final 字段都得到正确的初始化,而且一旦初始化后,它们的值不可变。

剖析@RequiredArgsConstructor原理

为了能透彻剖析Lombok@RequiredArgsConstructor 的逻辑,这里我们借助注解处理器(Annotation Processor)来模拟实现对在编译阶段生成和修改字节码,以对Lombok中的@RequiredArgsConstructor的解析进行模拟。

为此,我们首先自定义一个MyRequiredArgsConstructor注解

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface MyRequiredArgsConstructor {

    boolean includeAllFields() default false;
}

然后,我们编写一个MyRequiredArgsConstructorProcessor来对我们自定义的MyRequiredArgsConstructor进行解析。其逻辑如下:

java 复制代码
// ... 省略包信息导入

@SupportedAnnotationTypes("com.example.annotation.MyRequiredArgsConstructor")
public class MyRequiredArgsConstructorProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        <1> 遍历Class文件,判断其是否有MyRequiredArgsConstructor注解
        for (Element element : roundEnv.getElementsAnnotatedWith(MyRequiredArgsConstructor.class)) {
            if (element instanceof TypeElement) {
                TypeElement typeElement = (TypeElement) element;

               <2> 收集MyRequiredArgsConstructor注解所修饰类中字段信息
                List<VariableElement> finalFields = new ArrayList<>();
                List<FieldSpec> fields = new ArrayList<>();

                for (Element enclosedElement : typeElement.getEnclosedElements()) {
                    if (enclosedElement.getKind() == ElementKind.FIELD) {
                        VariableElement field = (VariableElement) enclosedElement;
                        finalFields.add(field);
                        FieldSpec fieldSpec = FieldSpec.builder(TypeName.get(field.asType()), field.getSimpleName().toString(), Modifier.PRIVATE, Modifier.FINAL)
                                .build();
                        fields.add(fieldSpec);
                    }
                }

                MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                        .addModifiers(Modifier.PUBLIC);

                // <3> 遍历属性内容,生成构造方法
                for (VariableElement field : finalFields) {
                    constructorBuilder.addParameter(TypeName.get(field.asType()), field.getSimpleName().toString());
                    constructorBuilder.addStatement("this.$N = $N", field.getSimpleName(), field.getSimpleName());
                }

                MethodSpec constructor = constructorBuilder.build();
                
                // <4> 进行class文件的回写
                TypeSpec classWithConstructor = TypeSpec.classBuilder(String.valueOf(typeElement.getSimpleName()))
                        .addModifiers(Modifier.PUBLIC)
                        .addMethod(constructor)
                        .addFields(fields)
                        .build();

                JavaFile javaFile = JavaFile.builder("com.example.pojo", classWithConstructor)
                        .build();

                try {
                    JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile("com.example.pojo." + typeElement.getSimpleName() + "WithConstructor");
                    Writer writer = sourceFile.openWriter();
                    javaFile.writeTo(writer);
                    writer.close();
                } catch (IOException e) {
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.toString());
                }
            }
        }
        return true;
    }
}

上述代码的process大致逻辑如下,其首先会对加载类进行逐个遍历,从而找出带有MyRequiredArgsConstructor注解的Class文件。

然后,对目标类文件中的字段信息进行逐个遍历,收集其所有的field字段信息,并存储到finalFields列表中。

最后,依据扫描得到的字段信息创建一个公共的构造函数,构造函数的入参即为类中的所有字段,并在构造函数中将参数赋值给对应的字段。

更进一步,我们将MyRequiredArgsConstructor标注在MyClass上,然后手动调用我们MyRequiredArgsConstructorProcessor进行编译其结果如下图所示。

(注:不熟悉MyRequiredArgsConstructor如何调用的可参考笔者之前的: Java注解能力提升:教你解析保留策略为源码阶段的注解

java 复制代码
@MyRequiredArgsConstructor
@SuppressWarnings("unused")
public class MyClass {

    private  String name;
    private int age;

    private String address;
    private String optionalField;
}

可以看到,我们自定义的MyRequiredArgsConstructorProcessor成功的对标有MyRequiredArgsConstructor类进行了解析,并生为其成了相关的构造器。

至此,我们便通过Jdk提供给我们的AbstractProcessor接口成功地对Lombk中的@RequiredArgsConstructor的解析原理进行了模拟。

(注:Lombok内部在实现对注解解析时其实也是通过继承AbstractProcessor接口,来完成对相关LomBok注解的解析处理,感兴趣的读者可自行对Lombok中的LombokProcessor进行分析)

明白了@RequiredArgsConstructor的用途以及工作原理后,接下来我们便来用@RequiredArgsConstructor来改造我们的@Autowire

使用@RequiredArgsConstructor减少@Autowire的书写

正如我们前面介绍的那样,如果类上加上@RequiredArgsConstructor,那么需要注入的类所需的的关键字段需要通过final声明进行修饰。 为此我们对开头出现的UserService进行改造,具体如下所示:

java 复制代码
@Service
@RequiredArgsConstructor(onConstructor_ = {@Lazy, @Autowired})
public class UserService {


    private final xxxMapper  xxxMapper;
 
  
    private final xxx1Mapper xxx1Mapper;
    
  
    private final xxxService xxxService;

   // ... 此处省略更多的@Autowire注解的使用
}

到在上述代码中,我们在用到了@RequiredArgsConstructor 注解的onConstructor_ 属性,该属性允许你为生成的构造函数添加额外的注解。这个属性是一个注解数组,使你可以为 Lombok 自动生成的构造函数添加多个注解。因此上述代码最终生成的代码造类似于:

java 复制代码
public class UserService {


    private final xxxMapper  xxxMapper;
 
  
    private final xxx1Mapper xxx1Mapper;
    
  
    private final xxxService xxxService;

   // ... 此处省略更多的@Autowire注解的使用
   
   @Autowire
   @Lazy
   public UserService(xxxMapper xxxMapper ,xxx1Mapper xxx1Mapper , 
                              xxxService xxxService ) {
    // .... 省略属性注入
   }

}

笔者在使用@RequiredArgsConstructoronConstructor_属性中除了使用@Autowire外,还使用了@Lazy注解,这主要目的是为了解决因循环依赖的发生。

因为当 Spring 容器遇到 @Lazy 注解时,它不会立即创建该 bean 的实例,而是创建一个代理对象。只有当该代理对象第一次被访问时,Spring 容器才会创建真正的 bean 实例并进行注入。

总结

至此,我们就对Lombok中的@RequiredArgsConstructor的原理和使用场景进行了详细的介绍,结合@RequiredArgsConstructor我们完全可以对服务层频繁出现的@Autowire注解进行优化。

相关推荐
尚学教辅学习资料2 分钟前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理
雷神乐乐18 分钟前
File.separator与File.separatorChar的区别
java·路径分隔符
小刘|23 分钟前
《Java 实现希尔排序:原理剖析与代码详解》
java·算法·排序算法
逊嘘42 分钟前
【Java语言】抽象类与接口
java·开发语言·jvm
morris1311 小时前
【SpringBoot】Xss的常见攻击方式与防御手段
java·spring boot·xss·csp
monkey_meng1 小时前
【Rust中的迭代器】
开发语言·后端·rust
余衫马1 小时前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng1 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员1 小时前
java导出word文件(手绘)
java·开发语言·word