SpringBoot使用AOP實現(xiàn)日志記錄功能詳解
項目背景
在進行開發(fā)時,會遇到以下問題
要記錄請求參數(shù),在每個接口中加打印和記錄數(shù)據(jù)庫日志操作,影響代碼質(zhì)量,也不利于修改
@PostMapping(value = "/userValidPost")
public Response queryUserPost(@Valid @RequestBody UserInfo userInfo, BindingResult bindingResult) {
try {
//打印請求參數(shù)
log.info("Request : {}", JSON.toJSONString(userInfo));
//獲取返回數(shù)據(jù)
String result = "Hello " + userInfo.toString();
//打印返回結(jié)果
log.info("Response : {}", result);
//記錄數(shù)據(jù)庫日志
this.insertLog();
return Response.ok().setData(result);
} catch (Exception ex) {
//打印
log.info("Error : {}", ex.getMessage());
//記錄數(shù)據(jù)庫日志
this.insertLog();
return Response.error(ex.getMessage());
}解決方案
使用AOP記錄日志
1.切片配置
為解決這類問題,這里使用AOP進行日志記錄
/**
* 定義切點,切點為com.zero.check.controller包和子包里任意方法的執(zhí)行
*/
@Pointcut("execution(* com.zero.check.controller..*(..))")
public void webLog() {
}
/**
* 前置通知,在切點之前執(zhí)行的通知
*
* @param joinPoint 切點
*/
@Before("webLog() &&args(..,bindingResult)")
public void doBefore(JoinPoint joinPoint, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
FieldError error = bindingResult.getFieldError();
throw new UserInfoException(Response.error(error.getDefaultMessage()).setData(error));
}
//獲取請求參數(shù)
try {
String reqBody = this.getReqBody();
logger.info("REQUEST: " + reqBody);
} catch (Exception ex) {
logger.info("get Request Error: " + ex.getMessage());
}
}
/**
* 后置通知,切點后執(zhí)行
*
* @param ret
*/
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) {
//處理完請求,返回內(nèi)容
try {
logger.info("RESPONSE: " + JSON.toJSONString(ret));
} catch (Exception ex) {
logger.info("get Response Error: " + ex.getMessage());
}
}然后在執(zhí)行時就會發(fā)現(xiàn),前置通知沒有打印內(nèi)容
2019-12-25 22:08:27.875 INFO 4728 --- [nio-9004-exec-1] com.zero.check.aspect.WebLogAspect : get Post Request Parameter err : Stream closed
2019-12-25 22:08:27.875 INFO 4728 --- [nio-9004-exec-1] com.zero.check.aspect.WebLogAspect : REQUEST:
2019-12-25 22:08:27.922 INFO 4728 --- [nio-9004-exec-1] c.z.c.controller.DataCheckController : Response : {"id":"1","roleId":2,"userList":[{"userId":"1","userName":"2"}]}
2019-12-25 22:08:27.937 INFO 4728 --- [nio-9004-exec-1] com.zero.check.aspect.WebLogAspect : RESPONSE: {"code":"ok","data":"Hello UserInfo(id=1, userList=[User(userId=1, userName=2)], roleId=2)","requestid":"bbac6b56722040acb224f61a75af70ec"}
原因在ByteArrayInputStream的read方法中,其中有一個參數(shù)pos是讀取的起點,在接口使用了@RequestBody獲取參數(shù),就會導致AOP中獲取到的InputStream為空
/**
* The index of the next character to read from the input stream buffer.
* This value should always be nonnegative
* and not larger than the value of <code>count</code>.
* The next byte to be read from the input stream buffer
* will be <code>buf[pos]</code>.
*/
protected int pos;
/**
* Reads the next byte of data from this input stream. The value
* byte is returned as an <code>int</code> in the range
* <code>0</code> to <code>255</code>. If no byte is available
* because the end of the stream has been reached, the value
* <code>-1</code> is returned.
* <p>
* This <code>read</code> method
* cannot block.
*
* @return the next byte of data, or <code>-1</code> if the end of the
* stream has been reached.
*/
public synchronized int read() {
return (pos < count) ? (buf[pos++] & 0xff) : -1;
}以下代碼用于測試讀取InputStream
@Test
public void testRequestInputStream() throws Exception {
request = new MockHttpServletRequest();
request.setCharacterEncoding("UTF-8");
request.setRequestURI("/ts/post");
request.setMethod("POST");
request.setContent("1234567890".getBytes());
InputStream inputStream = request.getInputStream();
//調(diào)用這個方法,會影響到下次讀取,下次再調(diào)用這個方法,讀取的起始點會后移6個byte
//inputStream.read(new byte[6]);
//ByteArrayOutputStream生成對象的時候,是生成一個100大小的byte的緩沖區(qū),寫入的時候,是把內(nèi)容寫入內(nèi)存中的一個緩沖區(qū)
ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(100);
int i = 0;
byte[] b = new byte[100];
while ((i = inputStream.read(b)) != -1) {
byteOutput.write(b, 0, i);
}
System.out.println(new String(byteOutput.toByteArray()));
inputStream.close();
}調(diào)用inputStream.read(new byte[6]);打印結(jié)果
7890
不調(diào)用inputStream.read(new byte[6]);打印結(jié)果
1234567890
正常情況下,可以使用InputStream的reset()方法重置讀取的起始點,但ServletInputStream不支持這個方法,所以ServletInputStream只能讀取一次。
2.RequestWrapper
要多次讀取ServletInputStream的內(nèi)容,可以實現(xiàn)一個繼承HttpServletRequestWrapper的方法RequestWrapper,并重寫里面的getInputStream方法,這樣就可以多次獲取輸入流,如果要對請求對象進行封裝,可以在這里進行。
package com.zero.check.wrapper;
import com.alibaba.fastjson.util.IOUtils;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
/**
* @Description:
* @author: wei.wang
* @since: 2019/12/23 8:24
* @history: 1.2019/12/23 created by wei.wang
*/
@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper {
private final String body;
/**
* 獲取HttpServletRequest內(nèi)容
*
* @param request
*/
public RequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder stringBuilder = new StringBuilder();
try (InputStream inputStream = request.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, IOUtils.UTF8))) {
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} catch (IOException ex) {
log.info("RequestWrapper error : {}", ex.getMessage());
}
body = stringBuilder.toString();
}
/**
* 獲取輸入流
*
* @return
* @throws IOException
*/
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes(IOUtils.UTF8));
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream(), IOUtils.UTF8));
}
}3.ChannelFilter
實現(xiàn)一個新的過濾器,在里面使用復寫后的requestWrapper,就可以實現(xiàn)ServletInputStream的多次讀取,如果要對請求對象進行鑒權(quán),可以在這里進行。
package com.zero.check.filter;
import com.zero.check.wrapper.RequestWrapper;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* @Description:
* @author: wei.wang
* @since: 2019/11/21 15:07
* @history: 1.2019/11/21 created by wei.wang
*/
@Component
@WebFilter(urlPatterns = "/*",filterName = "filter")
public class ChannelFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if(servletRequest instanceof HttpServletRequest) {
requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest);
}
if(requestWrapper == null) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
//使用復寫后的wrapper
filterChain.doFilter(requestWrapper, servletResponse);
}
}
@Override
public void destroy() {
}
}4.測試
POSTMAN
接口
localhost:9004/check/userValidPost
請求方式 post
請求參數(shù)
{
"id": "1",
"roleId": 2,
"userList": [
{
"userId": "1",
"userName": "2"
}
]
}AOP打印日志
可以看到WebLogAspect成功打印了請求和返回結(jié)果
2019-12-25 23:48:45.047 INFO 17236 --- [nio-9004-exec-5] com.zero.check.aspect.WebLogAspect : REQUEST: {
"id": "1",
"roleId": 2,
"userList": [
{
"userId": "1",
"userName": "2"
}
]
}
2019-12-25 23:48:45.047 INFO 17236 --- [nio-9004-exec-5] com.zero.check.aspect.WebLogAspect : RESPONSE: {"code":"ok","data":"Hello UserInfo(id=1, userList=[User(userId=1, userName=2)], roleId=2)","requestid":"3a219b741a704f95a844faa10c3968f8"}
返回參數(shù)
{
"code": "ok",
"data": "Hello UserInfo(id=1, userList=[User(userId=1, userName=2)], roleId=2)",
"requestid": "3a219b741a704f95a844faa10c3968f8"
}JUNIT
DateCheckServiceApplicationTests
package com.zero.check;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import java.beans.Transient;
@RunWith(SpringRunner.class)
@SpringBootTest
@WebAppConfiguration
public class DateCheckServiceApplicationTests {
//聲明request變量
private MockHttpServletRequest request;
@Before
public void init() throws IllegalAccessException, NoSuchFieldException {
System.out.println("開始測試-----------------");
request = new MockHttpServletRequest();
}
@Test
public void test() {
}
public MockHttpServletRequest getRequest() {
return request;
}
}DataCheckControllerTest
package com.zero.check.controller;
import com.zero.check.DateCheckServiceApplicationTests;
import com.zero.check.filter.ChannelFilter;
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import static org.junit.Assert.*;
/**
* @Description:
* @author: wei.wang
* @since: 2019/12/26 0:11
* @history: 1.2019/12/26 created by wei.wang
*/
@Slf4j
public class DataCheckControllerTest extends DateCheckServiceApplicationTests {
private MockMvc mockMvc;
@Autowired
private DataCheckController dataCheckController;
//測試前執(zhí)行,加載dataCheckController,并添加Filter
@Before
public void init() {
mockMvc = MockMvcBuilders.standaloneSetup(dataCheckController).addFilter(new ChannelFilter()).build();
}
@Test
public void userValidPost() throws Exception {
MvcResult result = mockMvc.perform(MockMvcRequestBuilders
.post("/check/userValidPost")
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
.content(String.valueOf("{\n" +
" \"id\": \"1\",\n" +
" \"roleId\": 2,\n" +
" \"userList\": [\n" +
" {\n" +
" \"userId\": \"1\",\n" +
" \"userName\": \"2\"\n" +
" }\n" +
" ]\n" +
"}")))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))// 預期返回值的媒體類型text/plain;charset=UTF-8
.andReturn();
}
@Test
public void userValidGet() {
}
@Test
public void testAspectQueryUserPost() {
}
@Test
public void testInputStream() throws Exception {
String str = "1234567890";
//ByteArrayInputStream是把一個byte數(shù)組轉(zhuǎn)換成一個字節(jié)流
InputStream inputStream = new FileInputStream("src/main/resources/data/demo.txt");
//調(diào)用這個方法,會影響到下次讀取,下次再調(diào)用這個方法,讀取的起始點會后移5個byte
inputStream.read(new byte[5]);
//ByteArrayOutputStream生成對象的時候,是生成一個100大小的byte的緩沖區(qū),寫入的時候,是把內(nèi)容寫入內(nèi)存中的一個緩沖區(qū)
ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(100);
int i = 0;
byte[] b = new byte[100];
while ((i = inputStream.read(b)) != -1) {
byteOutput.write(b, 0, i);
}
System.out.println(new String(byteOutput.toByteArray()));
inputStream.close();
}
@Test
public void testRequestInputStream() throws Exception {
MockHttpServletRequest request = getRequest();
request.setCharacterEncoding("UTF-8");
request.setRequestURI("/ts/post");
request.setMethod("POST");
request.setContent("1234567890".getBytes());
InputStream inputStream = request.getInputStream();
//調(diào)用這個方法,會影響到下次讀取,下次再調(diào)用這個方法,讀取的起始點會后移6個byte
inputStream.read(new byte[6]);
inputStream.reset();
//ByteArrayOutputStream生成對象的時候,是生成一個100大小的byte的緩沖區(qū),寫入的時候,是把內(nèi)容寫入內(nèi)存中的一個緩沖區(qū)
ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(100);
int i = 0;
byte[] b = new byte[100];
while ((i = inputStream.read(b)) != -1) {
byteOutput.write(b, 0, i);
}
System.out.println(new String(byteOutput.toByteArray()));
inputStream.close();
}
}測試結(jié)果

