接口幂等性
1、对接口幂等性的了解
所谓的接口幂等性就是执行多次跟执行一次是一样的结果。
那为什么需要这样的特点呢?因为时常在网站中会出现由于网络的问题,导致客户提交一个表单不能及时的响应,这时客户就很有可能重复的点击按钮提交表单,作为开发人员自然知道,不能重复提交表单,但是客户不知道,最后导致的结果就是同样的结果在数据库中出现了很多次,导致数据冗余严重。
但是若是使用接口幂等性解决,则不管用户点击多少次都只会将第一次点击作为有效点击,其余的均是无效点击。
在项目中,接口幂等性也不是对于每一个方法的,看自己的业务功能的需要,就比如以下:
public interface UserService { // 查看所有的用户 (不需要接口幂等性) List<User> findAllUser(); // 根据用户ID查询用户(不需要接口幂等性) User findUserByID(Integer userId); // 创建用户(需要接口幂等性) Integer addUser(User user); // 删除用户(不需要接口幂等性) Integer deleteUser(Integer userId); // 更新用户姓名(不需要接口幂等性) Integer updateUserName(User user); // 更新用户年龄(需要接口幂等性) Integer updateUserAge(User user); }
2、实现接口幂等性的原理
想要实现接口幂等性,最主要的就是需要一个token,这个token就是提交修改标识,服务器会根据这个标识判断是否会对这个请求进行多次的处理,例如如下图:
会发现,在真正跳转到要提交的这个页面之前,是需要与服务器进行交互,然后让服务器产生一个token再将这个token传递给即将跳转的将提交的页面,该页面提交之后,服务器会拦截,判断是否存在redis,如果存在的话,就说明第一次请求,则继续执行以下逻辑,如果token不存在,就说明这个不是第一次操作,因为第一次操作完了之后,就会把token删除掉,如果token为空,则拦截不做后续处理。
主要实现原理:Token、Redis、拦截器
3、实现接口幂等性的代码
第一、项目依赖引入<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.3</version> </parent> <dependencies> <!-- mysql driver --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- mybatis-plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.3.0</version> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> </dependency> <!-- web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- thymeleaf --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- hutool --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.13</version> </dependency> </dependencies>
//===================controller层 /** * @Description: 测试接口幂等性的控制层 * @Author: huidou 惠豆 * @CreateTime: 2022/6/11 16:13 */ @Controller @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @Autowired private StringRedisTemplate stringRedisTemplate; /** * 查看所有的用户 (不需要接口幂等性) * @return */ @RequestMapping("/findAllUser") public ModelAndView findAllUser(HttpServletRequest request) { List<User> userList = userService.findAllUser(); ModelAndView modelAndView = new ModelAndView(); modelAndView.setViewName("index"); modelAndView.addObject("userList",userList); return modelAndView; } /** * 根据用户ID查询用户(不需要接口幂等性) * @param userId * @return */ @RequestMapping("/findUserByID") public User findUserByID(Integer userId) { User user = userService.findUserByID(userId); return user; } /** * 跳转到添加用户界面 * 中间这个跳转是很重要的。因为在没有跳转到添加用户页面之前,先让服务器产生token,保存在redis中,在传递给我将提交的表单 * 作为一个隐藏域保存起来。当提交的时候,作为第一次,那就能获得token,所以能继续执行,完成操作之后,就会把token删除调 * 之后从第二次开始反复调用,就会判断redis中是否还存在token,显然没存在则,拦截器对其做出拦截,不继续执行下一步,直接 * 中断,返回提示信息。 */ @RequestMapping("/toadduser") public ModelAndView toadduser() { System.out.println("0ckjscjscdsv"); ModelAndView modelAndView = new ModelAndView(); // 获取token String token = UUID.randomUUID().toString(); // 保存token到redis中,以便第二次请求的时候,辨别token是否存在。 stringRedisTemplate.opsForValue().set(token,Thread.currentThread().getId() + ""); modelAndView.setViewName("add"); modelAndView.addObject("token",token); return modelAndView; } /** * 创建用户(需要接口幂等性) * ApiIdempotentAnn :这个注解表示我将使用这个接口幂等性实现 * 第一次,我添加了注解,但是发现没有生效,为啥? * 其实就是因为我没有编写跳转控制器逻辑,直接跳转到添加用户的控制器,但是在用户控制器里面又是产生token以及传递到 * add页面,然后,反复循环,所以才会造成幂等性注解失效。 * @param user * @return */ @ApiIdempotentAnn @RequestMapping("/addUser") public String addUser(User user) throws InterruptedException { // 让当前线程暂时睡眠一秒,以便产生网络抖动,重复调用。 Thread.sleep(1000); Integer result = userService.addUser(user); if (result >= 1) { return "redirect:/user/index"; } return "add"; } /** * 删除用户(不需要接口幂等性) * @param userId * @return */ @RequestMapping("/deleteUser") public Integer deleteUser(Integer userId) { Integer deleteUser = userService.deleteUser(userId); return deleteUser; } /** * 更新用户姓名(不需要接口幂等性) * @param user * @return */ @RequestMapping("/updateUserName") public Integer updateUserName(User user) { Integer integer = userService.updateUserName(user); return integer; } /** * 更新用户年龄(需要接口幂等性) * @param user * @return */ @RequestMapping("/updateUserAge") public Integer updateUserAge(User user) { Integer integer = userService.updateUserAge(user); return integer; } } //===================service层 /** * @Description: 测试接口幂等性的服务层实现类 * @Author: huidou 惠豆 * @CreateTime: 2022/6/11 16:14 */ @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; /** * 窜寻所有的用户(不需要接口幂等性) * @return */ @Override public List<User> findAllUser() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); List<User> userList = userMapper.selectList(queryWrapper); return userList; } /** * 根据用户ID查询用户(不需要接口幂等性) * @param userId * @return */ @Override public User findUserByID(Integer userId) { User user = userMapper.selectById(userId); return user; } /** * 创建用户(需要接口幂等性) * @param user * @return */ @Override public Integer addUser(User user) { int result = userMapper.insert(user); return result; } /** * 删除用户(不需要接口幂等性) * @param userId * @return */ @Override public Integer deleteUser(Integer userId) { int result = userMapper.deleteById(userId); return result; } /** * 更新用户姓名(不需要接口幂等性) * @param user * @return */ @Override public Integer updateUserName(User user) { int update = userMapper.updateById(user); return update; } /** * 更新用户年龄(需要接口幂等性) * @param user * @return */ @Override public Integer updateUserAge(User user) { int update = userMapper.updateById(user); return update; } } //===================mapper层 @Repository public interface UserMapper extends BaseMapper<User> { } //===================pojo @Data public class User { @TableId(type = IdType.ASSIGN_ID) // 标记为雪花算法 private Long id; private String name; private Integer age; }
CREATE TABLE `user` ( `id` bigint(20) NOT NULL COMMENT '主键ID', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `age` int(11) DEFAULT NULL COMMENT '年龄', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
第四、配置文件 application.yml
spring: application: name: Interface-Idempotency redis: host: 192.168.205.149 port: 6379 connect-timeout: 5000 datasource: password: root username: root url: jdbc:mysql://localhost:3306/school thymeleaf: cache: false
// 1、首先,需要自定义一个注解,该注解可以标识一个方法,表示该方法使用接口幂等性特点 /** * @Description: 接口幂等性注解(表示被该注解标注的方法,就会进行接口幂等性,实现其灵活性) * @Author: huidou 惠豆 * @CreateTime: 2022/6/11 16:13 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ApiIdempotentAnn { boolean value() default true; } // 2、其次,自定义一个拦截器,实现对带有自定义注解的方法实现逻辑判断 /** * @Description: 自定义一个拦截器,该拦截器的目的就是对请求进行拦截,判断其是否存在token,根据token的存在与否判断时候进行下一步操作 * 这个时候有人就会提出一个问题:既然是拦截器要拦截请求,那要拦截那些请求,是所有的请求吗?如果是所有的请求,那你这个拦截器就不合理 * 答案:其实是这针对所有的请求,但是会根据其方法上是否带有幂等性注解来判断是否要拦截,也就是说,所有的请求都会拦截,只不过带有幂等 * 性注解的方***被进一步判断处理,不带幂等性注解的方***进行直接跳过。 * @Author: huidou 惠豆 * @CreateTime: 2022/6/11 16:13 */ @Component public class MyInceptor implements HandlerInterceptor { @Autowired private StringRedisTemplate redisTemplate; /** * prehandler表示是在控制器执行方法之前执行 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // HandlerMethod 封装很多属性, 在访问请求方法的时候可以方便的访问到访问的参数 方法上的注解 HandlerMethod handlerMethod = (HandlerMethod) handler; // 获得方法 Method method = handlerMethod.getMethod(); // 判断这个方法有没有添加幂等的注解 boolean methodAnnotationPresent = method.isAnnotationPresent(ApiIdempotentAnn.class); // 判断是否开启幂等性处理。 if(methodAnnotationPresent && method.getAnnotation(ApiIdempotentAnn.class).value()){ // 验证接口幂等性 boolean result = this.checkToken(request); if (result){ // 放行 return true; }else { response.setContentType("application/json;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.print("重复调用"); writer.close(); response.flushBuffer(); return false; } } return false; } /** * 验证token有效性 * @param request * @return */ public boolean checkToken(HttpServletRequest request){ // 获取token String token = request.getParameter("token"); // 判断 if (null == token || "".equals(token)){ return false; } return redisTemplate.delete(token); } } // 3、再次,将拦截器注册到webmvc中 /** * @Description: 实现将自定义拦截器配置到MVC配置中。这个时候有人就想发问:我之前在javaweb和ssm框架中也使用过 * 拦截器和过滤器,也相继实现过HandlerInterceptor接口和Filter接口,但是,我并没有将其注册到 * mvcconfigurer中,也可以使用啊,对的,是可以但是你却在springmvc.xml配置文件中使用<mvc:interceptor> * 标签注册了一个bean这个bean对应的class就是拦截器。 * @Author: huidou 惠豆 */ @Configuration public class MyWebMVC implements WebMvcConfigurer { @Autowired private MyInceptor myInceptor; @Override public void addInterceptors(InterceptorRegistry registry) { // 白名单 忽略拦截 ArrayList<String> strings = new ArrayList<>(); strings.add("/user/findAllUser"); strings.add("/user/toAddUser"); registry.addInterceptor(myInceptor).excludePathPatterns(strings); } }
add页面 <!DOCTYPE html> <html lang="en"xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>注册用户</title> </head> <body style="text-align: center"> <div> <form method="post" action="/user/addUser"> <input hidden type="text" th:value="${token}" name="token" > <label>名字:</label><input name="name" type="text" placeholder="请输入名字"> <label>年纪:</label><input name="age" type="number" placeholder="请输入年纪"> <input type="submit" value="注册"> </form> </div> </body> </html> index页面 <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body style="text-align: center"> <div> <table border="1" align="center"> <tr> <th>id</th> <th>名字</th> <th>年纪</th> <th>操作</th> </tr> <tr th:each="u : ${userList}"> <td th:text="${u.id}"></td> <td th:text="${u.name}"></td> <td th:text="${u.age}"></td> <td> <a th:href="@{/user/updateUserName}">更新</a> </td> </tr> </table> <a th:href="@{/user/toadduser}">添加用户</a> </div> </body> </html>
在这个项目中,最重要的就是配置拦截器这段的代码,它是接口幂等性功能的核心。