自定義註解!絕對是程式設計師裝逼的利器!!
阿新 • • 發佈:2020-11-10
[GitHub 18k Star 的Java工程師成神之路,不來了解一下嗎!](https://github.com/hollischuang/toBeTopJavaer)
[GitHub 18k Star 的Java工程師成神之路,真的不來了解一下嗎!](https://github.com/hollischuang/toBeTopJavaer)
[GitHub 18k Star 的Java工程師成神之路,真的真的不來了解一下嗎!](https://github.com/hollischuang/toBeTopJavaer)
相信很多人對Java中的註解都很熟悉,比如我們經常會用到的一些如@Override、@Autowired、@Service等,這些都是JDK或者諸如Spring這類框架給我們提供的。
在以往的面試過程中,我發現,關於註解的知識很多程式設計師都僅僅停留在使用的層面上,很少有人知道註解是如何實現的,更別提使用自定義註解來解決實際問題了。
但是其實,我覺得一個好的程式設計師的標準就是懂得如何優化自己的程式碼,那在程式碼優化上面,如何精簡程式碼,去掉重複程式碼就是一個至關重要的話題,在這個話題領域,自定義註解絕對可以算得上是一個大大的功臣。
所以,**在我看來,會使用自定義註解 ≈ 好的程式設計師。**
那麼,本文,就來介紹幾個,作者在開發中實際用到的幾個例子,向你介紹下如何使用註解來提升你程式碼的逼格。
### 基本知識
在Java中,註解分為兩種,元註解和自定義註解。
很多人誤以為自定義註解就是開發者自己定義的,而其它框架提供的不算,但是其實上面我們提到的那幾個註解其實都是自定義註解。
關於"元"這個描述,在程式設計世界裡面有都很多,比如"元註解"、"元資料"、"元類"、"元表"等等,這裡的"元"其實都是從meta翻譯過來的。
一般我們把**元註解理解為描述註解的註解**,**元資料理解為描述資料的資料**,**元類理解為描述類的類**...
所以,在Java中,除了有限的幾個固定的"描述註解的註解"以外,所有的註解都是自定義註解。
在JDK中提供了4個標準的用來對註解型別進行註解的註解類(元註解),他們分別是:
@Target
@Retention
@Documented
@Inherited
除了以上這四個,所有的其他註解全部都是自定義註解。
這裡不準備深入介紹以上四個元註解的作用,大家可以自行學習。
本文即將提到的幾個例子,都是作者在日常工作中真實使用到的場景,這例子有一個共同點,那就是都用到了Spring的AOP技術。
什麼是AOP以及他的用法相信很多人都知道,這裡也就不展開介紹了。
### 使用自定義註解做日誌記錄
不知道大家有沒有遇到過類似的訴求,就是希望在一個方法的入口處或者出口處做統一的日誌處理,比如記錄一下入參、出參、記錄下方法執行的時間等。
如果在每一個方法中自己寫這樣的程式碼的話,一方面會有很多程式碼重複,另外也容易被遺漏。
這種場景,就可以使用自定義註解+切面實現這個功能。
假設我們想要在一些web請求的方法上,記錄下本次操作具體做了什麼事情,比如新增了一條記錄或者刪除了一條記錄等。
首先我們自定義一個註解:
/**
* Operate Log 的自定義註解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OpLog {
/**
* 業務型別,如新增、刪除、修改
*
* @return
*/
public OpType opType();
/**
* 業務物件名稱,如訂單、庫存、價格
*
* @return
*/
public String opItem();
/**
* 業務物件編號表示式,描述瞭如何獲取訂單號的表示式
*
* @return
*/
public String opItemIdExpression();
}
因為我們不僅要在日誌中記錄本次操作了什麼,還需要知道被操作的物件的具體的唯一性標識,如訂單號資訊。
但是每一個介面方法的引數型別肯定是不一樣的,很難有一個統一的標準,那麼我們就可以藉助Spel表示式,即在表示式中指明如何獲取對應的物件的唯一性標識。
有了上面的註解,接下來就可以寫切面了。主要程式碼如下:
/**
* OpLog的切面處理類,用於通過註解獲取日誌資訊,進行日誌記錄
*
* @author Hollis
*/
@Aspect
@Component
public class OpLogAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(OpLogAspect.class);
@Autowired
HttpServletRequest request;
@Around("@annotation(com.hollis.annotation.OpLog)")
public Object log(ProceedingJoinPoint pjp) throws Exception {
Method method = ((MethodSignature)pjp.getSignature()).getMethod();
OpLog opLog = method.getAnnotation(OpLog.class);
Object response = null;
try {
// 目標方法執行
response = pjp.proceed();
} catch (Throwable throwable) {
throw new Exception(throwable);
}
if (StringUtils.isNotEmpty(opLog.opItemIdExpression())) {
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(opLog.opItemIdExpression());
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(opLog.opType(), opLog.opItem(), itemId);
}
return response;
}
private void handle(OpType opType, String opItem, String opItemId) {
// 通過日誌列印輸出
LOGGER.info("opType = " + opType.name() +",opItem = " +opItem + ",opItemId = " +opItemId);
}
}
以上切面中,有幾個點需要大家注意的:
1、使用@Around註解來指定對標註了OpLog的方法設定切面。 2、使用Spel的相關方法,通過指定的表示,從對應的引數中獲取到目標物件的唯一性標識。 3、再方法執行成功後,輸出日誌。
有了以上的切面及註解後,我們只需要在對應的方法上增加註解標註即可,如:
@RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
@OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#id")
public @ResponseBody
HashMap view(@RequestParam(name = "id") String id)
throws Exception {
}
上面這種是入參的引數列表中已經有了被操作的物件的唯一性標識,直接使用`#id`指定即可。
如果被操作的物件的唯一性標識不在入參列表中,那麼可能是入參的物件中的某一個屬性,用法如下:
@RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
@OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#orderVo.id")
public @ResponseBody
HashMap update(OrderVO orderVo)
throws Exception {
}
以上,即可從入參的OrderVO物件的id屬性的值獲取。
如果我們要記錄的唯一性標識,在入參中沒有的話,應該怎麼辦呢?最典型的就是插入方法,插入成功之前,根本不知道主鍵ID是什麼,這種怎麼辦呢?
我們上面的切面中,做了一件事情,就是我們把方法的返回值也會使用表示式進行一次解析,如果可以解析得到具體的值,可以是可以。如以下寫法:
@RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
@OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#insertResult.id")
public @ResponseBody
InsertResult insert(OrderVO orderVo)
throws Exception {
return orderDao.insert(orderVo);
}
以上,就是一個簡單的使用自定義註解+切面進行日誌記錄的場景。下面我們再來看一個如何使用註解做方法引數的校驗。
### 使用自定義註解做前置檢查
當我們對外部提供介面的時候,會對其中的部分引數有一定的要求,比如某些引數值不能為空等。大多數情況下我們都需要自己主動進行校驗,判斷對方傳入的值是否合理。
這裡推薦一個使用HibernateValidator + 自定義註解 + AOP實現引數校驗的方式。
首先我們會有一個具體的入參類,定義如下:
public class User {
private String idempotentNo;
@NotNull(
message = "userName can't be null"
)
private String userName;
}
以上,對userName引數註明不能為null。
然後再使用hibernate validator定義一個工具類,用於做引數校驗。
/**
* 引數校驗工具
*
* @author Hollis
*/
public class BeanValidator {
private static Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(true)
.buildValidatorFactory().getValidator();
/**
* @param object object
* @param groups groups
*/
public static void validateObject(Object object, Class... groups) throws ValidationException {