项目背景
在进行开发时,会遇到以下问题
要记录请求参数,在每个接口中加打印和记录数据库日志操作,影响代码质量,也不利于修改
@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
5.代码Git地址
git@github.com:A-mantis/SpringBootDataCheck.git
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。