自定义注解!绝对是程序员装逼的利器!!
GitHub 18k Star 的Java工程師成神之路,不來(lái)了解一下嗎!
GitHub 18k Star 的Java工程師成神之路,真的不來(lái)了解一下嗎!
GitHub 18k Star 的Java工程師成神之路,真的真的不來(lái)了解一下嗎!
相信很多人對(duì)Java中的注解都很熟悉,比如我們經(jīng)常會(huì)用到的一些如@Override、@Autowired、@Service等,這些都是JDK或者諸如Spring這類框架給我們提供的。
在以往的面試過(guò)程中,我發(fā)現(xiàn),關(guān)于注解的知識(shí)很多程序員都僅僅停留在使用的層面上,很少有人知道注解是如何實(shí)現(xiàn)的,更別提使用自定義注解來(lái)解決實(shí)際問(wèn)題了。
但是其實(shí),我覺得一個(gè)好的程序員的標(biāo)準(zhǔn)就是懂得如何優(yōu)化自己的代碼,那在代碼優(yōu)化上面,如何精簡(jiǎn)代碼,去掉重復(fù)代碼就是一個(gè)至關(guān)重要的話題,在這個(gè)話題領(lǐng)域,自定義注解絕對(duì)可以算得上是一個(gè)大大的功臣。
所以,在我看來(lái),會(huì)使用自定義注解 ≈ 好的程序員。
那么,本文,就來(lái)介紹幾個(gè),作者在開發(fā)中實(shí)際用到的幾個(gè)例子,向你介紹下如何使用注解來(lái)提升你代碼的逼格。
基本知識(shí)
在Java中,注解分為兩種,元注解和自定義注解。
很多人誤以為自定義注解就是開發(fā)者自己定義的,而其它框架提供的不算,但是其實(shí)上面我們提到的那幾個(gè)注解其實(shí)都是自定義注解。
關(guān)于"元"這個(gè)描述,在編程世界里面有都很多,比如"元注解"、"元數(shù)據(jù)"、"元類"、"元表"等等,這里的"元"其實(shí)都是從meta翻譯過(guò)來(lái)的。
一般我們把元注解理解為描述注解的注解,元數(shù)據(jù)理解為描述數(shù)據(jù)的數(shù)據(jù),元類理解為描述類的類...
所以,在Java中,除了有限的幾個(gè)固定的"描述注解的注解"以外,所有的注解都是自定義注解。
在JDK中提供了4個(gè)標(biāo)準(zhǔn)的用來(lái)對(duì)注解類型進(jìn)行注解的注解類(元注解),他們分別是:
@Target @Retention @Documented @Inherited除了以上這四個(gè),所有的其他注解全部都是自定義注解。
這里不準(zhǔn)備深入介紹以上四個(gè)元注解的作用,大家可以自行學(xué)習(xí)。
本文即將提到的幾個(gè)例子,都是作者在日常工作中真實(shí)使用到的場(chǎng)景,這例子有一個(gè)共同點(diǎn),那就是都用到了Spring的AOP技術(shù)。
什么是AOP以及他的用法相信很多人都知道,這里也就不展開介紹了。
使用自定義注解做日志記錄
不知道大家有沒有遇到過(guò)類似的訴求,就是希望在一個(gè)方法的入口處或者出口處做統(tǒng)一的日志處理,比如記錄一下入?yún)ⅰ⒊鰠ⅰ⒂涗浵路椒▓?zhí)行的時(shí)間等。
如果在每一個(gè)方法中自己寫這樣的代碼的話,一方面會(huì)有很多代碼重復(fù),另外也容易被遺漏。
這種場(chǎng)景,就可以使用自定義注解+切面實(shí)現(xiàn)這個(gè)功能。
假設(shè)我們想要在一些web請(qǐng)求的方法上,記錄下本次操作具體做了什么事情,比如新增了一條記錄或者刪除了一條記錄等。
首先我們自定義一個(gè)注解:
/*** Operate Log 的自定義注解*/ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface OpLog {/*** 業(yè)務(wù)類型,如新增、刪除、修改** @return*/public OpType opType();/*** 業(yè)務(wù)對(duì)象名稱,如訂單、庫(kù)存、價(jià)格** @return*/public String opItem();/*** 業(yè)務(wù)對(duì)象編號(hào)表達(dá)式,描述了如何獲取訂單號(hào)的表達(dá)式** @return*/public String opItemIdExpression(); }因?yàn)槲覀儾粌H要在日志中記錄本次操作了什么,還需要知道被操作的對(duì)象的具體的唯一性標(biāo)識(shí),如訂單號(hào)信息。
但是每一個(gè)接口方法的參數(shù)類型肯定是不一樣的,很難有一個(gè)統(tǒng)一的標(biāo)準(zhǔn),那么我們就可以借助Spel表達(dá)式,即在表達(dá)式中指明如何獲取對(duì)應(yīng)的對(duì)象的唯一性標(biāo)識(shí)。
有了上面的注解,接下來(lái)就可以寫切面了。主要代碼如下:
/*** OpLog的切面處理類,用于通過(guò)注解獲取日志信息,進(jìn)行日志記錄** @author Hollis*/ @Aspect @Component public class OpLogAspect {private static final Logger LOGGER = LoggerFactory.getLogger(OpLogAspect.class);@AutowiredHttpServletRequest 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 {// 目標(biāo)方法執(zhí)行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();// 獲取參數(shù)值Object[] args = pjp.getArgs();// 獲取運(yùn)行時(shí)參數(shù)的名稱LocalVariableTableParameterNameDiscoverer discoverer= new LocalVariableTableParameterNameDiscoverer();String[] parameterNames = discoverer.getParameterNames(method);// 將參數(shù)綁定到context中if (parameterNames != null) {for (int i = 0; i < parameterNames.length; i++) {context.setVariable(parameterNames[i], args[i]);}}// 將方法的resp當(dāng)做變量放到context中,變量名稱為該類名轉(zhuǎn)化為小寫字母開頭的駝峰形式if (response != null) {context.setVariable(CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, response.getClass().getSimpleName()),response);}// 解析表達(dá)式,獲取結(jié)果String itemId = String.valueOf(expression.getValue(context));// 執(zhí)行日志記錄handle(opLog.opType(), opLog.opItem(), itemId);}return response;}private void handle(OpType opType, String opItem, String opItemId) {// 通過(guò)日志打印輸出LOGGER.info("opType = " + opType.name() +",opItem = " +opItem + ",opItemId = " +opItemId);} }以上切面中,有幾個(gè)點(diǎn)需要大家注意的:
1、使用@Around注解來(lái)指定對(duì)標(biāo)注了OpLog的方法設(shè)置切面。 2、使用Spel的相關(guān)方法,通過(guò)指定的表示,從對(duì)應(yīng)的參數(shù)中獲取到目標(biāo)對(duì)象的唯一性標(biāo)識(shí)。 3、再方法執(zhí)行成功后,輸出日志。
有了以上的切面及注解后,我們只需要在對(duì)應(yīng)的方法上增加注解標(biāo)注即可,如:
@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 { }上面這種是入?yún)⒌膮?shù)列表中已經(jīng)有了被操作的對(duì)象的唯一性標(biāo)識(shí),直接使用#id指定即可。
如果被操作的對(duì)象的唯一性標(biāo)識(shí)不在入?yún)⒘斜碇?#xff0c;那么可能是入?yún)⒌膶?duì)象中的某一個(gè)屬性,用法如下:
@RequestMapping(method = {RequestMethod.GET, RequestMethod.POST}) @OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#orderVo.id") public @ResponseBody HashMap update(OrderVO orderVo)throws Exception { }以上,即可從入?yún)⒌腛rderVO對(duì)象的id屬性的值獲取。
如果我們要記錄的唯一性標(biāo)識(shí),在入?yún)⒅袥]有的話,應(yīng)該怎么辦呢?最典型的就是插入方法,插入成功之前,根本不知道主鍵ID是什么,這種怎么辦呢?
我們上面的切面中,做了一件事情,就是我們把方法的返回值也會(huì)使用表達(dá)式進(jìn)行一次解析,如果可以解析得到具體的值,可以是可以。如以下寫法:
@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); }以上,就是一個(gè)簡(jiǎn)單的使用自定義注解+切面進(jìn)行日志記錄的場(chǎng)景。下面我們?cè)賮?lái)看一個(gè)如何使用注解做方法參數(shù)的校驗(yàn)。
使用自定義注解做前置檢查
當(dāng)我們對(duì)外部提供接口的時(shí)候,會(huì)對(duì)其中的部分參數(shù)有一定的要求,比如某些參數(shù)值不能為空等。大多數(shù)情況下我們都需要自己主動(dòng)進(jìn)行校驗(yàn),判斷對(duì)方傳入的值是否合理。
這里推薦一個(gè)使用HibernateValidator + 自定義注解 + AOP實(shí)現(xiàn)參數(shù)校驗(yàn)的方式。
首先我們會(huì)有一個(gè)具體的入?yún)㈩?#xff0c;定義如下:
public class User {private String idempotentNo;@NotNull(message = "userName can't be null")private String userName; }以上,對(duì)userName參數(shù)注明不能為null。
然后再使用hibernate validator定義一個(gè)工具類,用于做參數(shù)校驗(yàn)。
/*** 參數(shù)校驗(yàn)工具** @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 {Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);if (constraintViolations.stream().findFirst().isPresent()) {throw new ValidationException(constraintViolations.stream().findFirst().get().getMessage());}} }以上代碼,會(huì)對(duì)一個(gè)bean進(jìn)行校驗(yàn),一旦失敗,就會(huì)拋出ValidationException。
接下來(lái)定義一個(gè)注解:
/*** facade接口注解, 用于統(tǒng)一對(duì)facade進(jìn)行參數(shù)校驗(yàn)及異常捕獲* <pre>* 注意,使用該注解需要注意,該方法的返回值必須是BaseResponse的子類* </pre>*/@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Facade {}這個(gè)注解里面沒有任何參數(shù),只用于標(biāo)注那些方法要進(jìn)行參數(shù)校驗(yàn)。
接下來(lái)定義切面:
/*** Facade的切面處理類,統(tǒng)一統(tǒng)計(jì)進(jìn)行參數(shù)校驗(yàn)及異常捕獲** @author Hollis*/ @Aspect @Component public class FacadeAspect {private static final Logger LOGGER = LoggerFactory.getLogger(FacadeAspect.class);@AutowiredHttpServletRequest request;@Around("@annotation(com.hollis.annotation.Facade)")public Object facade(ProceedingJoinPoint pjp) throws Exception {Method method = ((MethodSignature)pjp.getSignature()).getMethod();Object[] args = pjp.getArgs();Class returnType = ((MethodSignature)pjp.getSignature()).getMethod().getReturnType();//循環(huán)遍歷所有參數(shù),進(jìn)行參數(shù)校驗(yàn)for (Object parameter : args) {try {BeanValidator.validateObject(parameter);} catch (ValidationException e) {return getFailedResponse(returnType, e);}}try {// 目標(biāo)方法執(zhí)行Object response = pjp.proceed();return response;} catch (Throwable throwable) {return getFailedResponse(returnType, throwable);}}/*** 定義并返回一個(gè)通用的失敗響應(yīng)*/private Object getFailedResponse(Class returnType, Throwable throwable)throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {//如果返回值的類型為BaseResponse 的子類,則創(chuàng)建一個(gè)通用的失敗響應(yīng)if (returnType.getDeclaredConstructor().newInstance() instanceof BaseResponse) {BaseResponse response = (BaseResponse)returnType.getDeclaredConstructor().newInstance();response.setSuccess(false);response.setResponseMessage(throwable.toString());response.setResponseCode(GlobalConstant.BIZ_ERROR);return response;}LOGGER.error("failed to getFailedResponse , returnType (" + returnType + ") is not instanceof BaseResponse");return null;} }以上代碼,和前面的切面有點(diǎn)類似,主要是定義了一個(gè)切面,會(huì)對(duì)所有標(biāo)注@Facade的方法進(jìn)行統(tǒng)一處理,即在開始方法調(diào)用前進(jìn)行參數(shù)校驗(yàn),一旦校驗(yàn)失敗,則返回一個(gè)固定的失敗的Response,特別需要注意的是,這里之所以可以返回一個(gè)固定的BaseResponse,是因?yàn)槲覀儠?huì)要求我們的所有對(duì)外提供的接口的response必須繼承BaseResponse類,這個(gè)類里面會(huì)定義一些默認(rèn)的參數(shù),如錯(cuò)誤碼等。
之后,只需要對(duì)需要參數(shù)校驗(yàn)的方法增加對(duì)應(yīng)注解即可:
@Facade public TestResponse query(User user) {}這樣,有了以上注解和切面,我們就可以對(duì)所有的對(duì)外方法做統(tǒng)一的控制了。
其實(shí),以上這個(gè)facadeAspect我省略了很多東西,我們真正使用的那個(gè)切面,不僅僅做了參數(shù)檢查,還可以做很多其他事情。比如異常的統(tǒng)一處理、錯(cuò)誤碼的統(tǒng)一轉(zhuǎn)換、記錄方法執(zhí)行時(shí)長(zhǎng)、記錄方法的入?yún)⒊鰠⒌鹊取?/p>
總之,使用切面+自定義注解,我們可以統(tǒng)一做很多事情。除了以上的這幾個(gè)場(chǎng)景,我們還有很多相似的用法,比如:
統(tǒng)一的緩存處理。如某些操作需要在操作前查緩存、操作后更新緩存。這種就可以通過(guò)自定義注解+切面的方式統(tǒng)一處理。
代碼其實(shí)都差不多,思路也比較簡(jiǎn)單,就是通過(guò)自定義注解來(lái)標(biāo)注需要被切面處理的累或者方法,然后在切面中對(duì)方法的執(zhí)行過(guò)程進(jìn)行干預(yù),比如在執(zhí)行前或者執(zhí)行后做一些特殊的操作。
使用這種方式可以大大減少重復(fù)代碼,大大提升代碼的優(yōu)雅性,方便我們使用。
但是同時(shí)也不能過(guò)度使用,因?yàn)樽⒔饪此坪?jiǎn)單,但是其實(shí)內(nèi)部有很多邏輯是容易被忽略的。就像我之前寫過(guò)一篇《Spring官方都推薦使用的@Transactional事務(wù),為啥我不建議使用!》中提到的觀點(diǎn)一樣,無(wú)腦的使用切面和注解,可能會(huì)引入一些不必要的問(wèn)題。
不管怎么說(shuō),自定義注解卻是是一個(gè)很好的發(fā)明,可以減少很多重復(fù)代碼。快快在你的項(xiàng)目中用起來(lái)吧。
總結(jié)
以上是生活随笔為你收集整理的自定义注解!绝对是程序员装逼的利器!!的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: git 出现 fatal: refusi
- 下一篇: 【高并发】面试官:讲讲高并发场景下如何优