接口幂等性

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为空,则拦截不做后续处理。

主要实现原理:TokenRedis拦截器

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>

第二、MVC三层架构
//===================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;

}

第三、Sql语句建表
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>

在这个项目中,最重要的就是配置拦截器这段的代码,它是接口幂等性功能的核心。
全部评论

相关推荐

11-04 14:10
东南大学 Java
_可乐多加冰_:去市公司包卖卡的
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务