Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring @Async not allowing use of autowired beans [duplicate]

I have a fully-annotation-driven Spring Boot 1.3.5 app which has this asynchronous service which needs to autowire another service bean (And in the future it will need to autowire a repository bean, but I'm not there yet) in order to perform some business logic:

@Service
public class AsyncService {

    @Autowired
    public HelpingService helpingService;

    @Async
    public Future<String> doFoo(String someArgument)
            throws InterruptedException {
        Thread.sleep(3000);
        System.out.println("about to do Foo "+someArgument);
        String result = "";

        try {
            result = helpingService.getSomeStuff(someArgument);
        }
        catch (Exception e) {
            e.printStackTrace();
        }   
        return new AsyncResult<String>(hello);
    }
}

That method above is being called from a @Controller bean, which has other endpoints (Non-async) that work as expected also using this

@Controller
public class MyController extends BaseController {

    @Autowired
    HelpingService helpingService;
    @Autowired
    AsyncService asyncService;

    @RequestMapping(method=RequestMethod.GET, value={"/rest/threads/getIp/{jobId}"}, produces={"application/json"})
    public ResponseEntity<?> getLog(@PathVariable("jobId") String jobId) throws InterruptedException {
        asyncService.doFoo(jobId);
        return new ResponseEntity<>(HttpStatus.OK);
    }
}

And here's helpingService's implementation (It's an interface), calling any method works perfectly fine when I'm not doing it from the @Async method above:

@Service
@Validated
public class HelpingServiceImpl implements HelpingService {

    @Autowired
    HttpSession httpSession;

    @Value(value="${projName}")
    private String projName;

    public String getServerAddress(){
        AuthRegion region = (AuthRegion) httpSession.getAttribute("region");
        if (region != null)
            return region.getServerAddress();
        else
            return null;
    }


    @Override
    public String getSomeStuff(String jobId) {
        String responseString = "";

        String projName = this.projName;
        String serverAddress = getServerAddress(); // Code stops here with an exception

        // Some code here that works fine outside this thread

        return responseString;
    }
}

This is the exception being caught:

about to do Foo (267)
java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
    at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131)
    at org.springframework.web.context.support.WebApplicationContextUtils.currentRequestAttributes(WebApplicationContextUtils.java:309)
    at org.springframework.web.context.support.WebApplicationContextUtils.access$400(WebApplicationContextUtils.java:64)
    at org.springframework.web.context.support.WebApplicationContextUtils$SessionObjectFactory.getObject(WebApplicationContextUtils.java:366)
    at org.springframework.web.context.support.WebApplicationContextUtils$SessionObjectFactory.getObject(WebApplicationContextUtils.java:361)
    at org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler.invoke(AutowireUtils.java:307)
    at com.sun.proxy.$Proxy96.getAttribute(Unknown Source)
    at corp.fernandopcg.myapp.service.ThreadServiceImpl.getRundeckServerPort(ThreadServiceImpl.java:45)
    at corp.fernandopcg.myapp.service.ThreadServiceImpl.getJobExecutionOutput(ThreadServiceImpl.java:65)
    at corp.fernandopcg.myapp.service.AsyncService.doFoo(AsyncService.java:40)
    at corp.fernandopcg.myapp.service.AsyncService$$FastClassBySpringCGLIB$$7e164220.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:720)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
    at org.springframework.aop.interceptor.AsyncExecutionInterceptor$1.call(AsyncExecutionInterceptor.java:115)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

I added (With some changes as I couldn't extend AsyncConfigurer at the same time as SpringBootServletInitializer, and I had to catch an exception not mentiones there) the taskExecutor part to my Application main class as follows, guided by this tutorial which does look similar to what I need, in my opinion

@SpringBootApplication
@EnableAsync
@EnableJpaRepositories(repositoryFactoryBeanClass = DataTablesRepositoryFactoryBean.class)
public class MyApplication extends SpringBootServletInitializer implements AsyncConfigurer{

    public static void main(String[] args)  {
        SpringApplication.run(MyApplication.class, args);
    }

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("SomeRandomLookup-");
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        // TODO Auto-generated method stub
        return null;
    }

}

Can I tell my @Async service to be able to use other services of the application? Because if that's not possible, I don't really see the use of these threading mechanism.

like image 574
fernandopcg Avatar asked Feb 01 '17 16:02

fernandopcg


1 Answers

This is a great illustration of why request-scope injection can be problematic. Your HelpingServiceImpl has a hidden dependency on the request-specific HttpSession, which looks like a field but is actually a proxy that is resolved by Spring on each call to always refer to the "current" request (using a thread-local variable).

The problem is that by making your call @Async, you're separating the HelpingServiceImpl invocation from the request that triggered it, and there's no longer the implicit connection of being on the same thread that would allow it to pull information from the globalish context.

The most straightforward fix is to make your dependencies explicit--instead of having your HelpingServiceImpl grab the region directly off of the HttpSession, pass the region to it as a method parameter.

like image 127
chrylis -cautiouslyoptimistic- Avatar answered Oct 13 '22 15:10

chrylis -cautiouslyoptimistic-