围绕 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
(...)

因此,如果它在您自己的应用程序中不起作用,要么与您的描述有所不同,要么您误解了日志。