Spring异步发送http请求

背景

目前系统中,有个别的查询比较慢,大概需要几秒才能返回结果。
用户查询开始到返回结果到页面,此处是一个同步的过程,如果做成异步的能提高系统响应的性能。
最近发现servlet3.0有一个新的特性,新增HTTP请求的异步处理,详细请参考。
由于项目采用的SpringMVC做的,所以查看了下SpringMVC的资料,发现3.2版本对于异步处理有良好的封装。

初识@Async

一个简单的demo
@Component //Component Service Controller在Spring 3.2中是等价的
class AsyncMethods{
    @Async //异步标签
    public void testAsyn(){
        long time = System.currentTimeMillis();
        System.err.println(Thread.currentThread().getId());
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.err.println("\nasyn total time:"+(System.currentTimeMillis()-time));
    }
}
@Service //Component Service Controller在Spring 3.2中是等价的
class ClassA{
    @Autowired
    AsyncMethods asyncMethods; // 实例的名字必须和类名完全一样,然后首字母小写

    public testAsync(){
        System.err.println(Thread.currentThread().getId());
        logger.info("enter time:" + System.currentTimeMillis());
        asyncMethods.testAsyn()
        logger.info("leave time:" + System.currentTimeMillis());
    }

}
spring配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans default-autowire="byName"
    xmlns="http://www.springframework.org/schema/beans"    
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context" 
    xmlns:task="http://www.springframework.org/schema/task"     
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
       ">
    <!--扫描项目实例化@Component,@Service,@Controller修饰的类-->
    <context:component-scan base-package="com.your_app" /> 
    <task:annotation-driven /> <!--加入注解驱动 允许@Async-->
</beans>

@Async的实现机制

以下引自Sping3.2.x文档

The Spring Framework provides abstractions for asynchronous execution and scheduling of tasks with theTaskExecutorand TaskScheduler interfaces.To use Servlet 3 async request processing, you need to update web.xml to version 3.0

web.xml 3.0才支持异步。

以下是官方已经实现的全部7个TaskExecuter。Spring宣称对于任何场景,这些TaskExecuter完全够用了:
名字 特点
SimpleAsyncTaskExecutor 每次请求新开线程,没有最大线程数设置
SyncTaskExecutor 不是异步的线程
ConcurrentTaskExecutor 少用这个,多用ThreadPoolTaskExecutor
SimpleThreadPoolTaskExecutor 监听Spring’s lifecycle callbacks,并且可以和Quartz的Component兼容
ThreadPoolTaskExecutor 最常用。要求jdk版本大于等于5。可以在程序而不是xml里修改线程池的配置
TimerTaskExecutor
WorkManagerTaskExecutor

使用ThreadPoolTaskExecutor(传统方式)

官方demo:
比起从线程池取一个线程再执行, 你仅仅需要把你的Runnable类加入到队列中,然后TaskExecutor用它内置的规则决定何时开始取一个线程并执行该Runnable类

先在xml中添加bean的配置:

<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
  <property name="corePoolSize" value="5" />
  <property name="maxPoolSize" value="10" />
  <property name="queueCapacity" value="25" />
</bean>

<bean id="taskExecutorExample" class="TaskExecutorExample">
  <constructor-arg ref="taskExecutor" />
</bean>
  配置解释 
 int corePoolSize:线程池维护线程的最小数量. 
 int maximumPoolSize:线程池维护线程的最大数量. 
 long keepAliveTime:空闲线程的存活时间. 
 TimeUnit unit: 时间单位,现有纳秒,微秒,毫秒,秒枚举值. 
 BlockingQueue<Runnable> workQueue:持有等待执行的任务队列. 
 RejectedExecutionHandler handler: 用来拒绝一个任务的执行,有两种情况会发生这种情况。 
     一是在execute方法中若addIfUnderMaximumPoolSize(command)为false,即线程池已经饱和; 
     二是在execute方法中, 发现runState!=RUNNING || poolSize == 0,即已经shutdown,
         就调用ensureQueuedTaskHandled(Runnable command),在该方法中有可能调用reject。

1. 当池子大小小于corePoolSize,就新建线程,并处理请求
2. 当池子大小等于corePoolSize,把请求放入workQueue中,池子里的空闲线程就去workQueue中取任务并处理
3. 当workQueue放不下任务时,就新建线程入池,并处理请求,如果池子大小撑到了maximumPoolSize,就用RejectedExecutionHandler来做拒绝处理
4. 当池子的线程数大于corePoolSize时,多余的线程会等待keepAliveTime长时间,如果无请求可处理就自行销毁其会优先创建  CorePoolSiz 线程, 当继续增加线程时,先放入Queue中,当 CorePoolSiz  和 Queue 都满的时候,就增加创建新线程,当线程达到MaxPoolSize的时候,就会抛出错 误 org.springframework.core.task.TaskRejectedException
5. 另外MaxPoolSize的设定如果比系统支持的线程数还要大时,会抛出java.lang.OutOfMemoryError: unable to create new native thread 异常。

