餐考资料:
注解解析
注解用途

1.注解的实质

所有的注解类型都继承自这个普通的接口(Annotation)

如, @Override 的定义,其实它本质上就是:

1
2
3
public interface Override extends Annotation{

}

2.元注解

『元注解』是用于修饰注解的注解,通常用在注解的定义上

  • @Target:注解的作用目标
  • @Retention:注解的生命周期
  • @Documented:注解是否应当被包含在 JavaDoc 文档中
  • @Inherited:是否允许子类继承该注解
@Target 用于指明被修饰的注解最终可以作用的目标是谁,也就是指明,你的注解到底是用来修饰方法的?修饰类的?还是用来修饰字段属性的。

我们可以通过以下的方式来为这个 value 传值:

@Target(value = {ElementType.FIELD})

被这个 @Target 注解修饰的注解将只能作用在成员字段上,不能用于修饰方法或者类。

其中,ElementType 是一个枚举类型,有以下一些值:

  • ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上
  • ElementType.FIELD:允许作用在属性字段上
  • ElementType.METHOD:允许作用在方法上
  • ElementType.PARAMETER:允许作用在方法参数上
  • ElementType.CONSTRUCTOR:允许作用在构造器上
  • ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
  • ElementType.ANNOTATION_TYPE:允许作用在注解上
  • ElementType.PACKAGE:允许作用在包上
@Retention 用于指明当前注解的生命周期

同样的,它也有一个 value 属性:

@Retention(value = RetentionPolicy.RUNTIME

RetentionPolicy 依然是一个枚举类型,它有以下几个枚举值可取:

  • RetentionPolicy.SOURCE:当前注解编译期可见,不会写入 class 文件
  • RetentionPolicy.CLASS:类加载阶段丢弃,会写入 class 文件
  • RetentionPolicy.RUNTIME:永久保存,可以反射获取

@Retention 注解指定了被修饰的注解的生命周期,

  1. 是只能在编译期可见,编译后会被丢弃
  2. 会被编译器编译进 class 文件中,无论是类或是方法,乃至字段,他们都是有属性表的,而 JAVA 虚拟机也定义了几种注解属性表用于存储注解信息,但是这种可见性不能带到方法区,类加载时会予以丢弃
  3. 最后一种则是永久存在的可见性。
@Documented 注解修饰的注解,当我们执行 JavaDoc 文档打包时会被保存进 doc 文档,反之将在打包时丢弃。
@Inherited 注解修饰的注解是具有可继承性的,也就说我们的注解修饰了一个类,而该类的子类将自动继承父类的该注解。

3.JAVA 的内置三大注解

除了上述四种元注解外,JDK 还为我们预定义了另外三种注解,它们是:

  • @Override
  • @Deprecated
  • @SuppressWarnings
@Override 注解:该方法已被替代,它的定义如下:
1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

它没有任何的属性,所以并不能存储任何其他信息。它只能作用于方法之上,编译结束后将被丢弃。是一种典型的『标记式注解』,仅被编译器可知。

@Deprecated :标记当前的类或者方法或者字段等已经不再被推荐使用了,可能下一次的 JDK 版本就会删除

@SuppressWarnings 主要用来压制 java 的警告,它的基本定义如下:


value 属性需要你主动的传值,这个 value 代表的就是需要被压制的警告类型。

4.注解自定义及其应用

只有@Retention定义为RetentionPolicy.RUNTIME时,我们才能通过注解反射获取到注解。

1.自定义一个注解
  1. 自定义注解
1
2
3
4
5
6
@Target(ElementType.FIELD)  //  注解用于字段上
@Retention(RetentionPolicy.RUNTIME) // 保留到运行时,可通过注解获取
public @interface MyField {
String description();
int length();
}
  1. 通过反射获取注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyFieldTest {

//使用我们的自定义注解
@MyField(description = "用户名", length = 12)
private String username;

@Test
public void testMyField(){

// 获取类模板
Class c = MyFieldTest.class;

// 获取所有字段
for(Field f : c.getDeclaredFields()){
// 判断这个字段是否有MyField注解
if(f.isAnnotationPresent(MyField.class)){
MyField annotation = f.getAnnotation(MyField.class);
System.out.println("字段:[" + f.getName() + "], 描述:[" + annotation.description() + "], 长度:[" + annotation.length() +"]");
}
}

}
}
2.应用场景一:自定义注解+拦截器 实现登录校验
  1. 我们使用springboot拦截器实现这样一个功能,如果方法上加了@LoginRequired,则提示用户该接口需要登录才能访问,否则不需要登录。 首先定义一个LoginRequired注解
1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {

}
  1. 然后写两个简单的接口,访问sourceA,sourceB资源
1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class IndexController {

@GetMapping("/sourceA")
public String sourceA(){
return "你正在访问sourceA资源";
}

@GetMapping("/sourceB")
public String sourceB(){
return "你正在访问sourceB资源";
}
}
  1. 没添加拦截器之前成功访问
  2. 实现spring的HandlerInterceptor 类先实现拦截器,但不拦截,只是简单打印日志,如下:
1
2
3
4
5
6
7
public class SourceAccessInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("进入拦截器了");
return true;
}
}
  1. 实现spring类WebMvcConfigurer,创建配置类把拦截器添加到拦截器链中
