【Java杂记】注解:自定义注解示例

183 阅读5分钟

注解(也被成为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据。简单来说注解的作用就是将我们的需要的数据储存起来,在以后的某一个时刻(可能是编译时,也可能是运行时)去调用它

  • 作为特定的标记,用于告诉编译器一些信息
  • 编译时动态处理,如动态生成代码
  • 运行时动态处理,作为额外信息的载体,如得到注解信息

通过 @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 基础示例

  1. 自定义注解及参数
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Msg {
  String DEFAULT_MSG = "msg";
  // 在使用该注解时,可以传入String类型参数msg
  String msg() default DEFAULT_MSG;
}
  1. 使用自定义注解,并传入参数
@Msg(msg = "Test")
public class Test {
    
}
  1. 获取注解中传入的参数
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("/**");
    }
}

可以看到上面是拦截了所有请求,所以现在我们就来通过自定义注解实现指定拦截:

  1. 定义一个LoginRequired注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
    
}
  1. 将@LoginRequired加到要拦截方法上
@RestController
public class IndexController {

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

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

}
  1. 改造拦截器的 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>
  1. 定义一个注解@MyLog
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
    
}
  1. 通过切面类实现@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 + "方法执行结束");

    }
}
  1. 使用@MyLog,加上我们的自定义注解:
    @MyLog
    @GetMapping("/sourceC/{source_name}")
    public String sourceC(@PathVariable("source_name") String sourceName){
        return "你正在访问sourceC资源";
    }

启动springboot web项目,输入访问地址

访问项目 切面成功