背景

为什么要再次梳理一下java注解,显而易见,因为重要啊。也是为研究各大类开源框架做铺垫,只有弄清楚Java注解相关原理,才能看懂大部分框架底层的设计。

缘起

注解也叫做元数据,是JDK1.5版本开始引入的一个特性,用来对代码进行标记说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解修饰。其本身不包含任何业务逻辑。
一般注解大类分为三种:

  • JDK自带的相关注解
  • 自定义的注解
  • 第三方的(例如相关的框架中的注解)

注解三步走:定义、配置、解析

  • 定义:定义标记
  • 配置:把标记打到需要用到的代码中
  • 解析:在编译器或运行时检测到标记,并进行特殊操作

元注解

什么是元注解?元注解的作用就是负责注解其他注解。元注解有以下五种:

  • @Retention:指定其所修饰的注解的保留策略
  • @Document:该注解是一个标记注解,用于指示一个注解将被文档化
  • @Target:用来限制注解的使用范围
  • @Inherited:该注解使父类的注解能被其子类继承
  • @Repeatable:该注解是Java8新增的注解,用于开发重复注解

@Retention注解

用于指定被修饰的注解可以保留多长时间,即指定JVM策略在哪个时间点上删除当前注解。
目前存在以下三种策略

策略值 功能描述
Retention.SOURCE 注解只在源文件中保留,在编译期间删除
Retention.CLASS 注解只在编译期间存在于.class文件中,运行时JVM不可获取注解信息,该策略值也是默认值
Retention.RUNTIME 运行时JVM可以获取注解信息(反射),是最长注解持续期

@Document注解

@Document注解用于指定被修饰的注解可以被javadoc工具提取成文档。定义注解类时使用@Document注解进行修饰,则所有使用该注解修饰的程序元素的API文档中将会包含该注解说明。

@Target注解

@Target注解用来限制注解的使用范围,即指定被修饰的注解能用于哪些程序单元。标记注解方式如下:@Target({应用类型1, 应用类型2,…})【@Target(ElementType.FIELD)】
枚举值的介绍如下:

枚举值 功能描述
ElementType.Type 可以修饰类、接口、注解或枚举类型
ElementType.FIELD 可以修饰属性(成员变量),包括枚举常量
ElementType.METHOD 可以修饰方法
ElementType.PAPAMETER 可以修饰参数
ElementType.CONSTRUCTOR 可以修饰构造方法
ElementType.LOCAL_VARIABLE 可以修饰局部变量
ElementType.ANNOTATION_TYPE 可以修饰注解类
ElementType.PACKAGE 可以修饰包
ElementType.TYPE_PARAMETER JDK8之后的新特性,表示该注解能写在类型变量的声明语句中(如,泛型声明)
ElementType.TYPE_USE JDK8之后的新特性,表示该注解能写在使用类型的任何语句中(例如:声明语句、泛型和强制转换语句中的类型)

@Inherited注解

@Inherited注解指定注解具有继承性,如果某个注解使用@Inherited进行修饰,则该类使用该注解时,其子类将自动被修饰。
按照以上三步走的流程,咱们这里来举例子写代码说明一下:
(1)定义注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface InheritedExtend {
    String comment();
    int order() default 1;
}

(2)配置:标记打到类上

@InheritedExtend(comment ="注解继承",order = 2)
public class Base {
}

(3)解析:获取注解并解析做测试

public class InheritedDemo extends Base{
    public static void main(String[] args) {
        //从本类中获取父类注解信息
        InheritedExtend extend = InheritedDemo.class.getAnnotation(InheritedExtend.class);
        //输出InheritedExtend注解成员信息
        System.out.println(extend.comment()+":"+extend.order());
        //打印出InheritedDemo是否类是否具有@InheritedExtend修饰
                          System.out.println(InheritedDemo.class.isAnnotationPresent(InheritedExtend.class));
    }
}

结果输出:
注解继承:2 true
以上结果就很好地说明了该注解的继承性质。

@Repeatable注解

