Java核心知识体系2:注解机制详解

1 Java 注解基础

注解是 JDK1.5 版本开始引入的一个特性,用于对程序代码的说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。
它主要的作用有以下四方面:

  • 生成 javadoc 文档,通过在代码里面标识元数据生成 javadoc 文档。
  • 编译期的检查,通过标识的元数据让编译器在编译期间对代码进行验证。
  • 编译时动态处理,编译时通过代码中标识的元数据动态处理,比如动态生成代码。
  • 运行时动态处理,运行时通过代码中标识的元数据动态处理,比如使用反射技术注入实例。

注解的常见分类有三种:

  • Java 自带的标准注解,包括 @Override、@Deprecated 和 @SuppressWarnings,分别代表 方法重写、某个类或方法过时、以及忽略警告,用这些注解标明后编译器就会进行检查。
  • 元注解,元注解是用于定义注解的注解,包括 @Retention、@Target、@Inherited、@Documented 等 6 种
    • @Retention:指定其所修饰的注解的保留策略
    • @Document:该注解是一个标记注解,用于指示一个注解将被文档化
    • @Target:用来限制注解的使用范围
    • @Inherited:该注解使父类的注解能被其子类继承
    • @Repeatable:该注解是 Java8 新增的注解,用于开发重复注解
    • 类型注解(Type Annotation):该注解是 Java8 新增的注解,可以用在任何用到类型的地方
  • 自定义注解,可以根据自己的需求定义注解,并可用元注解对自定义注解进行注解。

接下来我们通过这三种分类来逐一理解注解。

1.1 Java 内置注解

我们先从 Java 内置注解开始说起,先看下下面的代码:

class Parent {
    public void rewriteMethod() {
}

}

class Child extends Parent {

<span class="hljs-comment">/**
    * 重载父类的 rewriteMethod() 方法
    */</span>
<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">rewriteMethod</span>(<span class="hljs-params"></span>) {
}

<span class="hljs-comment">/**
    * 被弃用的过时方法
    */</span>
<span class="hljs-meta">@Deprecated</span>
<span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">oldMethod</span>(<span class="hljs-params"></span>) {
}

<span class="hljs-comment">/**
    * 忽略告警
    * 
    * <span class="hljs-doctag">@return</span>
    */</span>
<span class="hljs-meta">@SuppressWarnings</span>(<span class="hljs-string">"keep run"</span>)
<span class="hljs-keyword">public</span> <span class="hljs-title class_">List</span> <span class="hljs-title function_">infoList</span>(<span class="hljs-params"></span>) {
    <span class="hljs-title class_">List</span> list = <span class="hljs-keyword">new</span> <span class="hljs-title class_">ArrayList</span>();
    <span class="hljs-keyword">return</span> list;
}

}

Java 1.5 开始自带的标准注解,包括 @Override、@Deprecated 和 @SuppressWarnings:

  • @Override:表示当前类中的方法定义将覆盖父类中的方法
  • @Deprecated:表示该代码段被弃用,但是可以使用,只是编译器会发出警告而已
  • @SuppressWarnings:表示关闭编译器的警告信息
    我们再具体看下这几个内置注解,同时通过这几个内置注解中的元注解的定义来引出元注解。

1.1.1 内置注解 - @Override

我们先来看一下这个注解类型的定义:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

从它的定义我们可以看到,这个注解可以被用来修饰方法,并且它只在编译时有效,在编译后的 class 文件中便不再存在。这个注解的作用我们大家都不陌生,那就是告诉编译器被修饰的方法是重写的父类的中的相同签名的方法,编译器会对此做出检查,
若发现父类中不存在这个方法或是存在的方法签名不同,则会报错。

1.1.2 内置注解 - @Deprecated

这个注解的定义如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

从它的定义我们可以知道,它会被文档化,能够保留到运行时,能够修饰构造方法、属性、局部变量、方法、包、参数、类型。这个注解的作用是告诉编译器被修饰的程序元素已被“废弃”,不再建议用户使用。

