思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
@Autowire
可以说是我们日常开发中使用最频繁的一个注解了, 相信你在日常开发中一定看到过如下类似的代码:
java
public class UserService {
@Autowire
private xxxMapper xxxMapper;
@Autowire
private xxx1Mapper xxx1Mapper;
@Autowire
private xxxService xxxService;
// ... 此处省略更多的@Autowire注解的使用
}
事实上,在实际的业务中有时需要注入的属性能达到十几个之多处,也就是说@Autowire
在一个类中会频繁的重复出现。
那有没一种方式能让我们不用每次都重复写@Autowire
的方式呢?肯定是有的,答案就是今天我们讨论的@RequiredArgsConstructor
。
什么是@RequiredArgsConstructor
@RequiredArgsConstructor
是 Lombok
提供的注解之一,其主要用于在类上,并为类中标有 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
修饰,所以仅能根据name
和age
两个属性来完成构建。
众所周知,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 ) {
// .... 省略属性注入
}
}
笔者
在使用@RequiredArgsConstructor
的onConstructor_
属性中除了使用@Autowire
外,还使用了@Lazy
注解,这主要目的是为了解决因循环依赖的发生。
因为当 Spring
容器遇到 @Lazy
注解时,它不会立即创建该 bean
的实例,而是创建一个代理对象。只有当该代理对象第一次被访问时,Spring
容器才会创建真正的 bean
实例并进行注入。
总结
至此,我们就对Lombok
中的@RequiredArgsConstructor
的原理和使用场景进行了详细的介绍,结合@RequiredArgsConstructor
我们完全可以对服务层频繁出现的@Autowire
注解进行优化。