JFianl通过拦截器实现自动重试功能

由于某旧项目业务模块需要重构,模块逻辑不复杂,但可能由于之前项目赶工期就写的很复杂,并且掺杂了大量重试代码,于是……重构完模块后,就着手处理这些侵入重试代码。调研发现jfinal的拦截器目前不支持做末端免侵入重试,于是……

1. 修改JFinal的Invocation.class以支持末端(方法调用)重试功能(如果是处理拦截器调用链的异常重试,jfinal默认就支持)

public class Invocation {
    // ......

    
    public void invoke() {
       if (index < inters.length) {
          inters[index++].intercept(this);
       }
       // 修改的地方1。原为:else if (index++ == inters.length) {
       else if (index == inters.length) {
          try {
             // Invoke the action
             if (action != null) {
                returnValue = action.getMethod().invoke(target, args);
             }
             // Invoke the callback
             else {
                returnValue = callback.call(args);
             }
             // 修改的地方2:将index++移动到此处,便于支持异常重试
             index++;
          }
          catch (InvocationTargetException e) {
             Throwable t = e.getTargetException();
             if (t == null) {t = e;}
             throw t instanceof RuntimeException ? (RuntimeException)t : new RuntimeException(t);
          }
          catch (RuntimeException e) {
             throw e;
          }
          catch (Throwable t) {
             throw new RuntimeException(t);
          }
       }
    }

    // ......
}

2. 添加Retry注解,如果有需求可以加字段,以支持重试前后做特殊处理等。

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

    /**
     * 重试次数
     * @return 重试次数
     */
    int times() default 1;

    /**
     * 重试时间间隔,毫秒
     * @return 重试时间间隔
     */
    int timeInterval() default 0;

    /**
     * 需要处理的异常类型
     * @return 异常类型
     */
    Class<? extends Exception>[] exception();

}

3. 添加RetryInterceptor拦截器

public class RetryInterceptor implements Interceptor {
    private final Log logger = Log.getLog(RetryInterceptor.class);

    @Override
    public void intercept(Invocation inv) {
        final Retry retry = inv.getMethod().getAnnotation(Retry.class);

        // 不需要重试、重试次数小于1或未设置重试条件(需重试处理的异常)
        if (null == retry || retry.times() < 1 || 0 == retry.exception().length) {
            inv.invoke();
            return;
        }

        // 重试处理逻辑
        for (int i = 0; i <= retry.times(); i++) {
            try {
                inv.invoke();
            } catch (Exception e) {
                // 最后一次重试抛出异常,则不处理
                if (i == retry.times()) {
                    throw e;
                }

                // 不在要处理的异常之列,也不处理
                if (Arrays.stream(retry.exception())
                        .noneMatch(item -> item.isInstance(e) || Arrays.stream(e.getSuppressed()).anyMatch(item::isInstance))) {
                    throw e;
                }

                if (i > 0 && logger.isDebugEnabled()) {
                    logger.debug("[retry][%s.%s()] %d"
                            .formatted(inv.getTarget().getClass().getName(), inv.getMethodName(), i), e);
                }
            }
        }
    }

}

4. controller层测试及service层测试

@Path(value = "/test")
public class TestController extends Controller {
    private final AtomicInteger atomicInteger = new AtomicInteger(0);
    
    @Retry(times = 3, exception = RuntimeException.class)
    @Before(RetryInterceptor.class)
    public String retry() {
        final int i = atomicInteger.incrementAndGet();
        if (i % 4 != 0) {
            throw new RuntimeException("test: " + i);
        }
        return "Hello World! ";
    }
}

由于个人比较菜,代码多有不足,请大佬们多多指教哈!

评论区

JFinal

2023-06-18 11:35

这个实现思路不仅创新,而且优雅,以前没人这么想过

经过这个改造,去除老项目代码中的重试代码,简单、清爽

Invocation 中 index++ 的改动,证明对 jfinal 核心有深刻把控,这个地方确实是为了防止 action 被调用多次的,点赞 + 收藏 一波

北流家园网

2023-06-19 08:40

能不能提示到提示前台:5s后重试...

BTMTimor

2023-06-20 15:33

@北流家园网 可以实现,建议直接写个全局异常拦截器去做这事。

热门分享

扫码入社