1
2
3
4
5
6
7
@Configuration
public class InterceptorTrainConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SourceAccessInterceptor()).addPathPatterns("/**");
}
}

此时拦截成功!

  1. 在sourceB方法上添加我们的登录注解@LoginRequired
1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class IndexController {
@GetMapping("/sourceA")
public String sourceA(){
return "你正在访问sourceA资源";
}
@LoginRequired
@GetMapping("/sourceB")
public String sourceB(){
return "你正在访问sourceB资源";
}
}
  1. 简单实现登录拦截逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("进入拦截器了");

// 反射获取方法上的LoginRequred注解
HandlerMethod handlerMethod = (HandlerMethod)handler;
LoginRequired loginRequired = handlerMethod.getMethod().getAnnotation(LoginRequired.class);
if(loginRequired == null){
return true;
}

// 有LoginRequired注解说明需要登录,提示用户登录
response.setContentType("application/json; charset=utf-8");
response.getWriter().print("你访问的资源需要登录");
return false;
}

运行成功,访问sourceB时需要登录了,访问sourceA则不用登录

3.应用场景二:自定义注解+AOP 实现日志打印
  1. 先导入切面需要的依赖包
  2. 定义一个注解@MyLog
1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {

}
  1. 定义一个切面类,见如下代码注释理解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Aspect // 1.表明这是一个切面类
@Component
public class MyLogAspect {

// 2. PointCut表示这是一个切点,@annotation表示这个切点切到一个注解上,后面带该注解的全类名
// 切面最主要的就是切点,所有的故事都围绕切点发生
// logPointCut()代表切点名称
@Pointcut("@annotation(me.zebin.demo.annotationdemo.aoplog.MyLog)")
public void logPointCut(){};

// 3. 环绕通知
@Around("logPointCut()")
public void logAround(ProceedingJoinPoint joinPoint){
// 获取方法名称
String methodName = joinPoint.getSignature().getName();
// 获取入参
Object[] param = joinPoint.getArgs();

StringBuilder sb = new StringBuilder();
for(Object o : param){
sb.append(o + "; ");
}
System.out.println("进入[" + methodName + "]方法,参数为:" + sb.toString());

// 继续执行方法
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println(methodName + "方法执行结束");

}
}
  1. 在步骤二中的IndexController写一个sourceC进行测试,加上我们的自定义注解:
1
2
3
4
5
@MyLog
@GetMapping("/sourceC/{source_name}")
public String sourceC(@PathVariable("source_name") String sourceName){
return "你正在访问sourceC资源";
}
  1. 启动springboot web项目,输入访问地址