Java Spring - how to inject #Value to a data object? - java

I have a data object (with getters\setter only) that needs to be aware of the Spring profile, i.e.
#Value("${spring.profiles.active}")
private String profile;
I added a logic to one of it's 'set' method that checks the profile, i.e.
public void setItem(Item msg) {
if (environmentProperties.isDevMode()) {
this.msg= msg;
}
}
since this class is often marshal\unmarhsalled externally, so, of course the #Value isn't being populated - sine I didn't use spring Autowire to create the class instance... I tried defined the class as component, and autowire to an external class that holds the profile #Value - but it doesn't work
I use spring 3.2 - with no XML definition.
any suggestions?
b.t.w.
that data-objects often wrapped inside an exception class - so when it's created the profile should also be known to the data-object...
thanks!
EDITED:
using ApplicationContextAware doesn't work - I get null the 'setApplicationContext' method is never invoked.
also trying to get context directly doesn't work - get null instead when using:
'ApplicationContext ctx = ContextLoader.getCurrentWebApplicationContext();'
FIXED:
I've eventually found an example how to access the context staticly from an external class:
#Configuration
public class ApplicationContextContainer implements ApplicationContextAware {
private static ApplicationContext CONTEXT;
/**
* This method is called from within the ApplicationContext once it is
* done starting up, it will stick a reference to itself into this bean.
*
* #param context a reference to the ApplicationContext.
*/
#Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
CONTEXT = context;
}
/**
* This is about the same as context.getBean("beanName"), except it has its
* own static handle to the Spring context, so calling this method statically
* will give access to the beans by name in the Spring application context.
* As in the context.getBean("beanName") call, the caller must cast to the
* appropriate target class. If the bean does not exist, then a Runtime error
* will be thrown.
*
* #param beanName the name of the bean to get.
* #return an Object reference to the named bean.
*/
public static Object getBean(String beanName) {
return CONTEXT.getBean(beanName);
}

If I understand you correctly you want to inject into Objects not managed by Spring, but created by some other code that internally calls new and returns objects e.g. a serialization framework.
To inject unmanaged Objects you will need to configure either load-time or compile-time weaving. Load-time weaving requires an agent argument and lib when you start your VM, some containers might do this for you.
Compile-time weaving requires the use of the AspectJ compiler.
Below you will find a complete example using Maven and Spring-Boot:
E.g. run it with:
mvn spring-boot:run -Drun.arguments="--spring.profiles.active=dev"
DemoApplication.java:
package com.example;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.aspectj.EnableSpringConfigured;
import org.springframework.stereotype.Component;
#SpringBootApplication
public class DemoApplication {
#EnableSpringConfigured
#ComponentScan("com.example")
public static class AppConfiguration {
#Value("${spring.profiles.active}")
String profile;
#Bean
public String profile() {
return profile;
}
}
#Configurable
public static class SomePojo {
#Autowired
private String profile;
public void print() {
System.out.println(this + "\t" + profile);
}
}
#Component
public static class Runner {
public void run() {
new SomePojo().print();
new SomePojo().print();
}
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args).getBean(Runner.class).run();
}
}
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.3.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.8</version>
<configuration>
<complianceLevel>1.8</complianceLevel>
<aspectLibraries>
<aspectLibrary>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</aspectLibrary>
</aspectLibraries>
</configuration>
<executions>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

From your description, you're trying to inject the property into POJO, which is used in marshalling. With this structure you may look for different workarounds with static-based/any other complex solutions.
I'd suggest to separate the bean which is used as POJO from the logic which depends on the property value. You can extract that logic to BeanService (which can be placed to Spring context) and handle it on that level, so that you separate the responsibility between Service and Data layers.

