Spring-boot: Can't send email in a @Async method which called in a unit test.

Created on 3 Nov 2016  ·  4Comments  ·  Source: spring-projects/spring-boot

Here is a bug or some wrong usage for me.

Spring boot dependency:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.4.1.RELEASE</version>
        <relativePath/>
    </parent>

Java version java version "1.8.0_102
My os is: macOS Sierra 10.12.1
ide version:
```IntelliJ IDEA 2016.2.5
Build #IU-162.2228.15, built on October 14, 2016

JRE: 1.8.0_112-release-287-b2 x86_64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o

The `MailService`:

import com.google.common.collect.Maps;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring4.SpringTemplateEngine;

import javax.mail.internet.MimeMessage;
import java.util.Map;

/**

  • mail service
  • Created by [email protected] on 2016/10/31.
    */
    @Service
    public class MailService {
    public enum Template {
    ShareUgc;
    }
public Map<Template, String> templateNameMap;
private Map<Template, String> subjectMap;

public MailService() {
    templateNameMap = Maps.newHashMap();
    subjectMap = Maps.newHashMap();

    templateNameMap.put(Template.ShareUgc, "template-share-ugc");

    subjectMap.put(Template.ShareUgc, "share a ugc to you");
}

@Value("${spring.mail.from}")
private String from;

@Autowired
private JavaMailSender sender;

@Autowired
private SpringTemplateEngine templateEngine;

@Async
public void sendAsync(
        final Template template,
        final Context templateContext,
        final String to
) {
    MimeMessagePreparator mimeMessagePreparator = new MimeMessagePreparator() {
        @Override
        public void prepare(MimeMessage mimeMessage) throws Exception {
            MimeMessageHelper helper = new MimeMessageHelper(mimeMessage);
            helper.setFrom(from);
            helper.setTo(to);
            helper.setSubject(subjectMap.get(template));

            String htmlTemplate = MailService.this.renderMailTemplate(template, templateContext);

            helper.setText(htmlTemplate, true);
        }
    };

    System.out.println("=========== send");

    sender.send(mimeMessagePreparator);
}

public String renderMailTemplate(Template template, Context context){
    String htmlTemplate = templateEngine.process(templateNameMap.get(template), context);
    return htmlTemplate;
}

}

The `@Async` annotation is added to `sendAsync` method.

Unit test is:

import com.didi.km.api.App;
import org.apache.tomcat.jni.Thread;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
import org.thymeleaf.context.Context;

/**

  • mail test
  • Created by [email protected] on 2016/10/31.
    */
    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringBootTest(classes = App.class)
    @ActiveProfiles("unit-test")
    @Transactional
    @Rollback
    public class MailServiceTest {
@Autowired
private MailService mailService;

@Test
public void send() throws Exception {

    String to = "[email protected]";

    Context templateContext = new Context();
    templateContext.setVariable("username", "jack");

    mailService.sendAsync(MailService.Template.ShareUgc, templateContext, to);

// java.lang.Thread.sleep(10000);
}
}

There is a `sleep` method under the service method call. 
If add `sleep` method, email work. Without this line, I can't recieve email send from my `MailService`. 

And another different is the `Thymeleaf` out put info.

If the sleep is uncomment, console output those lines:

...
12:30:53.955[SimpleAsyncTaskExecutor-1] INFO o.t.T.CONFIG - [THYMELEAF] TEMPLATE ENGINE CONFIGURATION:
[THYMELEAF] * Cache Factory implementation: org.thymeleaf.cache.StandardCacheManager
[THYMELEAF] * Template modes:
[THYMELEAF] * VALIDXML
[THYMELEAF] * XHTML
[THYMELEAF] * VALIDXHTML
[THYMELEAF] * LEGACYHTML5
[THYMELEAF] * HTML5
[THYMELEAF] * XML
[THYMELEAF] * Template resolvers (in order):
[THYMELEAF] * [1] org.thymeleaf.templateresolver.ClassLoaderTemplateResolver
[THYMELEAF] * Message resolvers (in order):
[THYMELEAF] * org.thymeleaf.spring4.messageresolver.SpringMessageResolver
[THYMELEAF] * Dialect: org.thymeleaf.spring4.dialect.SpringStandardDialect
[THYMELEAF] * Prefix: "th"
[THYMELEAF] TEMPLATE ENGINE CONFIGURED OK org.thymeleaf.TemplateEngine.CONFIG:123
...

But if not sleep, those line are disappeared.

This is the key output of uncomment sleep method:

12:30:53.625[main] INFO c.d.k.a.s.m.MailServiceTest - Started MailServiceTest in 17.274 seconds (JVM running for 18.508) com.didi.km.api.service.mail.MailServiceTest:57
12:30:53.696[main] INFO o.s.t.c.t.TransactionContext - Began transaction (1) for test context [DefaultTestContext@4b53f538 testClass = MailServiceTest, testInstance = com.didi.km.api.service.mail.MailServiceTest@35555145, testMethod = send@MailServiceTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@134593bf testClass = MailServiceTest, locations = '{}', classes = '{class com.didi.km.api.App, class com.didi.km.api.App}', contextInitializerClasses = '[]', activeProfiles = '{unit-test}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.SpringBootTestContextCustomizer@63753b6d, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@520a3426, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@6e6c3152], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]]]; transaction manager [org.springframework.jdbc.datasource.DataSourceTransactionManager@5909ae90]; rollback [true] org.springframework.test.context.transaction.TransactionContext:101
12:30:53.803[main] INFO o.s.s.a.AnnotationAsyncExecutionInterceptor - No TaskExecutor bean found for async processing org.springframework.scheduling.annotation.AnnotationAsyncExecutionInterceptor:245
=========== send
12:30:53.875[SimpleAsyncTaskExecutor-1] INFO o.t.TemplateEngine - [THYMELEAF] INITIALIZING TEMPLATE ENGINE org.thymeleaf.TemplateEngine:825
12:30:53.947[SimpleAsyncTaskExecutor-1] INFO o.t.t.AbstractTemplateResolver - [THYMELEAF] INITIALIZING TEMPLATE RESOLVER: org.thymeleaf.templateresolver.ClassLoaderTemplateResolver org.thymeleaf.templateresolver.AbstractTemplateResolver:99
12:30:53.947[SimpleAsyncTaskExecutor-1] INFO o.t.t.AbstractTemplateResolver - [THYMELEAF] TEMPLATE RESOLVER INITIALIZED OK org.thymeleaf.templateresolver.AbstractTemplateResolver:110
12:30:53.947[SimpleAsyncTaskExecutor-1] INFO o.t.m.AbstractMessageResolver - [THYMELEAF] INITIALIZING MESSAGE RESOLVER: org.thymeleaf.spring4.messageresolver.SpringMessageResolver org.thymeleaf.messageresolver.AbstractMessageResolver:72
12:30:53.948[SimpleAsyncTaskExecutor-1] INFO o.t.m.AbstractMessageResolver - [THYMELEAF] MESSAGE RESOLVER INITIALIZED OK org.thymeleaf.messageresolver.AbstractMessageResolver:78
12:30:53.955[SimpleAsyncTaskExecutor-1] INFO o.t.T.CONFIG - [THYMELEAF] TEMPLATE ENGINE CONFIGURATION:
[THYMELEAF] * Cache Factory implementation: org.thymeleaf.cache.StandardCacheManager
[THYMELEAF] * Template modes:
[THYMELEAF] * VALIDXML
[THYMELEAF] * XHTML
[THYMELEAF] * VALIDXHTML
[THYMELEAF] * LEGACYHTML5
[THYMELEAF] * HTML5
[THYMELEAF] * XML
[THYMELEAF] * Template resolvers (in order):
[THYMELEAF] * [1] org.thymeleaf.templateresolver.ClassLoaderTemplateResolver
[THYMELEAF] * Message resolvers (in order):
[THYMELEAF] * org.thymeleaf.spring4.messageresolver.SpringMessageResolver
[THYMELEAF] * Dialect: org.thymeleaf.spring4.dialect.SpringStandardDialect
[THYMELEAF] * Prefix: "th"
[THYMELEAF] TEMPLATE ENGINE CONFIGURED OK org.thymeleaf.TemplateEngine.CONFIG:123
12:30:53.955[SimpleAsyncTaskExecutor-1] INFO o.t.TemplateEngine - [THYMELEAF] TEMPLATE ENGINE INITIALIZED org.thymeleaf.TemplateEngine:838
12:31:03.825[main] INFO o.s.t.c.t.TransactionContext - Rolled back transaction for test context [DefaultTestContext@4b53f538 testClass = MailServiceTest, testInstance = com.didi.km.api.service.mail.MailServiceTest@35555145, testMethod = send@MailServiceTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@134593bf testClass = MailServiceTest, locations = '{}', classes = '{class com.didi.km.api.App, class com.didi.km.api.App}', contextInitializerClasses = '[]', activeProfiles = '{unit-test}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.SpringBootTestContextCustomizer@63753b6d, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@520a3426, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@6e6c3152], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]]]. org.springframework.test.context.transaction.TransactionContext:136
12:31:03.833[Thread-6] INFO o.s.w.c.s.GenericWebApplicationContext - Closing org.springframework.web.context.support.GenericWebApplicationContext@16eccb2e: startup date [Thu Nov 03 12:30:39 CST 2016]; root of context hierarchy org.springframework.web.context.support.GenericWebApplicationContext:982

Process finished with exit code 0

this is the key output without sleep method:

12:34:01.170[main] INFO c.d.k.a.s.m.MailServiceTest - Started MailServiceTest in 20.466 seconds (JVM running for 21.829) com.didi.km.api.service.mail.MailServiceTest:57
12:34:01.244[main] INFO o.s.t.c.t.TransactionContext - Began transaction (1) for test context [DefaultTestContext@4b53f538 testClass = MailServiceTest, testInstance = com.didi.km.api.service.mail.MailServiceTest@70bc3a9c, testMethod = send@MailServiceTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@134593bf testClass = MailServiceTest, locations = '{}', classes = '{class com.didi.km.api.App, class com.didi.km.api.App}', contextInitializerClasses = '[]', activeProfiles = '{unit-test}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.SpringBootTestContextCustomizer@63753b6d, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@520a3426, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@6e6c3152], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]]]; transaction manager [org.springframework.jdbc.datasource.DataSourceTransactionManager@5909ae90]; rollback [true] org.springframework.test.context.transaction.TransactionContext:101
12:34:01.311[main] INFO o.s.s.a.AnnotationAsyncExecutionInterceptor - No TaskExecutor bean found for async processing org.springframework.scheduling.annotation.AnnotationAsyncExecutionInterceptor:245
=========== send
12:34:01.323[main] INFO o.s.t.c.t.TransactionContext - Rolled back transaction for test context [DefaultTestContext@4b53f538 testClass = MailServiceTest, testInstance = com.didi.km.api.service.mail.MailServiceTest@70bc3a9c, testMethod = send@MailServiceTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@134593bf testClass = MailServiceTest, locations = '{}', classes = '{class com.didi.km.api.App, class com.didi.km.api.App}', contextInitializerClasses = '[]', activeProfiles = '{unit-test}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.SpringBootTestContextCustomizer@63753b6d, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@520a3426, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@6e6c3152], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]]]. org.springframework.test.context.transaction.TransactionContext:136
12:34:01.333[Thread-6] INFO o.s.w.c.s.GenericWebApplicationContext - Closing org.springframework.web.context.support.GenericWebApplicationContext@16eccb2e: startup date [Thu Nov 03 12:33:43 CST 2016]; root of context hierarchy org.springframework.web.context.support.GenericWebApplicationContext:982

Process finished with exit code 0

```

And I did add @EnableAsync annotation at some config class, And some basic JavaMailSender config and Thymeleaf config at the yml config file which I use.

invalid

Most helpful comment

Without sleep JVM was closed during sendAsync executing.
U can use CompletableFuture instead of void .

CompletableFuture<Void> future=mailService.sendAsync(MailService.Template.ShareUgc, templateContext, to);
future.get();//waiting for future completed

Sorry for my poor English

All 4 comments

Without sleep JVM was closed during sendAsync executing.
U can use CompletableFuture instead of void .

CompletableFuture<Void> future=mailService.sendAsync(MailService.Template.ShareUgc, templateContext, to);
future.get();//waiting for future completed

Sorry for my poor English

Thanks @shenliuyang

@shenliuyang It's very kind of you to answer my question. Your nick name looks like a chinese pinyin. Would you like to add my wechat jacks808? we can talk more.

@jacks808 我已经发送了微信添加好友请求。

Was this page helpful?
0 / 5 - 0 ratings