目录
1.4表单参数(application/x-www-form-urlencoded)
2.打印日志代码(RequestLogInfoFilter )
1.日志效果
1.1json参数(application/json)
请求:
日志:
1.2文件参数(multipart/form-data)
请求:
日志:
1.3路径参数
请求:
日志:
1.4表单参数(application/x-www-form-urlencoded)
请求:
日志:
2.打印日志代码(RequestLogInfoFilter )
package com.kzj.myJar.springboot.common.filter;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.core.ApplicationPart;
import org.apache.tomcat.util.http.fileupload.disk.DiskFileItem;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Slf4j
@Component
/*
* 请求日志打印
* 可打印: 请求方法类型(Get,Post,Put,Delete)
* 请求url路径
* 请求uri
* 请求Content-Type头(application/json,application/x-www-form-urlencode,multipart/form-data等)
* 请求Accept头(application/json,text/html等)
* 请求路径参数(?key1=value1&key2=value2)
* 请求ip:port信息
* 请求体(json信息,表单信息,multipart文件等信息)
* 重点:请求体;
* -非multipart 请求体可以直接通过getInputStream获取,但会导致后续解析请求参数的时候,参数为null; 可通过代理增强方法,使其可重复读取即可
* -multipart 不可以直接通过getInputStream,因为在
* DispatcherServlet.doDispatcher:this.checkMultipart(request)
* -> DispatcherServlet.checkMultipart:return this.multipartResolver.resolveMultipart(request)
* -> StandardServletMultipartResolver.resolveMultipart:return new StandardMultipartHttpServletRequest(request, this.resolveLazily)
* -> StandardMultipartHttpServletRequest:this.parseRequest(request)->StandardMultipartHttpServletRequest.parseRequest:Collection<Part> parts = request.getParts()
* -> RequestFacade.getParts: return this.request.getParts() ->Request.getParts: this.parseParts(true)
* -> Request.parseParts:List<FileItem> items = upload.parseRequest(new ServletRequestContext(this))
* -> FileUploadBase.parseRequest : FileItemIterator iter = this.getItemIterator(ctx)
* -> FileUploadBase.getItemIterator: return new FileItemIteratorImpl(this, ctx)
* -> FileItemIteratorImpl.FileItemIteratorImpl this.findNextItem()
* -> FileItemIteratorImpl.findNextItem: MultipartStream multi = this.getMultiPartStream()
* -> FileItemIteratorImpl.getMultiPartStream: this.init(this.fileUploadBase, this.ctx)
* -> input = new LimitedInputStream(this.ctx.getInputStream(), this.sizeMax)
* FileItemIteratorImpl.init 中 new LimitedInputStream(this.ctx.getInputStream(), this.sizeMax) 通过this.ctx.getInputStream()(this.ctx:Request),获取了流,并读取指定长度的byte转为了LimitedInputStream,
* 若提前通过getInputStream()获取流中信息,会导致后续Multipart为null;
* 可以通过getParts方法,首次调用会将请求体转为Part对象,后续的调用会返回已有的parts
*
* tips:请求体->multipartFile对象 见 DispatcherServlet.doDispatcher:this.checkMultipart(request)
* -> DispatcherServlet.checkMultipart:this.multipartResolver.resolveMultipart(request)
* -> StandardServletMultipartResolver:resolveMultipart:return new StandardMultipartHttpServletRequest(request, this.resolveLazily)
* -> StandardMultipartHttpServletRequest.StandardMultipartHttpServletRequest:this.parseRequest(request)
* -> StandardMultipartHttpServletRequest.parseRequest:files.add(part.getName(), new StandardMultipartFile(part, filename))
*/
public class RequestLogInfoFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if(!(servletRequest instanceof RequestFacade)){
//放行
filterChain.doFilter(servletRequest,servletResponse);
}
RequestFacade request = (RequestFacade) servletRequest;
if(isMultipart(request))
{
List<MultiPart> multiPartList = getMultiPartList(request);
printLog(request,true,null,multiPartList);
} else {
request = getProxyRequestFacade(request);
printLog(request, false, request.getInputStream(), null);
}
filterChain.doFilter(request,servletResponse);
}
/**
* 打印日志
* @param request 请求对象
* @param isMultipart 是否为Multipart请求 true:是 false 否
* @param requestBody 请求体的输入流,Multipart请求时为null
* @param multiPartList MultiPart列表,封装了Multipart请求时的参数名,临时文件目录等信息,非Multipart请求时为null
* @throws IOException
*/
public void printLog(RequestFacade request,boolean isMultipart,InputStream requestBody,List<MultiPart> multiPartList) throws IOException {
String body="";
/*
* 是 multipart请求
*/
if(isMultipart){
for (int i = 0; i < multiPartList.size(); i++) {
MultiPart multiPart = multiPartList.get(i);
if(multiPart.isFile())
body+=String.format("%d - multipart param: %s , fileName: %s , fileContent:\n%s\n",i,multiPart.getParamName(),multiPart.getSubmitFileName(), new String(multiPart.getFileBytes()));
else
body+=String.format("%d - multipart param: %s , value: %s\n",i,multiPart.getParamName(),multiPart.getParamValue());
}
}
/**
* 代理httpServletRequest对象,增强 getInputStream方法 使流可重复读取,保证读取流中的数据后不影响后续的Controller方法参数;
*/
else body= StreamUtils.copyToString(requestBody, Charset.defaultCharset());
/**
* 打印信息,格式不满意可自行定义
*/
log.info("\n" +
"+-----------------+--------------------------------------------------------------------------------+\n" +
String.format("|%-15s |%-80s|\n","method",request.getMethod())+ //content-Type头
String.format("|%-15s |%-80s|\n","url",request.getRequestURL().toString())+ //请求url路径
String.format("|%-15s |%-80s|\n","uri",request.getRequestURI())+ //包含 路径参数
String.format("|%-15s |%-80s|\n","Content-Type",request.getContentType())+ //content-Type头
String.format("|%-15s |%-80s|\n","Accept",request.getHeader("Accept"))+ //Accept头
String.format("|%-15s |%-80s|\n","Url-params",request.getQueryString())+ //包含路径中的?key1=value1&key2=value2
String.format("|%-15s |%-80s|\n","ip:port",request.getRemoteAddr()+":"+request.getRemoteUser())+ //发起请求的地址:端口
(body!=null&&!body.equals("")?String.format("|%-15s | \n%s\n","body",body):"")+ //请求体
"+-----------------+--------------------------------------------------------------------------------+\n"
);
}
/**
* 获取代理RequestFacade ,增强getInputStream方法使该方法能重复获取输入流 (cglib动态代理)
* @param requestFacade 原始RequestFacade
* @return 代理RequestFacade
*/
public RequestFacade getProxyRequestFacade(RequestFacade requestFacade){
Enhancer enhancer=new Enhancer();
enhancer.setSuperclass(RequestFacade.class);
enhancer.setCallback(new MethodInterceptor() {
private final RequestFacade original=requestFacade;
byte[] bytes=null;
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
if(!method.getName().equals("getInputStream"))
return methodProxy.invoke(original,args);
if(bytes==null){
ServletInputStream servletInputStream = (ServletInputStream)method.invoke(original, args);
bytes = StreamUtils.copyToByteArray(servletInputStream);
}
return new MyServletInputStream(bytes);
}
});
return (RequestFacade) enhancer.create(new Class[]{Request.class},new Object[]{null});
}
/**
* 判断是否为multipart请求
* 判断逻辑见 DispatcherServlet.doDispatch()->this.checkMultipart(request)->this.multipartResolver.isMultipart(request)->StandardServletMultipartResolver.isMultipart(request)
* @param httpServletRequest request
* @return 是否为multipart请求
*/
public boolean isMultipart(HttpServletRequest httpServletRequest){
String contentType = httpServletRequest.getContentType();
return contentType!=null&&contentType.toLowerCase().startsWith("multipart/");
}
/**
* 获取 MultiPart List
* @param httpServletRequest request
* @return MultiPart List
*/
public List<MultiPart> getMultiPartList(HttpServletRequest httpServletRequest){
List<MultiPart> result=new ArrayList<>();
try{
Collection<Part> parts = httpServletRequest.getParts();
Field fileItemField= ApplicationPart.class.getDeclaredField("fileItem");
fileItemField.setAccessible(true);
for(Part part:parts){
if(part instanceof ApplicationPart){
ApplicationPart applicationPart = (ApplicationPart) part;
Object o = fileItemField.get(applicationPart);
if(o instanceof DiskFileItem){
DiskFileItem diskFileItem = (DiskFileItem) o;
boolean isFile=!(applicationPart.getSubmittedFileName()==null||applicationPart.getSubmittedFileName().equals(""));
result.add(new MultiPart(applicationPart.getName(),isFile,isFile?applicationPart.getSubmittedFileName():null,diskFileItem.getStoreLocation()));
}
}
}
}catch (Exception e){
throw new RuntimeException("get Multipart List fail, error msg: "+e.getMessage(),e);
}
return result;
}
/**
* 自定义类 封装multipart 请求中参数名,临时文件等信息
*/
@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class MultiPart{
String paramName;
boolean isFile;
String submitFileName;
File tempFile;
private byte[] getBytes(){
try(FileInputStream fileInputStream=new FileInputStream(tempFile)){
return StreamUtils.copyToByteArray(fileInputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public byte[] getFileBytes(){
if(!isFile)
throw new RuntimeException("the MultiPart not is file, get value please use getParamValue()");
return getBytes();
}
public String getParamValue(){
if(isFile)
throw new RuntimeException("the MultiPart is file,get byte[] please use getFileBytes()");
return new String(getBytes());
}
}
public static class MyServletInputStream extends ServletInputStream{
ByteArrayInputStream byteArrayInputStream;
public MyServletInputStream(byte[] bytes){
byteArrayInputStream=new ByteArrayInputStream(bytes);
}
@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();
}
}
}
3.代码详情
3.1.概括
RequestLogInfoFilter 实现了javax.servlet.Filter接口,只需要@Component注解,即可使用;
Filter的执行流程在DispatcherServlet.doDispatcher方法(SpringMvc的执行流程)之前;
可打印: 请求方法类型(Get,Post,Put,Delete) 请求url路径 请求uri 请求Content-Type头(application/json,application/x-www-form-urlencode,multipart/form-data等) 请求Accept头(application/json,text/html等) 请求路径参数(?key1=value1&key2=value2) 请求ip:port信息 请求体(json信息,表单信息,multipart文件等信息) 重点:请求体; -非multipart 请求体可以直接通过getInputStream获取,但会导致后续解析请求参数的时候,参数为null; 可通过代理增强方法,使其可重复读取即可 -multipart 不可以直接通过getInputStream,因为在 DispatcherServlet.doDispatcher:this.checkMultipart(request) -> DispatcherServlet.checkMultipart:return this.multipartResolver.resolveMultipart(request) -> StandardServletMultipartResolver.resolveMultipart:return new StandardMultipartHttpServletRequest(request, this.resolveLazily) -> StandardMultipartHttpServletRequest:this.parseRequest(request)->StandardMultipartHttpServletRequest.parseRequest:Collection<Part> parts = request.getParts() -> RequestFacade.getParts: return this.request.getParts() ->Request.getParts: this.parseParts(true) -> Request.parseParts:List<FileItem> items = upload.parseRequest(new ServletRequestContext(this)) -> FileUploadBase.parseRequest : FileItemIterator iter = this.getItemIterator(ctx) -> FileUploadBase.getItemIterator: return new FileItemIteratorImpl(this, ctx) -> FileItemIteratorImpl.FileItemIteratorImpl this.findNextItem() -> FileItemIteratorImpl.findNextItem: MultipartStream multi = this.getMultiPartStream() -> FileItemIteratorImpl.getMultiPartStream: this.init(this.fileUploadBase, this.ctx) -> input = new LimitedInputStream(this.ctx.getInputStream(), this.sizeMax) FileItemIteratorImpl.init 中 new LimitedInputStream(this.ctx.getInputStream(), this.sizeMax) 通过this.ctx.getInputStream()(this.ctx:Request),获取了流,并读取指定长度的byte转为了LimitedInputStream, 若提前通过getInputStream()获取流中信息,会导致后续Multipart为null; 可以通过getParts方法,首次调用会将请求体转为Part对象,后续的调用会返回已有的parts ( tips:请求体->multipartFile对象 见 DispatcherServlet.doDispatcher:this.checkMultipart(request) -> DispatcherServlet.checkMultipart:this.multipartResolver.resolveMultipart(request) -> StandardServletMultipartResolver:resolveMultipart:return new StandardMultipartHttpServletRequest(request, this.resolveLazily) -> StandardMultipartHttpServletRequest.StandardMultipartHttpServletRequest:this.parseRequest(request) -> StandardMultipartHttpServletRequest.parseRequest:files.add(part.getName(), new StandardMultipartFile(part, filename))
)
3.2方法以及内部类
3.2.1方法
doFilter: Filter的过滤方法,内部判断了请求是否为Multipart请求? 是:通过getMultiPartList方法获取MulitiPart列表;否:通过getProxyRequestFacade方法获取代理对象; 并调用printLog方法打印日志; 最后放行; public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException
printLog:打印日志(具体的打印逻辑可自行调整) /** * 打印日志 * @param request 请求对象 * @param isMultipart 是否为Multipart请求 true:是 false 否 * @param requestBody 请求体的输入流,Multipart请求时为null * @param multiPartList MultiPart列表,封装了Multipart请求时的参数名,临时文件目录等信息,非Multipart请求时为null * @throws IOException */ public void printLog(RequestFacade request,boolean isMultipart,InputStream requestBody,List<MultiPart> multiPartList) throws IOException
getProxyRequestFacade:获取代理对象,增强getInputStream方法 /** * 获取代理RequestFacade ,增强getInputStream方法使该方法能重复获取输入流 (cglib动态代理) * @param requestFacade 原始RequestFacade * @return 代理RequestFacade */ public RequestFacade getProxyRequestFacade(RequestFacade requestFacade)
isMultipart:判断是否为Multipart请求 ** * 判断是否为multipart请求 * 判断逻辑见 DispatcherServlet.doDispatch()->this.checkMultipart(request) ->this.multipartResolver.isMultipart(request) ->StandardServletMultipartResolver.isMultipart(request) * @param httpServletRequest request * @return 是否为multipart请求 */ public boolean isMultipart(HttpServletRequest httpServletRequest)
getMultiPartList:获取MultiPart List,通过getParts方法获取Part,并通过反射获取一些private熟悉,并封装为MulitiPart对象 /** * 获取 MultiPart List * @param httpServletRequest request * @return MultiPart List */ public List<MultiPart> getMultiPartList(HttpServletRequest httpServletRequest)
3.2.2内部类
MultiPart:封装multipart 请求中参数名,临时文件等信息
MyServletInputStream:继承ServletInputStream,并实现了getInputStream方法;