一、什么是注解?
Annontation是Java5开始引入的新特征,中文名称叫注解。它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。为程序的元素(类、方法、成员变量)加上更直观更明了的说明,这些说明信息是与程序的业务逻辑无关,并且供指定的工具或框架使用。Annontation像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。
Java注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。包含在 java.lang.annotation 包中。
「注解的本质就是一个继承了 Annotation 接口的接口」
二、注解的用处
- 生成文档。这是最常见的,也是java 最早提供的注解。常用的有@param @return 等
- 跟踪代码依赖性,实现替代配置文件功能。比如Dagger 2 依赖注入,未来java 开发,将大量注解配置,具有很大用处;
- 在编译时进行格式检查。如@override 放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出。
三、元注解
java.lang.annotation 提供了四种元注解,专门注解其他的注解(在自定义注解的时候,需要使用到元注解):
@Documented – 注解是否将包含在JavaDoc中
@Retention – 什么时候使用该注解
@Target – 注解用于什么地方
@Inherited – 是否允许子类继承该注解
1、@Retention – 定义该注解的生命周期
public enum RetentionPolicy {
/**
* 在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。
* @Override, @SuppressWarnings都属于这类注解。
*/
SOURCE,
/** 在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式 */
CLASS,
/**
* 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。
* 我们自定义的注解通常使用这种方式。
*/
RUNTIME
}
2、Target – 表示该注解用于什么地方。默认值为任何元素,表示该注解用于什么地方。可用的ElementType 参数包括:
public enum ElementType {
/** 用于描述类、接口(包括注解类型) 或enum声明 */
TYPE,
/** 成员变量、对象、属性(包括enum实例) */
FIELD,
/** 用于描述方法 */
METHOD,
/** 用于描述参数 */
PARAMETER,
/** Constructor declaration */
CONSTRUCTOR,
/** 用于描述局部变量 */
LOCAL_VARIABLE,
/** 注释类型声明 */
ANNOTATION_TYPE,
/** 用于描述包 */
PACKAGE,
/** 表示该注解能使用在自定义类型参数 jdk1.8引入 */
TYPE_PARAMETER,
/** 表示该注解能使用在使用类型的任意语句中
* 包括示例创建时、类型强转时、方法形参中、泛型中使用
* jdk1.8引入
*/
TYPE_USE
}
3、@Documented – 一个简单的Annotations 标记注解,表示是否将注解信息添加在java 文档中。
4、@Inherited – 定义该注解和子类的关系。
@Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的annotation 类型被用于一个class,则这个annotation 将被用于该class 的子类。
四、JDK三个内置注解
1、@Override 限定重写父类方法, 实现接口方法。该注解只能用于方法
2、@Deprecated 用于表示所修饰的元素(类, 方法,构造器等等)已过时。
3、@SuppressWarnings 抑制编译器警告。它不是一个标记类型注解。
它有一个类型为String[] 的成员,这个成员的值为被禁止的警告名。
如:@SuppressWarnings("unchecked")
五、注解的原理
注解本质是一个继承了Annotation 的特殊接口,其具体实现类是Java 运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java 运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler 的invoke 方法。该方法会从memberValues 这个Map 中索引出对应的值。而memberValues 的来源是Java 常量池。
六、注解原理探究
上述内容我们简单介绍了注解的原理,也提到,「注解的本质就是一个继承了 Annotation 接口的接口」,现在我们就来从虚拟机的层面看看,注解的本质到底是什么。
首先,我们自定义一个注解类型:
/**
* 注解原理解析-测试用
* @author xiongxy
* @Date 2021/9/22 20:41
*/
@Target(value = {ElementType.METHOD,ElementType.FIELD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Hello {
String value();
}
这里我们指定了 Hello 这个注解只能修饰字段和方法,并且该注解永久存活,以便我们反射获取。虚拟机规范定义了一系列和注解相关的属性表,也就是说,无论是字段、方法或是类本身,如果被注解修饰了,就可以被写进字节码文件。属性表有以下几种:
- RuntimeVisibleAnnotations:运行时可见的注解
- RuntimeInVisibleAnnotations:运行时不可见的注解
- RuntimeVisibleParameterAnnotations:运行时可见的方法参数注解
- RuntimeInVisibleParameterAnnotations:运行时不可见的方法参数注解
- AnnotationDefault:注解类元素的默认值
给大家看虚拟机的这几个注解相关的属性表的目的在于,让大家从整体上构建一个基本的印象,注解在字节码文件中是如何存储的。所以,对于一个类或者接口来说,Class 类中提供了以下一些方法用于反射注解。
- getAnnotation:返回指定的注解
- isAnnotationPresent:判定当前元素是否被指定注解修饰
- getAnnotations:返回所有的注解
- getDeclaredAnnotation:返回本元素的指定注解
- getDeclaredAnnotations:返回本元素的所有注解,不包含父类继承而来的
下面看一个完整的例子。
首先,设置一个虚拟机启动参数,用于捕获 JDK 动态代理类。
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
然后main函数
/**
* 注解原理解析
* @author xiongxy
* @Date 2021/9/22 20:40
*
* 注意:需要设置一个虚拟机启动参数,用于捕获 JDK 动态代理类。
* -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
*/
public class Test {
@Hello("hello")
public static void main(String[] args) throws NoSuchMethodException{
Class cls = Test.class;
Method method = cls.getMethod("main", String[].class);
Hello annotation = method.getAnnotation(Hello.class);
}
}
当我们通过反射,也就是我们这里的 getAnnotation 方法去获取一个注解类实例的时候,其实 JDK 是通过动态代理机制生成一个实现我们注解(接口)的代理类。
我们运行程序后,会看到输出目录里有这么一个代理类,反编译之后是这样的:
代理类实现接口 Hello 并重写其所有方法,包括 value 方法以及接口 Hello 从 Annotation 接口继承而来的方法。
而这个关键的 InvocationHandler 实例是谁?我们点进去看看
上图中第三个,AnnotationInvocationHandler 是 JAVA 中专门用于处理注解的 Handler, 这个类的设计也非常有意思。
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private static final long serialVersionUID = 6182022883658399397L;
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
private transient volatile Method[] memberMethods = null;
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
这里有一个 memberValues,它是一个 Map 键值对,键是我们注解属性名称,值就是该属性当初被赋上的值。
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
byte var7 = -1;
switch(var4.hashCode()) {
case -1776922004:
if (var4.equals("toString")) {
var7 = 0;
}
break;
case 147696667:
if (var4.equals("hashCode")) {
var7 = 1;
}
break;
case 1444986633:
if (var4.equals("annotationType")) {
var7 = 2;
}
}
switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4);
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}
return var6;
}
}
}
}
大家注意看,我们的代理类代理了 Hello 接口中所有的方法,所以对于代理类中任何方法的调用都会被转到这里来。
var2 指向被调用的方法实例,而这里首先用变量 var4 获取该方法的简明名称,接着 switch 结构判断当前的调用方法是谁,如果是 Annotation 中的四大方法,将 var7 赋上特定的值。
如果当前调用的方法是 toString,equals,hashCode,annotationType 的话,AnnotationInvocationHandler 实例中已经预定义好了这些方法的实现,直接调用即可。
那么假如 var7 没有匹配上这四种方法,说明当前的方法调用的是自定义注解自己声明的方法,例如我们 Hello 注解的 value 方法。这种情况下,将从我们的注解 map 中获取这个注解属性对应的值。
最后,我们总结一下整个反射注解的原理:
首先,我们通过键值对的形式可以为注解属性赋值,像这样:@Hello(value = "hello")。
接着,你用注解修饰某个元素,编译器将在编译期扫描每个类或者方法上的注解,会做一个基本的检查,你的这个注解是否允许作用在当前位置,最后会将注解信息写入元素的属性表。
然后,当你进行反射的时候,虚拟机将所有生命周期在 RUNTIME 的注解取出来放到一个 map 中,并创建一个 AnnotationInvocationHandler 实例,把这个 map 传递给它。
最后,虚拟机将采用 JDK 动态代理机制生成一个目标注解的代理类,并初始化好处理器。
那么这样,一个注解的实例就创建出来了,它本质上就是一个代理类,我们应当去理解好 AnnotationInvocationHandler 中 invoke 方法的实现逻辑,这是核心。
一句话概括就是,通过方法名返回注解属性值。
七、自定义注解示例
下面我们通过定义两个自定义注解,借助 aspectj 的环绕通知来实现对业务表中的字典编码自动映射成字典值。
首先,我们定义两个自定义注解:ReplaceField属性注解,与ReplaceMethod 方法注解
/**
* 属性注解
* @author xiongxy
* @since 2021-09-11
*/
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReplaceField {
String name() default "";
String type() default ReplaceFieldType.FIELD;
}
/**
* 方法注解
* @author xiongxy
* @since 2021-09-11
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReplaceMethod {
}
接下来,我们定义一个切面
/**
* 处理枚举字段替换切面
*
* @author Xiongxy
* @date 2021-08-25
*/
@Slf4j
@Aspect
@Component
public class ReplaceFieldAspect {
/**
* 处理器工厂
*/
@Autowired
private ReplaceFieldFactory facotry;
/**
* 切点,使用注解的地方
*/
@Pointcut("@annotation(com.tuling.xxy.annotation.ReplaceMethod)")
public void init() {
}
/**
* 环绕通知,获取替换枚举的类型,替换枚举返回值
*
* @param point 连接点
* @return 返回值
*/
@Around("init()")
public Object doAfterRunning(ProceedingJoinPoint point) {
// 获取返回值
Object retVal = null;
try {
// 获取返回值
retVal = point.proceed();
// 获取返回对象的类型
String className = retVal.getClass().getName().substring(retVal.getClass().getName().lastIndexOf(".") + 1);
// 对应handler处理
ReplaceFieldHandler handler = facotry.get(ReplaceHandlerEnum.convert(className));
handler.replace(retVal);
return retVal;
} catch (Throwable throwable) {
log.error("替换枚举变量失败。", throwable);
}
return retVal;
}
}
我们还需要定义一个工厂处理类
@Slf4j
@Component
public class ReplaceFieldFactory {
@Autowired
DictionaryTestService dictionaryTestService;
@Autowired
RedisTemplate redisTemplate;
/**
* map,存放handler
*/
private Map<String, ReplaceFieldHandler> handlerMap = new HashMap<>();
/**
* 注册handler
*
* @param key handler key
* @param handler handler
*/
public void register(String key, ReplaceFieldHandler handler) {
if (handlerMap.containsKey(key)) {
log.error("handler is already exists!");
return;
}
handlerMap.put(key, handler);
}
/**
* 获取handler
*
* @param key key
* @return handler
*/
public ReplaceFieldHandler get(String key) {
return handlerMap.getOrDefault(key, null);
}
/**
* 替换数据的值
*
* @param obj 要替换的对象
*/
public void replace(Object obj) {
Class<?> objClass = obj.getClass();
Field[] declaredFields = objClass.getDeclaredFields();
for (Field field : declaredFields) {
ReplaceField annotation = field.getAnnotation(ReplaceField.class);
if (annotation != null) {
String type = annotation.type();
String name =
StringUtils.isNotEmpty(annotation.name()) ? annotation.name() : field.getName();
if (ReplaceFieldType.FIELD.equals(type)) {
field.setAccessible(true);
try {
Object fieldObj = field.get(obj);
if (null != fieldObj) {
String oldValue = fieldObj.toString();
if (oldValue.contains(",")) {
String[] split = oldValue.split(",");
StringBuilder value = new StringBuilder();
for (String str : split) {
String newValue = convert(name, str);
value.append(newValue).append(",");
}
field.set(obj,
value.toString().substring(0, value.lastIndexOf(",")));
} else {
String newValue = convert(name, fieldObj.toString());
field.set(obj, newValue);
}
}
} catch (IllegalAccessException e) {
log.error("替换枚举值失败。", e);
}
} else {
try {
field.setAccessible(true);
replace(field.get(obj));
} catch (IllegalAccessException e) {
log.error("替换枚举值失败。", e);
}
}
}
}
}
/**
* 通过类型和value获取新值
*
* @param type dic_group
* @param old dic_value
* @return 新值,没有数据返回旧值
*/
private String convert(String type, String old) {
String dicGroup = "";
if (ReplaceFieldConstant.dicGroupMap.containsKey(type)) {
dicGroup = ReplaceFieldConstant.dicGroupMap.getOrDefault(type, null);
} else {
dicGroup = CodeStringUtil.camel2Underline(type);
}
if (StringUtils.isNotBlank(dicGroup)) {
log.info("convert -- "+dicGroup+"_"+old);
DictionaryTest dicInfo = (DictionaryTest) redisTemplate
.boundValueOps(dicGroup + "_" + old).get();
if (null != dicInfo) {
return dicInfo.getDicName();
}
}
return old;
}
}
最后我们在需要转换的属性和方法上添加注解即可:
/**
* 实体类属性上加属性注解
*/
@ReplaceField(name = ReplaceFieldConstant.AGENCY_TYPE)
private String type;
/**service层业务方法上加方法注解*/
@Override
@ReplaceMethod
public List<AgencyTest> getAll() {
return agencyTestMapper.selectList(new QueryWrapper<>());
}
字典表数据:
业务表数据:
最终效果:
[
{
"id": 1,
"name": "代办1",
"title": "代办1标题",
"type": "任务代办,提醒代办,快速代办,个人代办",
"createTm": 1631541753000,
"updateTm": 1631541753000
},
{
"id": 2,
"name": "代办2",
"title": "代办2标题",
"type": "提醒代办",
"createTm": 1631368975000,
"updateTm": 1631368975000
}
......
]
完整项目我已上传到GitHub:
GitHub - 1989lili/interface-test: 通过自定义注解 + aspectj 的环绕通知来实现对业务表中的字典编码自动映射成字典值。
本篇参考文章: