Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Register http interface beans with the right package #29973

Closed
jabrena opened this issue Feb 15, 2023 · 2 comments
Closed

Register http interface beans with the right package #29973

jabrena opened this issue Feb 15, 2023 · 2 comments
Assignees
Labels
for: stackoverflow A question that's better suited to stackoverflow.com status: invalid An issue that we don't feel is valid

Comments

@jabrena
Copy link

jabrena commented Feb 15, 2023

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:

return ProxyFactory.getProxy(serviceType, new HttpServiceMethodInterceptor(httpServiceMethods));

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

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Feb 15, 2023
@simonbasle simonbasle added status: invalid An issue that we don't feel is valid for: stackoverflow A question that's better suited to stackoverflow.com and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Feb 15, 2023
@simonbasle
Copy link
Contributor

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...

@simonbasle simonbasle closed this as not planned Won't fix, can't repro, duplicate, stale Feb 15, 2023
@simonbasle simonbasle self-assigned this Feb 15, 2023
@jabrena
Copy link
Author

jabrena commented Feb 16, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
for: stackoverflow A question that's better suited to stackoverflow.com status: invalid An issue that we don't feel is valid
Projects
None yet
Development

No branches or pull requests

3 participants