SpringBoot使用AOP实现日志记录功能详解
作者:zero
项目背景
在进行开发时,会遇到以下问题
要记录请求参数,在每个接口中加打印和记录数据库日志操作,影响代码质量,也不利于修改
@PostMapping(value = "/userValidPost") public Response queryUserPost(@Valid @RequestBody UserInfo userInfo, BindingResult bindingResult) { try { //打印请求参数 log.info("Request : {}", JSON.toJSONString(userInfo)); //获取返回数据 String result = "Hello " + userInfo.toString(); //打印返回结果 log.info("Response : {}", result); //记录数据库日志 this.insertLog(); return Response.ok().setData(result); } catch (Exception ex) { //打印 log.info("Error : {}", ex.getMessage()); //记录数据库日志 this.insertLog(); return Response.error(ex.getMessage()); }
解决方案
使用AOP记录日志
1.切片配置
为解决这类问题,这里使用AOP进行日志记录
/** * 定义切点,切点为com.zero.check.controller包和子包里任意方法的执行 */ @Pointcut("execution(* com.zero.check.controller..*(..))") public void webLog() { } /** * 前置通知,在切点之前执行的通知 * * @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)); } //获取请求参数 try { String reqBody = this.getReqBody(); logger.info("REQUEST: " + reqBody); } catch (Exception ex) { logger.info("get Request Error: " + ex.getMessage()); } } /** * 后置通知,切点后执行 * * @param ret */ @AfterReturning(returning = "ret", pointcut = "webLog()") public void doAfterReturning(Object ret) { //处理完请求,返回内容 try { logger.info("RESPONSE: " + JSON.toJSONString(ret)); } catch (Exception ex) { logger.info("get Response Error: " + ex.getMessage()); } }
然后在执行时就会发现,前置通知没有打印内容
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方法中,其中有一个参数pos是读取的起点,在接口使用了@RequestBody获取参数,就会导致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(); //调用这个方法,会影响到下次读取,下次再调用这个方法,读取的起始点会后移6个byte //inputStream.read(new byte[6]); //ByteArrayOutputStream生成对象的时候,是生成一个100大小的byte的缓冲区,写入的时候,是把内容写入内存中的一个缓冲区 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(); }
调用inputStream.read(new byte[6]);打印结果
7890
不调用inputStream.read(new byte[6]);打印结果
1234567890
正常情况下,可以使用InputStream的reset()方法重置读取的起始点,但ServletInputStream不支持这个方法,所以ServletInputStream只能读取一次。
2.RequestWrapper
要多次读取ServletInputStream的内容,可以实现一个继承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内容 * * @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
实现一个新的过滤器,在里面使用复写后的requestWrapper,就可以实现ServletInputStream的多次读取,如果要对请求对象进行鉴权,可以在这里进行。
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
请求参数
{ "id": "1", "roleId": 2, "userList": [ { "userId": "1", "userName": "2" } ] }
AOP打印日志
可以看到WebLogAspect成功打印了请求和返回结果
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"}
返回参数
{ "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; //测试前执行,加载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数组转换成一个字节流 InputStream inputStream = new FileInputStream("src/main/resources/data/demo.txt"); //调用这个方法,会影响到下次读取,下次再调用这个方法,读取的起始点会后移5个byte inputStream.read(new byte[5]); //ByteArrayOutputStream生成对象的时候,是生成一个100大小的byte的缓冲区,写入的时候,是把内容写入内存中的一个缓冲区 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(); //调用这个方法,会影响到下次读取,下次再调用这个方法,读取的起始点会后移6个byte inputStream.read(new byte[6]); inputStream.reset(); //ByteArrayOutputStream生成对象的时候,是生成一个100大小的byte的缓冲区,写入的时候,是把内容写入内存中的一个缓冲区 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(); } }
测试结果
AOP打印参数
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实现日志记录功能详解的详细内容,更多关于SpringBoot AOP日志记录的资料请关注脚本之家其它相关文章!