注解的基本原理
自从技术架构从原本的繁杂 XML 配置式转成如今的微服务架构,其中大量使用的注解实现的配置让我产生了极大的兴趣。因为当我们点击进入注解的内部,哪怕是下载了源码,也都看不见具体实现的代码,但是确实的产生了作用。哪么注解在Java中是如何实现的呢?
1、注解是什么?
注解是一种标记式配置的语法糖
,其主要的作用是解耦,使各个模块之间实现松耦合,最早在JDK 1.5的时候被提出。方法上可以进行注解,类上也可以注解,字段属性上也可以注解,反正几乎需要配置的地方都可以进行注解。
2、注解的本质
查看源码 java.lang.annotation.Annotation
在JavaDoc下面有一句话
/**
* The common interface extended by all annotation types. Note that an
* interface that manually extends this one does <i>not</i> define
* an annotation type.
意思是说所有的注解类型都继承自这个公共接口(Annotation)。
再来看看我们经常使用的 @Override
注解的定义:
* @author Peter von der Ahé
* @author Joshua Bloch
* @jls 9.6.1.4 @Override
* @since 1.5
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
这是注解 @Override
的定义,其实它本质上就是:
public interface Override extends Annotation {
};
看到这里,我们可以下个定义,所谓的注解就是继承了注解接口的接口。
看到这里你或许又会问,他只是一个接口,没有看到具体的实现,那它是如何工作的呢?毕竟现在看来它只不是过是一个特殊的注释而已。
在 Java 中解析一个类或者方法的注解往往有两种形式,一种是编译期直接的扫描,一种是运行期反射(我们自定义注解使用的方式,后面会讲)。反射的事情我们待会说,而编译器的扫描指的是编译器在对 java 代码编译字节码的过程中会检测到某个类或者方法被一些注解修饰,这时它就会对于这些注解进行某些处理 。
上面讲的@Override
是编译期扫描的,在程序进行编译时,编译器会自动检查改子类的方法是否有向应的父类方法。而支持编译期产生作用的一般只支持JDK自带的几个注解。而我们自定义的注解,编译器是不会发现和执行的,这时需要RUNTIME
运行期间执行才能产生作用。
3、元注解
除了直接使用JDK 定义好的注解,我们还可以自定义注解,在JDK 1.5中提供了4个标准的用来对注解类型进行注解的注解类,我们称之为 meta-annotation(元注解 ),元注解是注解的注解,常用于定义注解,在Java中有四个元注解:
@Target:注解的作用目标
@Retention:注解的生命周期
@Documented:注解是否应当被包含在 JavaDoc 文档中
@Inherited:是否允许子类继承该注解
@Target注解的定义如下:
* @since 1.5
* @jls 9.6.4.1 @Target
* @jls 9.7.4 Where Annotations May Appear
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}
ElementType[] value()
是其方法,可以使用 枚举ElementType.XX
来为这个泛型传值。用于指定该注解的作用对象,常用的作用对象有如下:
ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上
ElementType.FIELD:允许作用在属性字段上
ElementType.METHOD:允许作用在方法上
ElementType.PARAMETER:允许作用在方法参数上
ElementType.CONSTRUCTOR:允许作用在构造器上
ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
ElementType.ANNOTATION_TYPE:允许作用在注解上
ElementType.PACKAGE:允许作用在包上
@Retention 用于指明当前注解的生命周期,它的基本定义如下:
* @author Joshua Bloch
* @since 1.5
* @jls 9.6.3.2 @Retention
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
RetentionPolicy value()
是其方法,可以使用 枚举RetentionPolicy.XX
来为这个泛型传值。用于指定该注解的声明周期,常用的枚举生命周期有如下:
RetentionPolicy.SOURCE:当前注解编译期可见,不会写入 class 文件,编译的时候会被擦除
RetentionPolicy.CLASS:类加载阶段丢弃,会写入 class 文件
RetentionPolicy.RUNTIME:永久保存,可以反射获取
@Retention
注解指定了被修饰的注解的生命周期,一种是只能在编译期可见,编译后会被丢弃,一种会被编译器编译进 class 文件中,无论是类或是方法,乃至字段,他们都是有属性表的,而 JAVA 虚拟机也定义了几种注解属性表用于存储注解信息,但是这种可见性不能带到方法区,类加载时会予以丢弃,最后一种则是永久存在的可见性。
剩下还有@Documented
和@Inherited
注解我们平常用得比较少,第一个注解声明当我们执行 JavaDoc 文档打包时会被保存进 doc 文档 ,而第二个注解声明这个注解是可以被继承的。
4、JDK内置的注解
JDK 内置了三个常用注解,它们分别是 @Override、@Deprecated 和@SuppressWarnings 。
@Deprecated 源码定义:
* @author Neal Gafter
* @since 1.5
* @jls 9.6.3.6 @Deprecated
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
根据上面讲到的知识,我们知道@Deprecated
这个注解是永久存在的,并且作用范围很广。这个注解主要是标记当前的类或者方法或者字段等已经不再被推荐使用了,可能下一次的 JDK 版本就会删除。
@SuppressWarnings 源码定义
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
@SuppressWarnings
注解主要用来压制警告,它有一个 value 属性需要你主动的传值,这个 value 代表一个什么意思呢?这个 value 代表的就是需要被压制的警告类型 。
5、自定义注解
除了上述JDK自带的注解之外,为了满足业务需求,为系统业务实现松耦合,我们可以通过自己实现自定义注解的方式来进行配置。从而使系统的可维护性大大提升。
自定义注解的实现基础依赖于 Java 的反射机制,即通过反射机制来获取类的所有信息,可以动态的创建对象和编译 。而我们自定义的注解信息则也是通过反射机制从虚拟机中获取注解所标注的信息,从而达到获取注解标注的配置结果。
前面我们说过,注解的本质是继承了`` Annotation接口的接口,而当我们通过反射,也就是我们这里的
getAnnotation()` 方法去获取一个注解类实例的时候,其实 JDK 是通过动态代理机制生成一个实现我们注解(接口)的代理类。 (关于动态代理目前还没深究,之后可以研究一下)
AnnotationInvocationHandler
是 JAVA 中专门用于处理注解的 Handler ,下面看下该类的源码:
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private static final long serialVersionUID = 6182022883658399397L;
private final Class<? extends Annotation> type;//Annotation类本身或子类
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.");
}
}
//动态代理的 invoke方法
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 {
AnnotationInvocationHandler
这个类有两个值得关注的成员变量,分别是 Map类型memberValues
和Method数组memberMethods
。
memberValues
:键是我们注解属性名称,值就是该属性当初被赋上的值 。也就是说我们标记的值在这里可以找到。memberMethods
:是我们标记的方法集。
invoke方法:我们的代理类代理了 注解接口中所有的方法,所以对于代理类中任何方法的调用都会被转到这里来。
至此,自定义注解完成了调用并产生作用。
6、自定义注解例子上手
知道自定义注解的使用,我们可以用自定义注解的方式实现日志的保存,代码如下:
面向切面编程往往让我们的开发更加低耦合,也大大减少了代码量,同时可以让我们更加专注于月微模块的开发,提取无关的东西,便于后期维护迭代。
1、新建一个SpringBoot工程
2、添加必须的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
可选的依赖
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
3、日志实体类
package com.space.aspect.bo;
import lombok.Data;
/**
* 系统日志bo
* @author zhuzhe
* @date 2018/6/4 9:36
* @email 1529949535@qq.com
*/
@Data
public class SysLogBO {
private String className;
private String methodName;
private String params;
private Long exeuTime;
private String remark;
private String createDate;
}
4、日志实现service
package com.space.aspect.service;
import com.space.aspect.bo.SysLogBO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* @author zhuzhe
* @date 2018/6/4 9:41
* @email 1529949535@qq.com
*/
@Slf4j
@Service
public class SysLogService {
public boolean save(SysLogBO sysLogBO){
// 这里就不做具体实现了
log.info(sysLogBO.getParams());
return true;
}
}
5、声明切面,完成日志记录
package com.space.aspect.aspect;
import com.google.gson.Gson;
import com.space.aspect.anno.SysLog;
import com.space.aspect.bo.SysLogBO;
import com.space.aspect.service.SysLogService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 系统日志切面
* @author zhuzhe
* @date 2018/6/4 9:27
* @email 1529949535@qq.com
*/
@Aspect // 使用@Aspect注解声明一个切面
@Component
public class SysLogAspect {
@Autowired
private SysLogService sysLogService;
/**
* 这里我们使用注解的形式
* 当然,我们也可以通过切点表达式直接指定需要拦截的package,需要拦截的class 以及 method
* 切点表达式: execution(...)
*/
@Pointcut("@annotation(com.space.aspect.anno.SysLog)")
public void logPointCut() {}
/**
* 环绕通知 @Around , 当然也可以使用 @Before (前置通知) @After (后置通知)
* @param point
* @return
* @throws Throwable
*/
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
Object result = point.proceed();
long time = System.currentTimeMillis() - beginTime;
try {
saveLog(point, time);
} catch (Exception e) {
}
return result;
}
/**
* 保存日志
* @param joinPoint
* @param time
*/
private void saveLog(ProceedingJoinPoint joinPoint, long time) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SysLogBO sysLogBO = new SysLogBO();
sysLogBO.setExeuTime(time);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
sysLogBO.setCreateDate(dateFormat.format(new Date()));
SysLog sysLog = method.getAnnotation(SysLog.class);
if(sysLog != null){
//注解上的描述
sysLogBO.setRemark(sysLog.value());
}
//请求的 类名、方法名 通过反射获得
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
sysLogBO.setClassName(className);
sysLogBO.setMethodName(methodName);
//请求的参数
Object[] args = joinPoint.getArgs();
try{
List<String> list = new ArrayList<String>();
for (Object o : args) {
list.add(new Gson().toJson(o));
}
sysLogBO.setParams(list.toString());
}catch (Exception e){ }
sysLogService.save(sysLogBO);
}
}
6、测试
package com.space.aspect.controller;
import com.space.aspect.anno.SysLog;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author zhuzhe
* @date 2018/6/4 9:47
* @email 1529949535@qq.com
*/
@RestController
public class TestController {
@SysLog("测试")
@GetMapping("/test")
public String test(@RequestParam("name") String name){
return name;
}
}
本文部分转载自 :
参考:https://blog.csdn.net/yaomingyang/article/details/80981004
参考:https://blog.csdn.net/hanruikai/article/details/78812976
参考:https://blog.csdn.net/wswswang/article/details/52373677
参考:https://blog.csdn.net/pengjunlee/article/details/79683621