AOP打印參數(shù)
2019-12-26 00:18:11.136 INFO 13016 --- [ main] com.zero.check.aspect.WebLogAspect : REQUEST: {
"id": "1",
"roleId": 2,
"userList": [
{
"userId": "1",
"userName": "2"
}
]
}
2019-12-26 00:18:11.542 INFO 13016 --- [ main] com.zero.check.aspect.WebLogAspect : RESPONSE: {"code":"ok","data":"Hello UserInfo(id=1, userList=[User(userId=1, userName=2)], roleId=2)","requestid":"68c469d15724474a937ef39d3c6ceccf"}
2019-12-26 00:18:11.579 INFO 13016 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'Process finished with exit code 0
代碼Git地址
git@github.com:A-mantis/SpringBootDataCheck.git
以上就是SpringBoot使用AOP實現(xiàn)日志記錄功能詳解的詳細內(nèi)容,更多關(guān)于SpringBoot AOP日志記錄的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot項目打成war包部署到tomcat遇到的一些問題
這篇文章主要介紹了springboot項目打成war包部署到tomcat遇到的一些問題,需要的朋友可以參考下2017-06-06
聊聊spring @Transactional 事務無法使用的可能原因
這篇文章主要介紹了spring @Transactional 事務無法使用的可能原因,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
Java 圖解Spring啟動時的后置處理器工作流程是怎樣的
spring的后置處理器有兩類,bean后置處理器,bf(BeanFactory)后置處理器。bean后置處理器作用于bean的生命周期,bf的后置處理器作用于bean工廠的生命周期2021-10-10

