框架中的可拓展的Filter了解嘛

659 阅读3分钟

看了看手上的项目日志参差不齐,有点难以忍受日志的打印情况,于是手撸两个Filter,让我在排查问题快速下手!!!

基于Dubbo的SPI拓展的日志Filter

关键代码

public class DubboLogFilter implements Filter {

    private final Logger logger = LoggerFactory.getLogger(getClass());
    
    private ThreadLocal<DubboLog> log = new ThreadLocal<DubboLog>();

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
             try {
            log.set(new DubboLog());
            log.get().setInterfaceName(invocation.getInvoker().getInterface().getName());
            log.get().setMethodName(invocation.getMethodName());
            log.get().setArgs(invocation.getArguments());
            log.get().setClient(RpcContext.getContext().getRemoteAddressString());
            log.get().setHost(RpcContext.getContext().getLocalAddressString());
            log.get().setRequestUrl(invoker.getUrl().getAbsolutePath());
            long startTime = System.currentTimeMillis();
            Result result = invoker.invoke(invocation);
            long elapsed = System.currentTimeMillis() - startTime;
            if (result.hasException() && invoker.getInterface() != GenericService.class) {
                log.get().setThrowableStr(ExceptionUtils.getStackTrace(result.getException()));
                logger.error(JSONObject.toJSONString(log.get()));
            } else {
                log.get().setResult(new Object[]{result.getValue()});
                log.get().setSpendTime(elapsed);
                logger.info(JSONObject.toJSONString(log.get()));
            }
            return result;
        } finally {
            log.remove();
        }
    }
}

使用阶段

上述代码只是定义了一个Filter,我们还需要在我们的项目中去使用它。

  1. 定义好这个filter之后,我们需要在对应filter所在的工程里面,再定义一个资源文件com.alibaba.dubbo.rpc.Filter。 写法: filter别 = filter全路径

  2. 以上东西都准备好了之后,我们就可以使用。

  • 当选择提供方打印日志,我们定义对外提供的service中加入该filter。(例:@Service(version = "1.0.0", filter = "dubboLogFilter")),这样每次有消费者来调用都会在服务方打印日志
  • 当选择消费方打印日志,我们定义该引用所需要加载的filter。(例:referenceBean.setFilter("dubboLogFilter")
  1. 这样我们就可以在对应的日志文件中看到打印结果集。
 [:20881-thread-2] com.montos.filter.DubboLogFilter         : {"args":[4],"client":"192.168.124.34:53302","host":"192.168.124.34:20881","interfaceName":"com.montos.interfaces.UserInterface","methodName":"getUser","requestUrl":"/com.montos.interfaces.UserInterface","result":[{"address":"4","age":4,"id":4,"name":"4"}],"spendTime":1}

拓展部分

目前自定义filter只是基于日志的请求参数以及相应返回值的打印。后期还可以进行这方面的拓展,主要有下面这个入口:

RpcContext 是一个 ThreadLocal 的临时状态记录器,当接收到 RPC 请求,或发起 RPC 请求时,RpcContext 的状态都会变化。比如:ABB 再调 C,则 B 机器上,在 BC 之前,RpcContext 记录的是 AB 的信息,在 BC 之后,RpcContext 记录的是 BC 的信息。后期可以考虑实现一个日志追踪上报的Filter

基于HTTP的SPI拓展的日志Filter

关键代码

public class HttpRequestFilter implements Filter {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    public ThreadLocal<HttpLog> log = new ThreadLocal<HttpLog>();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        try {
            log.set(new HttpLog());
            RequestWrapper requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest);
            ResponseWrapper responseWrapper = new ResponseWrapper((HttpServletResponse) servletResponse);

            Thread thread = Thread.currentThread();
            Map<String, String> map = new HashMap<String, String>();
            @SuppressWarnings("rawtypes")
            Enumeration headerNames = requestWrapper.getHeaderNames();
            while (headerNames.hasMoreElements()) {
                String key = (String) headerNames.nextElement();
                String value = requestWrapper.getHeader(key);
                map.put(key, value);
            }
            log.get().setUrl(requestWrapper.getRequestURL().toString());
            log.get().setRequest(StringUtils.isNotEmpty(requestWrapper.getQueryString()) ? requestWrapper.getQueryString() : getJSON(requestWrapper));
            log.get().setMethod(requestWrapper.getMethod());
            log.get().setClientIp(servletRequest.getRemoteAddr());
            log.get().setThread(new StringBuilder(thread.getName()).append("-").append(thread.getId()).toString());
            log.get().setHeader(map);
            log.get().setStart(System.currentTimeMillis());
            // before
            filterChain.doFilter(requestWrapper, responseWrapper);
            // after
            servletResponse.getOutputStream().write(responseWrapper.getContentAsBytes());//最后注意需要请reponsewrapper的内容写入到原始response
            log.get().setResponse(responseWrapper.getContent());
            log.get().setEnd(System.currentTimeMillis());
            if (logger.isTraceEnabled()) {
                logger.trace(JSONObject.toJSONString(log.get()));
            }
        } catch (Exception e) {
            // error
            log.get().setEnd(System.currentTimeMillis());
            log.get().setError(e.getMessage());
            if (logger.isErrorEnabled()) {
                logger.error(JSONObject.toJSONString(log.get()));
            }
        }
    }

    /**
     * @param request
     * @return
     * @throws IOException
     */
    public String getJSON(HttpServletRequest request) throws IOException {
        BufferedReader streamReader = new BufferedReader(new InputStreamReader(request.getInputStream(), "UTF-8"));
        StringBuilder responseStrBuilder = new StringBuilder();
        String inputStr;
        while ((inputStr = streamReader.readLine()) != null) {
            responseStrBuilder.append(inputStr);
        }
        return responseStrBuilder.toString();
    }

    @Override
    public void destroy() {
        log.remove();
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
}

