自定義註解-aop實現日誌記錄

關於註解,平時接觸的可很多,像是 @Controller、@Service、@Autowried 等等,不知道你是否有過這種疑惑,使用 @Service 註解的類成爲咱們的業務類,使用 @Controller 註解的類就成了請求的控制器,使用 @Autowried 註解的類就會幫咱們實現自動注入…css

之前,咱們只知道使用註解,今天咱們要手寫一個註解。html

1、以日誌記錄爲例

在沒有使用註解實現記錄日誌以前,咱們每每本身去調用日誌記錄的 Service,而後寫入數據庫表。java

今天咱們將從方法上添加自定義註解實現日誌自動記錄,以下:數據庫

52e77c79b07d49c6554ff2a0185d7f02.png52e77c79b07d49c6554ff2a0185d7f02.png

2、瞭解關於註解知識

JDK 提供了 meta-annotation 用於自定義註解的時候使用,這四個註解爲:@Target,@Retention,@Documented 和 @Inherited。編程

以 @Controller 爲例,其源碼也是如此:微信

fc6b30adb15c78f562e0de8e503e6881.pngfc6b30adb15c78f562e0de8e503e6881.png

咱們來看一下上邊提到的四個註解:app

註解 說明
@Target 用於描述註解的使用範圍,即:被描述的註解能夠用在什麼地方
@Retention 指定被描述的註解在什麼範圍內有效
@Documented 是一個標記註解,木有成員,用於描述其它類型的annotation 應該被做爲被標註的程序成員的公共 API,所以能夠被例如javadoc此類的工具文檔化
@Inherited 元註解是一個標記註解,@Inherited 闡述了某個被標註的類型是被繼承的。若是一個使用了 @Inherited 修飾的 annotation 類型被用於一個 class,則這個 annotation 將被用於該class的子類

3、開始咱們的自定義註解

兩個類:
SystemLog:自定義註解類,用於標記到方法、類上,如@SystemLog
SystemLogAspect:AOP實現切點攔截。框架

關於AOP的補充:
關於AOP面向切面編程概念啥的就不囉嗦了,還不瞭解的能夠自定百度了ide

描述AOP經常使用的一些術語有:
通知(Adivce)、鏈接點(Join point)、切點(Pointcut)、切面(Aspect)、引入(Introduction)、織入(Weaving)工具

關於術語的部分可參考:https://www.cnblogs.com/niceyoo/p/10162077.html

須要明確的核心概念:切面 = 切點 + 通知。

@Aspect 註解形式是 AOP 的一種實現,以下看一下咱們要寫的兩個類吧。

一、@SystemLog

定義咱們的自定義註解類

/**
 * 系統日誌自定義註解
 */

