实战SpringCloud通用请求字段拦截处理
背景
以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通用请求字段拦截处理的更多相关文章
- mybatis拦截器实现通用权限字段添加
实现效果 日常sql中直接使用权限字段实现权限内数据筛选,无需入参,直接使用,使用形式为:select * from crh_snp.channelinfo where short_code in ( ...
- chrome拓展开发实战:页面脚本的拦截注入
原文请访问个人博客:chrome拓展开发实战:页面脚本的拦截注入 目前公司产品的无线站点已经实现了业务平台组件化,所有业务组件的转场都是通过路由来完成,而各个模块是通过requirejs进行统一管理, ...
- http协议(七)通用首部字段
通用首部字段的意思,就是:请求和响应报文双方都会使用的首部 1.Cache-Control 通过指定它的指令,能操作缓存的工作机制 指令参数是可选的,多个指令通过“,”分隔 Cache-Control ...
- http协议基础(七)通用首部字段
通用首部字段的意思,就是:请求和响应报文双方都会使用的首部 1.Cache-Control 通过指定它的指令,能操作缓存的工作机制 指令参数是可选的,多个指令通过“,”分隔 Cache-Control ...
- 实战SpringCloud响应式微服务系列教程(第二章)
接上一篇:实战SpringCloud响应式微服务系列教程(第一章) 1.1.2背压 背压是响应式编程的核心概念,这一节也是我们了解响应式编程的重点. 1.背压的机制 在生产者/消费者模型中,我们意识到 ...
- 实战SpringCloud响应式微服务系列教程(第四章)
接上一篇: 实战SpringCloud响应式微服务系列教程(第一章) 实战SpringCloud响应式微服务系列教程(第二章) 实战SpringCloud响应式微服务系列教程(第三章) 1.1.4 引 ...
- 实战SpringCloud响应式微服务系列教程(第八章)构建响应式RESTful服务
本文为实战SpringCloud响应式微服务系列教程第八章,讲解构建响应式RESTful服务.建议没有之前基础的童鞋,先看之前的章节,章节目录放在文末. 1.使用springboot2.1.4构建RE ...
- 实战SpringCloud响应式微服务系列教程(第九章)使用Spring WebFlux构建响应式RESTful服务
本文为实战SpringCloud响应式微服务系列教程第九章,讲解使用Spring WebFlux构建响应式RESTful服务.建议没有之前基础的童鞋,先看之前的章节,章节目录放在文末. 从本节开始我们 ...
- web请求的拦截与处理
1,特定请求的拦截:spring或struct2的拦截器,指定拦截模式和处理的servlet: 2,非特定的恶意非法请求,web.xml的error-page元素可以接受tomcat返回的错误代码,并 ...
随机推荐
- 给Linux小白的CentOS8.1基本安装说明
写在前面的话:用过Linux的同学应该都会觉得Linux安装是件非常简单的事情,根本不值得用博客记下来!但是我发现,其实没接触过Linux的同学还真不一定会装,就像在IT行业工作过几年但一直用Wind ...
- Redis学习笔记(十八) 集群(下)
复制和故障转移 Redis集群中的节点分为主节点(master)和从节点(slave),其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制 的主节点下线时,代替下线主节点继续处理命令请求. ...
- JAVA 代码查错
1.abstract class Name { private String name; public abstract boolean isStupidName(String name){}} 大侠 ...
- Vue项目实战之改动饿了吗购物小球动画
html:没有写v-on: afterEnter函数了,因为执行不到,原因是enter的done: <div class="ball-container"><tr ...
- 并发系列(一)——线程池源码(ThreadPoolExecutor类)简析
前言 本文主要是结合源码去线程池执行任务的过程,基于JDK 11,整个过程基本与JDK 8相同. 个人水平有限,文中若有表达有误的,欢迎大伙留言指出,谢谢了! 一.线程池简介 1.1 使用线程池的优点 ...
- CentOS7.5搭建Hive2.3.3
一 Hive的下载 软件下载地址:https://mirrors.tuna.tsinghua.edu.cn/apache/hive/ 这里下载的版本是:apache-hive-2.3.3-bin.t ...
- pip未找到
命令终端运行 sudo easy_install pip 安装成功后最后会显示 Installed /Library/Python/2.7/site-packages/pip-9.0.1-py2.7. ...
- 关于时间格式 GMT,UTC,CST,ISO
GMT: 格林尼治所在地的标准时间 UTC: 协调世界时,又称世界统一时间.世界标准时间.国际协调时间.由于英文(CUT)和法文(TUC)的缩写不同,作为妥协,简称UTC. 协调世界时是以原子时秒长为 ...
- cb24a_c++_STL算法简介
算法概述 算法部分主要由头文件<algorithm>,<numeric>和<functional>组成. <algorithm>是所有STL ...
- C#结构体struct -0029
结构体 有时候我们仅需要一个小的数据结构,类提供的功能多于我们需要的功能:考虑到性能原因,最好使用结构体. 结构体是值类型,存储在栈中或存储为内联(如果结构体是存储在堆中的另一个对象的一部分). 例如 ...