发送电子邮件和 Spock Spring
了解如何通过 Grails 应用发送 AWS SES 和 SendGrid 电子邮件,并利用 Spock Spring 集成验证交互。
作者: Sergio del Amo
Grails 版本 3.3.5
1 入门
1.1 所需条件
若要完成此指南,你需要:
-
分配一些时间
-
一个出色的文本编辑器或 IDE
-
安装 JDK 1.7 或更高版本,并正确配置
JAVA_HOME
1.2 如何完成本指南
若要开始操作,请执行以下操作:
-
下载并解压缩源代码
或者
-
克隆 Git 存储库
git clone https://github.com/grails-guides/grails-email.git
Grails 指南存储库包含两个文件夹
-
initial
初始项目。通常是一个简单的 Grails 应用,其中包含一些额外代码,以便让你轻松上手。 -
complete
已完成的示例。它是按照指南中介绍的步骤操作并对initial
文件夹应用这些更改的结果。
若要完成指南,请转到 initial
文件夹
-
cd
到grails-guides/grails-email/initial
并按照下一部分中的说明操作。
如果你 cd 到 grails-guides/grails-email/complete ,则可以直接转到已完成示例。 |
如果你想从头开始,请使用 Grails Application Forge 创建一个新的 Grails 3 应用程序。
2 编写应用
使用 rest-api
配置文件创建应用
grails create-app example --profile=rest-api
2.1 控制器
将条目添加到 UrlMappings
package example.grails
class UrlMappings {
static mappings = {
...
..
.
post "/mail/send"(controller: 'mail', action: 'send')
}
}
创建一个 MailController
,该控制器使用协作者 emailService
来发送电子邮件。
package example.grails
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
@Slf4j
@CompileStatic
class MailController {
EmailService emailService
static allowedMethods = [send: 'POST']
def send(EmailCmd cmd) {
if ( cmd.hasErrors() ) {
render status: 422
return
}
log.info '{}', cmd.toString()
emailService.send(cmd)
render status: 200
}
}
上一个控制器使用 命令对象
package example.grails
import grails.compiler.GrailsCompileStatic
import grails.validation.Validateable
import groovy.transform.ToString
@ToString
@GrailsCompileStatic
class EmailCmd implements Validateable, Email {
String recipient
List<String> cc = []
List<String> bcc = []
String subject
String htmlBody
String textBody
String replyTo
static constraints = {
recipient nullable: false (1)
subject nullable: false (2)
htmlBody nullable: true
textBody nullable: true, validator: { String val, EmailCmd obj -> (3)
!(!obj.htmlBody && !val)
}
replyTo nullable: true
}
}
1 | recipient 是必需的 |
2 | subject 是必需的 |
3 | 你必须指定 textBody 或 htmlBody |
2.2 电邮服务
创建一个接口 - EmailService
。应用程序中存在的任何电子邮件集成都应该实现此接口。
package example.grails
import groovy.transform.CompileStatic
@CompileStatic
interface EmailService {
void send(Email email)
}
package example.grails
import groovy.transform.CompileStatic
@CompileStatic
interface Email {
String getRecipient()
List<String> getCc()
List<String> getBcc()
String getSubject()
String getHtmlBody()
String getTextBody()
String getReplyTo()
}
2.2.1 AWS SES
Amazon Simple Email Service (Amazon SES) 是一项基于云的电子邮件发送服务,旨在帮助数字营销人员和应用程序开发人员发送营销、通知和事务性电子邮件。对于使用电子邮件与客户保持联系的所有规模的企业来说,它都是一项可靠、经济高效的服务。
有一个 AWS SDK SES Grails 插件。然而,在本指南中,我们将直接集成 AWS SDK SES。
将依赖项添加到 AWS SES SDK
compile 'com.amazonaws:aws-java-sdk-ses:1.11.285'
添加可经由系统属性/命令行参数传递的配置属性
aws:
accessKeyId: '${AWS_ACCESS_KEY_ID}'
secretKey: '${AWS_SECRET_KEY}'
sourceEmail: '${AWS_SOURCE_EMAIL}'
ses:
region: '${AWS_REGION}'
创建封装与 SES 集成相关的两个服务
package example.grails
import com.amazonaws.auth.AWSCredentials
import com.amazonaws.auth.AWSCredentialsProvider
import com.amazonaws.auth.BasicAWSCredentials
import grails.config.Config
import grails.core.support.GrailsConfigurationAware
import groovy.transform.CompileStatic
@CompileStatic
class AwsCredentialsProviderService implements AWSCredentialsProvider, GrailsConfigurationAware { (1)
String accessKey
String secretKey
@Override
AWSCredentials getCredentials() {
return new BasicAWSCredentials(accessKey, secretKey)
}
@Override
void refresh() {
}
@Override
void setConfiguration(Config co) {
this.accessKey = co.getProperty('aws.accessKeyId', String)
if (!this.accessKey) {
throw new IllegalStateException('aws.accessKeyId not set')
}
this.secretKey = co.getProperty('aws.secretKey', String)
if (!this.secretKey) {
throw new IllegalStateException('aws.secretKey not set')
}
}
}
1 | 使用 GrailsConfigurationAware 检索配置值 |
package example.grails
import com.amazonaws.services.simpleemail.model.SendEmailResult
import grails.config.Config
import grails.core.support.GrailsConfigurationAware
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import com.amazonaws.services.simpleemail.AmazonSimpleEmailService
import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClientBuilder
import com.amazonaws.services.simpleemail.model.Body
import com.amazonaws.services.simpleemail.model.Content
import com.amazonaws.services.simpleemail.model.Destination
import com.amazonaws.services.simpleemail.model.Message
import com.amazonaws.services.simpleemail.model.SendEmailRequest
@Slf4j
@CompileStatic
class AwsSesMailService implements EmailService, GrailsConfigurationAware { (1)
String awsRegion
String sourceEmail
AwsCredentialsProviderService awsCredentialsProviderService
@Override
void setConfiguration(Config co) {
this.awsRegion = co.getProperty('aws.ses.region')
if (!this.awsRegion) {
throw new IllegalStateException('aws.ses.region not set')
}
this.sourceEmail = co.getProperty('aws.sourceEmail')
if (!this.sourceEmail) {
throw new IllegalStateException('aws.sourceEmaill not set')
}
}
private Body bodyOfEmail(Email email) {
if (email.htmlBody) {
Content htmlBody = new Content().withData(email.htmlBody)
return new Body().withHtml(htmlBody)
}
if (email.textBody) {
Content textBody = new Content().withData(email.textBody)
return new Body().withHtml(textBody)
}
new Body()
}
@Override
void send(Email email) {
if ( !awsCredentialsProviderService ) {
log.warn("AWS Credentials provider not configured")
return
}
Destination destination = new Destination().withToAddresses(email.recipient)
if ( email.getCc() ) {
destination = destination.withCcAddresses(email.getCc())
}
if ( email.getBcc() ) {
destination = destination.withBccAddresses(email.getBcc())
}
Content subject = new Content().withData(email.getSubject())
Body body = bodyOfEmail(email)
Message message = new Message().withSubject(subject).withBody(body)
SendEmailRequest request = new SendEmailRequest()
.withSource(sourceEmail)
.withDestination(destination)
.withMessage(message)
if ( email.getReplyTo() ) {
request = request.withReplyToAddresses()
}
try {
log.info("Attempting to send an email through Amazon SES by using the AWS SDK for Java...")
AmazonSimpleEmailService client = AmazonSimpleEmailServiceClientBuilder.standard()
.withCredentials(awsCredentialsProviderService)
.withRegion(awsRegion)
.build()
SendEmailResult sendEmailResult = client.sendEmail(request)
log.info("Email sent! {}", sendEmailResult.toString())
} catch (Exception ex) {
log.warn("The email was not sent.")
log.warn("Error message: {}", ex.message)
}
}
}
1 | 使用 GrailsConfigurationAware 检索配置值 |
2.2.2 SendGrid
SendGrid 是一项事务性电子邮件服务。
SendGrid 负责为全球最优秀、最杰出的公司发送数十亿封电子邮件。
有一个 SendGrid Grails 插件。然而,在本指南中,我们将直接集成 AWS SDK SES。
添加依赖项到 SendGrid SDK
compile 'com.sendgrid:sendgrid-java:4.1.2'
添加可经由系统属性/命令行参数传递的配置属性
sendgrid:
apiKey: '${SENDGRID_APIKEY}'
fromEmail: '${SENDGRID_FROM_EMAIL}'
创建封装与 SendGrid 集成相关的一个服务
package example.grails
import com.sendgrid.Personalization
import com.sendgrid.Content
import com.sendgrid.Mail
import com.sendgrid.SendGrid
import com.sendgrid.Request
import com.sendgrid.Response
import com.sendgrid.Method
import grails.config.Config
import grails.core.support.GrailsConfigurationAware
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
@Slf4j
@CompileStatic
class SendGridEmailService implements EmailService, GrailsConfigurationAware { (1)
String apiKey
String fromEmail
@Override
void setConfiguration(Config co) {
this.apiKey = co.getProperty('sendgrid.apiKey', String)
if (!this.apiKey) {
throw new IllegalStateException('sendgrid.apiKey not set')
}
this.fromEmail = co.getProperty('sendgrid.fromEmail', String)
if (!this.fromEmail) {
throw new IllegalStateException('sendgrid.apiKey not set')
}
}
protected Content contentOfEmail(Email email) {
if ( email.textBody ) {
return new Content("text/plain", email.textBody)
}
if ( email.htmlBody ) {
return new Content("text/html", email.htmlBody)
}
return null
}
@Override
void send(Email email) {
Personalization personalization = new Personalization()
personalization.subject = email.subject
com.sendgrid.Email to = new com.sendgrid.Email(email.recipient)
personalization.addTo(to)
if ( email.getCc() ) {
for ( String cc : email.getCc() ) {
com.sendgrid.Email ccEmail = new com.sendgrid.Email()
ccEmail.email = cc
personalization.addCc(ccEmail)
}
}
if ( email.getBcc() ) {
for ( String bcc : email.getBcc() ) {
com.sendgrid.Email bccEmail = new com.sendgrid.Email()
bccEmail.email = bcc
personalization.addBcc(bccEmail)
}
}
Mail mail = new Mail()
com.sendgrid.Email from = new com.sendgrid.Email()
from.email = fromEmail
mail.from = from
mail.addPersonalization(personalization)
Content content = contentOfEmail(email)
mail.addContent(content)
SendGrid sg = new SendGrid(apiKey)
Request request = new Request()
try {
request.with {
method = Method.POST
endpoint = "mail/send"
body = mail.build()
}
Response response = sg.api(request)
log.info("Status Code: {}", String.valueOf(response.getStatusCode()))
log.info("Body: {}", response.getBody())
if ( log.infoEnabled ) {
response.getHeaders().each { String k, String v ->
log.info("Response Header {} => {}", k, v)
}
}
} catch (IOException ex) {
log.error(ex.getMessage())
}
}
}
1 | 使用 GrailsConfigurationAware 检索配置值 |
2.3 资源
Grails 集成了 Spring 框架并基于 Spring 框架构建。
你可以通过在使用 Grails Spring DSL 的 grails-app/conf/spring/resources.groovy
中配置它们来轻松注册新的 Bean(或覆盖现有的 Bean)。
根据必填系统属性的存在,我们将启用 SendGrid 或 AWS SES 集成。
import example.grails.AwsSesMailService
import example.grails.SendGridEmailService
beans = {
if ( System.getProperty('AWS_REGION') && System.getProperty('AWS_SOURCE_EMAIL') && System.getProperty('AWS_ACCESS_KEY_ID') && System.getProperty('AWS_SECRET_KEY') ) {
emailService(AwsSesMailService) {
awsCredentialsProviderService = ref('awsCredentialsProviderService')
}
} else if ( System.getProperty('SENDGRID_FROM_EMAIL') && System.getProperty('SENDGRID_APIKEY') ) {
emailService(SendGridEmailService)
}
}
添加一个记录器以获得更大的可见性
...
..
.
logger('example.grails', INFO, ['STDOUT'], false)
要使用 SendGrid,请使用必要的系统属性启动该应用程序
$ ./gradlew [email protected] -DSENDGRID_APIKEY=XXXXXX bootRun
要使用 AWS SES,请使用必要的系统属性启动该应用程序
$ ./gradlew -DAWS_REGION=eu-west-1 [email protected] -DAWS_ACCESS_KEY_ID=XXXXXXXX -DAWS_SECRET_KEY=XXXXXXXX bootRun
2.4 测试
在我们接受测试中,我们不希望 bean emailService
成为 SendGridEmailService
或 AwsSesMailService
。我们可以通过模拟来对其交互进行验证。
spock-spring
模块提供支持,将 Spock 模拟和存根定义为 Spring bean。
添加 spock-spring
依赖
testCompile 'org.spockframework:spock-spring:1.1-groovy-2.4'
首先,您需要使用 @ComponentScan
注释 Application.groovy
。
package example.grails
import grails.boot.GrailsApp
import grails.boot.config.GrailsAutoConfiguration
import groovy.transform.CompileStatic
import org.springframework.context.annotation.ComponentScan
@ComponentScan('example.grails')
@CompileStatic
class Application extends GrailsAutoConfiguration {
static void main(String[] args) {
GrailsApp.run(Application, args)
}
}
在下一个测试中,我们使用嵌入式配置进行了用 @TestConfiguration
注解。我们使用 DetachedMockFactory
创建一个 EmailService
模拟。
package example.grails
import grails.plugins.rest.client.RestBuilder
import grails.plugins.rest.client.RestResponse
import grails.testing.mixin.integration.Integration
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import spock.lang.Specification
import spock.mock.DetachedMockFactory
@Integration
class MailControllerSpec extends Specification {
EmailService emailService
def "/mail/send interacts once email service"() {
given:
RestBuilder rest = new RestBuilder()
when:
RestResponse resp = rest.post("http://localhost:${serverPort}/mail/send") {
accept('application/json')
contentType('application/json')
json {
subject = 'Test'
recipient = '[email protected]'
textBody = 'Hola hola'
}
}
then:
resp.status == 200
1 * emailService.send(_) (1)
}
@TestConfiguration
static class EmailServiceConfiguration {
private DetachedMockFactory factory = new DetachedMockFactory()
@Bean
EmailService emailService() {
factory.Mock(EmailService)
}
}
}
1 | emailService.send 方法调用一次。 |
详细了解 Spring Spock 集成,请访问 Spock 文档。