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对线程访问数据的隔离来实现的,在实际生产项目应用中有很好的借鉴意义,希望对你有帮助。

到此这篇关于SpringCloud通用请求字段拦截处理方法的文章就介绍到这了,更多相关SpringCloud请求字段拦截内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • spring cloud zuul修改请求url的方法

    前言 在日常开发中,除了修改请求参数.设置响应header,响应body外,还有一种需求就是url重新,或者是修改url,这里简述一下怎么在zuul修改url.话不多说了,来一起看看详细的介绍吧. 转发配置 demo: ribbon: NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList listOfServers: 192.168.99.100,192.168.99.101 zuul: ro

  • springcloud 中 zuul 修改请求参数信息的方法

    Zuul是Netflix出品的一个基于JVM路由和服务端的负载均衡器. Zuul功能: 认证 压力测试 金丝雀测试 动态路由 负载削减 安全 静态响应处理 主动/主动交换管理 Zuul的规则引擎允许通过任何JVM语言来编写规则和过滤器, 支持基于Java和Groovy的构建. 配置属性 zuul.max.host.connections 已经被两个新的配置属性替代, zuul.host.maxTotalConnections (总连接数)和 zuul.host.maxPerRouteConnec

  • 解决Spring Cloud中Feign/Ribbon第一次请求失败的方法

    前言 在Spring Cloud中,Feign和Ribbon在整合了Hystrix后,可能会出现首次调用失败的问题,要如何解决该问题呢? 造成该问题的原因 Hystrix默认的超时时间是1秒,如果超过这个时间尚未响应,将会进入fallback代码.而首次请求往往会比较慢(因为Spring的懒加载机制,要实例化一些类),这个响应时间可能就大于1秒了.知道原因后,我们来总结一下解决放你. 解决方案有三种,以feign为例. 方法一 hystrix.command.default.execution.

  • Spring Cloud入门教程之Zuul实现API网关与请求过滤

    简介 Zuul是Netflix基于JVM的路由器和服务器端负载均衡器.最常用的场景是替换Nginx反向代理后台微服务供前端UI访问. Zuul使用Ribbon来定位一个通过发现转发的实例,所有请求都以hystrix命令执行,所以故障将显示在Hystrix指标中. 注:Zuul不包括发现客户端,因此对于基于服务ID的路由,需要在类路径中提供其中一个路由 Zuul是Spring Cloud提供的api网关和过滤组件,它提供如下功能: 认证 过滤 压力测试 Canary测试 动态路由 服务迁移 负载均

  • 详解SpringCloud Zuul过滤器返回值拦截

    Zuul作为网关服务,是其他各服务对外中转站,通过Zuul进行请求转发.这就涉及到部分数据是不能原封返回的,比如服务之间通信的凭证,用户的加密信息等等. 举个例子,用户服务提供一个登录接口,用户名密码正确后返回一个Token,此Token作为用户服务的通行证,那么用户登录成功后返回的Token就需要进行加密或者防止篡改处理.在到达用户服务其他接口前,就需要对Token进行校验,非法的Token就不需要转发到用户服务中了,直接在网关层返回信息即可. 要修改服务返回的信息,需要使用的是Zuul的过滤

  • spring cloud 之 Feign 使用HTTP请求远程服务的实现方法

    一.Feign 简介 在spring Cloud Netflix栈中,各个微服务都是以HTTP接口的形式暴露自身服务的,因此在调用远程服务时就必须使用HTTP客户端.我们可以使用JDK原生的URLConnection.Apache的Http Client.Netty的异步HTTP Client, Spring的RestTemplate.但是,用起来最方便.最优雅的还是要属Feign了. Feign是一种声明式.模板化的HTTP客户端.在Spring Cloud中使用Feign, 我们可以做到使用

  • SpringCloud通用请求字段拦截处理方法

    背景 以SpringCloud构建的微服务系统为例,使用前后端分离的架构,每个系统都会提供一些通用的请求参数,例如移动端的系统版本信息.IMEI信息,Web端的IP信息,浏览器版本信息等,这些参数可能放在header里,也可以放在参数里,如果这些参数需要在每个方法内声明定义,一来工作量太大,二是这些通用参数与业务接口方法耦合过紧,本身就是一个不好的设计. 这个问题该如何优雅地解决呢? 最佳实践 利用SpringMVC提供拦截器,对匹配的请求,抽取通用的header信息(假设通用字段全部放在hea

  • mybatis拦截器实现通用权限字段添加的方法

    实现效果 日常sql中直接使用权限字段实现权限内数据筛选,无需入参,直接使用,使用形式为: select * from crh_snp.channelinfo where short_code in (${commonEnBranchNo}) 注意事项说明 1.添加插件若使用xml形式mybatis可在配置文件中plugins标签中添加,本项目实际使用的为注解形式mybatis,需要通过SqlSessionFactoryBean代码方式添加或者SqlSessionFactoryBean的xml配

  • Angular中的请求拦截的方法

    目录 区分环境 添加拦截器 验证 在上一篇的文章Angular 中使用 Api 代理,我们处理了本地联调接口的问题,使用了代理. 我们的接口是单独编写的处理的,在实际的开发项目中,有众多的接口,有些需要登陆凭证,有些不需要.一个一个接口处理不妥,我们是否可以考虑对请求进行拦截封装呢? 本文章来实现下. 区分环境 我们需要对不同环境下的服务进行拦截.在使用 angular-cli 生成项目的时候,它已经自动做好了环境的区分,在 app/enviroments 目录下: environments ├

  • angular 用拦截器统一处理http请求和响应的方法

    想使用angularjs里的htpp向后台发送请求,现在有个用户唯一识别的token想要放到headers里面去,也就是{headres:{'token':1}} index.html里引入以下js: angular.module('app.factorys',[]) .factory('httpInterceptor',['$q','$injector','$localStorage',function ($q,$injector,$localStorage) { var httpInterc

  • Android拦截并获取WebView内部POST请求参数的实现方法

    起因: 有些时候自家APP中嵌入的H5页面并不是自家的.但是很多时候又想在H5不知情的情况下获取H5内部请求的参数,这应该怎么做到呢? 带着这个疑问,就有了这篇博客. 实现过程: 方案一: 最开始想到的方案是直接拦截H5中所有的请求: webView.setWebViewClient(new WebViewClient() { @Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResource

  • vue-resource请求实现http登录拦截或者路由拦截的方法

    本文介绍了vue-resource请求实现http登录拦截或者路由拦截的方法,分享给大家,具体如下: 项目需求 项目是前后台分离,前端负责数据对接,以及业务逻辑的处理,后台只需要给相应的接口即可 后台会控制接口的授权情况(即,未登录的接口,统一返回401未授权状态码,前端根据vue-resource的拦截器拦截符合条件的状态,并做出相应的处理.) 以下为两个系统的登录页面,作为请求超时或者登录拦截的跳转页面 以上是登录超时,登录弹窗框自动弹出来 代码片段 路由文件 export default

  • vue axios数据请求get、post方法及实例详解

    我们常用的有get方法以及post方法,下面简单的介绍一下这两种请求方法 vue中使用axios方法我们先安装axios这个方法 npm install --save axios 安装之后采用按需引入的方法,哪个页面需要请求数据就在哪个页面里引入一下. import axios from 'axios' 引入之后我们就可以进行数据请求了,在methods中创建一个方法 methods:{ getInfo(){ let url = "url" axios.get(url).then((r

  • 记一次线上SpringCloud Feign请求服务超时异常排查问题

    由于近期线上单量暴涨,第三方反馈部分工单业务存在查询处理失败现象,经排查是当前系统通过FeignClient调用下游系统出现部分超时失败(异常代码贴在下方). Caused by: feign.RetryableException: Read timed out executing POST http://xxxx        at feign.FeignException.errorExecuting(FeignException.java:84) ~[feign-core-10.1.0.j

  • springboot+mybatis拦截器方法实现水平分表操作

    目录 1.前言 2.MyBatis 允许使用插件来拦截的方法 3.Interceptor接口 4分表实现 4.1.大体思路 4.2.1 Mybatis如何找到我们新增的拦截服务 4.2.2 应该拦截什么样的对象 4.2.3 实现自定义拦截器 4.2.逐步实现 1.前言 业务飞速发展导致了数据规模的急速膨胀,单机数据库已经无法适应互联网业务的发展.由于MySQL采用 B+树索引,数据量超过阈值时,索引深度的增加也将使得磁盘访问的 IO 次数增加,进而导致查询性能的下降:高并发访问请求也使得集中式数

  • jquery easyui dataGrid动态改变排序字段名的方法

    jQuery easyui dataGrid 动态改变排序字段名,一般情况下,在使用的时候,我们会点击相应字段进行排序,这里以JAVA为例,后端的实体类字段有可能和数据库的字段不一致: 如:实体类中的属性为userName,前台filed="userName"而数据库的字段为user_name,这个时候如果把userName设置为排序列,然后去进行点击,就会抛出异常,因为dataGrid在排序的时候会以filed="userName"中的字段名为排序字段: 问题:

随机推荐