1.1.3 内置注解 - @SuppressWarnings

这个注解我们也比较常用到,先来看下它的定义:

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

它能够修饰的程序元素包括类型、属性、方法、参数、构造器、局部变量,只能存活在源码时,取值为 String[]。它的作用是告诉编译器忽略指定的警告信息,它可以取的值如下所示:

参数 作用 原描述
all 抑制所有警告 to suppress all warnings
boxing 抑制装箱、拆箱操作时候的警告 to suppress warnings relative to boxing/unboxing operations
cast 抑制映射相关的警告 to suppress warnings relative to cast operations
dep-ann 抑制启用注释的警告 to suppress warnings relative to deprecated annotation
deprecation 抑制过期方法警告 to suppress warnings relative to deprecation
fallthrough 抑制确在 switch 中缺失 breaks 的警告 to suppress warnings relative to missing breaks in switch statements
finally 抑制 finally 模块没有返回的警告 to suppress warnings relative to finally block that don’t return
hiding 抑制与隐藏变数的区域变数相关的警告 to suppress warnings relative to locals that hide variable()
incomplete-switch 忽略没有完整的 switch 语句 to suppress warnings relative to missing entries in a switch statement (enum case)
nls 忽略非 nls 格式的字符 to suppress warnings relative to non-nls string literals
null 忽略对 null 的操作 to suppress warnings relative to null analysis
rawtype 使用 generics 时忽略没有指定相应的类型 to suppress warnings relative to un-specific types when using
restriction 抑制与使用不建议或禁止参照相关的警告 to suppress warnings relative to usage of discouraged or
serial 忽略在 serializable 类中没有声明 serialVersionUID 变量 to suppress warnings relative to missing serialVersionUID field for a serializable class
static-access 抑制不正确的静态访问方式警告 to suppress warnings relative to incorrect static access
synthetic-access 抑制子类没有按最优方法访问内部类的警告 to suppress warnings relative to unoptimized access from inner classes
unchecked 抑制没有进行类型检查操作的警告 to suppress warnings relative to unchecked operations
unqualified-field-access 抑制没有权限访问的域的警告 to suppress warnings relative to field access unqualified
unused 抑制没被使用过的代码的警告 to suppress warnings relative to unused code

1.2 元注解

上述内置注解的定义中使用了一些元注解(注解类型进行注解的注解类),在 JDK 1.5 中提供了 4 个标准的元注解:@Target,@Retention,@Documented,@Inherited, 在 JDK 1.8 中提供了两个新的元注解 @Repeatable 和 @Native。

1.2.1 元注解 - @Target

Target 注解的作用是:描述注解的使用范围(即:被修饰的注解可以用在什么地方) 。

Target 注解用来说明那些被它所注解的注解类可修饰的对象范围:

  • packages、types(类、接口、枚举、注解类)
  • 类成员(方法、构造方法、成员变量、枚举值)
  • 方法参数和本地变量(如循环变量、catch 参数)
    在定义注解类时使用了 @Target 能够更加清晰的知道它能够被用来修饰哪些对象,它的取值范围定义在 ElementType 枚举中。枚举信息如下:
public enum ElementType { 
    TYPE, // 类、接口、枚举类 
    FIELD, // 成员变量(包括:枚举常量) 
    METHOD, // 成员方法 
    PARAMETER, // 方法参数 
    CONSTRUCTOR, // 构造方法 
    LOCAL_VARIABLE, // 局部变量 
    ANNOTATION_TYPE, // 注解类 
    PACKAGE, // 可用于修饰:包 
    TYPE_PARAMETER, // 类型参数,JDK 1.8 新增 
    TYPE_USE // 使用类型的任何地方,JDK 1.8 新增 
}

1.2.2 元注解 - @Retention & @RetentionTarget

Reteniton 注解的作用是:描述注解保留的时间范围(即:被描述的注解在它所修饰的类中可以被保留到何时) 。