Reject策略预定义有四种:
(1)ThreadPoolExecutor.AbortPolicy策略,是默认的策略,处理程序遭到拒绝将抛出运行时 RejectedExecutionException。
(2)ThreadPoolExecutor.CallerRunsPolicy策略 ,调用者的线程会执行该任务,如果执行器已关闭,则丢弃.
(3)ThreadPoolExecutor.DiscardPolicy策略,不能执行的任务将被丢弃.
(4)ThreadPoolExecutor.DiscardOldestPolicy策略,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程).

调用实例

import org.springframework.core.task.TaskExecutor;

public class TaskExecutorExample {

  private class MessagePrinterTask implements Runnable {

    private String message;

    public MessagePrinterTask(String message) {
      this.message = message;
    }

    public void run() {
      System.out.println(message);
    }

  }

  private TaskExecutor taskExecutor;

  public TaskExecutorExample(TaskExecutor taskExecutor) {
    this.taskExecutor = taskExecutor;
  }

  public void printMessages() {
    for(int i = 0; i < 25; i++) {
      taskExecutor.execute(new MessagePrinterTask("Message" + i));
    }
  }
}

推荐 - 使用ThreadPoolTaskExecutor(注解方式)

首先,为了以注解方式使用异步功能,我们需要在Spring的xml配置中定义相关的bean:

  1. 必须在*-servlet.xml而不是applicationContext.xml中定义@Async相关配置
    引自http://stackoverflow.com/questions/6610563/spring-async-not-working

In short, the context loaded by the ContextLoaderListener (generally from applicationContext.xml) is the parent of the context loaded by the DispatcherServlet (generally from -servlet.xml). If you have the bean with the @Async method declared/component-scanned in both contexts, the version from the child context (DispatcherServlet) will override the one in the parent context (ContextLoaderListener). I verified this by excluding that component from component scanning in the -servlet.xml – it now works as expected.

  1. 不推荐使用线程池版本

引自http://www.springframework.org/schema/task/spring-task-3.2.xsd

Specifies the Java.util.Executor instance to use when invoking asynchronous methods. If not provided, an instance of org.springframework.core.task.SimpleAsyncTaskExecutor will be used by default. Note that as of Spring 3.1.2, individual @Async methods may qualify which executor to use, meaning that the executor specified here acts as a default for all non-qualified @Async methods.

所以,如果我们仅仅添加,也可以使用@Async标签。然而,此时使用的是SimpleAsyncTaskExecutor。如“官方文档27章:Task Execution”中所述,SimpleAsyncTaskExecutor不会使用线程池,仅仅是为每一个请求新开一个线程。这样在大并发的业务场景下,发生OutOfMemory是不足为奇的。

<?xml version="1.0" encoding="UTF-8"?>
<!--Spring框架的xml标签定义文档, 可访问http://www.springframework.org/schema/task/查看最新task组件的xml标签文档-->
<beans xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="
                http://www.springframework.org/schema/task
                http://www.springframework.org/schema/task/spring-task-3.2.xsd">
    <!--扫描项目实例化@Component,@Service,@Controller修饰的类-->
    <context:component-scan base-package="com.your_app" /> 

    <!--create a SimpleAsyncTaskExecutor instance-->
    <task:annotation-driven/>
</beans>
  1. 推荐 - 使用线程池版本
<?xml version="1.0" encoding="UTF-8"?>
<!--Spring框架的xml标签定义文档, 可访问http://www.springframework.org/schema/task/查看最新task组件的xml标签文档-->
<beans xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="
                http://www.springframework.org/schema/task
                http://www.springframework.org/schema/task/spring-task-3.2.xsd">

    <!--扫描项目实例化@Component,@Service,@Controller修饰的类-->
    <context:component-scan base-package="com.your_app" />

    <!-- 在代码中@Async不加参数就会使用task:annotation-driven标签定义的executor-->
    <task:annotation-driven executor="myExecutor"/>
    <!-- 在代码中@Async("myExecutor")可以显式指定executor为"myExecutor"-->
    <task:executor id="myExecutor"
               pool-size="5-25"
               queue-capacity="100"
               rejection-policy="CALLER_RUNS"/>
</beans>

其中,注意到属性pool-size的值”5-25”是一个范围,这对应的是线程池的min和max容量,它们的意义请参考本文上一节的“配置说明”里的第3、4点。如果只有一个值,如pool-size=n, 意味着minSize==maxSize==n

而关于rejection-policy,官方文档里说:

By default, when a task is rejected, a thread pool executor will throw a TaskRejectedException. However, the rejection policy is actually configurable. The exception is thrown when using the default rejection policy which is the AbortPolicy implementation. For applications where some tasks can be skipped under heavy load, either the DiscardPolicy or DiscardOldestPolicy may be configured instead. Another option that works well for applications that need to throttle the submitted tasks under heavy load is the CallerRunsPolicy. Instead of throwing an exception or discarding tasks, that policy will simply force the thread that is calling the submit method to run the task itself. The idea is that such a caller will be busy while running that task and not able to submit other tasks immediately. Therefore it provides a simple way to throttle the incoming load while maintaining the limits of the thread pool and queue. Typically this allows the executor to “catch up” on the tasks it is handling and thereby frees up some capacity on the queue, in the pool, or both. Any of these options can be chosen from an enumeration of values available for the ‘rejection-policy’ attribute on the ‘executor’ element.

总结如下:

池满时的拒绝策略 效果
AbortPolicy(默认) 抛异常
DiscardPolicy or DiscardOldestPolicy 放弃该线程
CallerRunsPolicy 通知该线程的创建者,让其不要提交新的线程

如果使用的java当做配置文件,即spring boot中使用

@Configuration  
@EnableAsync  
public class ExecutorConfig {  

    /** Set the ThreadPoolExecutor's core pool size. */  
    private int corePoolSize = 10;  
    /** Set the ThreadPoolExecutor's maximum pool size. */  
    private int maxPoolSize = 200;  
    /** Set the capacity for the ThreadPoolExecutor's BlockingQueue. */  
    private int queueCapacity = 10;  

    @Bean  
    public Executor mySimpleAsync() {  
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
        executor.setCorePoolSize(corePoolSize);  
        executor.setMaxPoolSize(maxPoolSize);  
        executor.setQueueCapacity(queueCapacity);  
        executor.setThreadNamePrefix("MySimpleExecutor-");  
        executor.initialize();  
        return executor;  
    }  

    @Bean  
    public Executor myAsync() {  
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
        executor.setCorePoolSize(corePoolSize);  
        executor.setMaxPoolSize(maxPoolSize);  
        executor.setQueueCapacity(queueCapacity);  
        executor.setThreadNamePrefix("MyExecutor-");  

        // rejection-policy:当pool已经达到max size的时候,如何处理新任务  
        // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行  
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());  
        executor.initialize();  
        return executor;  
    }  
}  

这里定义了两个不同的Executor,第二个重新设置了pool已经达到max size时候的处理方法;同时指定了线程名字的前缀。

自定义Executor的使用:

/** 
 * Asynchronous Tasks 
 * @author Xu 
 * 
 */  
@Component  
public class AsyncTask {  
    protected final Logger logger = LoggerFactory.getLogger(this.getClass());  

    @Async("mySimpleAsync")  
    public Future<String> doTask1() throws InterruptedException{  
        logger.info("Task1 started.");  
        long start = System.currentTimeMillis();  
        Thread.sleep(5000);  
        long end = System.currentTimeMillis();  

        logger.info("Task1 finished, time elapsed: {} ms.", end-start);  

        return new AsyncResult<>("Task1 accomplished!");  
    }  

    @Async("myAsync")  
    public Future<String> doTask2() throws InterruptedException{  
        logger.info("Task2 started.");  
        long start = System.currentTimeMillis();  
        Thread.sleep(3000);  
        long end = System.currentTimeMillis();  

        logger.info("Task2 finished, time elapsed: {} ms.", end-start);  

        return new AsyncResult<>("Task2 accomplished!");  
    }  
}  

就是把上面自定义Executor的类名,放进@Async注解中。

  1. @Async的修饰对象是方法而不是类
@Async
void doSomething(String s) { //可以带参数!
    // this will be executed asynchronously
}

Spring的异步是基于方法而不是类!
Spring的异步是基于方法而不是类!
Spring的异步是基于方法而不是类!

说实话,鄙人认为基于方法是Spring的最大优点。负责Http的拦截器@RequestMapping(“”)是基于方法的,异步@Async也是基于方法的。不过也有两个约束:

约束一 调用者和被@Async修饰的方法必须定义在两个类中

@Component
class A{
    @Autowired
    B b;

    public void run(){
        b.doSomething();
    }
}
@Component
class B{
    @Async
    public void doSomething(){
    }
}

约束二 @Async和@PostConstruct不能同时在同一个类中使用. 分别写在两个类中,如下:

public class SampleBeanImpl implements SampleBean {

  @Async
  void doSomething() { … }
}


public class SampleBeanInititalizer {

  private final SampleBean bean;

  public SampleBeanInitializer(SampleBean bean) {
    this.bean = bean;
  }

  @PostConstruct
  public void initialize() {
    bean.doSomething();
  }
}

猜你喜欢

转载自blog.csdn.net/sinat_31908303/article/details/72320732
今日推荐