背景

以SpringCloud构建的微服务系统为例,使用前后端分离的架构,每个系统都会提供一些通用的请求参数,例如移动端的系统版本信息、IMEI信息,Web端的IP信息,浏览器版本信息等,这些参数可能放在header里,也可以放在参数里,如果这些参数需要在每个方法内声明定义,一来工作量太大,二是这些通用参数与业务接口方法耦合过紧,本身就是一个不好的设计。

这个问题该如何优雅地解决呢?

最佳实践

实现思路

  • 利用SpringMVC提供拦截器,对匹配的请求,抽取通用的header信息(假设通用字段全部放在header里)
  • 将每个请求的信息单独隔离开,互不干扰。
  • Controller层使用时,可以将在该请求线程(http线程)里将通用的header信息提取出来使用。
  • 请求线程完成时,相应的header头信息对象需要回收销毁。

实现方式

  • SpringMVA提供的HandlerInterceptorAdapter可以拿来使用,继承实现即可。
  • 使用ThreadLocal记录每个请求的信息,ThreadLocal有隔离线程变量的作用。
HandlerInterceptorAdapter的源码实现及注释
public abstract class HandlerInterceptorAdapter implements AsyncHandlerInterceptor {

	@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 在业务接口方法处理之前被调用,可以在这里对通用的header信息进行提取
return true;
} @Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
// 这个方法在业务接口方法执行完成后,生成SpringMVC ModelAndView之前被调用
// 今天这个案例我们不用此方法,故可以不实现。
} @Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
// 这个方法在DispatcherServlet完全处理完成后被调用,可以在这里对ThreadLocal的内容进行释放
} @Override
public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 这个方法用来处理异步主动,但也会先行调用preHandle,然后执行此方法,异步线程完成后会执行postHandle和afterCompletion两方法,这里暂时用不上。
}
}
ThreadLocal的源码主要实现及注释
public class ThreadLocal<T> {

    protected T initialValue() {
return null;
} public T get() {
// 获取当前的线程
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
} private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
} public void set(T value) {
// 获取当前的线程
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
} public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
} ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
} void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}

简单来说,ThreadLocal最关键的get()和set()方法,都是针对当前线程来操作的,调用set()方法时把值放到ThreadMap(Map的一种实现)中,以当前线程的hash值为key,get()方法则对应以当前线程作为key来取值,从而实现每个线程的数据是隔离的效果。

另附上ThreadLocal类源码解读的导图,仅供参考

案例实战

我们对实际业务系统进行简化处理,假定header信息固定有ip,uid,deviceId三个信息,按照上文的实现思路,开始案例演示。

DTO定义

通用的header信息,使用Dto对象进行封装:

@Data
public class CommonHeader implements Serializable { private static final long serialVersionUID = -3949488282201167943L; /**
* 真实ip
*/
private String ip; /**
* 设备id
*/
private String deviceId; /**
* 用户uid
*/
private Long uid; // 省略getter/setter/构造器
}

定义Request请求的封装类Dto,并引入ThreadLocal:

/**
* 将公共请求头信息放在ThreadLocal中去
*/
public class RequestWrap { private static ThreadLocal<CommonHeader> current = new ThreadLocal<>(); /**
* 获取静态的ThreadLocal对象
* @return
*/
public static ThreadLocal<CommonHeader> getCurrent() {
return current;
} /**
* 获取ip
* @return
*/
public static String getIp() {
CommonHeader request = current.get();
if (request == null) {
return StringUtils.EMPTY;
}
return request.getIp();
} /**
* 获取uid
* @return
*/
public static Long getUid() {
CommonHeader request = current.get();
if (request == null) {
return null;
}
return request.getUid();
} /**
* 获取封装对象
* @return
*/
public static CommonHeader getCommonReq() {
CommonHeader request = current.get();
if (request == null) {
return new CommonHeader(StringUtils.EMPTY, StringUtils.EMPTY,0L);
}
return request;
}
}

