Java注解是Java5引入的重要语言特性之一,它可以提供描述程序所需元数据信息,而这些信息是无法使用Java语言进行表达的。
注解的引入可以使我们能够使用编译器来验证格式,并存储程序额外信息。
注解又称元数据,为我们在代码中添加信息提供了一种方法,使得我们能够在稍后某个时刻方便的访问这些数据。
注解在一定程度上是将元数据和源代码文件结合在一起,而无需使用额外的配置文件进行管理。
注解的语法很简单,除了@符号的使用,基本与Java固有的写法一致,Java5中内置了三个注解:
- @Override
表示当前方法将覆盖父类中的方法,如果方法名拼错或方法签名与被覆盖的方法不一致,编译器会发出错误提示 - @Deprecated
标记当前方法、类、参数为已过期,当使用已过期方法时,编译器会发出Warning提示 - @SuppressWarning
关闭编译器Warning信息
大多数情况下,程序员主要是定义自己的注解,并编写自己的处理器来处理他们,以达到既定的目的。
1. 定义注解
使用Java内置元注解对自定义注解进行注解,并在需要添加元数据的地方添加自定义注解。
1.1. 元注解
Java中内置了四种元注解,专职负责注解其他注解
- @Target
表示该注解可以用于哪些地方,可用的ElementType有:- TYPE 类、接口、枚举等类型
- FIELD 属性说明
- METHOD 类方法
- PARAMETER 参数声明
- CONSTRUCTOR 构造函数
- LOCAL_VARIABLE 本地变量
- ANNOTATION_TYPE 注解类型
- PACKAGE 包
- TYPE_PARAMETER 类型参数(泛型)1.8
- TYPE_USE 类型 1.8
- @Retention
表明在哪个级别保存该注解信息,可选的RetentionPolicy有:- SOURCE
注解对编译器生效,但被编译器丢弃 - CLASS
注解在Class文件中生效,但被JVM丢失 - RUNTIME
在运行时保留注解,因此通过反射机制可以拿到注解信息
- SOURCE
- @Document
将此注解包含到JavaDoc中 - @Inherited
允许子类继承父类的注解
1.2. 自定义注解
1.2.1. 定义注解
下面是一个自动生成DTO的注解,可见注解的定义看起来很像接口定义,事实上,与接口定义一样,注解也会编译成class文件。
除@符外注解定义就像一个空接口,其中@Target和@Retention尤为重要,@Target用于定义注解将用于什么地方,@Retention用来定义该注解在哪个级别上可用。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
@Inherited
@Documented
public @interface GenDTO {
String pkgName();
}
此注解,使用在类型之上,作用于SOURCE,允许子类继承,并将其包含到Javadoc中。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
@Inherited
@Documented
public @interface GenDTOIgnore {
}
此注解为标记注解,没有设置注解元素,只做标记来用。
1.2.2. 注解元素
注解上一般会包含一些元素以表示某值,当处理注解时,程序或工具可用利用这些值。
GenDTO注解上有一个String元素pkgName,用于记录元信息,注解元素可以用的类型包括:
- 所有基本类型(int、float、boolean等)
- String
- Class
- Enum
- Annotation
- 以上类型的数组
如果使用其他类型,编译器会报错。
1.2.3. 注解默认值
编译器对元素的默认值有些过分苛刻。首先元素不能有不确定的值,及元素要么提供默认值,要么在使用的时候提供元素的值;其次,对于非基本类型的元素,都不能以null作为默认值。
这种约束使得处理器很难处理元素值缺失的情况,因为在注解中所有的元素都存在,并且都有相应的值。为了绕开这个限制,我们通常会定义一些特殊的值,如空字符串或负数,以表示该值不存在。
1.2.4. 注解不支持继承
不能用extends来继承某个@interface,这是一件比较遗憾的事。
2. 使用注解
从语法角度看,注解的使用方式几乎与修饰符(public、static、void等)的使用一模一样,一个比较大的区别是注解上可以设置注解元素,以便注解处理器使用。
@GenDTO(pkgName = "com.example.annotation.dto")
public class TestEntity {
private String name;
private Long id;
private Integer age;
private Date birthday;
@GenDTOIgnore
private List address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public List getAddress() {
return address;
}
public void setAddress(List address) {
this.address = address;
}
}
3. 定义注解处理器
常见的注解处理方案主要包括应用于SOURCE阶段的apt(annotation proccess tools)和应用于RUNTIME阶段的反射。
3.1. apt处理器
注解处理工具apt,是Sun公司为了帮助注解处理过程而提供的工具。apt和javac一样,被设计为操作Java源文件,而不是编译后的类。
默认情况下,apt会在处理完源文件后编译他们,如果在系统构建的过程中自动创建了一些新的源文件,该文件会在新一轮的注解处理中接受检查。该工具一轮轮的处理,直到不在有新的源文件产生为止,然后在编译所有的源文件。
我们定义的每一个注解都需要自己的处理器,apt工具可以将多个注解处理器组合起来,已完成比较复杂的应用场景。
在使用apt生成的注解处理器时,我们无法使用Java的反射机制,因为我们操作的是Java源文件,而不是编译后的类。
基于apt的注解处理器开发,主要包括两个步骤:
- 实现Processor接口定制自己的注解处理器
- 添加注解处理器相关配置
3.1.1. 自定义Processor
自定义Processor,需要提供一个公共的无参构造函数,框架工具通过该构造函数实例化Processor,并由工具统一调用。
Processor 交互流程
框架工具与Processor的交互流程如下:
- 如果不使用现有的Processor对象,则通过Processor的无参构造函数创建新的实例对象
- 工具调用init方法对Processor进行初始化
- 工具调用getSupportedAnnotationTypes、getSupportedOptions 和 getSupportedSourceVersion方法,了解注解具体的应用信息。这些方法只在每次运行时调用一次,并非每个处理都调用。
- 如果满足上述条件,调用Processor对象的process方法,进行注解处理。
Processor 核心方法
Processor核心方法如下:
- void init(ProcessingEnvironment processingEnv) 用处理器环境初始化 Processor
- Set
getSupportedAnnotationTypes() 返回此 Processor 支持的注释类型的名称 - SourceVersion getSupportedSourceVersion() 返回此注释 Processor 支持的最新的源版本
- Set
getSupportedOptions() 返回此 Processor 识别的选项 - boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) 处理先前产生的类型元素上的注释类型集,并返回这些注释是否由此 Processor处理
- Iterable extends Completion> getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText) 向工具框架返回某一注释的建议 completion 迭代
AbstractProcessor
一般情况下,我们很少直接实现Processor,而是继承AbstractProcessor,并在它基础上进行扩展。
AbstractProcessor对Processor接口进行了扩展,以方便扩展。
AbstractProcessor核心方法:
- void init(ProcessingEnvironment processingEnv)
环境初始化方法,将processingEnv字段设置为 processingEnv 参数的值 - Set
getSupportedAnnotationTypes() 如果 processor类是使用SupportedAnnotationTypes注释的,则返回一个不可修改的集合,该集合具有与注释相的字符串集 - SourceVersion getSupportedSourceVersion()如果 processor类是使用SupportedSourceVersion注释的,则返回注释中的源版本
- Set
getSupportedOptions() 如果 processor类是使用SupportedOptions注释的,则返回一个不可修改的集合,该集合具有与注释相的字符串集 - Iterable extends Completion> getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText) 返回一个空的 completion 迭代
总体来说,AbstractProcessor主要做了以下几件事:
- init时保存ProcessingEnvironment
- 使用注解方式实现getSupported***相关方法
- getCompletions返回新迭代
至此,对于Processor的实现主要集中在procosse方法。
ProcessingEnvironment
ProcessingEnvironment主要用于Processor初始化,将上下文信息传递到Processor中,以方便后续操作。
核心方法如下:
- Elements getElementUtils() 返回用来在元素上进行操作的某些实用工具方法的实现
- Filer getFiler() 返回用来创建新源、类或辅助文件的 Filer
- Locale getLocale() 返回当前语言环境;如果没有有效的语言环境,则返回null
- Messager getMessager() 返回用来报告错误、警报和其他通知的 Messager
- Map
getOptions()返回传递给注释处理工具的特定于 processor 的选项 - SourceVersion getSourceVersion() 返回任何生成的源和类文件应该符合的源版本
- Types getTypeUtils()返回用来在类型上进行操作的某些实用工具方法的实现
process
这相当于每个处理器的主函数main()。你在这里写你的扫描、收集和处理注解的代码,以及生成Java文件。
一般情况下,process方法的处理逻辑如下:
- 提取注解中的元信息
- 生成java代码,通过拼接字符串或使用成熟的代码生成器生成java代码
- 将java代码通过filer写回工具,等待编译器处理
3.1.2. Processor配置
自定义处理器最终会打包成jar文件,由其他项目在编译环节调用,为了让编译器能识别自定义的Processor,需要使用SPI技术添加相关配置信息。
在META-INF/services目录下,新建javax.annotation.processing.Processor文件,每行一个,将注解器添加到该文件。
如javax.annotation.processing.Processor文件内容:
xxxx.GenCodeBasedEnumConverterProcessor
xxx.GenDTOProcessor
xxx.GenUpdaterProcessor
该文件打包至输出jar中,然后由编译器在编译环节使用。
3.2 反射处理器
Java5中对反射相关的接口进行扩展,用以通过反射机制获取RUNTIME阶段的注解信息,从而实现基于反射的注解处理器。
3.2.1 Annotation
Java使用Annotation类来表示程序中的注解,及所有注解都是Annotation接口的子类。
3.2.2. AnnotatedElement
Java新增AnnotatedElement接口,该接口代表程序中可以接受注解的程序元素,并提供统一的Annotation访问方式,赋予API通过反射获取Annotation的能力,当一个Annotation类型被定义为运行时后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。
AnnotatedElement接口是所有注解元素(Class、Method、Field、Package和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的下列方法来访问Annotation信息:
T getAnnotation(Class annotationClass)返回程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null - Annotation[] getAnnotations() 返回该程序元素上存在的所有注解
- boolean is AnnotationPresent(Class annotationClass) 判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false
- Annotation[] getDeclaredAnnotations() 返回直接存在于此元素上的所有注释。与此接口中的其他方法不同,该方法将忽略继承的注释。(如果没有注释直接存在于此元素上,则返回长度为零的一个数组)该方法的调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响。
AnnotatedElement子类涵盖所有可以出现Annotation的地方,其中包括:
- Constructor 构造函数
- Method 方法
- Class 类型
- Field 字段
- Package 包
- Parameter 参数
- AnnotatedParameterizedType 泛型
- AnnotatedTypeVariable 变量
- AnnotatedArrayType 数组类型
- AnnotatedWildcardType
4. Java注解实战
4.1. 基于apt的代码生成器
现在系统都有严格的分层标准,每层负责不同的职责,以最大程度的解耦,其中最为核心的应该是Domain层(也称领域层),其上是application或service层,为了保证domain层的安全性,不允许直接将其进行暴露,最常用的一种方式便是,将其转化为DTO在并外提供服务。
其中Domain对象与DTO之间结构同步,耗费了很大的人力资源,而且大多情况下都是些机械性的代码,没有什么太大的挑战。而这个场景便是代码生成器所擅长的领域。
4.1.1. 设计目标
通过基于注解的代码生成器,根据Domain对象的结构,自动生成BaseDomainDTO作为父类,用于维护通用的结构,DomainDTO从BaseDomainDTO中继承,在享受通用结构的同时,为特殊需求提供扩展点。
4.1.2. 自定义注解
自定义注解包括GenDTO和GenDTOIgnore。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
@Inherited
@Documented
public @interface GenDTO {
String pkgName();
}
标记于Domain类上,用于说明该类需要生成BaseDomainDTO
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
@Inherited
@Documented
public @interface GenDTOIgnore {
}
注解与Domain中的字段上,在生成BaseDomainDTO时,忽略该字段
4.1.3. 自定义注解处理器
自定义注解处理器,在编译过程中,读取注解信息,并完成BaseDomainDTO的代码生成。
public abstract class BaseProcessor extends AbstractProcessor {
/**
* 需要处理的注解类(GenDTO)
*/
private final Class aClass;
/**
* 用于回写新生成的java文件
*/
private Filer filer;
private Messager messager;
public BaseProcessor(Class aClass) {
this.aClass = aClass;
}
/**
*
* @return
*/
@Override
public Set getSupportedAnnotationTypes() {
return Sets.newHashSet(aClass.getCanonicalName());
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.filer = processingEnv.getFiler();
this.messager = processingEnv.getMessager();
}
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 获取所有标记该注解的元素
Set elements = roundEnv.getElementsAnnotatedWith(this.aClass);
for (Element element : ElementFilter.typesIn(elements)){
A a = (A) element.getAnnotation(this.aClass);
// 循环处理每一个标记对象
foreachClass(a, element, roundEnv);
}
return false;
}
protected abstract void foreachClass(A a, Element element, RoundEnvironment roundEnv);
/**
* 获取元素的所有字段信息
* @param element
* @param filter
* @return
*/
protected Set findFields(Element element, Predicate filter){
return ElementFilter.fieldsIn(element.getEnclosedElements()).stream()
.filter(filter)
.map(variableElement -> new TypeAndName(variableElement))
.collect(Collectors.toSet());
}
/**
* 获取元素中所有的Getter方法
* @param element
* @param filter
* @return
*/
protected Set findGetter(Element element, Predicate filter){
return ElementFilter.methodsIn(element.getEnclosedElements()).stream()
.filter(filter)
.filter(executableElement -> isGetter(executableElement.getSimpleName().toString()))
.map(executableElement -> new TypeAndName(executableElement))
.collect(Collectors.toSet());
}
private boolean isGetter(String s) {
return s.startsWith("id") || s.startsWith("get");
}
/**
* 获取基于注解的忽略过滤器
* @param iClass
* @param
* @return
*/
protected Predicate filterForIgnore(Class iClass){
return new IgnoreFilter(iClass);
}
/**
* 忽略过滤器,如果元素上添加了Ignore注解,对其进行忽略
* @param
*/
protected static class IgnoreFilter implements Predicate{
private final Class iClass;
public IgnoreFilter(Class iClass) {
this.iClass = iClass;
}
@Override
public boolean test(Element variableElement) {
return variableElement.getAnnotation(iClass) == null;
}
}
/**
* 生成Java文件
* @param typeSpecBuilder
* @param pkgName
*/
protected void createJavaFile(TypeSpec.Builder typeSpecBuilder, String pkgName) {
try {
JavaFile javaFile = JavaFile.builder(pkgName, typeSpecBuilder.build())
.addFileComment(" This codes are generated automatically. Do not modify!")
.build();
javaFile.writeTo(filer);
System.out.print(javaFile);
this.messager.printMessage(WARNING, javaFile.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
@Value
protected static class TypeAndName{
private final String name;
private final TypeName type;
public TypeAndName(VariableElement variableElement) {
this.name = variableElement.getSimpleName().toString();
this.type = TypeName.get(variableElement.asType());
}
public TypeAndName(ExecutableElement executableElement) {
this.name = getFieldName(executableElement.getSimpleName().toString());
this.type = TypeName.get(executableElement.getReturnType());
}
private String getFieldName(String s) {
String r = null;
if (s.startsWith("get")){
r = s.substring(3, s.length());
}else if (s.startsWith("is")){
r = s.substring(2, s.length());
}else {
r = s;
}
return r.substring(0, 1).toLowerCase() + r.substring(1, r.length());
}
}
}
BaseProcessor继承自AbstractProcessor,对通用行为进行封装。
@AutoService(Processor.class)
public class GenDTOProcessor extends BaseProcessor {
public GenDTOProcessor() {
super(GenDTO.class);
}
@Override
protected void foreachClass(GenDTO genDTO, Element element, RoundEnvironment roundEnv) {
String className = "Base" + element.getSimpleName().toString() + "DTO";
Set typeAndNames = Sets.newHashSet();
// 获取元素中字段信息
Set fields = findFields(element, filterForIgnore(GenDTOIgnore.class));
typeAndNames.addAll(fields);
// 获取元素中的Getter信息
Set getters = findGetter(element, filterForIgnore(GenDTOIgnore.class));
typeAndNames.addAll(getters);
// 生成Java类, 类名为className, 添加Data注解,并使用public、abstract关键字描述
TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder(className)
.addAnnotation(Data.class)
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT);
// 生成构造函数
MethodSpec.Builder cMethodSpecBuilder = MethodSpec.constructorBuilder()
.addParameter(TypeName.get(element.asType()), "source")
.addModifiers(Modifier.PROTECTED);
for (TypeAndName typeAndName : typeAndNames) {
// 声明BaseDomainDTO中的字段信息,Setter方法设置为private,Getter方法设置为public
FieldSpec fieldSpec = FieldSpec.builder(typeAndName.getType(), typeAndName.getName(), Modifier.PRIVATE)
.addAnnotation(AnnotationSpec.builder(Setter.class)
.addMember("value", "$T.PRIVATE", AccessLevel.class)
.build())
.addAnnotation(AnnotationSpec.builder(Getter.class)
.addMember("value", "$T.PUBLIC", AccessLevel.class)
.build())
.build();
typeSpecBuilder.addField(fieldSpec);
String fieldName = typeAndName.getName().substring(0, 1).toUpperCase() + typeAndName.getName().substring(1, typeAndName.getName().length());
// 构造函数中添加设置语句
cMethodSpecBuilder.addStatement("this.set$L(source.get$L())", fieldName, fieldName);
}
// 将构造函数添加到类中
typeSpecBuilder.addMethod(cMethodSpecBuilder.build());
// 生成Java文件
String pkgName = genDTO.pkgName();
createJavaFile(typeSpecBuilder, pkgName);
}
}
生成BaseDomainDTO的核心逻辑全部在GenDTOProcessor中,详见内部注解。
有一个点需要特殊注意,GenDTOProcessor类上添加了@AutoService(Processor.class)注解,这是google auto-service中的注解,用于完成Processor服务的自动注册,及在META-INF/services/javax.annotation.processing.Processor文件中添加com.example.annotation.dto.GenDTOProcessor配置内容。
4.1.4. 使用注解
根据实际需求,在对于的class中添加注解即可。
/**
* 设置BaseTestEntityDTO所存在的包
*/
@GenDTO(pkgName = "com.example.annotation.dto")
public class TestEntity {
private String name;
private Long id;
private Integer age;
private Date birthday;
/**
* 忽略该字段
*/
@GenDTOIgnore
private List address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
@GenDTOIgnore
public List getAddress() {
return address;
}
public void setAddress(List address) {
this.address = address;
}
}
执行maven编译,自动生成BaseTestEntityDTO,并自动编译。
生成代码如下:
// This codes are generated automatically. Do not modify!
package com.example.annotation.dto;
import com.example.annotation.entity.TestEntity;
import java.lang.Integer;
import java.lang.Long;
import java.lang.String;
import java.util.Date;
import java.util.List;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
@Data
public abstract class BaseTestEntityDTO {
@Setter(AccessLevel.PRIVATE)
@Getter(AccessLevel.PUBLIC)
private Integer age;
@Setter(AccessLevel.PRIVATE)
@Getter(AccessLevel.PUBLIC)
private Date birthday;
@Setter(AccessLevel.PRIVATE)
@Getter(AccessLevel.PUBLIC)
private Long id;
@Setter(AccessLevel.PRIVATE)
@Getter(AccessLevel.PUBLIC)
private String name;
protected BaseTestEntityDTO(TestEntity source) {
this.setAge(source.getAge());
this.setBirthday(source.getBirthday());
this.setId(source.getId());
this.setName(source.getName());
}
}
此时便可以据此创建TestEntityDTO。
@Data
public class TestEntityDTO extends BaseTestEntityDTO{
private List address;
private TestEntityDTO(TestEntity source) {
super(source);
this.address = Lists.newArrayList(source.getAddress());
}
}
对于需要特殊处理的属性,进行特殊处理。如address的copy。
4.2. Spring基于反射的RequestMapping
Spring一直是java注解的使用大户,特别是RUNTIME级别、基于反射的注解,几乎无处不在。
4.2.1. 设计目标
模拟SpringMVC中的RequestMapping注解,收集path与method的映射关系。
4.2.2. 自定义注解
自定义RequestMappiing注解。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RequestMapping {
String path() default "";
}
Retention设置为RUNTIME,以通过反射获取注解信息。
4.2.3. 自定义注解处理器
public class RequestMappingParser {
private final Class tClass;
public RequestMappingParser(Class tClass){
this.tClass = tClass;
}
public Set parse(){
RequestMapping cRequestMapping = this.tClass.getAnnotation(RequestMapping.class);
String rootPath = cRequestMapping != null ? cRequestMapping.path() : "";
if (!rootPath.startsWith("/")){
rootPath = "/" + rootPath;
}
Set parserResults = Sets.newHashSet();
for (Method method : this.tClass.getMethods()){
RequestMapping mRequestMapping = method.getAnnotation(RequestMapping.class);
if (mRequestMapping == null){
continue;
}
String mPath = mRequestMapping.path();
if ("".equals(mPath)){
continue;
}
String path = null;
if (mPath.startsWith("/")){
path = rootPath + mPath;
}else {
path = rootPath + "/" + mPath;
}
parserResults.add(new ParserResult(path, method));
}
return parserResults;
}
@Data
static class ParserResult{
private final String path;
private final Method method;
}
}
4.2.4. 使用注解
@RequestMapping(path = "/root")
public class RequestMappingObject {
@RequestMapping(path = "method")
public void method(){
}
@RequestMapping(path = "method1")
public void method1(){
}
@RequestMapping(path = "method2")
public void method2(){
}
@RequestMapping(path = "method3")
public void method3(){
}
@RequestMapping(path = "method4")
public void method4(){
}
}
4.2.5. 处理结果
public class RequestMappingTest {
@Test
public void print(){
new RequestMappingParser<>(RequestMappingObject.class)
.parse()
.forEach(parserResult -> {
System.out.print(parserResult.getPath());
System.out.print("-->");
System.out.print(parserResult.getMethod());
System.out.println();
});
}
}
运行结果如下:
/root/method2-->public void com.example.annotation.reflect.RequestMappingObject.method2()
/root/method3-->public void com.example.annotation.reflect.RequestMappingObject.method3()
/root/method-->public void com.example.annotation.reflect.RequestMappingObject.method()
/root/method4-->public void com.example.annotation.reflect.RequestMappingObject.method4()
/root/method1-->public void com.example.annotation.reflect.RequestMappingObject.method1()
5. 总结
总体来说,注解从两个层面简化了硬编码的工作量,给一些反复工作提供了另一种解决方案。基于注解的代码生成器便是这个领域的一大利器;注解与反射的结合,可以在运行时获取注解元数据,给程序的动态扩展提供了一个想象空间,特别是Spring等框架,将其用到了极致。
文章所用代码已经上传至码云,如有需要请自行下载:https://gitee.com/litao851025/books/tree/master/annotation-demo