Reteniton 注解用来限定那些被它所注解的注解类在注解到其他类上以后,可被保留到何时,一共有三种策略,定义在 RetentionPolicy 枚举中。枚举如下:

public enum RetentionPolicy {
<span class="hljs-type">SOURCE</span>,    <span class="hljs-comment">// 源文件保留</span>
<span class="hljs-type">CLASS</span>,       <span class="hljs-comment">// 编译期保留,默认为该值,CLASS</span>
<span class="hljs-type">RUNTIME</span>   <span class="hljs-comment">// 运行期保留,可通过反射去获取注解信息</span>

}

我们测试下这三种策略,在定义注解类的时候什么区别:

@Retention(RetentionPolicy.SOURCE)
public @interface SourcePolicy {
  // 源文件保留策略
}

@Retention(RetentionPolicy.CLASS)
public @interface ClassPolicy {
// 编译器保留策略
}

@Retention(RetentionPolicy.RUNTIME)
public @interface RuntimePolicy {
// 运行期保留策略
}

上面已经定义好了三个注解类,我们再用这三个注解类再去注解方法,如下:

public class RetentionTest {
<span class="hljs-variable">@SourcePolicy</span>
public void sourcePolicy() {
}

<span class="hljs-variable">@ClassPolicy</span>
public void classPolicy() {
}

<span class="hljs-variable">@RuntimePolicy</span>
public void runtimePolicy() {
}

}

通过执行 javap -verbose RetentionTest 命令获取到的 RetentionTest 的 class 字节码内容如下。

{
  public retention.RetentionTest();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial   #1            // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

public void sourcePolicy();
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 7: 0

public void classPolicy();
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 11: 0
RuntimeInvisibleAnnotations:
0: #11()

public void runtimePolicy();
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 15: 0
RuntimeVisibleAnnotations:
0: #14()
}

从 RetentionTest 的字节码内容我们可以得出以下两点结论:

  • 编译器并没有记录下 sourcePolicy() 方法的注解信息
  • 编译器使用 RuntimeInvisibleAnnotations 去记录 classPolicy() 方法的注解信息
  • 编译器使用 RuntimeVisibleAnnotations 去记录 runtimePolicy() 方法的注解信息

1.2.3 元注解 - @Documented

Documented 注解的作用如下:使用 javadoc 工具为类生成帮助文档,并确认是否保留注解信息。

以下代码在使用 Javadoc 工具可以生成 @DocAnnotation 注解信息。

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DocAnnotation {

<span class="hljs-keyword">public</span> String <span class="hljs-title function_">value</span><span class="hljs-params">()</span> <span class="hljs-keyword">default</span> <span class="hljs-string">"default"</span>;

}

@DocAnnotation("some method doc")
public void testMethod() {
  // 测试方法的文档注解
}

1.2.4 元注解 - @Inherited

Inherited 注解的作用:被它修饰的 Annotation 将具有继承特性。父类使用了被 @Inherited 修饰的 Annotation,则子类将自动具备该注解。

我们来测试下这个注解:

  • 定义 @Inherited 注解:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface InheritedAnnotation {
    String [] values();
    int number();
}

  • 使用这个注解
@InheritedAnnotation(values = {"brand"}, number = 100)
public class UserInfo {
}

class Customer extends UserInfo {
@Test
public void testMethod(){
Class clazz = Student.class;
Annotation[] annotations = clazz.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation.toString());
}
}
}

  • 输出
xxx.InheritedAnnotation(values=[brand], number=100)

虽然 Customer 类没有显示地被注解 @InheritedAnnotation,但是它的父类 UserInfo 被注解,而且 @InheritedAnnotation 被 @Inherited 注解,因此 Customer 类自动继承注解

1.2.4 元注解 - @Repeatable (Java8)

Repeatable 是可重复使用的意思,允许在同一声明的类型 (类,属性,或方法) 中,可以多次使用同一个注解

JDK8 之前要想实现注解重复使用,需要组合模式,编写和可读性都不是很好

public @interface Pet {
     String myPet();
}

public @interface Pets {
Pet[] value();
}

