關於註解,平時接觸的可很多,像是 @Controller、@Service、@Autowried 等等,不知道你是否有過這種疑惑,使用 @Service 註解的類成爲咱們的業務類,使用 @Controller 註解的類就成了請求的控制器,使用 @Autowried 註解的類就會幫咱們實現自動注入…css
之前,咱們只知道使用註解,今天咱們要手寫一個註解。html
1、以日誌記錄爲例
在沒有使用註解實現記錄日誌以前,咱們每每本身去調用日誌記錄的 Service,而後寫入數據庫表。java
今天咱們將從方法上添加自定義註解實現日誌自動記錄,以下:數據庫

2、瞭解關於註解知識
JDK 提供了 meta-annotation 用於自定義註解的時候使用,這四個註解爲:@Target,@Retention,@Documented 和 @Inherited。編程
以 @Controller 爲例,其源碼也是如此:微信

咱們來看一下上邊提到的四個註解: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.PARAMETER, ElementType.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;
}
}
流程補充:
- 經過 @Pointcut 定義帶有 @SystemLog 註解的方法或類爲切入點,能夠理解成,攔截全部帶該註解的方法。
- @Before 前置通知用於記錄請求時的時間
- @AfterReturning 用於獲取返回值,主要使用 getControllerMethodInfo() 方法,採用類反射機制獲取請求參數,最後調用 LogService 保存至數據庫。
額外補充:
關於 SecurityContextHolder 的使用爲 Spring Security 用於獲取用戶,實現記錄請求用戶的需求,可根據本身框架狀況選擇,如使用 shiro 獲取當前用戶爲 SecurityUtils.getSubject().getPrincipal(); 等等。
若是文章有錯的地方歡迎指正,你們互相留言交流。習慣在微信看技術文章,想要獲取更多的Java資源的同窗,能夠關注微信公衆號:niceyoo
