当前位置: 代码迷 >> 综合 >> ThreadLocal-实现http请求、返回统一记录处理
  详细解决方案

ThreadLocal-实现http请求、返回统一记录处理

热度:79   发布时间:2023-09-29 07:37:22.0

背景

       在项目中我们很多时候都是涉及到的外部的请求,一般我们在处理请求时,都需要根据不同的参数做不同的返回,甚至不同的错误信息返回不同的错误码和错误信息。所以在方法中我们需要有请求和返回参数。就像下面这种:

@ResponseBody
@RequestMapping(value = "/test", produces = "application/json; charset=UTF-8", method = RequestMethod.POST)
public Object test(HttpServletRequest request,HttpServletResponse response) {...}

       这样spring就会自动把HttpServletRequest 和HttpServletResponse两个对象自动注入到方法中。

      但是这样是有不足的的。第一:当请求惨数太多时,再多这两个参数就让人看起来更难受了。第二: 当我后续的处理层级特别多时,我就必须包这两个参数都传递下去。才能保证我能正确的返回。第三:如果我原来没有,后续突然有想加上去,这样改的代码就很多很麻烦了。

        所以我就会想有没有一种办法在请求之初就能吧这两个参数记录下来,到我想用的时候就直接取出来用就好了。就像一个全局缓存。请求来的时候,放到缓存里,其他地方处理的时候就直接获取,请求处理结束就直接删掉就好。

      要是想这个功能,首先需要满足一下几个条件:

      第一:在请求到达时就必须记录两个参数,这个可以使用拦截器实现。

      第二:请求结束就需要将这两个删掉,否则回导致内存越来越大。这个拦截器也可以做到。

      第二:每个请求的处理流程必须能正确的获取到这两个参数,否则就会导致返回和请求错乱。这个可以使用ThreadLocal实现。

Threadlocal

       每个线程持有一个ThreadLocalMap对象。ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。每个线程都对应一个ThreadLocalMap, 而threadlocal负责访问和维护ThreadLocalMap。ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中。

       所以Threadlocal是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据。ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

      ThreadLocal的主要方法有get,set,remove三个方法。进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。get方法中是通过正在使用的线程来设置参数的,这样就能保证设置的是线程自己的参数。