public class RepeatAnnotationOV {
@Pets({@Pet(myPet="dog"),@Pet(myPet="cat")})
public void workMethod(){
}
}

由另一个注解来存储重复注解,在使用时候,用存储注解 Authorities 来扩展重复注解。

Java 8 中的做法:

@Repeatable(Pets.class)
public @interface Pet {
     String myPet();
}

public @interface Pets {
Pet[] value();
}

public class RepeatAnnotationNV {
@Pet(role="dog")
@Pet(role="cat")
public void workMethod(){ }
}

不同的地方是,创建重复注解 Authority 时,加上 @Repeatable, 指向存储注解 Authorities,在使用时候,直接可以重复使用 Authority 注解。从上面例子看出,java 8 里面做法更适合常规的思维,可读性强一点

1.2.5 元注解 - @Native (Java8)

使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可

1.3 注解与反射接口

定义注解后,如何获取注解中的内容呢?反射包 java.lang.reflect 下的 AnnotatedElement 接口提供这些方法。这里注意:只有注解被定义为 RUNTIME 后,该注解才能是运行时可见,当 class 文件被装载时被保存在 class 文件中的 Annotation 才会被虚拟机读取。

AnnotatedElement 接口是所有程序元素(Class、Method 和 Constructor)的父接口,所以程序通过反射获取了某个类的 AnnotatedElement 对象之后,程序就可以调用该对象的方法来访问 Annotation 信息。我们看下具体的先关接口

  • boolean isAnnotationPresent(Class<?extends Annotation> annotationClass)
    判断该程序元素上是否包含指定类型的注解,存在则返回 true,否则返回 false。注意:此方法会忽略注解对应的注解容器。
  • T getAnnotation(Class annotationClass)
    返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回 null。
  • Annotation[] getAnnotations()
    返回该程序元素上存在的所有注解,若没有注解,返回长度为 0 的数组。
  • T[] getAnnotationsByType(Class annotationClass)
    返回该程序元素上存在的、指定类型的注解数组。没有注解对应类型的注解时,返回长度为 0 的数组。该方法的调用者可以随意修改返回的数组,而不会对其他调用者返回的数组产生任何影响。getAnnotationsByType 方法与 getAnnotation 的区别在于,getAnnotationsByType 会检测注解对应的重复注解容器。若程序元素为类,当前类上找不到注解,且该注解为可继承的,则会去父类上检测对应的注解。
  • T getDeclaredAnnotation(Class annotationClass)
    返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注释。如果没有注释直接存在于此元素上,则返回 null
  • T[] getDeclaredAnnotationsByType(Class annotationClass)
    返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注释
  • Annotation[] getDeclaredAnnotations()
    返回直接存在于此元素上的所有注解及注解对应的重复注解容器。与此接口中的其他方法不同,该方法将忽略继承的注解。如果没有注释直接存在于此元素上,则返回长度为零的一个数组。该方法的调用者可以随意修改返回的数组,而不会对其他调用者返回的数组产生任何影响。

1.4 自定义注解

当我们理解了内置注解, 元注解和获取注解的反射接口后,我们便可以开始自定义注解了。这个例子我把上述的知识点全部融入进来, 代码很简单:

  • 定义自己的注解
package com.helenlyn.common.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**

  • <p>Description: 水果供应者注解 </p>

  • <p>Copyright: Copyright (c) 2021 </p>

  • <p>Company: helenlyn Co., Ltd. </p>

  • @author brand

  • @date 2021/5/16 16:35

  • <p>Update Time: </p>

  • <p>Updater: </p>

  • <p>Update Comments: </p>
    /
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface FruitProvider {
    /
    *

    • 供应商编号
    • @return
      */
      public int id() default -1;

    /**

    • 供应商名称
    • @return
      */
      public String name() default "";

    /**

    • 供应商地址
    • @return
      */
      public String address() default "";
      }
  • 使用注解
package com.helenlyn.common.dto;

