文章首發於個人網站
本節中,您將學習如何在 Spring Boot 中使用 AOP 切面統一處理請求日誌,打印進出參相關參數。
一、先看看日誌輸出效果
可以看到,每個對於每個請求,開始與結束一目瞭然,並且打印了以下參數:
- URL: 請求接口地址;
- HTTP Method: 請求的方法,是 POST, GET, 還是 DELETE 等;
- Class Method: 對應 Controller 的全路徑以及調用的哪個方法;
- IP: 請求 IP 地址;
- Request Args: 請求入參,以 JSON 格式輸出;
- Response Args: 響應出參,以 JSON 格式輸出;
- Time-Consuming: 請求耗時;
效果應該還不錯吧!接下來就讓我們一步一步去實現該功能, 首先, 。
二、添加 Maven 依賴
在項目 pom.xml 文件中添加依賴:
<dependency>
<groupid>
org.springframework.boot
/<groupid>
<artifactid>
spring-boot-starter-aop
/<artifactid>
/<dependency>
<dependency>
<groupid>
com.google.code.gson
/<groupid>
<artifactid>
gson
/<artifactid>
<version>
2.8.5
/<version>
/<dependency>
三、配置 AOP 切面
在配置 AOP 切面之前,我們需要了解下 aspectj 相關注解的作用:
- @Aspect:聲明該類為一個註解類;
- @Pointcut:定義一個切點,後面跟隨一個表達式,表達式可以定義為某個 package 下的方法,也可以是自定義註解等;
- 切點定義好後,就是圍繞這個切點做文章了:
- @Before: 在切點之前,織入相關代碼;
- @After: 在切點之後,織入相關代碼;
- @AfterReturning: 在切點返回內容後,織入相關代碼,一般用於對返回值做些加工處理的場景;
- @AfterThrowing: 用來處理當織入的代碼拋出異常後的邏輯處理;
- @Around: 在切入點前後織入代碼,並且可以自由的控制何時執行切點;
接下來,定義一個 WebLogAspect.java 切面類,代碼如下:
package
site
.
exception
.
springbootaopwebrequest
.
aspect
;
import
com
.
.
gson
.
Gson
;
import
org
.
aspectj
.
lang
.
JoinPoint
;
import
org
.
aspectj
.
lang
.
ProceedingJoinPoint
;
import
org
.
aspectj
.
lang
.
annotation
.*;
import
org
.
slf4j
.
Logger
;
import
org
.
slf4j
.
LoggerFactory
;
import
org
.
springframework
.
stereotype
.
Component
;
import
org
.
springframework
.
web
.
context
.
request
.
RequestContextHolder
;
import
org
.
springframework
.
web
.
context
.
request
.
ServletRequestAttributes
;
import
javax
.
servlet
.
http
.
HttpServletRequest
;
/**
* @author www.exception.site (exception 教程網)
* @date 2019/2/12
* @time 14:03
* @discription
**/
@Aspect
@Component
public
class
WebLogAspect
{
private
final
static
Logger
logger
=
LoggerFactory
.
getLogger
(
WebLogAspect
.
class
);
/** 以 controller 包下定義的所有請求為切入點 */
@Pointcut
(
"execution(public * site.exception.springbootaopwebrequest.controller..*.*(..))"
)
public
void
webLog
()
{}
/**
* 在切點之前織入
* @param joinPoint
* @throws Throwable
*/
@Before
(
"webLog()"
)
public
void
doBefore
(
JoinPoint
joinPoint
)
throws
Throwable
{
// 開始打印請求日誌
ServletRequestAttributes
attributes
=
(
ServletRequestAttributes
)
RequestContextHolder
.
getRequestAttributes
();
HttpServletRequest
request
=
attributes
.
getRequest
();
// 打印請求相關參數
logger
.
info
(
"========================================== Start =========================================="
);
// 打印請求 url
logger
.
info
(
"URL : {}"
,
request
.
getRequestURL
().
toString
());
// 打印 Http method
logger
.
info
(
"HTTP Method : {}"
,
request
.
getMethod
());
// 打印調用 controller 的全路徑以及執行方法
logger
.
info
(
"Class Method : {}.{}"
,
joinPoint
.
getSignature
().
getDeclaringTypeName
(),
joinPoint
.
getSignature
().
getName
());
// 打印請求的 IP
logger
.
info
(
"IP : {}"
,
request
.
getRemoteAddr
());
// 打印請求入參
logger
.
info
(
"Request Args : {}"
,
new
Gson
().
toJson
(
joinPoint
.
getArgs
()));
}
/**
* 在切點之後織入
* @throws Throwable
*/
@After
(
"webLog()"
)
public
void
doAfter
()
throws
Throwable
{
logger
.
info
(
"=========================================== End ==========================================="
);
// 每個請求之間空一行
logger
.
info
(
""
);
}
/**
* 環繞
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around
(
"webLog()"
)
public
Object
doAround
(
ProceedingJoinPoint
proceedingJoinPoint
)
throws
Throwable
{
long
startTime
=
System
.
currentTimeMillis
();
Object
result
=
proceedingJoinPoint
.
proceed
();
// 打印出參
logger
.
info
(
"Response Args : {}"
,
new
Gson
().
toJson
(
result
));
// 執行耗時
logger
.
info
(
"Time-Consuming : {} ms"
,
System
.
currentTimeMillis
()
-
startTime
);
return
result
;
}
}
我們通過 @Aspect 聲明瞭 WebLogAspect.java 為切面類,之後,通過 @Pointcut 定義了打印請求日誌的切點,切點為 site.exception.springbootaopwebrequest.controller 包下所有的請求接口。
切點定義好後,我們通過 @Before 在切點之前打印請求的相關參數,通過 @Around 打印了請求接口的耗時時間,最後通過 @After 做了請求的收尾工作。
到這裡,切面相關的代碼就完成了!
三、測試
我們針對 GET, POST, 文件提交,以及多文件提交四種接口分別測試其效果, 接口定義如下:
package
site
.
exception
.
springbootaopwebrequest
.
controller
;
import
org
.
slf4j
.
Logger
;
import
org
.
slf4j
.
LoggerFactory
;
import
org
.
springframework
.
web
.
bind
.
annotation
.*;
import
org
.
springframework
.
web
.
multipart
.
MultipartFile
;
import
site
.
exception
.
springbootaopwebrequest
.
entity
.
User
;
/**
* @author www.exception.site (exception 教程網)
* @date 2019/2/16
* @time 21:03
* @discription
**/
@RestController
public
class
TestController
{
private
final
static
Logger
logger
=
LoggerFactory
.
getLogger
(
TestController
.
class
);
/**
* POST 方式接口測試
* @param user
* @return
*/
@PostMapping
(
"/user"
)
public
User
testPost
(
@RequestBody
User
user
)
{
logger
.
info
(
"testPost ..."
);
return
user
;
}
/**
* GET 方式接口測試
* @return
*/
@GetMapping
(
"/user"
)
public
String
testGet
(
@RequestParam
(
"username"
)
String
username
,
@RequestParam
(
"password"
)
String
password
)
{
logger
.
info
(
"testGet ..."
);
return
"success"
;
}
/**
* 單文件上傳接口測試
* @return
*/
@PostMapping
(
"/file/upload"
)
public
String
testFileUpload
(
@RequestParam
(
"file"
)
MultipartFile
file
)
{
logger
.
info
(
"testFileUpload ..."
);
return
"success"
;
}
/**
* 多文件上傳接口測試
* @return
*/
@PostMapping
(
"/multiFile/upload"
)
public
String
testMultiFileUpload
(
@RequestParam
(
"file"
)
MultipartFile
[]
file
)
{
logger
.
info
(
"testMultiFileUpload ..."
);
return
"success"
;
}
}
User.java:
package
site
.
exception
.
springbootaopwebrequest
.
entity
;
import
java
.
io
.
Serializable
;
import
java
.
util
.
Date
;
/**
* @author www.exception.site (exception 教程網)
* @date 2019/2/16
* @time 21:00
* @discription
**/
public
class
User
implements
Serializable
{
/**
* 用戶名
*/
private
String
username
;
/**
* 密碼
*/
private
String
password
;
/**
* 創建時間
*/
private
Date
createTime
;
public
String
getUsername
()
{
return
username
;
}
public
void
setUsername
(
String
username
)
{
this
.
username
=
username
;
}
public
String
getPassword
()
{
return
password
;
}
public
void
setPassword
(
String
password
)
{
this
.
password
=
password
;
}
public
Date
getCreateTime
()
{
return
createTime
;
}
public
void
setCreateTime
(
Date
createTime
)
{
this
.
createTime
=
createTime
;
}
}
3.1 GET 接口測試
請求 http://localhost:8080/user?username=張三&password=123456接口,觀察日誌打印情況:
GET 接口正常打印日誌!
3.2 POST 接口測試
通過 Postman 請求 http://localhost:8080/user POST 接口:
看看控制檯輸出效果:
POST 接口也是 OK 的!
3.3 單文件提交接口測試
請求單文件提交接口: http://localhost:8080/file/upload :
日誌輸出如下:
3.4 多文件提交接口測試
請求單文件提交接口: http://localhost:8080/multiFile/upload :
切面日誌輸出:
為何不用 FASTJSON
筆者在開始階段的確使用的是阿里 FASTJSON 來做出入參的打印,但是對於文件上傳接口,FASTJSON 轉換會出錯,故改用谷歌的 Gson。
GitHub 源碼地址
閱讀更多 小哈學Java 的文章