You're doing it wrong. Your code does not need to be aware of the profile. In your example, create a Message interface, and a number of bean implementations of this interface, one for each profile, each containing an appropriate message for that profile, and assign each one to a profile so that the bean is instantiated for that profile, and inject the instance into the class that needs the message.
So,
public interface Message { String getMessage(); }
#Profile("dev") #Component
public class DevMessage implements Message {
public String getMessage() { return "this is the dev message"; }
}
#Profile("prod") #Component
public class ProdMessage implements Message {
public String getMessage() { return "this is the production message"; }
}
If you prefer to describe your beans in your #Configuration class, you can mark a whole configuration with an #Profile, and have multiple configurations.
If you inject the Message instance into a class, you can call getMessage() on it. The profile will ensure that you have the appropriate implementation for your environment.
Edit:
I've just reread your question and realised that I've got this wrong. You have entity objects stored outside the application and instantiated through some code/framework. These aren't spring components, and so can't use the spring approach to dependency injection. In this case, don't use spring for them -- it doesn't work, doesn't have to work, and shouldn't work. If you haven't instantiated the object through spring, then it should have nothing to do with spring. I don't know your problem domain, but I've been using spring since it was invented and have never ever had to do this.

Related

I can't build a Spring Cloud Gateway project with OAuth 2.0

