请求信息打印类2.0.0(可获取请求体,以及请求的文件内容,且不影响后续的Controller层方法参数)

目录

1.日志效果

1.1json参数(application/json)

1.2文件参数(multipart/form-data)

1.3路径参数

1.4表单参数(application/x-www-form-urlencoded)

2.打印日志代码(RequestLogInfoFilter )

3.代码详情

3.1.概括

3.2方法以及内部类

3.2.1方法

3.2.2内部类


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方法;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值