工具类

这里添加一个简单的工具类,将HttpServletRequest通过getHeader方法,生成CommonHeader类:

public class HttpUtil {
/**
* 获取请求头信息
*
* @param request
* @return
*/
public static CommonHeader getCommonHeader(HttpServletRequest request) {
String UID = request.getHeader("uid");
Long uid = null;
if (StringUtils.isNotBlank(UID)) {
uid = Long.parseLong(UID);
}
return new CommonHeader(HttpUtil.getIp(request), request.getHeader("deviceId"), uid);
} /**
* 获取IP
*
* @param request
* @return
*/
public static String getIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For"); if (null != ip && !"".equals(ip.trim()) && !"unknown".equalsIgnoreCase(ip)) {
int index = ip.indexOf(',');
if (index != -1) {
return ip.substring(0, index);
} else {
return ip;
}
}
ip = request.getHeader("X-Real-IP");
if (null != ip && !"".equals(ip.trim()) && !"unknown".equalsIgnoreCase(ip)) {
return ip;
}
return request.getRemoteAddr();
}
}

拦截器类实现

最核心的实现终于出场了,这里继承HandlerInterceptorAdapter,这里作了简化处理:

/**
* 请求头处理
*
* @author yangfei
*/
@Component
public class BaseInterceptor extends HandlerInterceptorAdapter { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BaseInterceptor.class); @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
RequestWrap.getThreadLocal().set(HttpUtil.getCommonHeader(request));
return true;
} @Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
} @Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
RequestWrap.getThreadLocal().remove();
} @Override
public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
}
}

如上一章节描述的逻辑,在preHandle方法内将request中的ip,uid,deviceId封装到RequestWrap对象里,在afterCompletion中对该线程的ThreadLocal值进行释放。

业务接口方法的使用

在Controller类的接口方法中,如要获取uid信息,只需要调用RequestWrap.getUid()方法即可,再也不需要在每个接口上声明uid参数了,如下示例:

/**
* 获取用户基础信息
*/
@PostMapping(value = "/user/info")
public Response<UserInfo> getUserInfo() {
return userManager.getUserInfo(RequestWrap.getUid());
}

总结

这个实战的目标是解决通用header信息的在接口的重复定义问题,基于HandlerInterceptorAdapter拦截器的实现,ThreadLocal对线程访问数据的隔离来实现的,在实际生产项目应用中有很好的借鉴意义,希望对你有帮助。

专注Java高并发、分布式架构,更多技术干货分享与心得,请关注公众号:Java架构社区

可以扫左边二维码添加好友,邀请你加入Java架构社区微信群共同探讨技术

