springboot下用aop实现异步调用和重试功能

我们在开发中,调用第三方接口时,往往是提交数据,要异步去获取数据;今天我们用一个利用spring的安排来实现一下异步调用和异步重试的功能;

一、方式1

这个功能的结构图

springboot下用aop实现异步调用和重试功能_第1张图片

第一步:创建一个注解Retry

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
	
	 /**
	  * 延时执行时间  (单位:秒)
	  * @return
	  */
	 long delayed() default 0;
	 
	 /**
	  * 重复执行间隔时间  (单位:秒)
	  * @return
	  */
	 long interval() default 0;
	 
	 /**
	  * 重复的次数  最大10次
	  * @return
	  */
	 int retryTimes() default 1;
	 
}

创建类:

RetryInvorkParam 类 ;用于记录重复执行的一些参数
/**
 * 重试执行参数
 * @author chenhuaping
 * @date  2018年4月20日 下午2:54:39
 */
@Setter
@Getter
@ToString
public class RetryInvorkParam implements Serializable {

	private static final long serialVersionUID = 1L;
	
	/**
	 * 延迟时间
	 */
	private long delayed;
	
	/**
	 * 间隔时间
	 */
    private long interval;
    
    /**
     * 重复次数
     */
    private int retryTimes;
    
    /**
     * 执行的方法的对象
     */
    private Object target;
    
    /**
     * 执行的参数 
     */
    private Object[] args;
    
    /**
     * 执行的方法
     */
    private Method invorkMethod;
    
    /**
     * 当前执行的次数
     */
    private volatile long currentTimes;
    
    /**
     * 下一次执行的时间
     */
    private volatile Date nextInvorkTime;
    
    /**
     * 是否结束这重试
     */
    private volatile boolean isEnd;
	
}

创建异步执行的时候的任务:

/**
 * 方法执行任务
 * 
 * @author chenhuaping
 * @date 2018年4月20日 下午3:54:13
 */
public class RetryRunning implements Runnable {

	private final static Logger logger = LoggerFactory.getLogger(RetryRunning.class);
	/**
	 * 执行的参数
	 */
	private RetryInvorkParam invorkParam;

	public RetryRunning(RetryInvorkParam invorkParam) {
		super();
		this.invorkParam = invorkParam;
	}

	@Override
	public void run() {

		Method invorkMethod = invorkParam.getInvorkMethod();
		Object[] args = invorkParam.getArgs();
		Object target = invorkParam.getTarget();

		Object result = null;

		try {
			result = invorkMethod.invoke(target, args);
		} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
			logger.error("@Retry 执行方法错误, {}", invorkParam, e);
			invorkParam.setEnd(true);
		}

		if (result != null && result instanceof Boolean && (boolean) result == true) {
			invorkParam.setEnd(true);
		}

		if (invorkParam.isEnd()) {
			logger.info("@Retry method = {}, 重复执行了{}次,已经执行结束,param = {}",
					target.getClass() + "." + invorkMethod.getName(), invorkParam.getCurrentTimes(), invorkParam);
			return;
		}

		logger.info("@Retry 已经完成了{}方法,第{}次执行,总共需要执行{}次", target.getClass() + "." + invorkMethod.getName(),
				invorkParam.getCurrentTimes(), invorkParam.getRetryTimes());

	}

}

创建一个定时任务;每一秒去轮询一次重试任务列表:并把已经到时间的任务提交到线程池中:

/**
 * 线程提交执行引擎
 * 
 * @author chenhuaping
 * @date 2018年4月20日 下午4:22:00
 */
@Component
public class RetryScheduledEngine extends TimerTask implements InitializingBean {

	private final static List invorkParamList = new ArrayList<>();

	@Autowired
	private org.springframework.core.task.AsyncTaskExecutor asyncTaskExecutor;

	/**
	 * 开始任务的
	 */
	private synchronized void startEngine() {
		if (CollectionUtil.isNotEmpty(invorkParamList)) {
			Iterator iterator = invorkParamList.iterator();
			while (iterator.hasNext()) {
				RetryInvorkParam next = iterator.next();
				if (next.isEnd()) {
					iterator.remove();
					break;
				}
				// 执行时间
				Date now = new Date();
				Date nextInvorkTime = next.getNextInvorkTime();

				if (nextInvorkTime != null && nextInvorkTime.before(now)) {
					// 增加执行次数
					long currentTimes = next.getCurrentTimes();
					currentTimes++;
					next.setCurrentTimes(currentTimes);
					// 判断是否为最后一次
					if (currentTimes == next.getRetryTimes()) {
						next.setEnd(true);
					}

					// 计算下一次执行时间;
					nextInvorkTime = DateUtils.addSeconds(nextInvorkTime, Long.valueOf(next.getInterval()).intValue());
					next.setNextInvorkTime(nextInvorkTime);

					// 添加任务
					asyncTaskExecutor.submit(new RetryRunning(next));
				}
			}
		}
	}