import com.helenlyn.common.annotation.FruitColor;
import com.helenlyn.common.annotation.FruitName;
import com.helenlyn.common.annotation.FruitProvider;

/**

  • <p>Description: </p>

  • <p>Copyright: Copyright (c) 2021 </p>

  • <p>Company: helenlyn Co., Ltd. </p>

  • @author brand

  • @date 2021/5/16 16:28

  • <p>Update Time: </p>

  • <p>Updater: </p>

  • <p>Update Comments: </p>
    */
    public class AppleDto {
    @FruitName("Apple")
    private String appleName;

    @FruitColor(fruitColor=FruitColor.Color.RED)
    private String appleColor;

    @FruitProvider(id=1,name="helenlyn 贸易公司",address="福州 xx 路 xxx 大楼")
    private String appleProvider;
    }

  • 用反射接口获取注解信息
    在 FruitInfoUtil 中进行测试:
/**
 * <p>Description: FruitInfoUtil 注解实现 </p>
 * <p>Copyright: Copyright (c) 2021 </p>
 * <p>Company: helenlyn Co., Ltd.             </p>
 *
 * @author brand
 * @date 2021/5/16 16:37
 * <p>Update Time:                      </p>
 * <p>Updater:                          </p>
 * <p>Update Comments:                  </p>
 */