@Repeatable注解是Java8新增的注解,用于开发重复注解。在Java8之前,同一个程序元素前只能使用一个相同类型的注解,如果需要在同一个元素前使用多个相同类型的注解必须通过注解容器来实现。从Java8开始,允许使用多个相同的类型注解来修饰同一个元素,前提是该类型的注解是可重复的,即在定义注解时要用 @Repeatable元注解进行修饰。

(1)定义注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(AnnolContents.class)
public @interface RepeatableAnnol {
    String name() default "老猫";
    int age();
}

//注解为容器,
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface AnnolContents {
    //定义value成员变量,该成员变量可以接受多个@RepeatableAnnol注解
    RepeatableAnnol[] value();
}

(2)注解使用以及解析

@RepeatableAnnol(name = "张三",age = 12)
@RepeatableAnnol(age = 23)
public class RepeatableAnnolDemo {
    public static void main(String[] args) {
        RepeatableAnnol[] repeatableAnnols = RepeatableAnnolDemo.class.getDeclaredAnnotationsByType(RepeatableAnnol.class);

        for(RepeatableAnnol repeatableAnnol : repeatableAnnols){
            System.out.println(repeatableAnnol.name() + "----->" + repeatableAnnol.age());
        }

        AnnolContents annolContents = RepeatableAnnolDemo.class.getDeclaredAnnotation(AnnolContents.class);
        System.out.println(annolContents);
    }
}

结果输出:

张三----->12
老猫----->23
@com.ktdaddy.annotation.repeatable.AnnolContents(value={@com.ktdaddy.annotation.repeatable.RepeatableAnnol(name="张三", age=12), @com.ktdaddy.annotation.repeatable.RepeatableAnnol(name="老猫", age=23)})

自定义注解实战应用

利用注解+springAOP实现系统日志记录,主要用于记录相关的日志到数据库,当然,老猫这里的demo只会到日志打印层面,至于数据库落库存储有兴趣的小伙伴可以进行扩展。

以下是maven依赖:

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        
        <dependency>
              <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
        <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.79</version>
        </dependency>
    </dependencies>

注解的定义如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {
    String desc() default "";
}

这个地方只是定义了一个字段,当然大家也可以进行拓展。

接下来,咱们以这个注解作为切点编写相关的切面程序。具体代码如下:

@Aspect
@Component
@Order(0)
public class OperateLogAdvice {

    @Pointcut("@annotation(com.ktdaddy.annotation.OperateLog)")
    public void recordLog(){
    }

    @Around("recordLog()")
    public Object recordLogOne(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("进来了");
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        OperateLog operateLog = methodSignature.getMethod().getAnnotation(OperateLog.class);
        String spELString = operateLog.desc();
        //创建解析器
        SpelExpressionParser parser = new SpelExpressionParser();
        //获取表达式
        Expression expression = parser.parseExpression(spELString);
        //设置解析上下文(有哪些占位符,以及每种占位符的值)
        EvaluationContext context = new StandardEvaluationContext();
        //获取参数值
        Object[] args = joinPoint.getArgs();
        //获取运行时参数的名称
        DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
        String[] parameterNames = discoverer.getParameterNames(method);
        for (int i = 0; i < parameterNames.length; i++) {
            context.setVariable(parameterNames[i],args[i]);
        }
        //解析,获取替换后的结果
        String result = expression.getValue(context).toString();
        System.out.println(result);
        return joinPoint.proceed();
    }
}

关于切面这块不多做赘述,非本篇文章的重点。
接下来就可以在我们的代码层面使用相关的注解了,具体如下:

@Service
public class UserServiceImpl implements UserService {
    @OperateLog(desc = "#user.desc")
    public void saveUser(User user){
        System.out.println("测试注解...");
    }
}

关于controller层面就省略了,都是比较简单的。
通过上述切面以及注解解析,我们可以获取每次传参的参数内容,并且将相关的日志进行记录下来,当然这里面涉及到了SpEL表达式注入,相关的知识点,小伙伴们可以自行学习。
最终启动服务,并且请求之后具体的日志如下。

进来了
这是测试
测试注解...
{"age":12,"desc":"这是测试","id":1,"name":"张三","operator":"操作人"}

至此关于Java注解的回顾学习已经结束,之后咱们再去看一些底层代码的时候或许会轻松很多。