//set 方法
public void set(T value) {//获取当前线程Thread t = Thread.currentThread();//实际存储的数据结构类型ThreadLocalMap map = getMap(t);//如果存在map就直接set,没有则创建map并setif (map != null)map.set(this, value);elsecreateMap(t, value);}//获取参数
public T get() {Thread t = Thread.currentThread();  //获取当前线程ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);   从线程的ThreadLocalMap中获取数据if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue(); //没有则初始化一个null的值}//删除参数
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());  //从线程中获取线程自己的参数if (m != null)m.remove(this);}//getMap方法
ThreadLocalMap getMap(Thread t) {//thred中维护了一个ThreadLocalMapreturn t.threadLocals;}//createMap
void createMap(Thread t, T firstValue) {//实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocalst.threadLocals = new ThreadLocalMap(this, firstValue);
}

     使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。因为ThreadLocalMap生命周期和Thread的一样,它不会回收。所以当ThreadLocal为null,就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。

实现

请求池记录所有请求和返回对象实现

有三个ThreadLocal对象,分贝记录请求、返回、测试id对象。

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class BaseHttpParamPool {//记录请求对象private static ThreadLocal<HttpServletRequest> requestThreadLocal = new ThreadLocal<>();//记录返回对象private static ThreadLocal<HttpServletResponse> responseThreadLocal = new ThreadLocal<>();//测试idprivate static ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();public static void setHttpServletRequest(HttpServletRequest request){requestThreadLocal.set(request);}public static HttpServletRequest getHttpServletRequest(){return requestThreadLocal.get();}public static void removeHttpServletRequest(){requestThreadLocal.remove();}public static void setId(String id){stringThreadLocal.set(id);}public static String getId(){return stringThreadLocal.get();}public static void removeId(){stringThreadLocal.remove();}public static void setHttpServletResponse(HttpServletResponse response){responseThreadLocal.set(response);}public static HttpServletResponse getHttpServletResponse(){return responseThreadLocal.get();}public static void removeHttpServletResponse(){responseThreadLocal.remove();}}

拦截器实现

      自定义拦截器需要实现HandlerInterceptorAdapter接口并重写其中的三个方法。

     preHandle:在业务处理器处理请求之前被调用。预处理,可以进行编码、安全控制等处理。这里将请求、返回、测试id对象加入线程的TreadLocalMap中

postHandle:在业务处理器处理请求执行完成后,生成视图之前执行。后处理(调用了Service并返回ModelAndView,但未进行页面渲染),有机会修改ModelAndView; 这里不变,直接调用父类的postHandle方法。

afterCompletion:在DispatcherServlet完全处理完请求后被调用,可用于清理资源等。返回处理(已经渲染了页面),可以根据ex是否为null判断是否发生了异常,进行日志记录;这里将所有之前记录的参数全部remove掉。
public class BaseHttpParamInterceptor extends HandlerInterceptorAdapter {/*** 在方法被调用前执行。在该方法中可以做类似校验的功能。如果返回true,则继续调用下一个拦截器。* 如果返回false,则中断执行,也就是说我们想调用的方法 不会被执行,但是你可以修改response为你想要的响应*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String id = String.valueOf(System.currentTimeMillis());System.out.println(id + "--接口请求开始:"+ request.getRequestURI());//记录 HttpServletRequest 和 HttpServletResponseBaseHttpParamPool.setHttpServletRequest(request);BaseHttpParamPool.setHttpServletResponse(response);BaseHttpParamPool.setId(id);return super.preHandle(request, response, handler);}/*** 在业务处理器处理请求执行完成后,生成视图之前执行。后处理(调用了Service并返回ModelAndView,但未进行页面渲染),有机会修改ModelAndView;*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {super.postHandle(request, response, handler, modelAndView);}/*** 在DispatcherServlet完全处理完请求后被调用,可用于清理资源等。返回处理(已经渲染了页面),* 可以根据ex是否为null判断是否发生了异常,进行日志记录;*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println(BaseHttpParamPool.getId() + "--接口请求结束:"+ request.getRequestURI());//记录 HttpServletRequest 和 HttpServletResponseBaseHttpParamPool.removeHttpServletRequest();BaseHttpParamPool.removeHttpServletResponse();BaseHttpParamPool.removeId();super.afterCompletion(request, response, handler, ex);}
}

 实现拦截器之后开始注册拦截器。注册拦截需要实现WebMvcConfigurer接口并改写其中的的addInterceptors方法。并在方法中加入@Configuration注解。

@Configuration
public class WebConfigurer implements WebMvcConfigurer{@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new BaseHttpParamInterceptor());}
}

测试接口实现

实现两个测试用的接口。在测是接口中,我们就可以直接使用BaseHttpParamPool对象来获取线程参数了。

@RestController
@RequestMapping("/threadLocal")
public class TheadLocalController {@GetMapping("/test1")public String test1(){try {Thread.currentThread().sleep(5*1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(BaseHttpParamPool.getId() + "---- test1");return BaseHttpParamPool.getId() + "---- test1";}@GetMapping("/test2")public String test2(){System.out.println(BaseHttpParamPool.getId() + "---- test2");return BaseHttpParamPool.getId() + "---- test2";}
}

现在调用接口测试:

1603702245546--接口请求开始:/demo/threadLocal/test2
1603702245546---- test2
1603702245546--接口请求结束:/demo/threadLocal/test2
1603702246998--接口请求开始:/demo/threadLocal/test1
1603702248962--接口请求开始:/demo/threadLocal/test2
1603702248962---- test2
1603702248962--接口请求结束:/demo/threadLocal/test2
1603702250689--接口请求开始:/demo/threadLocal/test2
1603702250689---- test2
1603702250689--接口请求结束:/demo/threadLocal/test2

1603702246998---- test1
1603702246998--接口请求结束:/demo/threadLocal/test1

1603702254035--接口请求开始:/demo/threadLocal/test1
1603702255639--接口请求开始:/demo/threadLocal/test2
1603702255639---- test2
1603702255639--接口请求结束:/demo/threadLocal/test2

1603702254035---- test1
1603702254035--接口请求结束:/demo/threadLocal/test1

可以看到,没有个请求都能正确获取到自己的id。同理也可以正确获取到 HttpServletRequest 和 HttpServletResponse 对象。

  相关解决方案