	/**
	 * 提交任务
	 * 
	 * @param param
	 */
	public void submitTask(RetryInvorkParam param) {
		// 计算执行时间
		Date now = new Date();
		if (param.getDelayed() > 0) {
			param.setNextInvorkTime(DateUtils.addSeconds(now, Long.valueOf(param.getDelayed()).intValue()));
		} else {
			param.setNextInvorkTime(now);
		}
		invorkParamList.add(param);
	}

	@Override
	public void run() {
		this.startEngine();
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		Timer timer = new Timer();
		timer.schedule(this, 5000, 1000);
	}

//	@Retry(delayed=5,interval=10,retryTimes=10)
	public boolean  testRetryInvoke() {
		System.out.println("______________testRetryInvoke被执行_________________");
		return false;
	}
}

最后一步是创建切面配置类:

/**
 * 编写切面
 *
 * @author Anson
 *
 */
@Aspect // 注解声明一个切面
@Component // 受spring管理的容器
public class RetryAspect {

	@Autowired
	private RetryScheduledEngine RetryScheduledEngine;

	@Pointcut("@annotation(com.chenhua.common.core.threadpool.retry.Retry)") // 注解声明切点
	public void annotationPointcut() {
	};

	@Around("annotationPointcut()")
	public Object before(ProceedingJoinPoint joinPoint) {

		MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
		Method method = methodSignature.getMethod();
		Retry[] retryAnnotations = method.getAnnotationsByType(Retry.class);
		Retry retryAnnotation = retryAnnotations[0];

		long delayed = retryAnnotation.delayed();
		long interval = retryAnnotation.interval();
		int retryTimes = retryAnnotation.retryTimes();

		Object target = joinPoint.getTarget();
		Object[] args = joinPoint.getArgs();

		//构建参数
		RetryInvorkParam param = new RetryInvorkParam();
		param.setArgs(args);
		param.setCurrentTimes(0);
		param.setDelayed(delayed);
		param.setEnd(false);
		param.setInterval(interval);
		param.setInvorkMethod(method);
		param.setRetryTimes(retryTimes);
		param.setTarget(target);
		
		RetryScheduledEngine.submitTask(param);
		
		return false;
	}

}

以为这样就大功告成了吗?

--no no no

还有两个很重要的东西;增加两个包;(spring aop默认有接口的类是选用jdk的接口代理的方式来实现代理的;这种方式会导致,不在接口上的类是没有办法实现aop的)我们要增加两个cglib的实现切面的包,然后利用cglib的字节码重写技术实现代理,这样就不会出现调用报错的情况:


			org.aspectj
			aspectjrt
		
		
			org.aspectj
			aspectjweaver
		

版本都是1.8.3的

还有一个就是要修改aop的默认实现方式;spring boot需要在启动类中增加一个配置

@EnableAspectJAutoProxy(proxyTargetClass = true)

这样就搞定了;

 

缺点:1、 这样这些数据都保持在内存中,如果那些执行方法的参数引用的内容发生了变化;就会导致重试结果的变化;(参数最好能够用基本类型)

2、重试的信息是保存在内存中的;重启或者是服务挂了,就会丢失;

3、大量的重试应用数据的保存,可能会导致jvm的内存泄漏;

----------------------------------------------------------------------------------------------

二、方式2

经过后面的改良,我们用ThreadPoolTaskScheduler 延时线程来实现重试功能,代替上面的timer定时出发启动重试任务;

第一步:创建一个注解Retry

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
	
	 /**
	  * 延时执行时间  (单位:秒)
	  * @return
	  */
	 long delayed() default 0;
	 
	 /**
	  * 重复执行间隔时间  (单位:秒)
	  * @return
	  */
	 long interval() default 0;
	 
	 /**
	  * 重复的次数  最大10次
	  * @return
	  */
	 int retryTimes() default 1;
	 
}

创建类:

RetryInvorkParam 类 ;用于记录重复执行的一些参数
/**
 * 重试执行参数
 * @author chenhuaping
 * @date  2018年4月20日 下午2:54:39
 */
@Setter
@Getter
@ToString
public class RetryInvorkParam implements Serializable {

	private static final long serialVersionUID = 1L;
	
	/**
	 * 延迟时间
	 */
	private long delayed;
	
	/**
	 * 间隔时间
	 */
    private long interval;
    
    /**
     * 重复次数
     */
    private int retryTimes;
    
    /**
     * 执行的方法的对象
     */
    private Object target;
    
    /**
     * 执行的参数 
     */
    private Object[] args;
    
    /**
     * 执行的方法
     */
    private Method invorkMethod;
    
    /**
     * 当前执行的次数
     */
    private volatile long currentTimes;
    
    /**
     * 下一次执行的时间
     */
    private volatile Date nextInvorkTime;
    
    /**
     * 是否结束这重试
     */
    private volatile boolean isEnd;
    /**
     * 调用线程的引用--用来关闭线程
     */
    private Future future;
	
}
RetryRunning接口:
public class RetryRunning implements Runnable {

  private static final Logger logger = LoggerFactory.getLogger(RetryRunning.class);
  /** 执行的参数 */
  private RetryInvorkParam invorkParam;

  public RetryRunning(RetryInvorkParam invorkParam) {
    super();
    this.invorkParam = invorkParam;
  }

