1 整合springboot和redis环境的集成
2 配置请求的方法体和枚举类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Response {
private int status;
private String msg;
private Object data;
// 省略get、set、toString、无参有参构造方法
}
///
import lombok.Getter;
@Getter
public enum ResponseCode {
// 通用模块 1xxxx
ILLEGAL_ARGUMENT(10000, "参数不合法"),
REPETITIVE_OPERATION(10001, "请勿重复操作"),
;
ResponseCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
private Integer code;
private String msg;
}
3 自定义异常以及配置全局异常类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ServiceException extends RuntimeException {
private String code;
private String msg;
// 省略get、set、toString以及构造方法
}
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
public class MyControllerAdvice {
@ResponseBody
@ExceptionHandler(ServiceException.class)
public Response serviceExceptionHandler(ServiceException exception) {
Response response = new Response(Integer.valueOf(exception.getCode()), exception.getMsg(), null);
return response;
}
}
4 编写创建Token和验证Token的接口以及实现类【核心类】
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
@Service
public interface TokenService {
public Response createToken();
public Response checkToken(HttpServletRequest request);
}
//
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
@Service
public class TokenServiceImpl implements TokenService {
@Autowired
private RedisTemplate redisTemplate;
@Override
public Response createToken() {
// 生成uuid当作token
String token = UUID.randomUUID().toString().replaceAll("-", "");
// 将生成的token存入redis中
redisTemplate.opsForValue().set(token, token);
// 返回正确的结果信息
Response response = new Response(0, token.toString(), null);
return response;
}
@Override
public Response checkToken(HttpServletRequest request) {
// 从请求头中获取token
String token = request.getHeader("token");
if (StringUtils.isBlank(token)) {
// 如果请求头token为空就从参数中获取
token = request.getParameter("token");
// 如果都为空抛出参数异常的错误
if (StringUtils.isBlank(token)) {
throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getCode().toString(), ResponseCode.ILLEGAL_ARGUMENT.getMsg());
}
}
// 如果redis中不包含该token,说明token已经被删除了,抛出请求重复异常
if (!redisTemplate.hasKey(token)) {
throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getCode().toString(), ResponseCode.REPETITIVE_OPERATION.getMsg());
}
// 删除token
Boolean del = redisTemplate.delete(token);
// 如果删除不成功(已经被其他请求删除),抛出请求重复异常
if (!del) {
throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getCode().toString(), ResponseCode.REPETITIVE_OPERATION.getMsg());
}
return new Response(0, "校验成功", null);
}
}
5 配置需要幂等的自定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}
6 接口拦截器
public class ApiIdempotentInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class);
if (methodAnnotation != null) {
// 校验通过放行,校验不通过全局异常捕获后输出返回结果
tokenService.checkToken(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 {
}
}
7 配置拦截器
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiIdempotentInterceptor());
}
@Bean
public ApiIdempotentInterceptor apiIdempotentInterceptor() {
return new ApiIdempotentInterceptor();
}
}
8 写个controller测试
import com.example.demo_26.mindeng_3.domain.Food;
import com.example.demo_26.mindeng_3.mapper.FoodMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/token")
public class TokenController {
@Autowired
private TokenService tokenService;
@Autowired
private FoodMapper tBookOrderMapper;
@GetMapping
public Response token() {
return tokenService.createToken();
}
@GetMapping("/checktoken")
@ApiIdempotent()
public String demo(@RequestBody Food tBookOrder) {
// 处理请求
int insert = tBookOrderMapper.insert(tBookOrder);
return insert==1?"ok":"wrong";
}
}
9 测试
首先,通过调用/token接口生成一个token。
curl -X GET http://localhost:8080/token
然后,使用生成的token调用/token/checktoken接口进行验证。
curl -X POST http://localhost:8080/token/checktoken -H "token: generated-token"
如果再次使用相同的token调用,将会得到重复操作的错误:
这样,我们就通过token机制实现了接口的幂等性