Micronaut GraalVM Sendgrid AWS lambda 应用程序在尝试发送电子邮件时不断从 Sendgrid 返回 400
我使用 micronaut、java 和 sendgrid 构建了一个简单的电子邮件应用程序。将其用作部署到 AWS 的基本 java 应用程序,电子邮件 sendfine。我创建了另一个 lambda 来尝试使用
我使用 micronaut、java 和 sendgrid 构建了一个简单的电子邮件应用程序。将其用作部署到 AWS 的基本 java 应用程序,电子邮件发送正常。我创建了另一个 lambda 来尝试使用 GraalVM 功能。我只是遵循了 Micronaut 文档 guid,一切都编译成功,并使用 ./gradlew buildNativeLambda 进行构建。在 AWS 控制台中运行测试选项可以正常工作,并且应用程序不会返回任何错误,但是 sendgrid 返回了 400。我觉得我缺少了一些基本的东西。我尝试将我能想到的所有内容添加到 resources/META-INF/native-image/reflect-config.json 中。尝试了 micronaut 文档中显示的不同方法来运行 lambda,即使用 Controller 方法、FunctionRequestHandler 和 FunctionLambdaRuntime。使用每种方法,lambda 都可以工作,并且它会返回字符串或 APIGatewayProxyResponseEvent,没有问题。似乎在构建电子邮件时出现了一些问题,我感觉我只是在墙上乱扔垃圾,但什么都没粘住,无论我做何改变,我都不会收到新的错误代码或任何可以推动我朝着正确方向发展的东西。
- 当前的 build.gradle - 此时它已经非常臃肿,但就像我说的,我一直在努力让它工作,所以我似乎只是不断地添加东西,希望能有所改变
plugins {
id("com.github.johnrengelman.shadow") version "8.1.1"
id("io.micronaut.application") version "4.4.2"
id("com.diffplug.spotless") version "6.23.3"
id("io.micronaut.aot") version "4.4.2"
}
version = "0.1"
group = "example.micronaut"
repositories {
mavenCentral()
}
dependencies {
annotationProcessor("io.micronaut:micronaut-http-validation")
annotationProcessor("io.micronaut.serde:micronaut-serde-processor")
implementation("io.micronaut.email:micronaut-email-sendgrid")
implementation("io.micronaut:micronaut-http-client-jdk")
implementation("jakarta.mail:jakarta.mail-api:2.1.3")
implementation("io.micronaut.aws:micronaut-aws-lambda-events-serde")
implementation("io.micronaut.serde:micronaut-serde-jackson")
runtimeOnly("org.yaml:snakeyaml")
runtimeOnly("ch.qos.logback:logback-classic")
}
application {
mainClass = "example.micronaut.Application"
}
java {
sourceCompatibility = JavaVersion.toVersion("17")
targetCompatibility = JavaVersion.toVersion("17")
}
shadowJar {
// Ensure resources are included
mergeServiceFiles()
include 'EmailTemplate/**'
}
sourceSets {
main {
resources {
srcDirs = ['src/main/resources']
// include([ '**/*.properties', '**/*.yml', '**/*.json', '**/*.png', '**/*.html', '**/*.css','**/*.JPG'])
}
}
}
graalvmNative {
toolchainDetection = false
binaries {
main {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(17)
vendor = JvmVendorSpec.matching("GraalVM Community")
}
resources.autodetect()
metadataRepository { enabled = true }
imageName.set('graal-mail')
buildArgs.add('--verbose')
buildArgs.add('--initialize-at-build-time=kotlin.coroutines.intrinsics.CoroutineSingletons')
buildArgs.add('--initialize-at-run-time=reactor.core.publisher.Traces$StackWalkerCallSiteSupplierFactory')
buildArgs.add('--initialize-at-run-time=reactor.core.publisher.Traces$ExceptionCallSiteSupplierFactory')
}
}
}
micronaut {
runtime("lambda_provided")
testRuntime("junit5")
processing {
incremental(true)
annotations("example.micronaut.*")
}
aot {
// Please review carefully the optimizations enabled below
// Check https://micronaut-projects.github.io/micronaut-aot/latest/guide/ for more details
optimizeServiceLoading = false
convertYamlToJava = false
precomputeOperations = true
cacheEnvironment = true
optimizeClassLoading = true
deduceEnvironment = true
optimizeNetty = true
replaceLogbackXml = true
}
}
tasks.named("dockerfileNative") {
baseImage = "amazonlinux:2023"
jdkVersion = "17"
args(
"-XX:MaximumHeapSizePercent=80",
"-Dio.netty.allocator.numDirectArenas=0",
"-Dio.netty.noPreferDirect=true"
)
}
spotless {
java {
licenseHeaderFile(file("LICENSEHEADER"))
}
}
- 电子邮件生成器——此时我再次将注释放在可能不需要的地方,但似乎没有什么影响它,应用程序运行,但从 lambda GraalVM 发送电子邮件无法正确发送
package example.micronaut.services
import com.sendgrid.Response;
import example.micronaut.Util.MimeType;
import example.micronaut.Util.UtilMailService;
import io.micronaut.context.annotation.Value;
import io.micronaut.core.annotation.ReflectiveAccess;
import io.micronaut.email.BodyType;
import io.micronaut.email.Email;
import io.micronaut.email.sendgrid.SendgridEmailSender;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.mail.internet.MimeBodyPart;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
@Singleton
@ReflectiveAccess
public class TestCampaign {
private final UtilMailService utilMailService;
private final SendgridEmailSender sendgridEmailSender;
@Value("${micronaut.email.from.email}")
private String fromEmail;
@Inject
public TestCampaign(SendgridEmailSender sendgridEmailSender, UtilMailService utilMailService) {
this.sendgridEmailSender = sendgridEmailSender;
this.utilMailService = utilMailService;
}
public Response sendTestEmail() throws Exception {
AtomicInteger index = new AtomicInteger(0);
Email.Builder emailBuilder = getEmailBuilder();
utilMailService.getContacts("EmailTemplate/EmailListTest.json").forEach(contact -> {
if (index.getAndIncrement() == 0) {
emailBuilder.to(contact);
} else {
emailBuilder.bcc(contact);
}
});
return sendgridEmailSender.send(emailBuilder.build());
}
private Email.Builder getEmailBuilder() throws Exception {
Optional<String> bodyOption = utilMailService.readHtmlFile("EmailTemplate/StdEmail.html");
String body = bodyOption.orElse("Be Aware of Your Prescriptions at work");
return Email.builder()
.from(fromEmail)
.subject("subject")
.body(body, BodyType.HTML)
.attachment(utilMailService.buildAttachment("EmailTemplate/Meds1.png", "meds1.png", MimeBodyPart.ATTACHMENT, MimeType.IMAGE_PNG).build())
.attachment(utilMailService.buildAttachment("EmailTemplate/Meds2.png", "meds2.png", MimeBodyPart.ATTACHMENT, MimeType.IMAGE_PNG).build())
.attachment(utilMailService.buildAttachment("EmailTemplate/Meds3.JPG", "meds3.JPG", MimeBodyPart.ATTACHMENT, MimeType.IMAGE_JPEG).build())
.attachment(utilMailService.buildAttachment("EmailTemplate/AllMeds.png", "AllMeds.png", MimeBodyPart
- 当前实用程序可从 json 文件、附件和资源中的 html 页面添加联系人
package example.micronaut.Util;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.annotation.ReflectiveAccess;
import io.micronaut.core.io.IOUtils;
import io.micronaut.core.io.ResourceResolver;
import io.micronaut.core.type.Argument;
import io.micronaut.email.Attachment;
import io.micronaut.email.Contact;
import io.micronaut.serde.ObjectMapper;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.mail.internet.MimeBodyPart;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.*;
@Singleton
@ReflectiveAccess
public class UtilMailService {
private final ResourceResolver resourceResolver;
private final ObjectMapper objectMapper;
@Inject
public UtilMailService(ResourceResolver resourceResolver, ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.resourceResolver = resourceResolver;
}
public @NonNull Attachment.Builder buildAttachment(String path, String name, String disposition, MimeType type) throws Exception {
Optional<byte[]> fileBytes = getClasspathResourceAsBytes(path);
if (fileBytes.isEmpty()) {
throw new IllegalArgumentException("File not found! " + path);
}
Attachment.Builder newAttachment = Attachment.builder().filename(name).contentType(type.getMimeType()).content(fileBytes.get());
if (Objects.equals(disposition, MimeBodyPart.INLINE)) {
newAttachment.id(name).disposition(disposition);
}
return newAttachment;
}
public Optional<byte[]> getClasspathResourceAsBytes(String path) throws Exception {
Optional<URL> url = resourceResolver.getResource("classpath:" + path);
if (url.isPresent()) {
try (InputStream inputStream = url.get().openStream()) {
return Optional.of(inputStream.readAllBytes());
}
}
else {
return Optional.empty();
}
}
public List<Contact> getContacts(String path) throws IOException {
List<Contact> contactList = new ArrayList<>();
Map<String, String> contactMap = readJsonFileToMap(path).orElse(Map.of("[email protected]", "crash"));
contactMap.forEach((key, value) -> {
contactList.add(new Contact(key, value));
});
return contactList;
}
public @Nullable Optional<Map<String, String>> readJsonFileToMap(String resourcePath) throws IOException {
Optional<URL> url = resourceResolver.getResource("classpath:" + resourcePath);
if (url.isPresent()) {
try (InputStream inputStream = url.get().openStream()) {
return Optional.of(objectMapper.readValue(inputStream.readAllBytes(), Argument.mapOf(String.class, String.class)));
}
}
else {
return Optional.empty();
}
}
public Optional<String> readHtmlFile(String path) throws Exception {
Optional<URL> url = resourceResolver.getResource("classpath:" + path);
if (url.isPresent()) {
return Optional.of(IOUtils.readText(new BufferedReader(new InputStreamReader(url.get().openStream()))));
}
else {
return Optional.empty();
}
}
public Optional<String> getClasspathResourceAsText(String path) throws Exception {
Optional<URL> url = resourceResolver.getResource("classpath:" + path);
if (url.isPresent()) {
return Optional.of(IOUtils.readText(new BufferedReader(new InputStreamReader(url.get().openStream()))));
}
else {
return Optional.empty();
}
}
}
- 控制器代码
package example.micronaut;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.sendgrid.Response;
import example.micronaut.services.EmailSendingService;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.QueryValue;
import io.micronaut.serde.ObjectMapper;
import jakarta.inject.Inject;
import java.util.Collections;
@Controller
public class HomeController {
private final EmailSendingService emailSendingService;
@Inject
ObjectMapper objectMapper;
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
@Inject
public HomeController(EmailSendingService emailSendingService) {
this.emailSendingService = emailSendingService;
}
@Get
public APIGatewayProxyResponseEvent index(@QueryValue(defaultValue = "test") String campaign) {
try {
Response sendGridResponse = emailSendingService.sendCustomizedEmail(campaign);
String json = new String(objectMapper.writeValueAsBytes(Collections.singletonMap("message", response.getHeaders())));
response.setStatusCode(sendGridResponse.getStatusCode());
response.setBody(json);
}
catch (Exception e) {
response.setStatusCode(500);
response.setBody(String.valueOf(e.getMessage()));
}
return response;
}
}
- 当前运行时配置
- 测试响应
-- 执行日志
{
"statusCode": 200,
"headers": {
"Date": "Sat, 14 Sep 2024 18:05:23 GMT",
"Content-Type": "application/json"
},
"multiValueHeaders": {
"Date": [
"Sat, 14 Sep 2024 18:05:23 GMT"
],
"Content-Type": [
"application/json"
]
},
"body": "{\"statusCode\":400,\"body\":\"{\\\"message\\\":null}\"}",
"isBase64Encoded": false
}
-- 日志输出
START RequestId: b984c570-f7af-4d4c-a929-0ae8cf1fdcd7 Version: $LATEST
[36m18:05:23.612 [0;39m [1;30m[main] [0;39m [34mINFO [0;39m [35mi.m.e.sendgrid.SendgridEmailSender [0;39m - Status Code: 400
[36m18:05:23.612 [0;39m [1;30m[main] [0;39m [34mINFO [0;39m [35mi.m.e.sendgrid.SendgridEmailSender [0;39m - Body: {"errors":[{"message":"The from object must be provided for every email send. It is an object that requires the email parameter, but may also contain a name parameter. e.g. {\"email\" : \"[email protected]\"} or {\"email\" : \"[email protected]\", \"name\" : \"Example Recipient\"}.","field":"from.email","help":"http://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/errors.html#message.from"},{"message":"The personalizations field is required and must have at least one personalization.","field":"personalizations","help":"http://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/errors.html#-Personalizations-Errors"},{"message":"Unless a valid template_id is provided, the content parameter is required. There must be at least one defined content block. We typically suggest both text/plain and text/html blocks are included, but only one block is required.","field":"content","help":"http://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/errors.html#message.content"}]}
[36m18:05:23.612 [0;39m [1;30m[main] [0;39m [34mINFO [0;39m [35mi.m.e.sendgrid.SendgridEmailSender [0;39m - Headers {Strict-Transport-Security=max-age=600; includeSubDomains, Server=nginx, Access-Control-Allow-Origin=https://sendgrid.api-docs.io, Access-Control-Allow-Methods=POST, Connection=keep-alive, X-No-CORS-Reason=https://sendgrid.com/docs/Classroom/Basics/API/cors.html, Content-Length=980, Access-Control-Max-Age=600, Date=Sat, 14 Sep 2024 18:05:23 GMT, Access-Control-Allow-Headers=Authorization, Content-Type, On-behalf-of, x-sg-elas-acl, Content-Type=application/json}
END RequestId: b984c570-f7af-4d4c-a929-0ae8cf1fdcd7
REPORT RequestId: b984c570-f7af-4d4c-a929-0ae8cf1fdcd7 Duration: 2722.27 ms Billed Duration: 2723 ms Memory Size: 128 MB Max Memory Used: 113 MB
就像我说的,应用程序在未部署为 GraalVM 时也能正常工作。任何帮助或其他尝试都将不胜感激。
我尝试将我能想到的所有内容添加到 resources/META-INF/native-image/reflect-config.json 中。尝试了 micronaut 文档中显示的不同方法来运行 lambda ,即使用 Controller 方法、FunctionRequestHandler 和 FunctionLambdaRuntime。使用每种方法,lambda 都可以工作,并且它会返回字符串或 APIGatewayProxyResponseEvent,没有问题。构建电子邮件似乎有些问题