切面+自定義註解的一些玩法
這篇文章主要記錄一下切面+自定義註解在實際中的一些玩法。切面+自定義註解的玩法可能有很多,這篇主要說一下實現以下兩個功能:
1.@HttpLog自動記錄Http請求日誌
2.@TimeStamp自動註入時間戳
源碼 is here:切面+自定義註解的一些玩法
如何運行這個例子
1. 創建數據庫:
CREATE TABLE `t_user` (
`id` varchar(32) NOT NULL COMMENT ‘id‘,
`username` varchar(16) DEFAULT NULL COMMENT ‘用戶名‘,
`password` varchar(16) DEFAULT NULL COMMENT ‘密碼‘,
`del_flag` int(1) DEFAULT NULL COMMENT ‘刪除標識:0:已刪除 1:未刪除‘,
`created_at` datetime DEFAULT NULL COMMENT ‘創建時間‘,
`updated_at` datetime DEFAULT NULL COMMENT ‘更新時間‘,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
2 . 修改配置文件application.properties,配置合適的端口和正確的數據庫信息。
3 . 運行DemoApplication.java,啟動項目,成功啟動後瀏覽器打開http://localhost:8008/swagger-ui.html,便可以看到swagger API展示界面,進行API調試
前置知識
1. 自定義註解:如何自定義註解
2. AOP:什麽是AOP
適合人群
編程新手或者剛接觸這塊的老手
可能會了解
1. 如何自定義註解
2. 如何使用AOP
3. 自定義註解+AOP組合使用,自定義你想要的註解
4. 內含了 mybatis plus 使用的 demo,簡化mabatis的使用
5. 內置了一個全局唯一ID生成器,了解ID生成器
6. Swagger 管理調試API
7. 其他
實現解析
· @HttpLog自動記錄Http日誌
在很多時候我們要把一些接口的Http請求信息記錄到日誌裏面。通常原始的做法是利用日誌框架如log4j,slf4j等,在方法裏面打日誌log.info(xxxx)。但是這樣的工作無疑是單調而又重復的,我們可以采用自定義註解+切面的來簡化這一工作。通常的日誌記錄都在Controller裏面進行的比較多,我們可以實現這樣的效果:
我們自定義@HttpLog註解,作用域在類上,凡是打上了這個註解的Controller類裏面的所有方法都會自動記錄Http日誌。實現方式也很簡單,主要寫好切面表達式:
// 切面表達式,描述所有所有需要記錄log的類,所有有@HttpLog 並且有 @Controller 或 @RestController 類都會被代理
@Pointcut(@within(com.example.vzard.annotation.HttpLog) (@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)))
public void httpLog() {
}
@Before(httpLog())
public void preHandler(JoinPoint joinPoint) {
startTime.set(System.currentTimeMillis());
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
log.info(Current Url: {}, httpServletRequest.getRequestURI());
log.info(Current Http Method: {}, httpServletRequest.getMethod());
log.info(Current IP: {}, httpServletRequest.getRemoteAddr());
EnumerationString headerNames = httpServletRequest.getHeaderNames();
log.info(=======http headers=======);
while (headerNames.hasMoreElements()) {
String nextName = headerNames.nextElement();
log.info(nextName.toUpperCase() + : {}, httpServletRequest.getHeader(nextName));
}
log.info(======= header end =======);
log.info(Current Class Method: {}, joinPoint.getSignature().getDeclaringTypeName() + . + joinPoint.getSignature().getName());
log.info(Parms: {}, null != httpServletRequest.getQueryString() ? JSON.toJSONString(httpServletRequest.getQueryString().split()) : EMPTY);
}
@AfterReturning(returning = response, pointcut = httpLog())
public void afterReturn(Object response) {
log.info(Response: {}, JSON.toJSONString(response));
log.info(Spend Time: [ {}, System.currentTimeMillis() - startTime.get() + ms ]);
}
· @TimeStamp自動註入時間戳
我們的很多數據需要記錄時間戳,最常見的就是記錄created_at和updated_at,通常我們可以通常實體類中的setCreatedAt()方法來寫入當前時間,然後通過ORM來插入到數據庫裏,但是這樣的方法比較重復枯燥,給每個需要加上時間戳的類都要寫入時間戳很麻煩而且不小心會漏掉。另一個思路是在數據庫裏面設置默認值,插入的時候由數據庫自動生成當前時間戳,但是理想很豐滿,現實很骨感,在MySQL如果時間戳類型是datetime裏即使你設置了默認值為當前時間也不會在時間戳為空時插入數據時自動生成,而是會在已有時間戳記錄的情況下更新時間戳為當前時間,這並不是我們所需要的,比如我們不希望created_at每次更改記錄時都被刷新,另外的方法是將時間戳類型改為timestamp,這樣第一個類型為timestamp的字段會在值為空時自動生成,但是多個的話,後面的均不會自動生成。再有一種思路是,直接在sql裏面用now()函數生成,比如created_at = now()。但是這樣必須要寫sql,如果使用的不是主打sql流的orm不會太方便,比如hibernate之類的,並且也會加大sql語句的復雜度,同時sql的可移植性也會降低,比如sqlServer中就不支持now()函數。為了簡化這個問題,我們可以自定義@TimeStamp註解,打上該註解的方法的入參裏面的所有對象或者指定對象裏面要是有setCreatedAt、setUpdatedAt這樣的方法,便會自動註入時間戳,而無需手動註入,同時還可以指定只註入created_at或updated_at。實現主要代碼如下:
//所有打上@TimeStamp註解的方法作為切點
@Pointcut(@annotation(com.example.vzard.annotation.TimeStamp))
public void pointCut() {
}
@Before(pointCut() @annotation(timeStamp))
public void before(JoinPoint joinPoint, TimeStamp timeStamp) {
Long currentTime = System.currentTimeMillis();
Class type = timeStamp.type();
List argList = Arrays.stream(joinPoint.getArgs())
.filter(t - (t != null))
.filter(t - (t.getClass().getName().equals(type.getName())))
.collect(Collectors.toList());
for (Object arg : argList) {
Method[] methods = arg.getClass().getMethods();
for (Method m : methods) {
if (timeStamp.rank().equals(TimeStampRank.FULL)) {
setCurrentTime(m, arg, setUpdatedAt, setCreatedAt);
}
if (timeStamp.rank().equals(TimeStampRank.UPDATE)) {
setCurrentTime(m, arg, setUpdatedAt);
}
if (timeStamp.rank().equals(TimeStampRank.CREATE)) {
setCurrentTime(m, arg, setCreatedAt);
}
}
}
}
private void setCurrentTime(Method method, Object o, String... methodNames) {
for (String name : methodNames) {
if (method.getName().equals(name)) {
try {
method.setAccessible(true);
method.invoke(o, new Date(System.currentTimeMillis()));
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
自定義註解+AOP可以做很多事,比如根據註解動態切換數據源,根據註解做接口鑒權等等,讀者可以自己嘗試去實現。
?
切面+自定義註解的一些玩法