实战SpringCloud通用请求字段拦截处理的更多相关文章

  1. mybatis拦截器实现通用权限字段添加

    实现效果 日常sql中直接使用权限字段实现权限内数据筛选,无需入参,直接使用,使用形式为:select * from crh_snp.channelinfo where short_code in ( ...

  2. chrome拓展开发实战:页面脚本的拦截注入

    原文请访问个人博客:chrome拓展开发实战:页面脚本的拦截注入 目前公司产品的无线站点已经实现了业务平台组件化,所有业务组件的转场都是通过路由来完成,而各个模块是通过requirejs进行统一管理, ...

  3. http协议(七)通用首部字段

    通用首部字段的意思,就是:请求和响应报文双方都会使用的首部 1.Cache-Control 通过指定它的指令,能操作缓存的工作机制 指令参数是可选的,多个指令通过“,”分隔 Cache-Control ...

  4. http协议基础(七)通用首部字段

    通用首部字段的意思,就是:请求和响应报文双方都会使用的首部 1.Cache-Control 通过指定它的指令,能操作缓存的工作机制 指令参数是可选的,多个指令通过“,”分隔 Cache-Control ...

  5. 实战SpringCloud响应式微服务系列教程(第二章)

    接上一篇:实战SpringCloud响应式微服务系列教程(第一章) 1.1.2背压 背压是响应式编程的核心概念,这一节也是我们了解响应式编程的重点. 1.背压的机制 在生产者/消费者模型中,我们意识到 ...

  6. 实战SpringCloud响应式微服务系列教程(第四章)

    接上一篇: 实战SpringCloud响应式微服务系列教程(第一章) 实战SpringCloud响应式微服务系列教程(第二章) 实战SpringCloud响应式微服务系列教程(第三章) 1.1.4 引 ...

  7. 实战SpringCloud响应式微服务系列教程(第八章)构建响应式RESTful服务

    本文为实战SpringCloud响应式微服务系列教程第八章,讲解构建响应式RESTful服务.建议没有之前基础的童鞋,先看之前的章节,章节目录放在文末. 1.使用springboot2.1.4构建RE ...

  8. 实战SpringCloud响应式微服务系列教程(第九章)使用Spring WebFlux构建响应式RESTful服务

    本文为实战SpringCloud响应式微服务系列教程第九章,讲解使用Spring WebFlux构建响应式RESTful服务.建议没有之前基础的童鞋,先看之前的章节,章节目录放在文末. 从本节开始我们 ...

  9. web请求的拦截与处理

    1,特定请求的拦截:spring或struct2的拦截器,指定拦截模式和处理的servlet: 2,非特定的恶意非法请求,web.xml的error-page元素可以接受tomcat返回的错误代码,并 ...

随机推荐

  1. 给Linux小白的CentOS8.1基本安装说明

    写在前面的话:用过Linux的同学应该都会觉得Linux安装是件非常简单的事情,根本不值得用博客记下来!但是我发现,其实没接触过Linux的同学还真不一定会装,就像在IT行业工作过几年但一直用Wind ...

  2. Redis学习笔记(十八) 集群(下)

    复制和故障转移 Redis集群中的节点分为主节点(master)和从节点(slave),其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制 的主节点下线时,代替下线主节点继续处理命令请求. ...

  3. JAVA 代码查错

    1.abstract class Name { private String name; public abstract boolean isStupidName(String name){}} 大侠 ...

  4. Vue项目实战之改动饿了吗购物小球动画

    html:没有写v-on: afterEnter函数了,因为执行不到,原因是enter的done: <div class="ball-container"><tr ...

  5. 并发系列(一)——线程池源码(ThreadPoolExecutor类)简析

    前言 本文主要是结合源码去线程池执行任务的过程,基于JDK 11,整个过程基本与JDK 8相同. 个人水平有限,文中若有表达有误的,欢迎大伙留言指出,谢谢了! 一.线程池简介 1.1 使用线程池的优点 ...

  6. CentOS7.5搭建Hive2.3.3

    一 Hive的下载 软件下载地址:https://mirrors.tuna.tsinghua.edu.cn/apache/hive/  这里下载的版本是:apache-hive-2.3.3-bin.t ...

  7. pip未找到

    命令终端运行 sudo easy_install pip 安装成功后最后会显示 Installed /Library/Python/2.7/site-packages/pip-9.0.1-py2.7. ...

  8. 关于时间格式 GMT,UTC,CST,ISO

    GMT: 格林尼治所在地的标准时间 UTC: 协调世界时,又称世界统一时间.世界标准时间.国际协调时间.由于英文(CUT)和法文(TUC)的缩写不同,作为妥协,简称UTC. 协调世界时是以原子时秒长为 ...

  9. cb24a_c++_STL算法简介

    算法概述 算法部分主要由头文件<algorithm>,<numeric>和<functional>组成.       <algorithm>是所有STL ...

  10. C#结构体struct -0029

    结构体 有时候我们仅需要一个小的数据结构,类提供的功能多于我们需要的功能:考虑到性能原因,最好使用结构体. 结构体是值类型,存储在栈中或存储为内联(如果结构体是存储在堆中的另一个对象的一部分). 例如 ...