天天看點

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

作者:添甄
滿懷憂思,不如先幹再說!通過學習,重新定義自己!

在網上看很多文章都在說自定義注解的文法,這誰不會,我要的是怎麼在項目中用,什麼時候自定義,如果你是這樣的需求這篇文章一定可以滿足你!老樣子,文章很長很舒服,建議收藏反複閱讀!喜歡記得點贊哦~

本文章會從以下幾點全面講解Java注解:

  • 注解概念和分類
  • 自定義注解
  • 通過SpringBoot + AOP實作 2個 自定義注解的應用案例
  • 案例的深入優化,以及Spring架構中的相同注解源碼
  • Java8重複注解和類型注解

從JDK5剛推出注解,到Java8的重複注解全面介紹Java注解的所有知識點,看完本篇文章你将會:

  • 知道自定義注解怎麼在項目中運用了,媽媽再也不擔心在外邊受欺負了
  • 注解的面試題就不需要看了,看了,練了,想了就都明白了

注解概念

注解【Annotation】是Java5引入的新機制,也被稱為中繼資料,為我們在代碼中添加資訊提供了一種形式化的方法,可以了解為給代碼打一個标記,允許我們可以在稍後的某個時刻非常友善地使用這些資料。

Java注解也是對來自像C#之類的其他語言對Java造成的語言特性壓力所做出的一種回應。

注解特點

Java中的注解可以使用在類、構造器、方法、成員變量、參數等位置上。和 Javadoc 不同,Java 注解可以通過反射擷取注解内容。在編譯器生成類檔案時,注解可以存在于位元組碼中【注意是可以,不是一定,是可以控制的】。Java 虛拟機可以保留注解内容,在運作時可以通過反射機制擷取到注解内容 。 當然它也支援自定義注解【重點】。

注解重要的是現有的Java注解和第三方注解的作用和使用方法,以及自定義注解

小貼士:無論是自定義注解還是使用架構或者JDK提供的注解,都需要配合反射才能玩出花樣,因為大多數注解的作用是在運作時搞點事情,而反射機制就是在運作時可以獲得類資訊來搞事情,一啪即合 ——> 完美!隻有注解沒有反射就是耍流氓!

注解的好處:

  • 可以讀懂别人的代碼,特别是架構的代碼
  • 讓程式設計更加簡潔,代碼更加清晰,比如使用Spring、MyBatis等架構都要使用大量注解
  • 讓别人直呼内行,裝在無形之中