使用阶段

上面是Filter具体的实现,里面就是多了两个类RequestWrapper以及ResponseWrapper(这里是为了缓存InputStream以及OutputStream,原因是Servlet中获取参数的方法有冲突)。

  1. 框架中我们将该FilterBean的方式注入到容器中(或者有小伙伴直接基于注解@WebFilter以及@ServletComponentScan也是可以的)。

  2. 其次在FilterRegistrationBean中注入该Filter即可。

    @Bean(name = "httpRequestFilter")
    public Filter httpRequestFilter() {
        return new HttpRequestFilter();
    }

    @Bean
    public FilterRegistrationBean someFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(httpRequestFilter());
        registration.addUrlPatterns("/*");
        registration.setName("httpRequestFilter");
        registration.setOrder(1);
        return registration;
    }
  1. 日志中我们就可以看到我们刚刚的结果集
2020-08-26 19:56:35.134 [http-nio-8080-exec-9] TRACE com.montos.filter.HttpRequestFilter - {"clientIp":"***.***.***.***","end":1598442995134,"header":{"content-length":"103","referer":"referer","remoteip":"***.***.***.***","accept-language":"zh-CN,zh;q=0.9","cookie":"cookie","origin":"url","x-forwarded-for":"***.***.***.***,***.***.***.***","accept":"application/json, text/plain, */*","x-real-ip":"***.***.***.***","host":"host","connection":"close","content-type":"application/json","accept-encoding":"gzip, deflate","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36"},"method":"POST","request":"request","response":"response","start":1598442994744,"thread":"http-nio-8080-exec-9-65","url":"url"}

上面两个就是基于现有的项目进行开出来的一个日志拦截。对于现有项目可以进行直接加入,然后指定对应的日志级别再进行打印等等(没有采用Aspect的方式,因为觉得太重了,于是直接利用框架中的SPI进行拓展)。