接口幂等性
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>
在这个项目中,最重要的就是配置拦截器这段的代码,它是接口幂等性功能的核心。
