java如何讓程式碼變得優雅——自定義註解

KDoo發表於2021-01-01

一、什麼是註解

java中,註解分兩種,元註解和自定義註解。
我們常用的一些註解,如:@Autowired、@Override等都是自定義註解。

二、java的元註解

可以理解為描述註解的註解,除了這幾個元註解,所有註解都是自定義註解。

  • @Document:表示是否將註解資訊新增在java文件中
  • @Target:表示註解用於什麼地方。
    • ElementType.CONSTRUCTOR: 用於描述構造器
    • ElementType.FIELD: 成員變數、物件、屬性(包括enum例項)
    • ElementType.LOCAL_VARIABLE: 用於描述區域性變數
    • ElementType.METHOD: 用於描述方法
    • ElementType.PACKAGE: 用於描述包
    • ElementType.PARAMETER: 用於描述引數
    • ElementType.TYPE: 用於描述類、介面(包括註解型別) 或enum宣告
  • @Retention:定義該註解的生命週期
    • RetentionPolicy.SOURCE:在編譯階段丟棄。不寫入位元組碼。如:@Override,@SuppressWarnings都屬於這類註解。
    • RetentionPolicy.CLASS:在類載入的時候丟棄。在位元組碼檔案的處理中有用。註解預設使用這種方式
    • RetentionPolicy.RUNTIME:始終不會丟棄,執行期也保留該註解,因此可以使用反射機制讀取該註解資訊。
  • @Inherited:定義該註釋和子類的關係
    • 是一個標記註解,闡述了某個被標註的型別是被繼承的。如果一個使用了@Inherited修飾的annotation型別被用於一個class,則這個annotation將被用於該class的子類。

三、自定義註解

自定義註解編寫規則:

  • Annotation型定義為@interface,所有的Annotation會自動繼承java.lang.Annotation這一介面,並且不能再去繼承別的類或是介面。
  • 引數成員只能用public或預設(default)這兩個反問權修飾
  • 引數成員只能用八種基本資料型別和String、Enum、Class、annotations等資料型別,以及這些型別的陣列。
  • 要獲取類方法和欄位的註解資訊,必須通過java的反射技術來獲取Annotation物件,除此之外沒有別的獲取註解物件的方法

四、註解實現:記錄訪問日誌

4.1 pom檔案

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.9</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>30.1-jre</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

4.2 自定義一個註解

/**
 * @Author: KD
 * @Date: 2021/1/1 1:02 下午
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TheLog {

    /**
     * 業務物件名稱
     *
     * @return
     */
    public String name();

    /**
     * 描述瞭如何獲取id的表示式
     *
     * @return
     */
    public String idExpression();
}

這個類中定義了註解的兩個屬性,第一個是name,第二個是一個Spel表示式。

4.3 切面

/**
 * @Author: KD
 * @Date: 2021/1/1 1:02 下午
 */
@Aspect
@Component
public class TheAspect {

    private static final Logger LOGGER = LoggerFactory.getLogger(TheAspect.class);

    @Autowired
    HttpServletRequest request;

    @Around("@annotation(com.example.zhujie.anno.TheLog)")//對標註了TheLog註解的方法設定切面
    public Object log(ProceedingJoinPoint pjp) throws Exception {

        Method method = ((MethodSignature)pjp.getSignature()).getMethod();
        TheLog theLog = method.getAnnotation(TheLog.class);

        Object response = null;

        try {
            // 目標方法執行
            response = pjp.proceed();
        } catch (Throwable throwable) {
            throw new Exception(throwable);
        }

        if (StringUtils.isNotEmpty(theLog.idExpression())) {
            SpelExpressionParser parser = new SpelExpressionParser();
            Expression expression = parser.parseExpression(theLog.idExpression());

            EvaluationContext context = new StandardEvaluationContext();
            // 獲取引數值
            Object[] args = pjp.getArgs();

            // 獲取執行時引數的名稱
            LocalVariableTableParameterNameDiscoverer discoverer
                    = new LocalVariableTableParameterNameDiscoverer();
            String[] parameterNames = discoverer.getParameterNames(method);

            // 將引數繫結到context中
            if (parameterNames != null) {
                for (int i = 0; i < parameterNames.length; i++) {
                    context.setVariable(parameterNames[i], args[i]);
                }
            }

            // 將方法的resp當做變數放到context中,變數名稱為該類名轉化為小寫字母開頭的駝峰形式
            if (response != null) {
                context.setVariable(
                        CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, response.getClass().getSimpleName()),
                        response);
            }

            // 解析表示式,獲取結果
            String itemId = String.valueOf(expression.getValue(context));

            // 執行日誌記錄
            handle(theLog.name(), itemId);
        }

        return response;
    }


    private void handle(String name, String theId) {
        // 通過日誌列印輸出
        LOGGER.info("theType = " + name + ",theId = " + theId);
    }
}

這裡主要是通過aspectj中的類庫來實現反射,獲取註解屬性,然後解析Spel表示式,最後在handle方法中實現日誌的列印輸出。

4.4 使用註解

/**
 * @Author: KD
 * @Date: 2021/1/1 12:41 下午
 */
@Controller
public class TestController {

    @GetMapping("test")
    @ResponseBody
    @TheLog(name="test1",idExpression="#id")
    public String test(String id){
        return id;
    }

}

其中idExpression是Spel表示式,"#id"表示獲取方法中名為id的區域性變數。

訪問url:

localhost:8080/test?id=123123

日誌列印如下:

2021-01-01 13:43:38.157  INFO 52624 --- [nio-8080-exec-1] com.example.zhujie.anno.TheAspect        : theType = test1,theId = 123123

相關文章