public class FruitInfoUtil {
    public static String getFruitInfo(Class<?> clazz) {
        String strFruitName = "水果名称:";
        String strFruitColor = "水果颜色:";
        String strFruitProvicer = "供应商信息:";
    Field[] fields = clazz.<span class="hljs-title function_ invoke__">getDeclaredFields</span>();

    <span class="hljs-keyword">for</span> (Field field : fields) {
        <span class="hljs-keyword">if</span> (field.<span class="hljs-title function_ invoke__">isAnnotationPresent</span>(FruitName.<span class="hljs-keyword">class</span>)) {
            FruitName fruitName = (FruitName) field.<span class="hljs-title function_ invoke__">getAnnotation</span>(FruitName.<span class="hljs-keyword">class</span>);
            strFruitName += fruitName.<span class="hljs-title function_ invoke__">value</span>();
            System.out.<span class="hljs-title function_ invoke__">println</span>(strFruitName);
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (field.<span class="hljs-title function_ invoke__">isAnnotationPresent</span>(FruitColor.<span class="hljs-keyword">class</span>)) {
            FruitColor fruitColor = (FruitColor) field.<span class="hljs-title function_ invoke__">getAnnotation</span>(FruitColor.<span class="hljs-keyword">class</span>);
            strFruitColor += fruitColor.<span class="hljs-title function_ invoke__">fruitColor</span>().<span class="hljs-title function_ invoke__">toString</span>();
            System.out.<span class="hljs-title function_ invoke__">println</span>(strFruitColor);
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (field.<span class="hljs-title function_ invoke__">isAnnotationPresent</span>(FruitProvider.<span class="hljs-keyword">class</span>)) {
            FruitProvider fruitProvider = (FruitProvider) field.<span class="hljs-title function_ invoke__">getAnnotation</span>(FruitProvider.<span class="hljs-keyword">class</span>);
            strFruitProvicer = <span class="hljs-string">" 供应商编号:"</span> + fruitProvider.<span class="hljs-title function_ invoke__">id</span>() + <span class="hljs-string">" 供应商名称:"</span> + fruitProvider.<span class="hljs-title function_ invoke__">name</span>() + <span class="hljs-string">" 供应商地址:"</span> + fruitProvider.<span class="hljs-title function_ invoke__">address</span>();
            System.out.<span class="hljs-title function_ invoke__">println</span>(strFruitProvicer);
        }
    }
    <span class="hljs-keyword">return</span> String.<span class="hljs-title function_ invoke__">format</span>(<span class="hljs-string">"%s;%s;%s;"</span>, strFruitName, strFruitColor, strFruitProvicer);
}

}

  • 测试的输出
2022-07-09 11:33:41.688  INFO 5895 --- [TaskExecutor-35] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: cl-debug-rabbitmq-erp-service-7w0cpa.docker.sdp:9146
Hibernate: update UserBasicInfo set personName=? where personCode=?
 水果名称:Apple
 水果颜色:RED
 供应商编号:1 供应商名称:helenlyn 贸易公司 供应商地址:福州xx路xxx大楼

2 理解注解的原理

2.1 Java 8 提供了哪些新的注解

  • @Repeatable
  • ElementType.TYPE_USE
  • ElementType.TYPE_PARAMETER

ElementType.TYPE_USE(此类型包括类型声明和类型参数声明,是为了方便设计者进行类型检查) 包含了 ElementType.TYPE(类、接口(包括注解类型)和枚举的声明) 和 ElementType.TYPE_PARAMETER(类型参数声明), 可以看下面这个例子:

// 自定义 ElementType.TYPE_PARAMETER 注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_PARAMETER)
public @interface MyNotEmpty {
}

// 自定义 ElementType.TYPE_USE 注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
public @interface MyNotNull {
}

// 测试类
public class TypeParameterAndTypeUseAnnotation<@MyNotEmpty T>{

// 使用 TYPE_PARAMETER 类型,会编译不通过
// public @MyNotEmpty T test(@MyNotEmpty T a){
// new ArrayList<@MyNotEmpty String>();
// return a;
// }

// 使用 TYPE_USE 类型,编译通过
public @MyNotNull T test2(@MyNotNull T a){
new ArrayList<@MyNotNull String>();
return a;
}
}

2.2 注解支持继承吗?

注解是不支持继承的
不能使用关键字 extends 来继承某个 @interface,但注解在编译后,编译器会自动继承 java.lang.annotation.Annotation 接口。 虽然反编译后发现注解继承了 Annotation 接口,请记住,即使 Java 的接口可以实现多继承,但定义注解时依然无法使用 extends 关键字继承 @interface。 区别于注解的继承,被注解的子类继承父类注解可以用 @Inherited: 如果某个类使用了被 @Inherited 修饰的 Annotation,则其子类将自动具有该注解。

3 注解的应用场景

自定义注解多喝 AOP - 通过切面实现解耦

笔者曾经在 《基于 AOP 的动态数据源切换》 这篇文章中有个典型的例子,就是使用 AOP 切面来对多数据源进行使用场景的切换,下面展示下如何通过注解实现解耦的。

  • 自定义 Annotation,映射的目标范围为 类型和方法。
/**
 * @author brand
 * @Description: 数据源切换注解
 * @Copyright: Copyright (c) 2021
 * @Company: Helenlyn, Inc. All Rights Reserved.
 * @date 2021/12/15 7:36 下午
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String name() default "";
}
  • 编写 AOP 实现, 切面代码,以实现对注解的 PointCut, 切点拦截
/**
 * @author brand
 * @Description:
 * @Copyright: Copyright (c) 2021
 * @Company: Helenlyn, Inc. All Rights Reserved.
 * @date 2021/12/15 7:49 下午
 */
@Aspect
@Component
public class DataSourceAspect implements Ordered  {
    /**
     * 定义一个切入点,匹配到上面的注解 DataSource
     */
    @Pointcut("@annotation(com.helenlyn.dataassist.annotation.DataSource)")
    public void dataSourcePointCut() {
    }
<span class="hljs-comment">/**
 * Around 环绕方式做切面注入
 * <span class="hljs-doctag">@param</span> point
 * <span class="hljs-doctag">@return</span>
 * <span class="hljs-doctag">@throws</span> Throwable
 */</span>
<span class="hljs-meta">@Around("dataSourcePointCut()")</span>
<span class="hljs-keyword">public</span> Object <span class="hljs-title function_">around</span><span class="hljs-params">(ProceedingJoinPoint point)</span> <span class="hljs-keyword">throws</span> Throwable {
    <span class="hljs-type">MethodSignature</span> <span class="hljs-variable">signature</span> <span class="hljs-operator">=</span> (MethodSignature) point.getSignature();
    <span class="hljs-type">Method</span> <span class="hljs-variable">method</span> <span class="hljs-operator">=</span> signature.getMethod();
    <span class="hljs-type">DataSource</span> <span class="hljs-variable">ds</span> <span class="hljs-operator">=</span> method.getAnnotation(DataSource.class);
    <span class="hljs-type">String</span> <span class="hljs-variable">routeKey</span> <span class="hljs-operator">=</span> ds.name();  <span class="hljs-comment">// 从头部中取出注解的name(basic 或 cloudoffice 或 attend),用这个name进行数据源查找。</span>
    <span class="hljs-type">String</span> <span class="hljs-variable">dataSourceRouteKey</span> <span class="hljs-operator">=</span> DynamicDataSourceRouteHolder.getDataSourceRouteKey();
    <span class="hljs-keyword">if</span> (StringUtils.isNotEmpty(dataSourceRouteKey)) {
        <span class="hljs-comment">// StringBuilder currentRouteKey = new StringBuilder(dataSourceRouteKey);</span>
        routeKey = ds.name();
    }
    DynamicDataSourceRouteHolder.setDataSourceRouteKey(routeKey);
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">return</span> point.proceed();
    } <span class="hljs-keyword">finally</span> { <span class="hljs-comment">// 最后做清理,这个步骤很重要,因为我们的配置中有一个默认的数据源,执行完要回到默认的数据源。</span>
        DynamicDataSource.clearDataSource();
        DynamicDataSourceRouteHolder.clearDataSourceRouteKey();
    }
}

<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-type">int</span> <span class="hljs-title function_">getOrder</span><span class="hljs-params">()</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;
}

}

  • 测试,在 Control 中写三个测试方法
