围绕 Spring 类型为 void return 的目标方法的 AOP 会跳过代码执行
Around Spring AOP on target method with void return type skips code execution
据我了解,Spring AOP 上的 @Around
注释可以处理方法上的任何 return 类型;使用 void
类型 returning null.
这是记录方法持续时间的简单建议:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Profiling { }
@Aspect
@Component
public class ProfilingAspect {
// ...
@Around("@annotation(profilingAnnotation)")
public Object logDuration(ProceedingJoinPoint joinPoint, Profiling profilingAnnotation) throws Throwable {
long startTime = Instant.now().toEpochMilli();
Object result = null;
try {
result = joinPoint.proceed(); // on void methods, this supposed to return null
} catch (Throwable e) {
logger.error(e.getMessage(), e);
throw e;
} finally {
long endTime = Instant.now().toEpochMilli(); // Below is not ran all together
long duration = endTime - startTime;
logger.info(joinPoint.getSignature().toShortString()+": "+duration+"ms");
}
//return the result to the caller
return result;
}
}
但是,当调用此方法时,它不会 return 任何东西,而是完全跳过 proceed()
之后的所有代码。甚至 finally
块。
这是有问题的代码:
@GetMapping("/members/exportpdf")
@Profiling
public void exportToPDF(@RequestParam(required = false)String role, HttpServletResponse response) throws DocumentException, IOException, ExecutionException, InterruptedException {
CompletableFuture<List<GuildMember>> guildMembers;
if (role==null) {
guildMembers = guildService.findAll(); // Async Method
} else {
guildMembers = guildService.findByType(role); // Async Method
}
response.setContentType("application/pdf");
DateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss");
String currentDateTime = dateFormatter.format(new Date());
String headerKey = "Content-Disposition";
String headerValue = "inline; filename=guildmembers_" + currentDateTime + ".pdf";
response.setHeader(headerKey, headerValue);
PDFExporter exporter = new PDFExporter(guildMembers);
exporter.export(response).get(); // exporter.export(..) is an Async method returning CompletableFuture<Void>
}
这怎么可能?我在配置中遗漏了什么吗?还是 Spring 上的错误?
注意。我正在使用 Spring Boot 2.4.4 与启动依赖项
编辑。 PDFExporter.export()
在 HttpServletResponse
上使用 OutputStream
向用户打印 application/pdf
和 returns CompletableFuture<Void>
。至于为什么,该方法与上面的异步函数通信,因此我想保证操作以某种方式完成。
为了教大家什么是MCVE以及如何在这里更好地提问,我将向您展示我根据您的代码片段和描述创建的MCVE:
依赖类我们需要为了让代码编译:
package de.scrum_master.spring.q66958382;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Profiling {}
package de.scrum_master.spring.q66958382;
public class GuildMember {
private String name;
public GuildMember(String name) {
this.name = name;
}
@Override
public String toString() {
return "GuildMember{" +
"name='" + name + '\'' +
'}';
}
}
PDFExporter
效用:
也许您正在使用 PrimeFaces 的 PDFExporter
,但这只是一个猜测。在任何情况下,它似乎都不是 Spring 组件,因为稍后您调用构造函数而不是从应用程序上下文中获取 bean 实例。所以我在这里也将它建模为一个简单的 POJO。
package de.scrum_master.spring.q66958382;
import org.springframework.scheduling.annotation.Async;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class PDFExporter {
CompletableFuture<List<GuildMember>> guildMembers;
public PDFExporter(CompletableFuture<List<GuildMember>> guildMembers) {
this.guildMembers = guildMembers;
}
@Async
public CompletableFuture<Void> export(HttpServletResponse response) {
return CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
return null;
});
}
}
使用 @Async
方法的服务:
接下来,我对您的公会服务可能是什么样子进行了有根据的猜测:
package de.scrum_master.spring.q66958382;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Service
public class GuildService {
@Async
public CompletableFuture<List<GuildMember>> findAll() {
return CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
return Arrays.asList(new GuildMember("Jane"), new GuildMember("John"), new GuildMember("Eve"));
});
}
@Async
public CompletableFuture<List<GuildMember>> findByType(String role) {
return CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
return Collections.singletonList(new GuildMember("Eve"));
});
}
}
分析方面要定位的组件:
这是您的示例组件,只是在 guildMembers
初始化方面略有简化。但它根本没有改变功能:
package de.scrum_master.spring.q66958382;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@Component
public class MyComponent {
@Autowired
private GuildService guildService;
@GetMapping("/members/exportpdf")
@Profiling
public void exportToPDF(@RequestParam(required = false) String role, HttpServletResponse response) throws IOException, ExecutionException, InterruptedException {
CompletableFuture<List<GuildMember>> guildMembers = role == null ? guildService.findAll() : guildService.findByType(role);
response.setContentType("application/pdf");
DateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss");
String currentDateTime = dateFormatter.format(new Date());
String headerKey = "Content-Disposition";
String headerValue = "inline; filename=guildmembers_" + currentDateTime + ".pdf";
response.setHeader(headerKey, headerValue);
PDFExporter exporter = new PDFExporter(guildMembers);
exporter.export(response).get();
}
}
驱动申请+@EnableAsync
配置:
package de.scrum_master.spring.q66958382;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.scheduling.annotation.EnableAsync;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
@SpringBootApplication
@Configuration
@EnableAsync
public class DemoApplication {
public static void main(String[] args) throws InterruptedException, IOException, ExecutionException {
try (ConfigurableApplicationContext appContext = SpringApplication.run(DemoApplication.class, args)) {
doStuff(appContext);
}
}
private static void doStuff(ConfigurableApplicationContext appContext) throws InterruptedException, IOException, ExecutionException {
MyComponent myComponent = appContext.getBean(MyComponent.class);
myComponent.exportToPDF("admin", new MockHttpServletResponse());
}
}
分析方面:
最后但同样重要的是,这是一个方面。它也与您提供的相同,只是在如何 return 结果方面稍微不那么复杂。但同样,这不会改变该方面按预期工作的事实:
package de.scrum_master.spring.q66958382;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.time.Instant;
@Aspect
@Component
public class ProfilingAspect {
private static final Logger logger = LoggerFactory.getLogger(ProfilingAspect.class);
@Around("@annotation(profilingAnnotation)")
public Object logDuration(ProceedingJoinPoint joinPoint, Profiling profilingAnnotation) throws Throwable {
long startTime = Instant.now().toEpochMilli();
try {
return joinPoint.proceed();
}
catch (Throwable e) {
logger.error(e.getMessage(), e);
throw e;
}
finally {
long duration = Instant.now().toEpochMilli() - startTime;
logger.info(joinPoint.getSignature().toShortString() + ": " + duration + " ms");
}
}
}
控制台日志:
如果您 运行 应用程序,控制台日志显示:
. ____ _ __ _ _
/\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.8.RELEASE)
2021-04-06 09:18:10.793 INFO 13616 --- [ main] d.s.spring.q66958382.DemoApplication : Starting DemoApplication on Xander-Ultrabook with PID 13616 (C:\Users\alexa\Documents\java-src\spring-aop-playground\target\classes started by alexa in C:\Users\alexa\Documents\java-src\spring-aop-playground)
(...)
2021-04-06 09:18:14.809 INFO 13616 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2021-04-06 09:18:14.812 INFO 13616 --- [ main] d.s.spring.q66958382.DemoApplication : Started DemoApplication in 4.815 seconds (JVM running for 7.782)
(...)
2021-04-06 09:18:15.839 INFO 13616 --- [ main] d.s.spring.q66958382.ProfilingAspect : MyComponent.exportToPDF(..): 1014 ms
(...)
因此,如果它在您自己的应用程序中不起作用,要么与您的描述有所不同,要么您误解了日志。
据我了解,Spring AOP 上的 @Around
注释可以处理方法上的任何 return 类型;使用 void
类型 returning null.
这是记录方法持续时间的简单建议:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Profiling { }
@Aspect
@Component
public class ProfilingAspect {
// ...
@Around("@annotation(profilingAnnotation)")
public Object logDuration(ProceedingJoinPoint joinPoint, Profiling profilingAnnotation) throws Throwable {
long startTime = Instant.now().toEpochMilli();
Object result = null;
try {
result = joinPoint.proceed(); // on void methods, this supposed to return null
} catch (Throwable e) {
logger.error(e.getMessage(), e);
throw e;
} finally {
long endTime = Instant.now().toEpochMilli(); // Below is not ran all together
long duration = endTime - startTime;
logger.info(joinPoint.getSignature().toShortString()+": "+duration+"ms");
}
//return the result to the caller
return result;
}
}
但是,当调用此方法时,它不会 return 任何东西,而是完全跳过 proceed()
之后的所有代码。甚至 finally
块。
这是有问题的代码:
@GetMapping("/members/exportpdf")
@Profiling
public void exportToPDF(@RequestParam(required = false)String role, HttpServletResponse response) throws DocumentException, IOException, ExecutionException, InterruptedException {
CompletableFuture<List<GuildMember>> guildMembers;
if (role==null) {
guildMembers = guildService.findAll(); // Async Method
} else {
guildMembers = guildService.findByType(role); // Async Method
}
response.setContentType("application/pdf");
DateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss");
String currentDateTime = dateFormatter.format(new Date());
String headerKey = "Content-Disposition";
String headerValue = "inline; filename=guildmembers_" + currentDateTime + ".pdf";
response.setHeader(headerKey, headerValue);
PDFExporter exporter = new PDFExporter(guildMembers);
exporter.export(response).get(); // exporter.export(..) is an Async method returning CompletableFuture<Void>
}
这怎么可能?我在配置中遗漏了什么吗?还是 Spring 上的错误?
注意。我正在使用 Spring Boot 2.4.4 与启动依赖项
编辑。 PDFExporter.export()
在 HttpServletResponse
上使用 OutputStream
向用户打印 application/pdf
和 returns CompletableFuture<Void>
。至于为什么,该方法与上面的异步函数通信,因此我想保证操作以某种方式完成。
为了教大家什么是MCVE以及如何在这里更好地提问,我将向您展示我根据您的代码片段和描述创建的MCVE:
依赖类我们需要为了让代码编译:
package de.scrum_master.spring.q66958382;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Profiling {}
package de.scrum_master.spring.q66958382;
public class GuildMember {
private String name;
public GuildMember(String name) {
this.name = name;
}
@Override
public String toString() {
return "GuildMember{" +
"name='" + name + '\'' +
'}';
}
}
PDFExporter
效用:
也许您正在使用 PrimeFaces 的 PDFExporter
,但这只是一个猜测。在任何情况下,它似乎都不是 Spring 组件,因为稍后您调用构造函数而不是从应用程序上下文中获取 bean 实例。所以我在这里也将它建模为一个简单的 POJO。
package de.scrum_master.spring.q66958382;
import org.springframework.scheduling.annotation.Async;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class PDFExporter {
CompletableFuture<List<GuildMember>> guildMembers;
public PDFExporter(CompletableFuture<List<GuildMember>> guildMembers) {
this.guildMembers = guildMembers;
}
@Async
public CompletableFuture<Void> export(HttpServletResponse response) {
return CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
return null;
});
}
}
使用 @Async
方法的服务:
接下来,我对您的公会服务可能是什么样子进行了有根据的猜测:
package de.scrum_master.spring.q66958382;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Service
public class GuildService {
@Async
public CompletableFuture<List<GuildMember>> findAll() {
return CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
return Arrays.asList(new GuildMember("Jane"), new GuildMember("John"), new GuildMember("Eve"));
});
}
@Async
public CompletableFuture<List<GuildMember>> findByType(String role) {
return CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
return Collections.singletonList(new GuildMember("Eve"));
});
}
}
分析方面要定位的组件:
这是您的示例组件,只是在 guildMembers
初始化方面略有简化。但它根本没有改变功能:
package de.scrum_master.spring.q66958382;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@Component
public class MyComponent {
@Autowired
private GuildService guildService;
@GetMapping("/members/exportpdf")
@Profiling
public void exportToPDF(@RequestParam(required = false) String role, HttpServletResponse response) throws IOException, ExecutionException, InterruptedException {
CompletableFuture<List<GuildMember>> guildMembers = role == null ? guildService.findAll() : guildService.findByType(role);
response.setContentType("application/pdf");
DateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss");
String currentDateTime = dateFormatter.format(new Date());
String headerKey = "Content-Disposition";
String headerValue = "inline; filename=guildmembers_" + currentDateTime + ".pdf";
response.setHeader(headerKey, headerValue);
PDFExporter exporter = new PDFExporter(guildMembers);
exporter.export(response).get();
}
}
驱动申请+@EnableAsync
配置:
package de.scrum_master.spring.q66958382;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.scheduling.annotation.EnableAsync;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
@SpringBootApplication
@Configuration
@EnableAsync
public class DemoApplication {
public static void main(String[] args) throws InterruptedException, IOException, ExecutionException {
try (ConfigurableApplicationContext appContext = SpringApplication.run(DemoApplication.class, args)) {
doStuff(appContext);
}
}
private static void doStuff(ConfigurableApplicationContext appContext) throws InterruptedException, IOException, ExecutionException {
MyComponent myComponent = appContext.getBean(MyComponent.class);
myComponent.exportToPDF("admin", new MockHttpServletResponse());
}
}
分析方面:
最后但同样重要的是,这是一个方面。它也与您提供的相同,只是在如何 return 结果方面稍微不那么复杂。但同样,这不会改变该方面按预期工作的事实:
package de.scrum_master.spring.q66958382;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.time.Instant;
@Aspect
@Component
public class ProfilingAspect {
private static final Logger logger = LoggerFactory.getLogger(ProfilingAspect.class);
@Around("@annotation(profilingAnnotation)")
public Object logDuration(ProceedingJoinPoint joinPoint, Profiling profilingAnnotation) throws Throwable {
long startTime = Instant.now().toEpochMilli();
try {
return joinPoint.proceed();
}
catch (Throwable e) {
logger.error(e.getMessage(), e);
throw e;
}
finally {
long duration = Instant.now().toEpochMilli() - startTime;
logger.info(joinPoint.getSignature().toShortString() + ": " + duration + " ms");
}
}
}
控制台日志:
如果您 运行 应用程序,控制台日志显示:
. ____ _ __ _ _
/\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.8.RELEASE)
2021-04-06 09:18:10.793 INFO 13616 --- [ main] d.s.spring.q66958382.DemoApplication : Starting DemoApplication on Xander-Ultrabook with PID 13616 (C:\Users\alexa\Documents\java-src\spring-aop-playground\target\classes started by alexa in C:\Users\alexa\Documents\java-src\spring-aop-playground)
(...)
2021-04-06 09:18:14.809 INFO 13616 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2021-04-06 09:18:14.812 INFO 13616 --- [ main] d.s.spring.q66958382.DemoApplication : Started DemoApplication in 4.815 seconds (JVM running for 7.782)
(...)
2021-04-06 09:18:15.839 INFO 13616 --- [ main] d.s.spring.q66958382.ProfilingAspect : MyComponent.exportToPDF(..): 1014 ms
(...)
因此,如果它在您自己的应用程序中不起作用,要么与您的描述有所不同,要么您误解了日志。