这是我参与8月更文挑战的第14天,活动详情查看:8月更文挑战
因最近看接口幂等性解决方案,以及接口重复提交的问题,所以记录一下Spring的拦截器的使用.
1 拦截器的概述
1 拦截器说明及应用场景
拦截器: 本身会在请求进入Controller控制层前后做拦截处理.
常见使用场景:
-
接口重复提交校验 放置用户重复提交相同数据.
-
权限检查 当前用户是否登录,是否有权限访问数据.
-
日志记录 记录请求信息,输出成日志文件.
-
性能监控 慢日志.
2 Spring中的拦截器 HandlerInterceptor
public interface HandlerInterceptor {
/**
前处理回调方法,第三个参数为响应的处理器,自定义Controller
返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断,不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应;
*/
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
/**
后处理回调方法,实现处理器的后处理(在渲染视图之前),可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。
*/
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
/**
整个请求处理完毕回调方法,即在视图渲染完毕时回调,在性能监控中可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,但仅调用处理器执行链中.
*/
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
3 流程说明
1 拦截器按照执行顺序执行,而执行顺序是按照Spring配置文件中定义的顺序执行.在实现WebMvcConfigurer接口的配置类中,按照注册顺序执行,即先注册的拦截器先执行.
2 拦截器正常执行流程图
每个拦截器都返回True,则继续向下一个拦截器执行,最后进入控制层.
graph TD
A[HandlerInterceptor1.preHandle] -->B[HandlerInterceptor2.preHandle]
B[HandlerInterceptor2.preHandle] --> C[Controller控制层]
C[Controller控制层] --> D[HandlerInterceptor2.postHandle]
D[HandlerInterceptor2.postHandle] --> E[HandlerInterceptor1.postHandle]
E[HandlerInterceptor1.postHandle] --> F[View渲染]
F[View渲染] --> G[HandlerInterceptor2.afterCompletion]
G[HandlerInterceptor2.afterCompletion] --> H[HandlerInterceptor1.afterCompletion]
3 拦截器异常执行流程图
拦截器2返回false,即不满足拦截器设置的规则,直接返回.
graph TD
A[HandlerInterceptor1.preHandle] -->B[HandlerInterceptor2.preHandle]
B[HandlerInterceptor2.preHandle] --> C[Controller控制层]
C[Controller控制层] --> D[HandlerInterceptor2.postHandle]
D[HandlerInterceptor2.postHandle] --> E[HandlerInterceptor1.postHandle]
E[HandlerInterceptor1.postHandle] --> F[View渲染]
F[View渲染] --> G[HandlerInterceptor2.afterCompletion]
G[HandlerInterceptor2.afterCompletion] --> H[HandlerInterceptor1.afterCompletion]
B[HandlerInterceptor2.preHandle] --> Y[返回false,回到拦截器1]
Y[返回false,回到拦截器1] --> H[HandlerInterceptor1.afterCompletion]
2 简单使用案例
1 准备环境
搭建一个可以运行的SpringBoot环境.
1 准备文件
1 application.yml
server:
port: 8081
spring:
datasource:
driverClassName: com.mysql.jdbc.Driver
username: root
password: root
url: jdbc:mysql://localhost:3306/test
2 实体类
@Data
public class User {
private String id;
}
3 Controller控制器
@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerController {
@RequestMapping("/query")
public String queryById() {
User user = new User();
log.info("请求参数=={}", user.toString());
log.info("响应参数=={}", user.toString());
return "<h1>" + user.toString() + "<h1>";
}
}
4 拦截器1
@Component
@Slf4j
public class MyInterceptor implements HandlerInterceptor {
/**
* 请求处理前调用
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("========================进入拦截器1=================================");
log.info("请求request = {}",request.toString());
log.info("请求response = {}",response.toString());
log.info("请求handler = {}",handler.toString());
return true;
}
/**
* 请求处理后,渲染ModelAndView前调用
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("=============进入拦截器1,请求处理后,渲染ModelAndView前调用。=================================");
}
/**
* 渲染ModelAndView后调用
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("=============进入拦截器1,渲染ModelAndView后调用。=================================");
}
5 拦截器2
@Component
@Slf4j
public class MyInterceptor2 implements HandlerInterceptor {
/**
* 请求处理前调用
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("========================进入拦截器2=================================");
log.info("请求request = {}",request.toString());
log.info("请求response = {}",response.toString());
log.info("请求handler = {}",handler.toString());
return true;
}
/**
* 请求处理后,渲染ModelAndView前调用
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("=============进入拦截器2,请求处理后,渲染ModelAndView前调用。=================================");
}
/**
* 渲染ModelAndView后调用
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("=============进入拦截器2,渲染ModelAndView后调用。=================================");
}
}
6 配置类
@Configuration
public class MyConfigurer implements WebMvcConfigurer {
@Autowired
private MyInterceptor myInterceptor;
@Autowired
private MyInterceptor2 myInterceptor2;
/**
* 用来注册拦截器,我们自己写好的拦截器需要通过这里添加注册才能生效
* 注册的先后顺序就是拦截器的执行顺序,先注册,先执行
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 列如
// addPathPatterns("/**") 表示拦截所有的请求
// excludePathPatterns("/login", "/register") 表示除了登陆与注册之外
registry.addInterceptor(myInterceptor).addPathPatterns("/**").excludePathPatterns("/login", "/register");
registry.addInterceptor(myInterceptor2).addPathPatterns("/**").excludePathPatterns("/login", "/register");
}
/**
* 用来配置静态资源的,比如html,js,css,等等,列如在使用swagger文档时
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
}
}
3 效果验证
1 浏览器地址栏发送请求
http://localhost:8081/consumer/query
2 页面结果
3 日志结果
3 使用场景及解决方案
关于防重复提交或接口恶意多次请求
方法1 前台处理
前端: 按钮置灰不可选
方法2 通过一个唯一字符串
前端进入提交页面时,会调用后台,获取一个唯一的字符串返回,字符串在后台可保存有效时间,有效时间外,提交无效.待表单提交后,删除该唯一字符串.
方法3 后台通过注解+拦截器
1 准备环境,和上述一样
2 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
/**
* 保存时间,默认8秒
*
* @return
*/
int seconds() default 8;
/**
* 最大方式次数,默认1次
*
* @return
*/
int maxCount() default 1;
}
3 定义拦截器
@Component
@Slf4j
public class MyInterceptor implements HandlerInterceptor {
// 缓存方案
// 方案1 使用redis保存该ip在一定时间内调用接口的次数 (适用集群)
// @Autowired
// private RedisTemplate redisTemplate;
public static final int IntervalTime = 8;
// 方案2 谷歌的guava , 使用本地缓存,设置有效期8秒 (适用单体)
private final Cache<String, Integer> cache = CacheBuilder.newBuilder()
.expireAfterAccess(IntervalTime, TimeUnit.SECONDS).build();
/**
* 请求处理前调用
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("========================进入拦截器1=================================");
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 查看该方法上是否有需要拦截的注释
MyAnnotation myAnnotation = handlerMethod.getMethodAnnotation(MyAnnotation.class);
// 注解存在
if (myAnnotation != null) {
int maxCount = myAnnotation.maxCount();
int seconds = myAnnotation.seconds();
// 1 获取ip ip可能代理
// 代理服务器在请求转发时添加上去的
String ip = request.getHeader("x-forwarded-for");
log.info("x-forwarded-for = {} ", ip);
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
log.info("Proxy-Client-IP = {} ", ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
log.info("WL-Proxy-Client-IP = {} ", ip);
}
// remote_addr http协议传输的时候自动添加,不受请求头header的控制
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
log.info("remote_addr = {} ", ip);
}
// 缓存方案1 redis
// if (redisCache(response, maxCount, seconds, ip)) {return false;}
// 缓存方案2 Google的guava
if (googleCache(response, maxCount, seconds, ip)) {return false;}
}
// 不拦截,进入下一个拦截器
return true;
}
/**
* 使用Google的guava
* @param response
* @param maxCount
* @param seconds
* @param ip
* @return
* @throws IOException
*/
private boolean googleCache(HttpServletResponse response, int maxCount, int seconds, String ip) throws IOException, ExecutionException {
// 2 从缓存中获取该ip的访问次数
Integer count = cache.getIfPresent(ip);
// 3 判断是否满足注解设置要求
if (count == null) {
cache.put(ip,1);
} else if (count < maxCount) {
// 3.2 在有效期内,访问次数满足要求
cache.put(ip,++count);
} else {
// 3.3 超过最大访问次数,拒绝该请求
log.info("访问次数超过要求,请稍后访问系统 = {} ", ip);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
ResultResponse resultResponse = new ResultResponse();
resultResponse.setResult("操作太快,请稍后访问系统");
Object obj = JSONObject.toJSON(resultResponse);
response.getWriter().write(JSONObject.toJSONString(obj));
return true;
}
return false;
}
/**
* 使用redis作为缓存
* @param response
* @param maxCount
* @param seconds
* @param ip
* @return
* @throws IOException
*/
/* private boolean redisCache(HttpServletResponse response, int maxCount, int seconds, String ip) throws IOException {
// 2 从redis中获取该ip的访问次数
Integer count = (Integer) redisTemplate.opsForValue().get(ip);
// 3 判断是否满足注解设置要求
if (count == null) {
// 3.1 第一次访问,设置次数,有效期
redisTemplate.opsForValue().set(ip, 1);
redisTemplate.expire(ip, seconds, TimeUnit.SECONDS);
} else if (count < maxCount) {
// 3.2 在有效期内,访问次数满足要求
redisTemplate.opsForValue().set(ip, ++count);
} else {
// 3.3 超过最大访问次数,拒绝该请求
log.info("访问次数超过要求,请稍后访问系统 = {} ", ip);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
ResultResponse resultResponse = new ResultResponse();
resultResponse.setResult("操作太快,请稍后访问系统");
Object obj = JSONObject.toJSON(resultResponse);
response.getWriter().write(JSONObject.toJSONString(obj));
return true;
}
return false;
}*/
/**
* 请求处理后,渲染ModelAndView前调用
*
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("=============进入拦截器1,请求处理后,渲染ModelAndView前调用。=================================");
}
/**
* 渲染ModelAndView后调用
*
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("=============进入拦截器1,渲染ModelAndView后调用。=================================");
}
}
4 控制层添加自定义注解
@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerController {
@Autowired
private ConsumerService consumerService;
@MyAnnotation()
@RequestMapping("/query")
public String queryById() {
User user = new User();
log.info("请求参数=={}", user.toString());
log.info("响应参数=={}", user.toString());
return "<h1>" + user.toString() + "<h1>";
}
}
5 效果验证
1 在浏览器地址栏访问
http://localhost:8081/consumer/query
2 初次访问
3 在规定时间内多次访问
4 控制台日志信息
4 总结
关于Spring的拦截器,在平时的项目中,使用较多,使用的场景也很丰富,配合自定义注解,可以很好的实现一个开关效果.后续有其他场景,一一记录.