[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

注解分類

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

Java定義了一套注解,元注解在 java.lang.annotation 包,标準注解在 java.lang中:

  • @Override:使用在方法上,判斷是否是子類重寫父類的方法
  • @Deprecated:使用在方法或類上,标記為過時或棄用方法【類】,被該注解辨別的方法【類】一般都不建議再使用,老的API中有許多過時方法
  • @SuppressWarnings:使用在方法上,禁止警告注解,代碼不規範或者某變量未使用,代碼備援,過長等編輯器都會警告,有些潔癖公司不允許出現警告,這時可以利用該注解清除

作用在其他注解的注解【稱為:元注解】是:

  • @Retention:辨別這個注解怎麼儲存,是隻在代碼中,還是編入class檔案中,或者是在運作時可以通過反射通路
  • @Documented:标記這些注解是否包含在使用者文檔中
  • @Target:标記這個注解應該是哪種 Java 成員
  • @Inherited:标記這個注解是繼承于哪個注解類(預設 注解并沒有繼承于任何子類)

從 Java 7 開始,額外添加了 3 個注解:

  • @SafeVarargs:Java 7 開始支援,忽略任何使用參數為泛型變量的方法或構造函數調用産生的警告
  • @FunctionalInterface:Java 8 開始支援,辨別一個匿名函數或函數式接口
  • @Repeatable:Java 8 開始支援,辨別某注解可以在同一個聲明上使用多次,自定義注解需要使用到,也就是重複注解

Java内置注解

簡單使用一下常見的三個标準注解:@Override、@Deprecated、@SuppressWarnings

聲明父類:

public class Person {
    private String name;
    private Integer age;
    // 說話方法
    public void say() {
    }
}           

子類:

  • 繼承父類,通過 @Override 标記
  • 部分技能已經生疏,可以通過 @Deprecated 标記為過時,不建議使用,可能【翻車】
  • 聲明 str變量但是沒有使用,可以通過 @SuppressWarnings 注解,該注解有值,需要填入警告類型,unused就是未使用的意思,可以填寫多個值
public class Student extends Person{

    // 重寫注解
    @Override
    public void say() {
        System.out.println("我是學生");
    }
    // 過時注解,大學畢業之後籃球技能過時了不會了
    @Deprecated
    public String skill() {
        return "籃球";
    }

    // 排除警告注解,比如聲明變量沒有使用,編輯器就會報黃【警告提示】,添加該注解寫上 unused,就是禁止【未使用】警告
    @SuppressWarnings({"unused","rawtypes"})
    public void warning() {
        String str;
    }
}
           

SuppressWarnings所有值:

作用
all 抑制所有警告
boxing 抑制裝箱、拆箱操作時候的警告
cast 抑制映射相關的警告
dep-ann 抑制啟用注釋的警告
deprecation 抑制過期方法警告
fallthrough 抑制确在switch中缺失breaks的警告
finally 抑制finally子產品沒有傳回的警告
hiding 抑制相對于隐藏變量的局部變量的警告
incomplete-switch 忽略不完整的 switch 語句
nls 忽略非nls格式的字元
null 忽略對null的操作
rawtypes 使用generics時忽略沒有指定相應的類型
restriction 抑制禁止使用勸阻或禁止引用的警告
serial 忽略在serializable類中沒有聲明serialVersionUID變量
static-access 抑制不正确的靜态通路方式警告
synthetic-access 抑制子類沒有按最優方法通路内部類的警告
unchecked 抑制沒有進行類型檢查操作的警告
unqualified-field-access 抑制沒有權限通路的域的警告
unused 抑制沒被使用過的代碼的警告
小貼士:合理使用 @SuppressWarnings 注解可以友善調試和運維

自定義注解

自定義注解并可以使用自定義注解是邁向更高一層的重要表現,我們要明白為什麼要自定義注解,什麼時候自定義什麼樣的注解比較合适才是重中之重。

自定義注解的使用場景

自定義注解步驟:

  • 通過 @interface 關鍵字建立一個注解類,注解類中可以包含方法也可以不包含方法,這個方法可以認為是注解的參數
  • 使用元注解對自定義注解的功能和範圍進行限制

元注解:

元注解,就是定義注解的注解,也就是說這些元注解是的作用就是專門用來限制其它注解的注解。

元注解主要有五個:@Target,@Retention,@Documented,@Inherited,@Repeatable,其中 @Repeatable 是Java8新增的可重複注解。

@Target:用于指定注解的使用範圍,通過 ElemenetType 枚舉值決定

  • ElementType.TYPE:類、接口、注解、枚舉
  • ElementType.FIELD:字段、枚舉常量
  • ElementType.METHOD:方法
  • ElementType.PARAMETER:形式參數
  • ElementType.CONSTRUCTOR:構造方法
  • ElementType.LOCAL_VARIABLE:局部變量
  • ElementType.ANNOTATION_TYPE:注解
  • ElementType.PACKAGE:包
  • ElementType.TYPE_PARAMETER:類型參數
  • ElementType.TYPE_USE:類型使用
[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

@Retention:用于指定注解的保留政策,通過 RetentionPolicy 枚舉值決定

  • RetentionPolicy.SOURCE:注解隻保留在源碼中,在編譯時會被編譯器丢棄
  • RetentionPolicy.CLASS:(預設的保留政策) 注解會被保留在Class檔案中,但不會被加載到虛拟機中,運作時無法獲得
  • RetentionPolicy.RUNTIME:注解會被保留在Class檔案中,且會被加載到虛拟機中,可以在運作時通過反射獲得
[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

@Documented: 用于将注解包含在javadoc中,預設情況下,javadoc是不包括注解的,但如果使用了@Documented注解,則相關注解類型資訊會被包含在生成的文檔中,該注解沒有參數

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

@Inherited: 允許子類繼承父類中的注解

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

@Repeatable:Java8中引入的元注解,用于聲明标記的注解為可重複類型注解,可以在同一個地方多次使用

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術
發現所有的元注解上的@Target注解值為 ElementType.ANNOTATION_TYPE,意為該注解的作用範圍在注解上

案例:

需求:自定義一個名為 MyClassAnnonation 類級别的無參注解,定義一個使用在屬性上名為 Name 的帶屬性注解

代碼實作:

MyClassAnnonation:

package com.stt.annontation;

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

// ElementType.TYPE:這個注解可以使用在 類、接口、注解、枚舉上
@Target(ElementType.TYPE)
// RetentionPolicy.RUNTIME:注解被保留在Class檔案中,也會被加載到JVM中
@Retention(RetentionPolicy.RUNTIME)
public @interface MyClassAnnonation {

}           

Name:

package com.stt.annontation;

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

// 可以使用在字段上
@Target(ElementType.FIELD)
public @interface Name {
    // value帶小括号,是一個方法,有些文章寫value是參數,我感覺是不準确的
    String value();
    // 因為注解中可以寫成員變量【屬性】
    String job = "程式猿";
}           

Student類測試自定義注解:

package com.stt.annontation;

// 使用類注解
@MyClassAnnonation
public class Student {
    // 使用Name注解
    @Name(value = "添甄")
    private String name;

    public String getName() {
        return name;
    }
}           

錯誤應用1:比如@Name注解我們限制隻能使用在字段上,應用在方法上就會報錯

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

錯誤應用2:比如@Name注解需要傳參數,未傳參數也會報錯

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

深入一下:

這裡介紹注解的三個小知識點:

  • 巧用value方法
  • 預設值
  • 基本資料類型不能使用包裝類

value屬性:當注解中有value方法時,使用注解指派時,value方法可以不寫,直接寫具體的值即可

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

會不禁思考,是不是隻有一個方法時就可以省略屬性名直接填值,是以我們将value屬性名改成name試一下:

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

發現提示找不到value方法,說明預設會根據value進行比對,這裡說巧用value方法

預設值:我們可以通過default給注解參數預設值,如給value預設值為添甄:

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術
建議大家在自定義注解時,合理配置設定預設值,spring、MyBatis等架構的注解每個屬性都設定了預設值
[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

基本資料類型不能使用包裝類:比如聲明一個年齡,Integer無效,隻能使用int

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

以上就是自定義注解時的三個小知識點,大家知道,使用時避免踩坑。說到這不禁覺得我是真滴細!細節的細

求解:問看到這裡的小夥伴一個問題:注解裡邊的 value() 你們都稱呼為什麼?他正确的叫法應該是屬性還是方法還是直接叫成員還是其他的什麼呢?

注解應用

案例1:

需求:通過注解實作類似于Junit架構的@Test注解類似的功能,在程式運作時有@MyTest的注解的方法都執行

分析:

  • 建立名為MyTest的注解,限制使用在方法上
  • 因為程式運作時要檢測哪些方法上有@MyTest注解,是以該注解的保留機制應該是運作時期仍然儲存
  • 運作時通過反射掃描哪些方法上包含@MyTest注解,擷取到之後逐一執行

代碼實作:

注解:

package com.stt.annontation;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {
    
}           

方法标記:

package com.stt.annontation;

import java.lang.reflect.Method;

public class MyTestMain {

    @MyTest
    public void test1() {
        System.out.println("==========test1==========");
    }

    @MyTest
    public void test2() {
        System.out.println("==========test2==========");
    }

    @MyTest
    public void test3() {
        System.out.println("==========test3==========");
    }

    public void test4() {
        System.out.println("==========test4==========");
    }

    public static void main(String[] args) {
        // 1、建立測試類對象,通過反射運作方法時需要使用
        MyTestMain testMain = new MyTestMain();
        // 2、擷取類對象
        Class<MyTestMain> clazz = MyTestMain.class;
        // 3、擷取所有方法
        Method[] methods = clazz.getMethods();
        // 4、周遊所有的方法
        for (Method method : methods) {
            // 5、判斷哪些方法上有 @MyTest 注解
            if(method.isAnnotationPresent(MyTest.class)) {
                // 6、觸發執行,指明通過(1)建立的對象調用
                try {
                    method.invoke(testMain);
                } catch (Exception e) {
                    System.out.println("運作出錯!");
                }
            }
        }
    }
}           

運作結果:

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

test4方法沒有添加注解,是以就沒有運作,發現運作順序1、3、2,其實這個運作順序是随機的,如果想要控制執行順序,可以在注解中添加一個order值來實作,如下:

修改注解:添加order方法,值為int類型,預設值為0

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {
    int order() default 0;
}           

修改測試:在使用注解時添加順序,在使用時擷取到該注解,通過Stream根據order值排序之後再注意調用執行,就可以控制順序

public class MyTestMain {

    @MyTest(order = 1)
    public void test1() {
        System.out.println("==========test1==========");
    }

    @MyTest(order = 2)
    public void test2() {
        System.out.println("==========test2==========");
    }

    @MyTest(order = 3)
    public void test3() {
        System.out.println("==========test3==========");
    }

    public void test4() {
        System.out.println("==========test4==========");
    }

    public static void main(String[] args) {
        // 1、建立測試類對象,通過反射運作方法時需要使用
        MyTestMain testMain = new MyTestMain();
        // 2、擷取類對象
        Class<MyTestMain> clazz = MyTestMain.class;
        // 3、擷取所有方法
        Method[] methods = clazz.getMethods();
        List<Method> invokeMethods = new ArrayList<>();
        // 4、周遊所有的方法
        for (Method method : methods) {
            // 5、判斷哪些方法上有 @MyTest 注解
            if(method.isAnnotationPresent(MyTest.class)) {
                // 将方法添加到集合中
                invokeMethods.add(method);
            }
        }
        // 6、根據order值排序
        List<Method> methodList = invokeMethods.stream()
                .sorted(((o1, o2) -> o1.getAnnotation(MyTest.class).order() - o2.getAnnotation(MyTest.class).order()))
                .collect(Collectors.toList());
        // 7、根據排序後的方法執行,結果永遠是1,2,3
        methodList.forEach(method -> {
            try {
                method.invoke(testMain);
            } catch (Exception e) {
                System.out.println("運作錯誤!");
            }
        });
    }
}           

運作結果:

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

這樣是不就非常哇塞!

Spring中的類似注解:

@Order:就可以控制配置類的加載順序

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

@DependsOn:指定目前bean所依賴的beans。任何被指定依賴的bean都由Spring保證在目前bean之前建立

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

案例2:小高潮,這個案例一定要掌握,你的項目中絕對有這玩意

需求:在SpringBoot程式中,通過自定義注解 + aop實作日志記錄功能,在請求接口時,自動将記錄檔記錄到資料庫

分析:

  • 自定義Log注解,使用在類和方法上,并在運作時可以擷取到
  • 通過aop配置擷取哪些方法上定義了Log注解,并擷取到相關資訊
  • 将資訊整合到日志實體類中,存儲到MySQL
  • 為友善操作資料庫和實體類,引入mybatis-plus和lombok
  • 貼出核心代碼,springboot配置檔案和service、mapper就不貼出了

依賴:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.7.5</version>
    </dependency>
    <!-- SpringBoot 攔截器 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
        <version>2.7.5</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.24</version>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.2</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.36</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.31</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba.fastjson2</groupId>
        <artifactId>fastjson2</artifactId>
        <version>2.0.19</version>
    </dependency>
</dependencies>           

資料庫:

CREATE TABLE `sys_log` (
  `oper_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主鍵',
  `title` varchar(50) DEFAULT '' COMMENT '子產品标題',
  `business_type` int(11) DEFAULT '0' COMMENT '業務類型(0其它 1新增 2修改 3删除)',
  `method` varchar(100) DEFAULT '' COMMENT '方法名稱',
  `request_method` varchar(10) DEFAULT '' COMMENT '請求方式',
  `operator_type` int(11) DEFAULT '0' COMMENT '操作類别( 0、使用者端   1、平台管理端)',
  `oper_name` varchar(50) DEFAULT '' COMMENT '操作人員',
  `oper_url` varchar(255) DEFAULT '' COMMENT '請求URL',
  `oper_ip` varchar(128) DEFAULT '' COMMENT '主機位址',
  `oper_location` varchar(255) DEFAULT '' COMMENT '操作地點',
  `oper_param` varchar(2000) DEFAULT '' COMMENT '請求參數',
  `json_result` varchar(2000) DEFAULT '' COMMENT '傳回參數',
  `status` int(11) DEFAULT '0' COMMENT '操作狀态(1正常 0異常)',
  `error_msg` varchar(2000) DEFAULT '' COMMENT '錯誤消息',
  `oper_time` datetime DEFAULT NULL COMMENT '操作時間',
  PRIMARY KEY (`oper_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='記錄檔記錄';           

實體類:

package com.stt.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.time.LocalDateTime;

@Data
public class SysLog {

    @TableId(value = "oper_id", type = IdType.AUTO)
    private Long operId;

    private String title;

    private Integer businessType;

    private String method;

    private String requestMethod;

    private String operName;

    private String operUrl;

    private String operIp;

    private String operLocation;

    private String operParam;

    private String jsonResult;

    private Integer status;

    private String errorMsg;

    private LocalDateTime operTime;
}           

自定義注解:

package com.stt.annontation;

import java.lang.annotation.*;

@Target({ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {

    /**
     * 日志标題
     */
    public String title() default "";

    /**
     * 操作類型
     */
    public BusinessTypeEnum businessType() default BusinessTypeEnum.OTHER;
}           

BusinessTypeEnum枚舉:記錄什麼類型的操作

package com.stt.annontation;

public enum BusinessTypeEnum {
    /**
     * 其它
     */
    OTHER,

    /**
     * 新增
     */
    INSERT,

    /**
     * 修改
     */
    UPDATE,

    /**
     * 删除
     */
    DELETE,

    /**
     * 授權
     */
    GRANT,
}           

AOP配置:

package com.stt.annontation;

import com.alibaba.fastjson2.JSON;
import com.stt.entity.SysLog;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.HandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Map;

@Component
@Aspect
public class LogConfig {

    private static final Logger log = LoggerFactory.getLogger(LogConfig.class);
    // 引入日志Service,用于存儲資料進資料庫
    @Autowired
    private ISysLogService sysLogService;

    // 配置織入點-xxx代表自定義注解的存放位置,如:com.stt.annontation.Log
    @Pointcut("@annotation(com.stt.annontation.Log)")
    public void logPointCut() {}

    /**
     * 處理完請求後執行此處代碼
     *
     * @param joinPoint 切點
     */
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult)
    {
        handleLog(joinPoint, controllerLog, null, jsonResult);
    }

    /**
     * 如果處理請求時出現異常,在抛出異常後執行此處代碼
     *
     * @param joinPoint 切點
     * @param e 異常
     */
    @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e)
    {
        handleLog(joinPoint, controllerLog, e, null);
    }

    /**
     * 日志處理
     */
    protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult)
    {
        try {
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            // 擷取目前的使用者
            String userName = "添甄";
            // *========資料庫日志=========*//
            SysLog sysLog = new SysLog();
            sysLog.setStatus(1);
            // 請求的位址
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            assert requestAttributes != null;
            HttpServletRequest request = requestAttributes.getRequest();
            String ip = getIpAddr(request);
            sysLog.setOperIp(ip);
            sysLog.setOperUrl(request.getRequestURI());
            sysLog.setOperName(userName);

            if (e != null) {
                sysLog.setStatus(0);
                sysLog.setErrorMsg(e.getMessage().substring(0,2000));
            }
            // 設定方法名稱
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            sysLog.setMethod(className + "." + methodName + "()");
            // 設定請求方式
            sysLog.setRequestMethod(request.getMethod());
            // 處理設定注解上的參數
            getControllerMethodDescription(joinPoint, controllerLog, sysLog, jsonResult, request);
            // 儲存資料庫
            sysLog.setOperTime(LocalDateTime.now());
            // 将處理好的日至對象存儲進資料庫
            sysLogService.save(sysLog);
        } catch (Exception exp) {
            // 記錄本地異常日志
            log.error("==前置通知異常==");
            log.error("異常資訊:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }
    // 擷取操作ip位址
    public static String getIpAddr(HttpServletRequest request) {
        if (request == null) {
            return "unknown";
        }
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Forwarded-For");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
    // 擷取注解資訊
    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysLog sysLog, Object jsonResult, HttpServletRequest request) throws Exception {
        // 設定action動作
        sysLog.setBusinessType(log.businessType().ordinal());
        sysLog.setTitle(log.title());
    }

    private void setRequestValue(JoinPoint joinPoint, SysLog sysLog, HttpServletRequest request) throws Exception {
        String requestMethod = sysLog.getRequestMethod();
        if (RequestMethod.PUT.name().equals(requestMethod) || RequestMethod.POST.name().equals(requestMethod)) {
            String params = argsArrayToString(joinPoint.getArgs());
            sysLog.setOperParam(params.substring(0,2000));
        } else {
            Map<?, ?> paramsMap = (Map<?, ?>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
            sysLog.setOperParam(paramsMap.toString().substring(0,2000));
        }
    }
    // 解析方法參數資訊
    private String argsArrayToString(Object[] paramsArray) {
        StringBuilder params = new StringBuilder();
        if (paramsArray != null && paramsArray.length > 0) {
            for (Object o : paramsArray) {
                if (o != null && !isFilterObject(o)) {
                    try {
                        Object jsonObj = JSON.toJSON(o);
                        params.append(jsonObj.toString()).append(" ");
                    } catch (Exception e) {
                        log.error(e.getMessage());
                    }
                }
            }
        }
        return params.toString().trim();
    }

    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o) {
        Class<?> clazz = o.getClass();
        if (clazz.isArray()) {
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) o;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) o;
            for (Object value : map.entrySet()) {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
                || o instanceof BindingResult;
    }
}           

Controller:在需要記錄日志的請求方法上添加Log注解

package com.stt.controller;

import com.stt.annontation.BusinessTypeEnum;
import com.stt.annontation.Log;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("test")
public class TestController {

    @Log(title = "查詢清單",businessType = BusinessTypeEnum.OTHER)
    @GetMapping("list")
    public String list() {
        return "傳回資料清單";
    }

    @Log(title = "添加資料",businessType = BusinessTypeEnum.INSERT)
    @PostMapping
    public String save() {
        return "資料添加成功";
    }

    @Log(title = "修改資料",businessType = BusinessTypeEnum.UPDATE)
    @PutMapping
    public String update() {
        return "修改資料成功";
    }

    @Log(title = "删除資料",businessType = BusinessTypeEnum.DELETE)
    @DeleteMapping
    public String delete() {
        return "删除資料成功";
    }
}           

啟動項目之後請求接口效果如下:

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

請求對應接口,就會添加一條日志進庫,這就是通過自定義注解+aop實作的,把這個注解給公司小夥伴使用都直呼哇塞呢!

重複注解

自JDK5以來注解開始變得越來越流行,在各個架構中廣泛應用,在上邊我們也通過兩個案例實作了自定義注解,并投入使用,不過注解有一個很大的限制是:在同一個地方不能多次使用同一個注解。比如:

我有兩組過濾方式,一個方法上不能寫兩個相同的注解

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

就有老程式員繞開這個機制,寫一個大注解,裡邊包含一個小注解數組實作,如下

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

JDK 8引入了重複注解的概念,允許在同一個地方多次使用同一個注解。在JDK 8中使用@Repeatable注解定義重複注解

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

是以發現,其實和原本解決可重複注解的原理基本一緻,将多個注解放到一個容器中,隻不過Java8的這種方式将放入容器的動作隐藏起來了,編碼使用注解時更友善,更直覺,當我們通過反射擷取注解時,發現擷取到的其實是MyFilters注解

[直呼内行] 注解開發才是簡潔之道,讓同行直呼内行的Java注解技術

總結:

  • 如果需要兩個相同的注解,有兩種方式
  • 使用Java8之前的寫法,聲明一個大注解,包含需要重複的注解數組
  • 使用Java8之後的寫法,仍然需要聲明一個大注解,不過通過 @Repeatable 注解在需要重複的注解聲明上指定大注解
  • 兩者原理相同,通過反射擷取的注解類型都是大注解類型

類型注解

類型注解在開發時使用較少,個人感覺不重要,如果你有使用的經驗,還請指教。從Java 8開始,注解已經能應用于任何類型。這其中包括new操作符、類型轉換、instanceof檢查、泛型類型參數,以及implements和throws子句。這裡,我們舉了一個例子,這個例子中類型為String的變量name不能為空,是以我們使用了@NonNull對其進行注解

@NotNull String name = "添甄";           

我們也可以在集合的泛型上使用類型注解

List<@NotNull String> names = new ArrayList();           

利用好對類型的注解非常有利于我們對程式進行分析。這兩個例子中,通過這一工具我們可以確定getName不傳回空,names清單中的元素總是非空值。這會極大地幫助你減少代碼中不期而至的錯誤,不過用的比較少,增加了代碼量你懂的

總結:

  • 注解在Java程式設計中有舉足輕重的作用,配合反射使用讓編碼實作業務變的更簡潔,快速
  • 如果想要繼續晉升技術,走向架構師,資深開發,注解是你繞不開的技能點,可以幫助團隊封裝更好用的功能,與第一點相輔相成
  • 注解的重要之處在于合理的自定義注解,合理的使用注解
  • 注解不是注釋,新手小夥伴要厘清楚,面試時千萬不要說錯
  • 注釋的使用,架構中的注釋作用,原理也是面試時的高頻問題,多多練習,百煉成鋼
我是添甄,堅持高頻輸出知識,喜歡記得點贊,關注鼓勵一下!

繼續閱讀