@Target({ElementType.PARAMETERElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemLog {

        /**
         * 日誌名稱
         * @return
         */

        String description() default "";

        /**
         * 日誌類型
         * @return
         */

        LogType type() default LogType.OPERATION;
}
二、@SystemLogAspect

AOP攔截@SystemLog註解

/**
 * Spring AOP實現日誌管理
 * @author Exrickx
 */

@Aspect
@Component
@Slf4j
public class SystemLogAspect {

    private static final ThreadLocal<Date> beginTimeThreadLocal = new NamedThreadLocal<Date>("ThreadLocal beginTime");

    @Autowired
    private LogService logService;

    @Autowired
    private UserService userService;

    @Autowired(required = false)
    private HttpServletRequest request;

    /**
     * 定義切面,只置入帶 @SystemLog 註解的方法或類 
     * Controller層切點,註解方式
     * @Pointcut("execution(* *..controller..*Controller*.*(..))")
     */

    @Pointcut("@annotation(club.sscai.common.annotation.SystemLog)")
    public void controllerAspect() {

    }

    /**
     * 前置通知 (在方法執行以前返回)用於攔截Controller層記錄用戶的操做的開始時間
     * @param joinPoint 切點
     * @throws InterruptedException
     */

    @Before("controllerAspect()")
    public void doBefore(JoinPoint joinPoint) throws InterruptedException{

        ##線程綁定變量(該數據只有當前請求的線程可見)
        Date beginTime=new Date();
        beginTimeThreadLocal.set(beginTime);
    }


    /**
     * 後置通知(在方法執行以後並返回數據) 用於攔截Controller層無異常的操做
     * @param joinPoint 切點
     */

    @AfterReturning("controllerAspect()")
    public void after(JoinPoint joinPoint){
        try {
            String username = "";
            String description = getControllerMethodInfo(joinPoint).get("description").toString();
            Map<String, String[]> logParams = request.getParameterMap();
            String principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal().toString();
            ## 判斷容許不用登陸的註解
            if("anonymousUser".equals(principal)&&!description.contains("短信登陸")){
                return;
            }
            if(!"anonymousUser".equals(principal)){
                UserDetails user = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
                username = user.getUsername();
            }
            if(description.contains("短信登陸")){
                if(logParams.get("mobile")!=null){
                    String mobile = logParams.get("mobile")[0];
                    username = userService.findByMobile(mobile).getUsername()+"("+mobile+")";
                }
            }

            Log log = new Log();

            ##請求用戶
            log.setUsername(username);
            ##日誌標題
            log.setName(description);
            ##日誌類型
            log.setLogType((int)getControllerMethodInfo(joinPoint).get("type"));
            ##日誌請求url
            log.setRequestUrl(request.getRequestURI());
            ##請求方式
            log.setRequestType(request.getMethod());
            ##請求參數
            log.setMapToParams(logParams);
            ##請求開始時間
            Date logStartTime = beginTimeThreadLocal.get();

            long beginTime = beginTimeThreadLocal.get().getTime();
            long endTime = System.currentTimeMillis();
            ##請求耗時
            Long logElapsedTime = endTime - beginTime;
            log.setCostTime(logElapsedTime.intValue());

            ##調用線程保存至log表
            ThreadPoolUtil.getPool().execute(new SaveSystemLogThread(log, logService));

        } catch (Exception e) {
            log.error("AOP後置通知異常", e);
        }
    }


    /**
     * 保存日誌至數據庫
     */

    private static class SaveSystemLogThread implements Runnable {

        private Log log;
        private LogService logService;

        public SaveSystemLogThread(Log esLog, LogService logService{
            this.log = esLog;
            this.logService = logService;
        }

        @Override
        public void run() 
{

            logService.save(log);
        }
    }

    /**
     * 獲取註解中對方法的描述信息 用於Controller層註解
     * @param joinPoint 切點
     * @return 方法描述
     * @throws Exception
     */

    public static Map<String, Object> getControllerMethodInfo(JoinPoint joinPoint) throws Exception{

        Map<String, Object> map = new HashMap<String, Object>(16);
        ## 獲取目標類名
        String targetName = joinPoint.getTarget().getClass().getName();
        ## 獲取方法名
        String methodName = joinPoint.getSignature().getName();
        ## 獲取相關參數
        Object[] arguments = joinPoint.getArgs();
        ## 生成類對象
        Class targetClass = Class.forName(targetName);
        ## 獲取該類中的方法
        Method[] methods = targetClass.getMethods();

        String description = "";
        Integer type = null;

        for(Method method : methods) {
            if(!method.getName().equals(methodName)) {
                continue;
            }
            Class[] clazzs = method.getParameterTypes();
            if(clazzs.length != arguments.length) {
                ## 比較方法中參數個數與從切點中獲取的參數個數是否相同,緣由是方法能夠重載
                continue;
            }
            description = method.getAnnotation(SystemLog.class).description();
            type = method.getAnnotation(SystemLog.class).type().ordinal();
            map.put("description", description);
            map.put("type", type);
        }
        return map;
    }

}

流程補充:

  1. 經過 @Pointcut 定義帶有 @SystemLog 註解的方法或類爲切入點,能夠理解成,攔截全部帶該註解的方法。
  2. @Before 前置通知用於記錄請求時的時間
  3. @AfterReturning 用於獲取返回值,主要使用 getControllerMethodInfo() 方法,採用類反射機制獲取請求參數,最後調用 LogService 保存至數據庫。

額外補充:

關於 SecurityContextHolder 的使用爲 Spring Security 用於獲取用戶,實現記錄請求用戶的需求,可根據本身框架狀況選擇,如使用 shiro 獲取當前用戶爲 SecurityUtils.getSubject().getPrincipal(); 等等。

若是文章有錯的地方歡迎指正,你們互相留言交流。習慣在微信看技術文章,想要獲取更多的Java資源的同窗,能夠關注微信公衆號:niceyoo

相關文章
相關標籤/搜索