I'm trying to build a Backend Service project using the example from the site
Using Spring Cloud Gateway with OAuth 2.0 Patterns
Here is the repository itself
backend
Added dependencies
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>ru.test.gw.oauth.resource</groupId>
<artifactId>backresource</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>backresource</name>
<description>Demo project for Spring Boot</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>14</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
I moved the properties from the quotes-application.properties file to this project
server.port=11002
# Resource server settings
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=http://localhost:8484/auth/realms/demo/protocol/openid-connect/token/introspect
spring.security.oauth2.resourceserver.opaquetoken.client-id=gateway
spring.security.oauth2.resourceserver.opaquetoken.client-secret=dfdslksfkljweewrfsd
Added a class
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
import reactor.core.publisher.Mono;
// Custom ReactiveTokenIntrospector to map realm roles into Spring GrantedAuthorities
public class KeycloakReactiveTokenInstrospector implements ReactiveOpaqueTokenIntrospector {
private final ReactiveOpaqueTokenIntrospector delegate;
public KeycloakReactiveTokenInstrospector(ReactiveOpaqueTokenIntrospector delegate) {
this.delegate = delegate;
}
#Override
public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
return delegate.introspect(token)
.map( this::mapPrincipal);
}
protected OAuth2AuthenticatedPrincipal mapPrincipal(OAuth2AuthenticatedPrincipal principal) {
return new DefaultOAuth2AuthenticatedPrincipal(
principal.getName(),
principal.getAttributes(),
extractAuthorities(principal));
}
protected Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
//
Map<String,List<String>> realm_access = principal.getAttribute("realm_access");
List<String> roles = realm_access.getOrDefault("roles", Collections.emptyList());
List<GrantedAuthority> rolesAuthorities = roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
Set<GrantedAuthority> allAuthorities = new HashSet<>();
allAuthorities.addAll(principal.getAuthorities());
allAuthorities.addAll(rolesAuthorities);
return allAuthorities;
}
}
And the main class of the project
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.oauth2.server.resource.introspection.NimbusReactiveOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
import ru.test.gw.oauth.resource.backresource.security.KeycloakReactiveTokenInstrospector;
#SpringBootApplication
//#PropertySource("classpath:quotes-application.properties")
#EnableWebFluxSecurity
public class BackresourceApplication {
public static void main(String[] args) {
SpringApplication.run(BackresourceApplication.class, args);
}
#Bean
public SpringOpaqueTokenIntrospector keycloakIntrospector(OAuth2ResourceServerProperties props) {
NimbusReactiveOpaqueTokenIntrospector delegate = new NimbusReactiveOpaqueTokenIntrospector(
props.getOpaquetoken().getIntrospectionUri(),
props.getOpaquetoken().getClientId(),
props.getOpaquetoken().getClientSecret());
return new KeycloakReactiveTokenInstrospector(delegate);
}
}
And in this class I get an error on SpringOpaqueTokenIntrospector, writes that it is not defined. Although all the imports completely coincide with the training example.
If I add a dependency that the IDE tells me to
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
, then I get an error
Type mismatch: cannot convert from KeycloakReactiveTokenInstrospector to SpringOpaqueTokenIntrospector
What's the problem here? Is there some kind of dependency missing?
I completely repeated the structure of the project from the training material.
So far, I would like to build a project without errors.
Marcus is right in his comment, your keycloakIntrospector #Bean type should be ReactiveOpaqueTokenIntrospector (and not SpringOpaqueTokenIntrospector as declared in your conf)
Few facts:
SpringReactiveOpaqueTokenIntrospector is a ReactiveOpaqueTokenIntrospector but SpringOpaqueTokenIntrospector isn't
your KeycloakReactiveTokenInstrospector is (implements) a ReactiveOpaqueTokenIntrospector too but is neither a SpringReactiveOpaqueTokenIntrospector, SpringOpaqueTokenIntrospector nor OpaqueTokenIntrospector
Side notes
Introspection VS JWT decoding
Keycloak issues JWTs. JWT decoding is far more efficient than introspection: resource-server needs to fetch public-key only once from authorization-server to validate all incoming JWTs when introspection requires to submit access-token to authorization-server for each and every incoming request.
Also, you might not be able to implement multi-tenant scenarios with introspection: how to figure out by which issuer (Keycloak instance or realm) an opaque token was emitted? => you would have to "try" introspection on each issuer until one responds positively :/
Overriding introspector VS providing an authentication converter
If you switch to spring-security 5.8 or higher, customizing introspection is easier: you don't have to override the all introspector but can just provide a ReactiveOpaqueTokenAuthenticationConverter bean instead:
http.oauth2ResourceServer().opaqueToken().authenticationConverter(
(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) ->
new BearerTokenAuthentication(...));
This bean is called after introspection was successfuly completed (and token attributes retrieved) but before Authentication is instanciated and put in security-context which allows you to just map authorities from any attribute you like or completely switch the authentication implementation.
Simplifying your resource-server configuration
I host a set of libs to ease OAuth2 resource-server testing and configuration. There are various spring-boot starters depending on introspection or JWT decoding is used into servlet or reactive apps.
According to your case (reactive app with introspection), you should have a look at this sample with BearerTokenAuthentication and this other one with a custom authentication.
Configuration can be as simple as:
#EnableReactiveMethodSecurity
#Configuration
public class SecurityConfig {
}
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://localhost:8443/realms/master/protocol/openid-connect/token/introspect
spring.security.oauth2.resourceserver.opaquetoken.client-id=spring-addons-confidential
spring.security.oauth2.resourceserver.opaquetoken.client-secret=change-me
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,resource_access.spring-addons-public.roles,resource_access.spring-addons-confidential.roles
# this is probably too permissive, addapt to your needs
com.c4-soft.springaddons.security.cors[0]=/**
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webflux-introspecting-resource-server</artifactId>
<version>6.0.3</version><!-- warning, this version goes with spring-boot 3.0.0-RC1 -->
</dependency>

Spring Integration 5.1 - integration flow convertion with #IntegrationConverter doesn't work

I upgrade my Spring boot version from 2.0.5.RELEASE to 2.1.8.RELEASE (so Spring Integration from 5.0 to 5.1) and the automatic type casting inside integration flow doesn't work anymore. I am used to define a set of #IntegrationConverter components and automatic casting with the operation transform(Type.class, p -> p) inside integration flow code but with the new version it seems to be broken.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.grorg</groupId>
<artifactId>grointegration</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>grointegration</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-ip</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
And the Main.java file:
package org.grorg.grointegration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.convert.converter.Converter;
import org.springframework.integration.config.IntegrationConverter;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.Transformers;
import org.springframework.integration.ip.dsl.Tcp;
import org.springframework.stereotype.Component;
class Test {
#Override
public String toString() {
return "test";
}
}
#Component
#IntegrationConverter
class Convert implements Converter<String, Test> {
#Override
public Test convert(String s) {
return new Test();
}
}
#SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(GrointegrationApplication.class, args);
}
#Bean
public IntegrationFlow server() {
return IntegrationFlows
.from(Tcp.inboundGateway(Tcp.netServer(1234)))
.transform(Transformers.objectToString())
.transform(Test.class, id -> id) // in 2.1 I could use .convert(Test.class) but the problem is the same
.log()
.handle((p, h) -> "OK")
.get();
}
}
Use with a shell:
telnet localhost 1234
> test
> OK
[...]
With the previous version (2.0.5.RELEASE) the program work nicely like previously, but with the new version (2.1.8.RELEASE) I get this error (and no "OK" response):
org.springframework.integration.handler.ReplyRequiredException: No reply produced by handler 'server.org.springframework.integration.config.ConsumerEndpointFactoryBean#1', and its 'requiresReply' property is set to true.
[...]
What I have found is that the ConversionService has been remplaced by MessageConverter and now Jackson is used to transform message from one type to another.
Am I wrongly using type casting with integration flow? Do you have a new solution for casting object with the new version? Or is this just a regression?
Thanks in advance!
This is a bug in Spring Integration.
We just don't use a proper ConversionService in the GenericMessageConverter.
Please, raise a GH issue on the matter.
Meanwhile a workaround for you is like this:
#Bean(name = IntegrationContextUtils.ARGUMENT_RESOLVER_MESSAGE_CONVERTER_BEAN_NAME)
public static ConfigurableCompositeMessageConverter configurableCompositeMessageConverter(
#Qualifier(IntegrationUtils.INTEGRATION_CONVERSION_SERVICE_BEAN_NAME) ConversionService conversionService) {
return new ConfigurableCompositeMessageConverter(
Collections.singleton(new GenericMessageConverter(conversionService)));
}
So, we override whatever is configured by the framework and inject a proper IntegrationUtils.INTEGRATION_CONVERSION_SERVICE_BEAN_NAME bean which is supplied with your #IntegrationConverter component.

Changing Autowired bean type unexpectedly changes value?

I've tried to create an Application and ran into some funky behavior. First, I'll run through my setup. Here's my configuration class:
ProblemApp.java
#SpringBootApplication
public class ProblemApp
{
public static void main(String[] args)
{
var context = SpringApplication.run(ProblemApp.class);
var tblController = context.getBean(TableController.class);
tblController.printTable();
}
#Bean
public TableController getTableController()
{
return new TableController();
}
#Bean("componentTable")
public String[] getComponentTable() //weird
{
return new String[] { "application.components" };
}
}
Here's my component:
TableController.java
#Controller
public class TableController
{
private static final Logger log = LoggerFactory.getLogger(TableController.class);
#Autowired
private String[] componentTable; //weird
public void printTable()
{
log.info("Component table: " + Arrays.deepToString(componentTable));
}
}
This is my module-info.java and directory structure:
module problem.application
{
exports application;
exports controller;
opens controller to spring.core;
opens application to spring.core;
requires spring.context;
requires spring.boot;
requires spring.boot.autoconfigure;
requires spring.beans;
requires spring.core;
requires java.sql;
requires org.slf4j;
}
src/main/java/
application
ProblemApp.java
controller
TableController.java
module-info.java
src/test/java/ is empty
src/main/resources/ is empty
This is the pom.xml I use to retrieve dependencies (using Maven in Spring Tool Suite 4):
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>problem.application</groupId>
<artifactId>problem-app</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Problematic application</name>
<description>An application with some problems</description>
<build>
<sourceDirectory>src/main/java</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>12</release>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-parent -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
This code, as it is, works as I expected it to. The relevant part of the output is:
2019-09-11 10:55:21.351 INFO 21060 --- [ main] controller.TableController :
Component table: [application.components]
Now, I tried to change the type of Bean componentTable to Object[]. In ProblemApp:
#Bean("componentTable")
public Object[] getComponentTable() //weird
{
return new Object[] { "application.components" };
}
In TableController:
#Autowired
private Object[] componentTable; //weird
Suddenly, the output of the program changes drastically:
2019-09-11 10:57:05.630 INFO 10156 --- [ main] controller.TableController :
Component table: [org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#b40bb6e,
org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#3f28bd56,
org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#3276732,
org.springframework.context.annotation.ConfigurationClassPostProcessor#f74e835,
org.springframework.context.support.PropertySourcesPlaceholderConfigurer#48c35007,
org.springframework.boot.admin.SpringApplicationAdminMXBeanRegistrar#4b3c354a,
org.springframework.context.event.EventListenerMethodProcessor#31e3250d,
org.springframework.context.event.DefaultEventListenerFactory#19fe4644,
application.ProblemApp$$EnhancerBySpringCGLIB$$7e1f363f#21d8bcbe,
...
// more Spring-looking things and then environment information
What is going on here?
When auto-wring on an array or a list of type T , it will first try to inject all beans which the type is T into it. So for :
#Autowired
private String[] componentTable;
It will first try to inject all beans which the type is String . However since there are no such beans, it will then try to inject a bean which the type is String[] . As you define componentTable bean as String[] , it will be injected.
Follow the same logic on :
#Autowired
private Object[] componentTable;
It will first try to inject all beans which the type is Object. As every bean must be an Object type . It means all spring beans will be injected. Hence it prints out all the spring bean information.

IncompatibleClassChangeError when instrumenting running SpringBoot application with Byte Buddy

I would like to introduce Byte Buddy to my company and I have prepared a demo for my colleagues. Since we use Spring a lot, I thought the best example would be instrumentation of SpringBoot application. I have decided to add logs to RestController methods.
Instrumented application is a simple SpringBoot Hello World example:
#RestController
public class HelloController {
private static final String template = "Hello, %s!";
#RequestMapping("/hello")
public String greeting(
#RequestParam(value = "name", defaultValue = "World") String name) {
return String.format(template, name);
}
#RequestMapping("/browser")
public String showUserAgent(HttpServletRequest request) {
return request.getHeader("user-agent");
}
}
And here is my Byte Buddy agent:
public class LoggingAgent {
public static void premain(String agentArguments,
Instrumentation instrumentation) {
install(instrumentation);
}
public static void agentmain(String agentArguments,
Instrumentation instrumentation) {
install(instrumentation);
}
private static void install(Instrumentation instrumentation) {
createAgent(RestController.class, "greeting")
.installOn(instrumentation);
}
private static AgentBuilder createAgent(
Class<? extends Annotation> annotationType, String methodName) {
return new AgentBuilder.Default().type(
ElementMatchers.isAnnotatedWith(annotationType)).transform(
new AgentBuilder.Transformer() {
#Override
public DynamicType.Builder<?> transform(
DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader) {
return builder
.method(ElementMatchers.named(methodName))
.intercept(
MethodDelegation
.to(LoggingInterceptor.class)
.andThen(
SuperMethodCall.INSTANCE));
}
});
}
}
Interceptor logs method execution:
public static void intercept(#AllArguments Object[] allArguments,
#Origin Method method) {
Logger logger = LoggerFactory.getLogger(method.getDeclaringClass());
logger.info("Method {} of class {} called", method.getName(), method
.getDeclaringClass().getSimpleName());
for (Object argument : allArguments) {
logger.info("Method {}, parameter type {}, value={}",
method.getName(), argument.getClass().getSimpleName(),
argument.toString());
}
}
When executed with -javaagent parameter this example works well. When however I try to load the agent on the running JVM with Attach API:
VirtualMachine vm = VirtualMachine.attach(args[0]);
vm.loadAgent(args[1]);
vm.detach();
I've got the following exception on the first logging attempt:
Exception in thread "ContainerBackgroundProcessor[StandardEngine[Tomcat]]" java.lang.IncompatibleClassChangeError: Class ch.qos.logback.classic.spi.ThrowableProxy does not implement the requested interface ch.qos.logback.classic.spi.IThrowableProxy
at ch.qos.logback.classic.pattern.ThrowableProxyConverter.subjoinExceptionMessage(ThrowableProxyConverter.java:180)
at ch.qos.logback.classic.pattern.ThrowableProxyConverter.subjoinFirstLine(ThrowableProxyConverter.java:176)
at ch.qos.logback.classic.pattern.ThrowableProxyConverter.recursiveAppend(ThrowableProxyConverter.java:159)
at ch.qos.logback.classic.pattern.ThrowableProxyConverter.throwableProxyToString(ThrowableProxyConverter.java:151)
at org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter.throwableProxyToString(ExtendedWhitespaceThrowableProxyConverter.java:35)
at ch.qos.logback.classic.pattern.ThrowableProxyConverter.convert(ThrowableProxyConverter.java:145)
at ch.qos.logback.classic.pattern.ThrowableProxyConverter.convert(ThrowableProxyConverter.java:1)
at ch.qos.logback.core.pattern.FormattingConverter.write(FormattingConverter.java:36)
at ch.qos.logback.core.pattern.PatternLayoutBase.writeLoopOnConverters(PatternLayoutBase.java:114)
at ch.qos.logback.classic.PatternLayout.doLayout(PatternLayout.java:141)
at ch.qos.logback.classic.PatternLayout.doLayout(PatternLayout.java:1)
at ch.qos.logback.core.encoder.LayoutWrappingEncoder.doEncode(LayoutWrappingEncoder.java:130)
at ch.qos.logback.core.OutputStreamAppender.writeOut(OutputStreamAppender.java:187)
at ch.qos.logback.core.OutputStreamAppender.subAppend(OutputStreamAppender.java:212)
at ch.qos.logback.core.OutputStreamAppender.append(OutputStreamAppender.java:100)
at ch.qos.logback.core.UnsynchronizedAppenderBase.doAppend(UnsynchronizedAppenderBase.java:84)
at ch.qos.logback.core.spi.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:48)
at ch.qos.logback.classic.Logger.appendLoopOnAppenders(Logger.java:270)
at ch.qos.logback.classic.Logger.callAppenders(Logger.java:257)
at ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.java:421)
at ch.qos.logback.classic.Logger.filterAndLog_0_Or3Plus(Logger.java:383)
at ch.qos.logback.classic.Logger.log(Logger.java:765)
at org.slf4j.bridge.SLF4JBridgeHandler.callLocationAwareLogger(SLF4JBridgeHandler.java:221)
at org.slf4j.bridge.SLF4JBridgeHandler.publish(SLF4JBridgeHandler.java:303)
at java.util.logging.Logger.log(Unknown Source)
at java.util.logging.Logger.doLog(Unknown Source)
at java.util.logging.Logger.logp(Unknown Source)
at org.apache.juli.logging.DirectJDKLog.log(DirectJDKLog.java:181)
at org.apache.juli.logging.DirectJDKLog.error(DirectJDKLog.java:147)
at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run(ContainerBase.java:1352)
at java.lang.Thread.run(Unknown Source)
I run the example on 64-bit HotSpot with Java8:
java version "1.8.0_112"
Java(TM) SE Runtime Environment (build 1.8.0_112-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.112-b15, mixed mode)
Byte Buddy version is 1.4.32. Here is agent maven configuration:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pl.halun.demo.bytebuddy</groupId>
<artifactId>byte-buddy-agent-demo</artifactId>
<version>1.0</version>
<properties>
<jdk.version>1.8</jdk.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.4.32</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${jdk.version}</source>
<target>${jdk.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<finalName>${project.artifactId}-${project.version}-full</finalName>
<appendAssemblyId>false</appendAssemblyId>
<archive>
<manifestEntries>
<Premain-Class>pl.halun.demo.bytebuddy.logging.LoggingAgent</Premain-Class>
<Agent-Class>pl.halun.demo.bytebuddy.logging.LoggingAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>assemble-all</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
And here is pom file for the instrumented application:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pl.halun.demo.bytebuddy.instrumented.app</groupId>
<artifactId>byte-buddy-agent-demo-instrumented-app</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<properties>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</pluginRepository>
</pluginRepositories>
From my point of view it is very valuable option to add logs on the running server and I hate to loose this part of demo. I tried to experiment with different redefinition strategies, but until now nothing seems to work.
What you observe is a classical version conflict, I think. Spring Boot most likely comes with a version of ThrowableProxy that is not compatible to the version that is added with the Java agent. When loading a Java agent at runtime, Spring's version is already loaded whereas the startup attachment prepends the agent-bundled version on the class path where the version of your agent is loaded.
Java agents are typically added to the class path. This is also where your Spring boot application is living. You need to make sure that a Java agent does not contain dependencies that are incompatible with your application's dependencies or you need to shade all dependencies to avoid such conflicts.
There is however another problem: When writing a Java agent that is attached at runtime, you meet additional constraints on most JVMs where on HotSpot, you are not allowed to change the class file format of any class that is already loaded. There is also a chance that your class is already loaded where currently, no effect would be visible as you do not enable retransformation.
A runtime-capable agent would need to use the Advice component which inlines code into target code rather then using the classical delegation model:
class MyAdvice {
#Advice.OnMethodEnter
static void intercept(#Advice.BoxedArguments Object[] allArguments,
#Advice.Origin Method method) {
Logger logger = LoggerFactory.getLogger(method.getDeclaringClass());
logger.info("Method {} of class {} called", method.getName(), method
.getDeclaringClass().getSimpleName());
for (Object argument : allArguments) {
logger.info("Method {}, parameter type {}, value={}",
method.getName(), argument.getClass().getSimpleName(),
argument.toString());
}
}
}
You can use the above advice class by registering it as a visitor. Such visitors only apply to declared methods, i.e. not to inherited methods and inline their code into existing methods. This way, the logging will not be visible on the call stack and it also becomes legal to retransform already loaded classes:
new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(isAnnotatedWith(annotationType))
.transform(new AgentBuilder.Transformer() {
#Override
public DynamicType.Builder<?> transform(
DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader) {
return builder.visit(Advice.to(MyAdvice.class).on(named(methodName)));
}
});
As for the attachment, look into the byte-buddy-agent project which allows you to call:
ByteBuddyAgent.attach(agentJar, processId);
The above helper supports other VMs where the attachment API often lives in a different namespace.
Update: Here is the problem with Spring Boot. Spring Boot creates custom class loaders that have the system class loader (class path) as their parent. These class loaders consider classes from the system class loader first. When you add the agent, the entire Spring boot app is both on the class loader and in these child class loaders. A class like IThrowableProxy now exists twice in two class loaders but is not considered to be equal by the JVM. Depending on the state of the VM, some classes might already be linked to the original IThrowableProxy whereas other classes are loaded after the agent was attached and get linked to the new IThrowableProxy from the agent. Both classes are not equal and the error that you see is thrown where the VM complains that the class does not implement the correct IThrowableProxy (but the previous one). If the agent is attached at start up, this problem does not exist as the class path's IThrowableProxy is always loaded.
This is not an easy error to fix, in the end, Byte Buddy cannot help you with such class path issues and Spring Boot is quite free in its interpretation of the class loader contract. The easiest way would be to not use Spring Boot types in your agent. You can still match the annotation with for example
isAnnotatedWith(named("org.springframework.web.bind.annotation.RestController"))
The question is how you can communicate with Spring Boot. One work-arround would be to add all shared classes to the class path on start up. Typically, I do avoid the usage of shared classes altogether but only use them in the Advice classes where the code is inlined in the class loader of the target application. Simply set the Spring Boot dependency in provided scope, the advice code itself is never executed.

Aspect does not work with Spring boot application with external jar

I am trying to create a timer aspect for measuring methods run time.
I created an annotation named #Timer:
#Retention(RetentionPolicy.RUNTIME)
#Target(value = {ElementType.METHOD, ElementType.TYPE})
public #interface Timer {
String value();
}
And then I created the aspect as follows:
#Aspect
public class MetricAspect {
#Autowired
private MetricsFactory metricsFactory;
#Pointcut("#annotation(my.package.Timer)")
public void timerPointcut() {}
#Around("timerPointcut() ")
public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
/* Aspect logic here */
}
private Timer getClassAnnotation(MethodSignature methodSignature) {
Timer annotation;
Class<?> clazz = methodSignature.getDeclaringType();
annotation = clazz.getAnnotation(Timer.class);
return annotation;
}
I have a configuration class as follows:
#Configuration
#EnableAspectJAutoProxy
public class MetricsConfiguration {
#Bean
public MetricAspect notifyAspect() {
return new MetricAspect();
}
}
Everything up until here is defined in a packaged jar which I use as a dependency in my spring boot application
In my spring boot application I import the MetricsConfiguration and I debugged the code and saw that the MetricAspect bean is created.
I use it in code as follows:
#Service
public class MyService {
...
#Timer("mymetric")
public void foo() {
// Some code here...
}
...
}
But my code doesn't reach to the measure method. Not sure what I'm missing.
For completing the picture, I have these dependencies in my pom file added:
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.7.4</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.7.4</version>
</dependency>
</dependencies>
That's the #Configuration class that imports MetricsConfiguration:
#Configuration
#EnableAspectJAutoProxy
#Import(MetricsConfiguration.class)
#PropertySource("classpath:application.properties")
public class ApplicationConfiguration {
}
It's loaded with Spring's automagically configuration loading.
can #Component or #Configurable solve your issue?
#Aspect
#Component
public class yourAspect {
...
}
Enable Spring AOP or AspectJ
EDIT:
I created a project to simulate your issue, seems no problem after all. Is it affected by other issue?
https://github.com/zerg000000/spring-aspectj-test
I was unable to reproduce your problem using aspectJ 1.8.8 and spring 4.2.5. Here is my maven multi-module approach with aspect in separate jar.
I modified your code slightly but did not change any annotations. The only thing that might be differ is that I've added org.springframework:spring-aop dependency and defined my entrypoint as follows:
#Import(MetricsConfiguration.class)
#SpringBootApplication
public class Application {
// #Bean definitions here //
public static void main(String[] args) throws InterruptedException {
ApplicationContext ctx =
SpringApplication.run(Application.class, args);
ctx.getBean(MyService.class).doWork();
}
}
I had a similar problem where the aspect was built in a jar library, and the spring-boot application was else where. Turns out that the packages for the spring-boot application and the jar library were different. Due to which Spring was not looking into the package of the library to autowire into the application context.
So, had to include #ComponentScan({"base.package.application.*", "base.package.library.*"}) in the Application.java
If the external jar is Spring boot starter, you can config Aspect bean in AutoConfiguration:
(1)
#Aspect
public class MyAspect {
//....
}
(2)
package a.b.c
#Configuration
public class MyAutoConfiguration {
#Bean
MyAspect myAspect() {
return new MyAspect();
}
}
(3)config in spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
a.b.c.MyAutoConfiguration
If the external jar is not a Spring boot starter, just load Aspect as a bean, you can use #ComponentScan
Add this componentScan to resolve the issue.
#ComponentScan("package.of.aspect")
#Configuration
#EnableAspectJAutoProxy
#Import(MetricsConfiguration.class)
#PropertySource("classpath:application.properties")
public class ApplicationConfiguration {
}
Debugging spring-boot Aspectj aspects when pointcut itself has problems is not easy even with detailed logging: How to debug Spring AOP
Unfortunately, spring boot + spring-aop with annotations don't have many ways to debug aspects and why some classes, especially non-spring compoment jar classes, are not scanned, such as jar classes whose methods are in abstract classes or static final methods need the right pointcuts to work covering all classes/implementations even if they are component scanned.
The best way to debug an Asepct (or use core AOP and avoid spring-aop) is to enable aop.xml with configuration control using org/aspectj/aop.xml or META-INF/aop.xml, using the LTW aspectj weaver
-Daj.weaving.verbose=true -javaagent:~/.m2/repository/org/aspectj/aspectjweaver/1.9.5/aspectjweaver-1.9.5.jar
To debug all aspects/classes with Debug/Verbose logs to see why some classes are not being scanned:
...
this almost always helps figuring out the problem with the pointcut or class not getting picked.
Or, just use LTW aop, see, https://github.com/dsyer/spring-boot-aspectj
Adding
ComponentScan(basePackages = "com.github.something.annotation")
basePackages is the package where your aspect resides.
This solution work for me.
You need put #ComponentScan on MetricsConfiguration, as #Configuration will not automatically scan and load component.
I have tested, and it worked!
according to mojohaus explaination, you have to add build settings like below to Woven your aspect into all classes implementing your aspect interface.
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.11</version>
<configuration>
<complianceLevel>1.8</complianceLevel>
<includes>
<include>**/*.java</include>
<include>**/*.aj</include>
</includes>
<aspectDirectory>src/main/aspect</aspectDirectory>
<testAspectDirectory>src/test/aspect</testAspectDirectory>
<XaddSerialVersionUID>true</XaddSerialVersionUID>
<showWeaveInfo>true</showWeaveInfo>
<aspectLibraries>
<aspectLibrary>
<groupId>your aspect groupId</groupId>
<artifactId>your aspect artifactId</artifactId>
</aspectLibrary>
</aspectLibraries>
</configuration>
<executions>
<execution>
<id>compile_with_aspectj</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile_with_aspectj</id>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>${aspectj.runtime.version}</version>
</dependency>
<dependency>
<groupId>your aspect groupId</groupId>
<artifactId>youar aspect artifactId</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

Categories