spring-native
spring-native copied to clipboard
AOP improvement ideas
I created a small project and played around AOP with Spring Native.
Sample Classes
Interface based service:
public interface HelloService {
String hello();
}
public class HelloServiceImpl implements HelloService {
public String hello() {
return "Hello";
}
}
Class based service:
public class HiService {
public String hi() {
return "Hi";
}
}
Annotated class:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Greeter {
}
@Service
public class GreetingService {
@Greeter
public String greet() {
return "Greeting";
}
}
Aspect:
@Aspect
public class MyAspect {
@Around(value = "execution(* com.example.demonativeaop.HelloService.*(..))")
public String helloIntroduction(ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed() + " (hello intercepted)";
}
@Around(value = "execution(* com.example.demonativeaop.HiService.*(..))")
public String hiIntroduction(ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed() + " (hi intercepted)";
}
@Around(value = "@annotation(com.example.demonativeaop.Greeter)")
// @Around(value = " within(com.example.demonativeaop..*) && @target(com.example.demonativeaop.Greeter)")
public String greeterIntroduction(ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed() + " (greeter intercepted)";
}
}
Here are some of the findings and improvement ideas.
@Aspect
class
MyAspect
class is annotated with @Aspect
.
Declared as @Bean
When MyAspect
is declared as a @Bean
:
@Bean
public MyAspect myAspect() {
return new MyAspect();
}
The reflect-config.json
requires this entry.
{
"name": "com.example.demonativeaop.MyAspect",
"allDeclaredMethods": true
}
This is because methods in the @Aspect
class define pointcuts, but usually they are not referenced by the application.
Those methods need to be retained to create AOP proxies.
Improvement idea
Automatically add hint when a bean with @Aspect
is found.
Annotated as @Component
When MyAspect
class is annotated with @Componenet
, the component scan finds the bean.
In this case, no manual configuration is required.
@Aspect
@Component
public class MyAspect {
...
}
The generated reflect-config.json
has this entry:
{
"name": "com.example.demonativeaop.MyAspect",
"allDeclaredFields": true,
"allDeclaredConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredClasses": true,
"methods": [
{
"name": "<init>",
"parameterTypes": []
}
]
}
Reference in AOP expression
For a pointcut expression execution(* com.example.demonativeaop.HiService.*(..))
, the reflect-config.json
requires this entry:
{
"name": "com.example.demonativeaop.HiService",
"allDeclaredMethods": true
}
Otherwise, this exception is thrown at runtime:
...
Caused by: java.lang.IllegalArgumentException: warning no match for this type name: com.example.demonativeaop.HiService [Xlint:invalidAbsoluteTypeName]
at org.aspectj.weaver.tools.PointcutParser.parsePointcutExpression(PointcutParser.java:319) ~[na:na]
at org.springframework.aop.aspectj.AspectJExpressionPointcut.buildPointcutExpression(AspectJExpressionPointcut.java:227) ~[na:na]
...
For annotation based pointcut, @annotation(com.example.demonativeaop.Greeter)
, it requires the annotation to be in reflect-config.json
as well.
{
"name": "com.example.demonativeaop.Greeter",
"allDeclaredMethods": true
}
Without the entry, this error is thrown:
...
Caused by: java.lang.IllegalArgumentException: error Type referred to is not an annotation type: com$example$demonativeaop$Greeter
at org.aspectj.weaver.tools.PointcutParser.parsePointcutExpression(PointcutParser.java:319) ~[na:na]
at org.springframework.aop.aspectj.AspectJExpressionPointcut.buildPointcutExpression(AspectJExpressionPointcut.java:227) ~[na:na]
...
Improvement idea
It would be nice if AOT parses the AOP expression and figures out any classes that has reference to, then auto generate hints for them.
AOP Proxies
In spring-native, there are two phases for AOP proxies.
The first phase is at AOT.
For class based proxies, ConfigurationContributor
and ProxyGenerator
generate proxy classes with ByteBuddy(e.g. com.example.demonativeaop.HiService$$SpringProxy$7f5de2cd
) and write class files under target/generated-sources/spring-aot/src/main/resources/../*.class
.
Also, it adds the generated proxies to the reflect-config.json
in order for runtime to read them.
The next phase is at runtime.
The Target_DefaultAopProxyFactory
substitutes the AOP proxy creation.
For class based proxies, it uses BuildTimeAopProxy
(ObjenesisBuildTimeAopProxy
) which loads the ByteBuddy generated proxies and instantiate them.
To bridge these two phases, the ProxyConfiguration#getProxyClassName()
method guarantees the same name is generated based on the advised interfaces.
Class based proxy:
The HiService
is a class based proxy service.
In order to generate a proxy for this class at build time, this hint is required.
@AotProxyHint(targetClass=com.example.demonativeaop.HiService.class, proxyFeatures = ProxyBits.IS_STATIC)
The hint is read by ConfigurationContributor#generateBuildTimeClassProxies
and generates ByteBuddy proxy classes.(e.g. com.example.demonativeaop.HiService$$SpringProxy$7f5de2cd
)
Improvement Idea
Currently, the user needs to add @AotProxyHint
to trigger the ByteBuddy proxy generation.
It would be nice if this is not required; so that, build time automatically detects class based proxies for AOP and generates/persists proxies.
Interface based proxy
The HelloService
is interfaced based proxy service. (with HelloServiceImpl
)
This uses JdkDynamicAopProxy
to generate AOP proxy at runtime.
Since this is a regular dynamic proxy at runtime, proxy-config.json
requires this entry.
[
"com.example.demonativeaop.HelloService",
"org.springframework.aop.SpringProxy",
"org.springframework.aop.framework.Advised",
"org.springframework.core.DecoratingProxy"
]
Improvement idea
Since this proxy entry is infrastructure logic to generate dynamic proxy, it would be nice that AOT automatically adds this hint at build time.
Any thoughts @aclement?
Yep, step 1 was at least have some kind of system that would make anything in this space possible, and we have that now (and it seems to work pretty well). Then step 2 is build on that with various kinds of intelligence. I appreciate the detail/though in this writeup :)
Obviously there will be certain point cut patterns from which it will be tricky to determine the involved types - depending on how far you go with analysis. The AspectJ point cut parser is a standalone usable entity.
For this it is easy:
execution(* com.example.demonativeaop.HiService.*(..))
with simple analysis (string regex or preferably parsing the point cut with the AspectJ parser and walking the parsed structure).
But if there are more wildcards involved execution(* com.foo.bar..*.*(..))
it'll get trickier - having to drive point cut matching against potential component candidates. This should be doable however but we'd have to keep an eye on a potential explosion of proxy classes.
However, a first step of explicit type names would be a great step forward.
Spring Native is now superseded by Spring Boot 3 official native support, see the related reference documentation for more details.
As a consequence, I am closing this issue, and recommend trying your use case with latest Spring Boot 3 version. If you still experience the issue reported here, please open an issue directly on the related Spring project (Spring Framework, Data, Security, Boot, Cloud, etc.) with a reproducer.
Thanks for your contribution on the experimental Spring Native project, we hope you will enjoy the official native support introduced by Spring Boot 3.