/**
     * 无注解默认情况:数据源指向 basic
     * @return
     */
    @RequestMapping(value = "/default/{user_code}", method = RequestMethod.GET)
    public UserInfoDto getUserInfo(@PathVariable("user_code") String userCode) {
        return userInfoService.getUserInfo(userCode);
    }
<span class="hljs-comment">/**
 * 数据源指向attend
 * @return
 */</span>
@<span class="hljs-selector-tag">DataSource</span>(name= Constant.DATA_SOURCE_ATTEND_NAME)
@<span class="hljs-selector-tag">RequestMapping</span>(value = <span class="hljs-string">"/attend/{user_code}"</span>, method = RequestMethod.GET)
<span class="hljs-selector-tag">public</span> <span class="hljs-selector-tag">UserInfoDto</span> <span class="hljs-selector-tag">getUserInfoAttend</span>(<span class="hljs-variable">@PathVariable</span>(<span class="hljs-string">"user_code"</span>) String userCode) {
    <span class="hljs-selector-tag">return</span> <span class="hljs-selector-tag">userInfoService</span><span class="hljs-selector-class">.getUserInfo</span>(userCode);
}

<span class="hljs-comment">/**
 * 数据源指向cloud
 * @return
 */</span>
@<span class="hljs-selector-tag">DataSource</span>(name= Constant.DATA_SOURCE_CLOUD_NAME)
@<span class="hljs-selector-tag">RequestMapping</span>(value = <span class="hljs-string">"/cloud/{user_code}"</span>, method = RequestMethod.GET)
<span class="hljs-selector-tag">public</span> <span class="hljs-selector-tag">UserInfoDto</span> <span class="hljs-selector-tag">getUserInfoCloud</span>(<span class="hljs-variable">@PathVariable</span>(<span class="hljs-string">"user_code"</span>) String userCode) {
    <span class="hljs-selector-tag">return</span> <span class="hljs-selector-tag">userInfoService</span><span class="hljs-selector-class">.getUserInfo</span>(userCode);
}

  • 执行效果
    image
    image
    image

除此之外,我们可以看到很多日志管理、权限管理,也都是也是通过类似的注解机制来实现的,通过注解 +AOP 来最终实现模块之间的解耦,以及业务与系统层面的解耦 。