When a Developer defines a http interface, the new feature released in Spring Boot 3:
https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#rest-http-interface
The Bean is not registered in the Spring Container with the right package:
Example:
Http Interface definition:
public interface GodService {
@GetExchange()
List<String> getGods();
}
Http Interface configuration:
@Configuration(proxyBeanMethods = false)
public class WebConfiguration {
//Spring Web Client
@Value("${address}")
private String address;
@Bean
WebClient webClient() {
return WebClient.builder()
.baseUrl(address)
.build();
}
//Spring http interfaces
@Bean
GodService godService(WebClient client) {
HttpServiceProxyFactory httpServiceProxyFactory = HttpServiceProxyFactory
.builder(WebClientAdapter.forClient(client))
.build();
return httpServiceProxyFactory.createClient(GodService.class);
}
}
And you list all Beans running in the Spring Boot Container:
@TestConfiguration
public class BeanInventoryConfiguration {
@Autowired
private ConfigurableApplicationContext applicationContext;
public record Tuple(String beanName, String pkg) {}
public record BeanInventory(List<Tuple> beans) {}
@Bean
public BeanInventory getBeanInventory(ConfigurableApplicationContext applicationContext) {
BiFunction<ConfigurableApplicationContext, String, Tuple> getTuple = (context, beanName) -> {
String pkg = Objects.toString(context.getType(beanName).getCanonicalName());
return new Tuple(beanName, Objects.isNull(pkg) ? "" : pkg);
};
String[] allBeanNames = applicationContext.getBeanDefinitionNames();
return new BeanInventory(Arrays.stream(allBeanNames)
.map(str -> getTuple.apply(applicationContext, str))
.toList());
}
}
then the User Bean generated by a http interface is not registered with the right java package:
23 godService jdk.proxy2.$Proxy67
The right package should be in the example:
23 godService info.jab.ms.service.WebConfiguration
or
23 godService info.jab.ms.service
At the moment, all beans generated by the user, are registered with the right package for the annotations: @SpringBootApplication, @RestController, @Service, @Configuration & @Bean:
1 mainApplication info.jab.ms.MainApplication$$SpringCGLIB$$0
2 myController1 info.jab.ms.controller.MyController1
3 myController2 info.jab.ms.controller.MyController2
4 myController3 info.jab.ms.controller.MyController3
5 restemplate info.jab.ms.service.MyServiceRestTemplateImpl
6 webclient info.jab.ms.service.MyServiceWebClientImpl
7 webConfiguration info.jab.ms.service.WebConfiguration
But not the Bean registered by a http interface using the HttpServiceProxyFactory: https://github.com/spring-projects/spring-framework/blob/ac521a366ab93cb56836617f67c2f469e9347c81/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java#L99
How to reproduce the scenario? https://github.com/jabrena/spring-boot-http-client-poc/blob/main/src/test/java/info/jab/ms/BeanInventoryTests.java#L26-L44
Many thanks in advance
Juan Antonio
Comment From: simonbasle
What are you trying to achieve exactly? It seems you are working on a wrong assumption. There's no real "registering of a package" for beans, and what you obtain (through custom code) is merely the raw Class corresponding to each beanName in the beanFactory.
Spring Framework makes extensive and transparent use of proxies. Note that this can be influenced in various ways, including @Configuration(proxyBeanMethods = false) (which you use) or the marking of a bean as @Lazy.
The difference in "package names" you noticed is due to the presence of proxies, and to the fact that some are CGLib proxies and others like the godService one are JDK dynamic proxies. On a side note, you're not actually looking at package name information but rather the full Class#getCanonicalName()...
This issue is thus more of a question, which should preferably be on StackOverflow. Nevertheless, I've compiled some information on how to deal with the proxies below:
CGLib proxies work on a target class that the proxy subclasses (while possibly also adding interfaces). As a result, there is a definite answer to "what's the target class" and it's the Class#getSuperclass().
JDK proxies work on one or more interfaces. The fact that it can work on multiple interfaces is a likely reason why it can't generate a proxy class in the target class' package (since there is potentially multiple arbitrary candidate packages).
From the Proxy javadoc (emphasis mine):
If all the proxy interfaces are in exported or open packages: if all the proxy interfaces are public, then the proxy class is public in an unconditionally exported but non-open package. The name of the package and the module are unspecified.
Now, how to accommodate proxies in that code that lists bean names and "packages"?
You could try to use AopUtils, but you need an instance of the bean. That could be problematic if the bean is not a singleton, or is lazy, or in general if you want to avoid instantiation for the sole purpose of discovering the package.
AopUtils.getTargetClass correctly covers CGLib proxies (for the reasons explained above) as well as the TargetClassAware interface. For JDK proxies there is more work as you need to inspect the interfaces and find the most relevant one (again, no clear candidate due to the fact that JDK proxies can proxy a combination of interfaces). This is further complicated by the fact that all Spring Framework proxies are marked with 2-3 additional interfaces (e.g. SpringProxy). The AopProxyUtils#proxiedUserInterfaces(Object bean) method can help filtering out these markers, at least, when you have a bean instance.
It would look like this:
record BeanInformation(String beanName, String packageName, String beanClass) {};
final List<BeanInformation> beans = new ArrayList<>();
final String[] names = beanFactory.getBeanDefinitionNames();
for (String name : names) {
final Object bean = beanFactory.getBean(name);
Class<?> targetClass = AopUtils.getTargetClass(bean);
if (AopUtils.isJdkDynamicProxy(bean)) {
Class<?>[] proxiedInterfaces = AopProxyUtils.proxiedUserInterfaces(bean);
Assert.assertTrue("Only one proxied interface expected", proxiedInterfaces.length == 1);
targetClass = proxiedInterfaces[0];
}
beans.add(new BeanInformation(name, targetClass.getPackageName(), targetClass.getSimpleName()));
}
beans.forEach(info -> System.err.println(info.beanName() + " -> " + info.beanClass() + " in package " + info.packageName()));
If you only want to deal with Class and not bean instances, it gets trickier as you have to detect a Class is a proxy class manually:
- without an instance, the TargetClassAware case cannot be resolved
- the CGLib case can be detected with beanClass.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)
- the JDK proxy case can be detected with Proxy.isProxyClass(beanClass)
- You will also have to manually filter interfaces in the case of a JDK Proxy class to correctly find the relevant user interface...
Comment From: jabrena
Good morning @simonbasle,
First, I would like to give you thanks because I was wrong in my approach and obviously, I don´t have that expertise in Proxies. Said it, I will continue learning about Spring and the part about Proxies because it is a gap in my side.
Many thanks
Juan Antonio