注解(也被成为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据。简单来说注解的作用就是将我们的需要的数据储存起来,在以后的某一个时刻(可能是编译时,也可能是运行时)去调用它:
- 作为特定的标记,用于告诉编译器一些信息
- 编译时动态处理,如动态生成代码
- 运行时动态处理,作为额外信息的载体,如得到注解信息
通过 @interface 关键字定义注解。注解和接口类似,注解内部可以定义常量和方法,但方法不能有参数,返回值只能是基本类型、字符串、Class、枚举、注解、及以上类型的数组,可以包含默认值。
因为自定义注解,是使用元注解来实现的,所以我们先详细的了解一下元注解,然后再通过几个例子来讲解如何实现和使用自定义注解。
1.元注解
元注解就是定义注解的注解,包含@Target、@Retention、@Inherited、@Documented这四种。
1.1 @Target:注解的使用目标
- ElementType.PACKAGE 注解作用于包
- ElementType.TYPE 注解作用于类型(类,接口,注解,枚举)
- ElementType.ANNOTATION_TYPE 注解作用于注解
- ElementType.CONSTRUCTOR 注解作用于构造方法
- ElementType.METHOD 注解作用于方法
- ElementType.PARAMETER 注解作用于方法参数
- ElementType.FIELD 注解作用于属性
- ElementType.LOCAL_VARIABLE 注解作用于局部变量
@Target注解源码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Target {
ElementType[] value();
}
注解方法返回值是ElementType[],ElementType枚举类型,枚举值就是@Target注解的可取值。方法名value,这样在使用注解时,可以不需要指定方法名。
1.2 @Retention:注解的生命周期
@Retention注解,翻译为持久力、保持力。即用来修饰自定义注解的生命力。
- RetentionPolicy.SOURCE 源码中保留,编译期可以处理
- RetentionPolicy.CLASS Class文件中保留,Class加载时可以处理(默认)
- RetentionPolicy.RUNTIME 运行时保留,运行中可以处理
@Retention注解源码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Retention {
RetentionPolicy value();
}
注解方法返回值是枚举类型RetentionPolicy,枚举值就是@Retention注解的可取值。
1.3 @Inherited:注解将被用于子类,是一个标记注解
使用@Inherited修饰的注解作用于一个类,则该注解将被用于该类的子类
@Inherited注解源码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Inherited {
}
1.4 @Documented:注解可以文档化,是一个标记注解
在生成javadoc的时候,是不包含注释的,但是如果注解被@Documented修饰,则生成的文档就包含该注解
@Documented注解源码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Documented {
}
2.注解使用示例
2.1 基础示例
- 自定义注解及参数
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Msg {
String DEFAULT_MSG = "msg";
// 在使用该注解时,可以传入String类型参数msg
String msg() default DEFAULT_MSG;
}
- 使用自定义注解,并传入参数
@Msg(msg = "Test")
public class Test {
}
- 获取注解中传入的参数
public class Main {
public static void main(String[] args) {
// 获取使用注解的Class对象
Test test = new Test();
Class tClass = test.getClass();
// 获取注解实例对象,并获取到传入的参数
Msg anno = (Msg) tClass.getAnnotation(Msg.class);
System.out.println(anno.msg()); // Test
}
}
因为Msg注解的生命周期为RetentionPolicy.RUNTIME,所以可以运行时通过反射获取。对于编译器处理的注解,可以使用APT处理。
2.2 自定义注解+拦截器 实现登录校验
接下来,我们使用springboot拦截器实现这样一个功能,如果方法上加了@LoginRequired,则提示用户该接口需要登录才能访问,否则不需要登录。
首先,写两个简单的接口,访问sourceA,sourceB资源
@RestController
public class IndexController {
@GetMapping("/sourceA")
public String sourceA(){
return "你正在访问sourceA资源";
}
@GetMapping("/sourceB")
public String sourceB(){
return "你正在访问sourceB资源";
}
}
没添加拦截器之前成功访问
然后写一个拦截器(实现HandlerInterceptor接口),并将拦截器配置到mvc配置中(实现WebMvcConfigurer接口)
public class SourceAccessInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object
handler) throws Exception {
System.out.println("进入拦截器了");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object
handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object
handler, Exception ex) throws Exception {
}
}
---------------------------------------------------------------------------------------------------
@Configuration
public class InterceptorTrainConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 设置拦截所有请求
registry.addInterceptor(new SourceAccessInterceptor()).addPathPatterns("/**");
}
}
可以看到上面是拦截了所有请求,所以现在我们就来通过自定义注解实现指定拦截:
- 定义一个LoginRequired注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
- 将@LoginRequired加到要拦截方法上
@RestController
public class IndexController {
@GetMapping("/sourceA")
public String sourceA(){
return "你正在访问sourceA资源";
}
@LoginRequired
@GetMapping("/sourceB")
public String sourceB(){
return "你正在访问sourceB资源";
}
}
- 改造拦截器的 preHandle 方法,只拦截有@LoginRequired注解的方法
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object
handler) throws Exception {
System.out.println("进入拦截器了");
// 反射获取当前请求相应的方法
HandlerMethod handlerMethod = (HandlerMethod)handler;
// 获得该方法上的LoginRequired实例对象
LoginRequired loginRequired =
handlerMethod.getMethod().getAnnotation(LoginRequired.class);
// 如果方法上没有LoginRequired注解,就不拦截
if(loginRequired == null){
return true;
}
// 有LoginRequired注解说明需要登录,提示用户登录
response.setContentType("application/json; charset=utf-8");
response.getWriter().print("你访问的资源需要登录");
return false;
}
运行成功,访问sourceB时需要登录了,访问sourceA则不用登录
2.3 自定义注解+AOP 实现日志打印
先导入切面需要的依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 定义一个注解@MyLog
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
- 通过切面类实现@MyLog功能,见如下代码注释理解:
@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 + "方法执行结束");
}
}
- 使用@MyLog,加上我们的自定义注解:
@MyLog
@GetMapping("/sourceC/{source_name}")
public String sourceC(@PathVariable("source_name") String sourceName){
return "你正在访问sourceC资源";
}
启动springboot web项目,输入访问地址