自定義註解!絕對是程序員裝大佬的利器!!

作者 l Hollis

來源 l Hollis(ID:hollischuang)

相信很多人對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 {
        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
        if (constraintViolations.stream().findFirst().isPresent()) {
            throw new ValidationException(constraintViolations.stream().findFirst().get().getMessage());
        }
    }
}

以上代碼,會對一個bean進行校驗,一旦失敗,就會拋出ValidationException。

接下來定義一個註解:

/**
 * facade接口註解, 用於統一對facade進行參數校驗及異常捕獲
 * <pre>
 *      注意,使用該註解需要注意,該方法的返回值必須是BaseResponse的子類
 * </pre>
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Facade {
}

這個註解裏面沒有任何參數,只用於標註那些方法要進行參數校驗。

接下來定義切面:

/**
 * Facade的切面處理類,統一統計進行參數校驗及異常捕獲
 * @author Hollis
 */
@Aspect
@Component
public class FacadeAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(FacadeAspect.class);
    @Autowired
    HttpServletRequest 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();
        //循環遍歷所有參數,進行參數校驗
        for (Object parameter : args) {
            try {
                BeanValidator.validateObject(parameter);
            } catch (ValidationException e) {
                return getFailedResponse(returnType, e);
            }
        }
        try {
            // 目標方法執行
            Object response = pjp.proceed();
            return response;
        } catch (Throwable throwable) {
            return getFailedResponse(returnType, throwable);
        }
    }
    /**
     * 定義並返回一個通用的失敗響應
     */
    private Object getFailedResponse(Class returnType, Throwable throwable)
        throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //如果返回值的類型爲BaseResponse 的子類,則創建一個通用的失敗響應
        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;
    }
}

以上代碼,和前面的切面有點類似,主要是定義了一個切面,會對所有標註@Facade的方法進行統一處理,即在開始方法調用前進行參數校驗,一旦校驗失敗,則返回一個固定的失敗的Response。

特別需要注意的是,這裏之所以可以返回一個固定的BaseResponse,是因爲我們會要求我們的所有對外提供的接口的response必須繼承BaseResponse類,這個類裏面會定義一些默認的參數,如錯誤碼等。

之後,只需要對需要參數校驗的方法增加對應註解即可:

@Facade
public TestResponse query(User user) {
}

這樣,有了以上註解和切面,我們就可以對所有的對外方法做統一的控制了。

其實,以上這個facadeAspect我省略了很多東西,我們真正使用的那個切面,不僅僅做了參數檢查,還可以做很多其他事情。比如異常的統一處理、錯誤碼的統一轉換、記錄方法執行時長、記錄方法的入參出參等等。

總之,使用切面+自定義註解,我們可以統一做很多事情。除了以上的這幾個場景,我們還有很多相似的用法,比如:

統一的緩存處理。如某些操作需要在操作前查緩存、操作後更新緩存。這種就可以通過自定義註解+切面的方式統一處理。

代碼其實都差不多,思路也比較簡單,就是通過自定義註解來標註需要被切面處理的累或者方法,然後在切面中對方法的執行過程進行干預,比如在執行前或者執行後做一些特殊的操作。

使用這種方式可以大大減少重複代碼,大大提升代碼的優雅性,方便我們使用。

但是同時也不能過度使用,因爲註解看似簡單,但是其實內部有很多邏輯是容易被忽略的。就像我之前寫過一篇《Spring官方都推薦使用的@Transactional事務,爲啥我不建議使用!》中提到的觀點一樣,無腦的使用切面和註解,可能會引入一些不必要的問題。

不管怎麼說,自定義註解卻是是一個很好的發明,可以減少很多重複代碼。快快在你的項目中用起來吧。

推 薦

程序人生公衆號全新搜索技能上線啦!

只要在公衆號後臺回覆消息

就能自動回覆想搜索的內容啦!

簡直是程序員必備的搜索神器!

猜猜回覆“Mysql安裝”會出現什麼

點分享點點贊點在看
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章