防重插件封面.jpg

前言

針對於我們現在常用的RESTful API通常我們需要對請求進行唯一標識,也就是每次都要帶上一個請求號,如reqNO

對於入庫這種操作數據庫的請求我們一般要保證他的唯一性,一個請求號通常只能用一次,所以需要我們對這種請求加上校驗機制。

該需求的實現思路是通過自定義annotation,只給需要進行校驗的接口加上註解。然後通過切面使用了註解的接口將每次請求號存進Redis,每次都進行判斷是否存在這個請求號即可。

來看下加上本次插件的實際效果:


重複請求號01.jpg

重複請求號02.jpg

重複請求號03.jpg

自定義註解

首先我們要自定義一個註解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckReqNo {

    String desc() default "";
}

(ps:這裏並不過多的講解註解相關的知識)。

首先使用@interface來聲明一個註解。接着利用Java為我們提供的三個元註解來定義CheckReqNo註解。

其中@Target表明這個註解被用於什麼地方,使用ElementType.METHOD表明被應用到方法上,還有一些其他值可以查看java.lang.annotation.ElementType這個枚舉類型。

@Retention註解表明我們的註解在什麼範圍內有效,這裏配置的RetentionPolicy.RUNTIME表明在運行時可以通過反射來獲取。

@Documented看字面意思應該也能猜到是用於生成JavaDoc文檔的。

其中定義了一個desc()的方法其實並沒有用到,但如果需要在使用註解的時候需要自定義一些filed(域)的需求可以按照這樣的方式寫到這裏,通過反射都可以獲取到具體的值。
如:@CheckReqNo(desc = "abc")就可以獲取到"abc"的值。

切面註解

按照之前的想法是在對所有使用了該註解的方法進行切面:

@Aspect
@Component
public class ReqNoDrcAspect {

    private static Logger logger = LoggerFactory.getLogger(ReqNoDrcAspect.class);

    @Value("${redis.prefixReq:reqNo}")
    private String prefixReq ;

    @Value("${redis.day:1}")
    private long day ;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @PostConstruct
    public void init() throws Exception {
        logger.info("SSM-REQUEST-CHECK init......");
    }

    @Pointcut("@annotation(com.crossoverJie.request.anotation.CheckReqNo)")
    public void checkRepeat(){

    }

    @Before("checkRepeat()")
    public void before(JoinPoint joinPoint) throws Exception {
        BaseRequest request;
        request = getBaseRequest(joinPoint);
        if(request != null){
            final String reqNo = request.getReqNo();
            if(StringUtil.isEmpty(reqNo)){
                throw new RuntimeException("reqNo不能為空");
            }else{
                try {
                    String tempReqNo = redisTemplate.opsForValue().get(prefixReq +reqNo);
                    logger.debug("tempReqNo="+tempReqNo);

                    if((StringUtil.isEmpty(tempReqNo))){
                        redisTemplate.opsForValue().set(prefixReq + reqNo, reqNo, day, TimeUnit.DAYS);
                    }else{
                        throw new RuntimeException("請求號重複,reqNo="+reqNo);
                    }

                } catch (RedisConnectionFailureException e){
                    logger.error("redis操作異常",e);
                    throw new RuntimeException("need redisService") ;
                }
            }
        }

    }



     public static BaseRequest getBaseRequest(JoinPoint joinPoint) throws Exception {
         BaseRequest returnRequest = null;
         Object[] arguments = joinPoint.getArgs();
         if(arguments != null && arguments.length > 0){
             returnRequest = (BaseRequest) arguments[0];
         }
         return returnRequest;
     }
}

使用@Aspect來定義了一個切面。
其中prefixReq,day域可以自定義緩存請求號時的key前綴以及緩存的時間。

最關鍵的一點是用
@Pointcut("@annotation(com.crossoverJie.request.anotation.CheckReqNo)")
定義了一個切入點,這樣所有使用@CheckReqNo的註解都會被攔截。

接下來的邏輯就比較簡單了,在每次請求之前進行攔截。

先去Redis中查看這個請求號(ps:反射獲取)是否存在,如果不存在則通過並將本次的請求號緩存起來。如果存在則拋出異常。

使用註解

可以在jdbc.properties配置文件中自定義前綴和緩存時間

#redis前綴
redis.prefixReq=reqNo
#redis緩存時間 默認單位為天
redis.day=1

不定義也可以,會使用默認值。

由於該註解是需要加到controller層,因此我們得使用CGLIB代理。
這裡有一個坑,需要將開啟CGLIB的配置配置到我們web.xml中的

<!-- Spring MVC servlet -->
    <servlet>
        <servlet-name>SpringMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
    </servlet>

這裏所定義的spring-mvc.xml文件中,不然springMVC所在的子容器是無法被父容器所加載的。

使用實例:

    @CheckReqNo
    @RequestMapping(value = "/createRedisContent",method = RequestMethod.POST)
    @ResponseBody
    public BaseResponse<NULLBody> createRedisContent(@RequestBody RedisContentReq redisContentReq){
        BaseResponse<NULLBody> response = new BaseResponse<NULLBody>() ;

        Rediscontent rediscontent = new Rediscontent() ;
        try {
            CommonUtil.setLogValueModelToModel(redisContentReq,rediscontent);
            rediscontentMapper.insertSelective(rediscontent) ;
            response.setReqNo(redisContentReq.getReqNo());
            response.setCode(StatusEnum.SUCCESS.getCode());
            response.setMessage(StatusEnum.SUCCESS.getMessage());
        }catch (Exception e){
            logger.error("system error",e);
            response.setReqNo(response.getReqNo());
            response.setCode(StatusEnum.FAIL.getCode());
            response.setMessage(StatusEnum.FAIL.getMessage());
        }

        return response ;

    }

統一異常controller

/**
 *
 * ClassName: ErrorController <br/>
 * Function: 錯誤異常統一處理. <br/>
 * @author crossoverJie
 * @version
 * @since JDK 1.7
 */
@ControllerAdvice
public class ErrorController {
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Object processUnauthenticatedException(NativeWebRequest request, Exception e) {
        logger.error("請求出現異常:", e);

        BaseResponse<NULLBody> response = new BaseResponse<NULLBody>();
        response.setCode(StatusEnum.FAIL.getCode());
        if (e instanceof RuntimeException){
            response.setMessage(e.getMessage());

        } else {
            response.setMessage(StatusEnum.FAIL.getMessage());
        }
        return response ;
    }
}

這樣當controller層出現異常之後都會進入這裏進行統一的返回。

總結

至此整個插件的流程已經全部OK,從中可以看出Spring AOP在實際開發中的各種好處。
之前的幾篇文章也有應用到:

  • 在JavaWeb應用中使用Redis
  • 動態切換數據源

不知不覺這個小白入門的SSM系列已經更新了14篇了,在GitHub也有了500多顆星了,期間也和不少朋友有過交流、探討,感謝大家的支持。

接下來可能不太會更新這個系列了,由於博主現在所在的項目組採用的是目前比較流行的SpringBoot+SpringCloudDocker的方式來進行架構的,所以之後的重心肯定會移到這方面,用過SpringBoot之後相信大家肯定也回不去了。

所以之後我會繼續更新SpringBoot+SpringCloud相關的文章,歡迎持續關注,持續拍磚(ps:這個插件也會用springBoot重寫一遍)

插件地址:https://github.com/crossoverJie/SSM-REQUEST-CHECK.git

項目地址:https://github.com/crossoverJie/SSM.git

個人博客地址:http://crossoverjie.top


weixinchat.jpg