java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot AOP日志记录

SpringBoot使用AOP实现日志记录功能详解

作者:zero

这篇文章主要为大家介绍了SpringBoot使用AOP实现日志记录功能详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

项目背景

在进行开发时,会遇到以下问题

要记录请求参数,在每个接口中加打印和记录数据库日志操作,影响代码质量,也不利于修改

@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日志记录的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文