使用自定義註解和切面AOP實現Java程式增強
1.註解介紹
1.1註解的本質
Oracle官方對註解的定義為:
Annotations, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.
註解是元數據的一種形式,它提供有關程式的數據,該數據不屬於程式本身。 註解對其注釋的程式碼操作沒有直接影響。
而在JDK
的Annotation介面中有一行注釋如此寫到:
/**
* The common interface extended by all annotation types.
* ...
*/
public interface Annotation {...}
這說明其他註解都擴展自 Annotation
這個介面,也就是說註解的本質就是一個介面。
以 Spring Boot 中的一個註解為例:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
String value() default "";
}
它實際上相當於:
public interface Component extends Annotation{...}
而@interface
可以看成是一個語法糖。
1.2註解的要素
依然來看 @Component
這個例子:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
String value() default "";
}
在註解定義上有幾個註解@Target, @Retention, @Documented
,被稱為 元註解。
所謂元註解就是說明註解的註解
Java
中的元註解共有以下幾個:
1.2.1 @Target
@Target
顧名思義,這個註解標識了被修飾註解的作用對象。我們看看它的源碼:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}
可以看到,這個註解的 value 值是一個數組,這也就意味著註解的作用對象可以有多個。 其取值範圍都在ElementType
這個枚舉之中:
public enum ElementType {
/** 類、介面、枚舉定義 */
TYPE,
/** 欄位,包括枚舉值 */
FIELD,
/** 方法 */
METHOD,
/** 參數 */
PARAMETER,
/** 構造方法 */
CONSTRUCTOR,
/** 局部變數 */
LOCAL_VARIABLE,
/** 元註解 */
ANNOTATION_TYPE,
/** 包定義 */
PACKAGE...
}
不同的值代表被註解可修飾的範圍,例如TYPE
只能修飾類、介面和枚舉定義。這其中有個很特殊的值叫做 ANNOTATION_TYPE
, 是專門表示元註解的。
在回過頭來看 @Component
這個例子, Target
取值為 TYPE
。熟悉 Spring Boot
的同學也一定知道,@Component
確實是不能放到方法或者屬性前面的。
1.2.2@Retention
@Retention
註解指定了被修飾的註解的生命周期。定義如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
可以看到這個註解帶一個 RetentionPolicy
的枚舉值:
public enum RetentionPolicy {
SOURCE,
CLASS,
RUNTIME
}
SOURCE
表示註解編譯時可見,編譯完後就被丟棄。這種註解一般用於在編譯器做一些事情;CLASS
表示在編譯完後寫入 class 文件,但在類載入後被丟棄。這種註解一般用於在類載入階段做一些事情;RUNTIME
則表示註解會一直起作用。
1.2.3 @Documented
這個註解比較簡單,表示是否添加到 java doc
中。
1.2.4 @Inherited
這個也比較簡單,表示註解是否被繼承。這個註解不是很常用。
注意:元註解只在定義註解時被使用!
1.3 註解的構成
從上面的元註解可以了解到,一個註解可以關聯多個 ElementType
,但只能有一個 RetentionPolicy
:
Java 中有三個常用的內置註解,其實相信大家都用過或者見過。不過在了解了註解的真實面貌以後,不妨重新認識一下吧!
1.4 Java內置註解
1.4.1 @Override
@Override
它的定義為:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
可見這個註解沒有任何取值,只能修飾方法,而且RetentionPolicy 為 SOURCE,說明這是一個僅在編譯階段起作用的註解。
它的真實作用想必大家一定知道,就是在編譯階段,如果一個類的方法被 @Override
修飾,編譯器會在其父類中查找是否有同簽名函數,如果沒有則編譯報錯。可見這確實是一個除了在編譯階段就沒什麼用的註解。
1.4.2 @Deprecated
@Deprecated
它的定義為:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
這個註解也沒有任何取值,能修飾所有的類型,永久存在。這個註解的作用是,告訴使用者被修飾的程式碼不推薦使用了,可能會在下一個軟體版本中移除。這個註解僅僅起到一個通知機制,如果程式碼調用了被@Deprecated 修飾的程式碼,編譯器在編譯時輸出一個編譯告警。
1.4.3 @SuppressWarnings
@SuppressWarnings
它的定義為:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
/**
* The set of warnings that are to be suppressed by the compiler in the
* annotated element. Duplicate names are permitted. The second and
* successive occurrences of a name are ignored. The presence of
* unrecognized warning names is <i>not</i> an error: Compilers must
* ignore any warning names they do not recognize. They are, however,
* free to emit a warning if an annotation contains an unrecognized
* warning name.
*
* <p> The string {@code "unchecked"} is used to suppress
* unchecked warnings. Compiler vendors should document the
* additional warning names they support in conjunction with this
* annotation type. They are encouraged to cooperate to ensure
* that the same names work across multiple compilers.
* @return the set of warnings to be suppressed
*/
String[] value();
}
這個註解有一個字元串數組的值,需要我們使用註解的時候傳遞。可以在類型、屬性、方法、參數、構造函數和局部變數前使用,聲明周期是編譯期。
這個註解的主要作用是壓制編譯告警的。
2.AOP介紹(AspectJ暫不討論)
2.1 Spring AOP基本概念
- 是一種動態編譯期增強性AOP的實現
- 與IOC進行整合,不是全面的切面框架
- 與動態代理相輔相成
- 有兩種實現:基於jdk動態代理、cglib
2.2 Spring AOP與AspectJ區別
- Spring的AOP是基於動態代理的,動態增強目標對象,而AspectJ是靜態編譯時增強,需要使用自己的編譯器來編譯,還需要織入器
- 使用AspectJ編寫的java程式碼無法直接使用javac編譯,必須使用AspectJ增強的ajc增強編譯器才可以通過編譯,寫法不符合原生Java的語法;而Spring AOP是符合Java語法的,也不需要指定編譯器去編譯,一切都由Spring 處理。
2.3 使用步驟
- 定義業務組件
- 定義切點(重點)
- 定義增強處理方法(切面方法)
這邊用下面例子的AOP類來進行說明 (基於Spring AOP的)
/**
* @Author Song
* @Date 2020/5/26 9:50
* @Version 1.0
*/
@Slf4j
@Aspect
@Component
public class EagleEyeAspect {
@Pointcut("@annotation(com.ctgu.song.plantfactory.v2.annotation.EagleEye)")
public void eagleEye() {
}
@Around("eagleEye()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Signature signature = pjp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
EagleEye eagleEye = method.getAnnotation(EagleEye.class);
String desc = eagleEye.desc();
log.info("============請求開始==========");
log.info("請求鏈接:{}", request.getRequestURI().toString());
log.info("介面描述:{}", desc);
log.info("請求類型:{}", request.getMethod());
log.info("請求方法:{}.{}", signature.getDeclaringTypeName(), signature.getName());
log.info("請求IP:{}", request.getRemoteAddr());
log.info("請求入參:{}", JSON.toJSONString(pjp.getArgs()));
Object result = pjp.proceed();
long end = System.currentTimeMillis();
log.info("請求耗時:{}ms", end - begin);
log.info("請求返回:{}", JSON.toJSONString(result));
log.info("=============請求結束===========");
return result;
}
}
這邊先不看程式碼的具體內容,先簡單介紹一下用到AOP中常用的註解
- @Aspect : 指定切面類;
- @Pointcut:公共切入點表達式
- 通知方法
- 前置通知(@Before) 目標方法執行之前,執行註解的內容
- 後置通知(@After)目標方法執行之後,執行註解的內容
- 返回通知 (@AfterReturning)目標方法返回後,執行註解的內容
- 異常通知 (@AfterThrowing)目標方法拋出異常後,執行註解的內容
- 環繞通知 (@Around)目標方法執行前後,分別執行一些程式碼
注意 定義好切片類後要將其加入Spring容器內才能使用哦 (可以使用@Component註解)
3. 具體實現(一個例子)
1.首先定義一個註解,程式碼如下
/**
* @Author Song
* @Date 2020/5/26 9:44
* @Version 1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface EagleEye {
/**
* @Retention(RetentionPolicy.RUNTIME)
* 定義了註解的生命周期為運行時
* <p>
* @Target(ElementType.METHOD)
* 定義了註解的作用域為方法
* <p>
* Documented
* 標識該註解可以被JavaDoc記錄
* <p>
* 定義註解名稱為EagleEye(鷹眼,哈哈~~)
* <p>
* 定義一個元素desc,用來描述被修飾的方法
* <p>
* 介面描述
*
* @return
*/
String desc() default "";
}
2.定義切片內並寫好自己想要增強的方法
直接貼程式碼了~~
/**
* @Author Song
* @Date 2020/5/26 9:50
* @Version 1.0
*/
@Slf4j
@Aspect
@Component
public class EagleEyeAspect {
@Pointcut("@annotation(com.ctgu.song.plantfactory.v2.annotation.EagleEye)")
public void eagleEye() {
}
@Around("eagleEye()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Signature signature = pjp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
EagleEye eagleEye = method.getAnnotation(EagleEye.class);
String desc = eagleEye.desc();
log.info("============請求開始==========");
log.info("請求鏈接:{}", request.getRequestURI().toString());
log.info("介面描述:{}", desc);
log.info("請求類型:{}", request.getMethod());
log.info("請求方法:{}.{}", signature.getDeclaringTypeName(), signature.getName());
log.info("請求IP:{}", request.getRemoteAddr());
log.info("請求入參:{}", JSON.toJSONString(pjp.getArgs()));
Object result = pjp.proceed();
long end = System.currentTimeMillis();
log.info("請求耗時:{}ms", end - begin);
log.info("請求返回:{}", JSON.toJSONString(result));
log.info("=============請求結束===========");
return result;
}
}
在@Pointcut里通過@annotation來配置切點,代表我們的AOP切面會切到所有用EagleEye註解修飾的類。
然後使用@Around環繞通知在被註解的方法前後執行一些程式碼
Object result = pjp.proceed();
這行程式碼之前就是執行目標方法之前需要執行的程式碼 ,這行程式碼之後就是執行目標方法之後需要執行的程式碼
3. 註解的使用
只需要在需要被註解的方法上面使用自己的註解就行了 這裡拿我自己項目中的一個Controller中的方法舉例
@EagleEye(desc = "分頁查詢實驗")
@GetMapping("/experiment")
@ApiOperation("分頁查詢實驗")
public RsBody<Page<ExperimentVO2>> pageExperiment(ExperimentQueryDTO queryDTO) {
log.info("請求分頁查詢實驗的方法pageExperiment,請求參數為{}", queryDTO.toString());
RsBody<Page<ExperimentVO2>> rsBody = new RsBody<>();
IPage<Experiment> page = experimentV2Service.page(new Page<>(queryDTO.getCurrent() - 1, queryDTO.getSize()), new LambdaQueryWrapper<Experiment>()
.like(queryDTO.getExperimentId() != null, Experiment::getExperimentId, queryDTO.getExperimentId())
.eq(queryDTO.getExperimentStatus() != null, Experiment::getExperimentStatus, queryDTO.getExperimentStatus())
.between(queryDTO.getStartTime() != null && queryDTO.getEndTime() != null, Experiment::getStartTime, queryDTO.getStartTime(), queryDTO.getEndTime())
.orderBy(true, false, Experiment::getExperimentId));
//組裝Vo
List<ExperimentVO2> experimentVOList = new ArrayList<>();
for (Experiment experiment : page.getRecords()) {
ExperimentVO2 experimentVO = new ExperimentVO2();
experimentVO.setExperimentId(experiment.getExperimentId());
PlantInfo byPlantId = plantService.findByPlantId(experiment.getPlantId());
if (byPlantId != null) {
experimentVO.setPlantName(byPlantId.getPlantName());
} else {
experimentVO.setPlantName("植物被刪除");
}
experimentVO.setStartTime(experiment.getStartTime());
experimentVO.setEndTime(experiment.getEndTime());
experimentVO.setExperimentPurpose(experiment.getExperimentPurpose());
experimentVO.setExperimentDescription(experiment.getExperimentDescription());
experimentVO.setExperimentAddress(experiment.getExperimentAddress());
experimentVO.setExperimentPersonName(userService.findById(experiment.getExperimentPersonId()).getUserName());
experimentVO.setCronType(experiment.getCronType());
experimentVO.setExperimentStatus(experiment.getExperimentStatus());
experimentVO.setExperimentResult(experiment.getExperimentResult());
experimentVOList.add(experimentVO);
}
Page<ExperimentVO2> pageVo = new Page<ExperimentVO2>();
pageVo.setPages(page.getPages());
pageVo.setRecords(experimentVOList);
pageVo.setTotal(page.getTotal());
pageVo.setSize(page.getSize());
pageVo.setCurrent(page.getCurrent());
return rsBody.setBody(true).setData(pageVo);
}
4.測試情況
好的 萬事俱備 讓我們運行一下程式 並訪問這個方法 (過程略過)
很有意思吧~~