  @Override
  public void run() {

    if (invorkParam.isEnd()) {
      invorkParam.getFuture().cancel(false);
      return;
    }

    Method invorkMethod = invorkParam.getInvorkMethod();
    Object[] args = invorkParam.getArgs();
    Object target = invorkParam.getTarget();

    Object result = null;

    // 执行时间
    Date now = new Date();
    Date nextInvorkTime = invorkParam.getNextInvorkTime();

    // 增加执行次数
    long currentTimes = invorkParam.getCurrentTimes();
    currentTimes++;
    invorkParam.setCurrentTimes(currentTimes);
    // 判断是否为最后一次
    if (currentTimes == invorkParam.getRetryTimes()) {
      invorkParam.setEnd(true);
      invorkParam.setNextInvorkTime(null);
    } else {
      // 计算下一次执行时间;
      nextInvorkTime =
          DateUtils.addSeconds(nextInvorkTime, Long.valueOf(invorkParam.getInterval()).intValue());
      invorkParam.setNextInvorkTime(nextInvorkTime);
    }

    try {
      result = invorkMethod.invoke(target, args);
    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
      logger.error("@Retry 执行方法错误, {}", invorkParam, e);
      invorkParam.setEnd(true);
    }

    if (result != null && result instanceof Boolean && (boolean) result == true) {
      invorkParam.setEnd(true);
    }

    if (invorkParam.isEnd()) {
      logger.info(
          "@Retry method = {}, 重复执行了{}次,已经执行结束,param = {}",
          target.getClass() + "." + invorkMethod.getName(),
          invorkParam.getCurrentTimes(),
          invorkParam);
      invorkParam.getFuture().cancel(false);
      return;
    }

    logger.info(
        "@Retry 已经完成了{}方法,第{}次执行,总共需要执行{}次",
        target.getClass() + "." + invorkMethod.getName(),
        invorkParam.getCurrentTimes(),
        invorkParam.getRetryTimes());
  }
}

提交重试任务引擎:

@Component
public class RetryScheduledEngine {

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

  //  private static final List invorkParamList = new LinkedList<>();

  @Autowired private ThreadPoolTaskScheduler threadPoolTaskScheduler;


  /**
   * 提交任务
   *
   * @param param
   */
  public void submitTask(RetryInvorkParam param) {
    // 计算执行时间
    Date now = new Date();
    if (param.getDelayed() > 0) {
      param.setNextInvorkTime(
          DateUtils.addSeconds(now, Long.valueOf(param.getDelayed()).intValue()));
    } else {
      param.setNextInvorkTime(now);
    }
    ScheduledFuture scheduledFuture =
        threadPoolTaskScheduler.scheduleWithFixedDelay(
            new RetryRunning(param), param.getNextInvorkTime(), param.getInterval() * 1000);
    param.setFuture(scheduledFuture);
  }

  /** 测试方法 */
  @Retry(delayed = 5, interval = 2, retryTimes = 10)
  public boolean testRetryInvoke() {
    System.out.println("______________testRetryInvoke被执行_________________");
    return false;
  }
}

切面:

@Aspect // 注解声明一个切面
@Component // 受spring管理的容器
public class RetryAspect {

  @Autowired private RetryScheduledEngine RetryScheduledEngine;

  @Pointcut("@annotation(com.chenhua.common.core.threadpool.retry.Retry)") // 注解声明切点
  public void annotationPointcut() {};

  @Around("annotationPointcut()")
  public Object before(ProceedingJoinPoint joinPoint) {

    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    Method method = methodSignature.getMethod();
    Retry[] retryAnnotations = method.getAnnotationsByType(Retry.class);
    Retry retryAnnotation = retryAnnotations[0];

    long delayed = retryAnnotation.delayed();
    long interval = retryAnnotation.interval();
    int retryTimes = retryAnnotation.retryTimes();

    Object target = joinPoint.getTarget();
    Object[] args = joinPoint.getArgs();

    // 构建参数
    RetryInvorkParam param = new RetryInvorkParam();
    param.setArgs(args);
    param.setCurrentTimes(0);
    param.setDelayed(delayed);
    param.setEnd(false);
    param.setInterval(interval);
    param.setInvorkMethod(method);
    param.setRetryTimes(retryTimes);
    param.setTarget(target);

    RetryScheduledEngine.submitTask(param);

    return false;
  }

这个和方式一的不一样就是定时执行这个是靠 ThreadPoolTaskScheduler 去实现的;也是一样的,都需要去添加修改spring boot的aop的实现方式 

还有一个就是要修改aop的默认实现方式;spring boot需要在启动类中增加一个配置

@EnableAspectJAutoProxy(proxyTargetClass = true)

 

github: https://github.com/chenhua-China/common.git

三、方式3

 第三中方式就是最好能够用mq的延时队列来实现重试,或者是延时处罚调用的功能,有两个好处,

 一、触发的点不是系统中,这样可以避免系统重启延时或者重试触发点丢失,不能重试了, 比较可靠

二、不需要担心java参数对象引用更改的问题;

你可能感兴趣的:(spring,cloud,spring,boot)