SpringBoot使用AOP實(shí)現(xiàn)日志記錄功能詳解
項(xiàng)目背景
在進(jìn)行開發(fā)時(shí),會(huì)遇到以下問題
要記錄請求參數(shù),在每個(gè)接口中加打印和記錄數(shù)據(jù)庫日志操作,影響代碼質(zhì)量,也不利于修改
@PostMapping(value = "/userValidPost") public Response queryUserPost(@Valid @RequestBody UserInfo userInfo, BindingResult bindingResult) { try { //打印請求參數(shù) log.info("Request : {}", JSON.toJSONString(userInfo)); //獲取返回?cái)?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進(jìn)行日志記錄
/** * 定義切點(diǎn),切點(diǎn)為com.zero.check.controller包和子包里任意方法的執(zhí)行 */ @Pointcut("execution(* com.zero.check.controller..*(..))") public void webLog() { } /** * 前置通知,在切點(diǎn)之前執(zhí)行的通知 * * @param joinPoint 切點(diǎn) */ @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()); } } /** * 后置通知,切點(diǎn)后執(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í)行時(shí)就會(huì)發(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方法中,其中有一個(gè)參數(shù)pos是讀取的起點(diǎn),在接口使用了@RequestBody獲取參數(shù),就會(huì)導(dǎo)致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)用這個(gè)方法,會(huì)影響到下次讀取,下次再調(diào)用這個(gè)方法,讀取的起始點(diǎn)會(huì)后移6個(gè)byte //inputStream.read(new byte[6]); //ByteArrayOutputStream生成對象的時(shí)候,是生成一個(gè)100大小的byte的緩沖區(qū),寫入的時(shí)候,是把內(nèi)容寫入內(nèi)存中的一個(gè)緩沖區(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()方法重置讀取的起始點(diǎn),但ServletInputStream不支持這個(gè)方法,所以ServletInputStream只能讀取一次。
2.RequestWrapper
要多次讀取ServletInputStream的內(nèi)容,可以實(shí)現(xiàn)一個(gè)繼承HttpServletRequestWrapper的方法RequestWrapper,并重寫里面的getInputStream方法,這樣就可以多次獲取輸入流,如果要對請求對象進(jìn)行封裝,可以在這里進(jìn)行。
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
實(shí)現(xiàn)一個(gè)新的過濾器,在里面使用復(fù)寫后的requestWrapper,就可以實(shí)現(xiàn)ServletInputStream的多次讀取,如果要對請求對象進(jìn)行鑒權(quán),可以在這里進(jì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 { //使用復(fù)寫后的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))// 預(yù)期返回值的媒體類型text/plain;charset=UTF-8 .andReturn(); } @Test public void userValidGet() { } @Test public void testAspectQueryUserPost() { } @Test public void testInputStream() throws Exception { String str = "1234567890"; //ByteArrayInputStream是把一個(gè)byte數(shù)組轉(zhuǎn)換成一個(gè)字節(jié)流 InputStream inputStream = new FileInputStream("src/main/resources/data/demo.txt"); //調(diào)用這個(gè)方法,會(huì)影響到下次讀取,下次再調(diào)用這個(gè)方法,讀取的起始點(diǎn)會(huì)后移5個(gè)byte inputStream.read(new byte[5]); //ByteArrayOutputStream生成對象的時(shí)候,是生成一個(gè)100大小的byte的緩沖區(qū),寫入的時(shí)候,是把內(nèi)容寫入內(nèi)存中的一個(gè)緩沖區(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)用這個(gè)方法,會(huì)影響到下次讀取,下次再調(diào)用這個(gè)方法,讀取的起始點(diǎn)會(huì)后移6個(gè)byte inputStream.read(new byte[6]); inputStream.reset(); //ByteArrayOutputStream生成對象的時(shí)候,是生成一個(gè)100大小的byte的緩沖區(qū),寫入的時(shí)候,是把內(nèi)容寫入內(nèi)存中的一個(gè)緩沖區(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實(shí)現(xiàn)日志記錄功能詳解的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot AOP日志記錄的資料請關(guān)注腳本之家其它相關(guān)文章!
- SpringBoot使用AOP實(shí)現(xiàn)統(tǒng)一角色權(quán)限校驗(yàn)
- Spring AOP實(shí)現(xiàn)功能權(quán)限校驗(yàn)功能的示例代碼
- SpringBoot中使用AOP實(shí)現(xiàn)日志記錄功能
- Spring AOP如何自定義注解實(shí)現(xiàn)審計(jì)或日志記錄(完整代碼)
- 在springboot中使用AOP進(jìn)行全局日志記錄
- Spring AOP實(shí)現(xiàn)復(fù)雜的日志記錄操作(自定義注解)
- SpringAop實(shí)現(xiàn)操作日志記錄
- springMVC自定義注解,用AOP來實(shí)現(xiàn)日志記錄的方法
- 使用Spring AOP做接口權(quán)限校驗(yàn)和日志記錄
相關(guān)文章
學(xué)習(xí)Java之IO流的基礎(chǔ)概念詳解
這篇文章主要給大家介紹了Java中的IO流,我們首先要搞清楚一件事,就是為什么需要IO流這個(gè)東西,但在正式學(xué)習(xí)IO流的使用之前,小編有必要帶大家先了解一下IO流的基本概念,需要的朋友可以參考下2023-09-09springboot項(xiàng)目打成war包部署到tomcat遇到的一些問題
這篇文章主要介紹了springboot項(xiàng)目打成war包部署到tomcat遇到的一些問題,需要的朋友可以參考下2017-06-06聊聊spring @Transactional 事務(wù)無法使用的可能原因
這篇文章主要介紹了spring @Transactional 事務(wù)無法使用的可能原因,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07Java 圖解Spring啟動(dòng)時(shí)的后置處理器工作流程是怎樣的
spring的后置處理器有兩類,bean后置處理器,bf(BeanFactory)后置處理器。bean后置處理器作用于bean的生命周期,bf的后置處理器作用于bean工廠的生命周期2021-10-10