IncompatibleClassChangeError when instrumenting running SpringBoot application with Byte Buddy - java

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.

Related

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.

How to solve unable to drive module description for ../selenium-server-standalone-3.141.59.jar [duplicate]

This question already has answers here:
Unable to derive module descriptor: Provider {class X} not in module
(2 answers)
Closed 1 year ago.
Error occurred during initialization of boot layer
java.lang.module.FindException: Unable to derive module descriptor for C:\Users\admin\eclipse-workspace\Testing\lib\selenium-server-standalone.jar
Caused by: java.lang.module.InvalidModuleDescriptorException: Provider class org.eclipse.jetty.http.Http1FieldPreEncoder not in module
package Testing;
import com.thoughtworks.selenium.DefaultSelenium;
import com.thoughtworks.selenium.Selenium;
public class Testing {
public static void main(String[] args) throws InterruptedException
{
Selenium selenium= new DefaultSelenium("localhost",4444,"firefox","http://www.calculator.net");
selenium.start();
selenium.open("/");
selenium.windowMaximize();
selenium.click("xpath=.//*[#id=''hl3']/li[3]/a");
Thread.sleep(4000);
selenium.focus("id=cpar1");
selenium.type("css=input[id=\"cpar1\"]", "10");
selenium.focus("id=cpar2");
selenium.type("css=input[id=\"cpar2\"]", "50");
(selenium).click("xpath=.//*[#id='content']/table[1]/tbody/tr[2]/td/input[2]");
// verify if the result is 5
Thread.sleep(4000);
String result = selenium.getText("xpath=.//*[#id='content']/p[2]/font/b");
//String result = selenium.getValue("xpath=.//*[#id='cpar3']");
System.out.println("Result:"+result);
if (result.equals("5")/*== "5"*/){
System.out.println("Pass");
}
else{
System.out.println("Fail");
}
}
}
I would recommend reconsidering using Selenium Remote Control as it is quite outdated approach which is no longer supported, current stable version of Selenium Java client is 3.141.59 and it provides WebDriver API which is a W3C Standard as of now.
Once you implement option 1 get rid of those Thread.sleep() as it's a some form of a performance anti-pattern, go for Explicit Wait instead, check out How to use Selenium to test web applications using AJAX technology for comprehensive explanation and code examples.
It's better to use a dependency management solution like Apache Maven which will automatically detect and download your project transitive dependencies. A relevant pom.xml file would be something like:
<?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>selenium</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.141.59</version>
</dependency>
</dependencies>
</project>

Getting a RootDoc in jdk11

I am trying to test out some code that works with Java Doc, it is used under the maven-javadoc-plugin. I am trying to get it to work under jdk11. I am after an implementation of RootDoc which I can use when running tests.
Currently the tests use EasyDoclet which gives me a RootDoc like so:
EasyDoclet easyDoclet = new EasyDoclet(new File("dir"), "com.foo.bar");
RootDoc rootDoc = easyDoclet.getRootDoc()
However I could not get this to work under jdk11.
The first issue I had was tools.jar is missing so I changed my pom.xml to have:
<dependency>
<groupId>org.seamless</groupId>
<artifactId>seamless-javadoc</artifactId>
<version>1.1.1</version>
<exclusions>
<exclusion>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- maybe this will get what ever was in tools.jar -->
<dependency>
<groupId>com.github.olivergondza</groupId>
<artifactId>maven-jdk-tools-wrapper</artifactId>
<version>0.1</version>
</dependency>
This lead to many instances of:
java.lang.NoClassDefFoundError: com/sun/tools/javadoc/PublicMessager
The PublicMessager class seems to exist to make public some constructors, I am not sure why it exists under the com.sun.tools package. I tried to make a copy of this class:
public static class PublicMessager extends
com.sun.tools.javadoc.main.Messager {
public PublicMessager(Context context, String s) {
super(context, s);
}
public PublicMessager(Context context, String s, PrintWriter printWriter, PrintWriter printWriter1, PrintWriter printWriter2) {
super(context, s, printWriter, printWriter1, printWriter2);
}
}
And the error message changes to:
java.lang.IllegalAccessError: superclass access check failed: class com.fun.javadoc.FooBar$PublicMessager (in unnamed module #0x4abdb505) cannot access class com.sun.tools.javadoc.main.Messager (in module jdk.javadoc) because module jdk.javadoc does not export com.sun.tools.javadoc.main to unnamed module #0x4abdb50
I exposed jdk.javadoc to the unnamed module using:
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-Dfile.encoding=UTF-8</argLine>
<argLine>--add-opens=jdk.javadoc/com.sun.tools.javadoc.main=ALL-UNNAMED</argLine>
</configuration>
</plugin>
</plugins>
</build>
This meant that my custom version of PublicMessager would no longer have the errors shown however the version from seamless under com.sun.tools could not be found. I made my own version of EasyDoclet which used my PublicMessager however it turned out that the following two classes are missing:
import com.sun.tools.javadoc.JavadocTool;
import com.sun.tools.javadoc.ModifierFilter;
At this point I am not sure what to do. halp!
Perhaps an alternative would be to instead find the jdk11 equivalent of RootDoc which I think is DocletEnvironment and then some how get an implementation of that, I have no idea how to get an implementation of DocletEnvironment.

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

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.

Standalone Java Websocket client NoClassDefFoundError by ContainerProvider

I'm new to Java, but I have to use it to do a small WebSocket related project.
So, I installed JDK 1.8.0 and NetBeans 8.1 on my CentOS 7 in a VirtualBox.
I added the tyrus-standalone-client-jdk 1.12 plug-in in the pom.xml to make the standalone Websocket client, and it built fine. However, I ran into the error below:
[root#cet7 ~]# java -jar "/root/NetBeansProjects/Switchclient/target/Switchclient-1.0-SNAPSHOT.jar"
Exception in thread "main" java.lang.NoClassDefFoundError: javax/websocket/ContainerProvider
at org.sample.switchclient.Switchclient.main(Switchclient.java:21)
Caused by: java.lang.ClassNotFoundException: javax.websocket.ContainerProvider
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 1 more
[root#cet7 ~]# java -version
java version "1.8.0_65"
Java(TM) SE Runtime Environment (build 1.8.0_65-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.65-b01, mixed mode)
I did a bit more searching and found that the "fully qualified classname of the container implementation of ContainerProvider must be listed in the META-INF/services/javax.websocket.ContainerProvider file in the implementation JAR file" for the ServiceLoader API according to Oracle documentation. So, I added the serviceloader-maven-plugin to the pom.xml. The result was that it did generate the META-INF/services/javax.websocket.ContainerProvider file, but without any content, and the runtime error continued to persist. I tried to modify the contents bellow manually and re-pack it into a JAR but it did not worked:
org.glassfish.tyrus.container.inmemory.InMemoryContainerProvider
org.glassfish.tyrus.client.ClientManager
I've attached the Java file and the pom.xml. I've worked for hours and haven't a clue what the issue is, so any response to this thread will be appreciated.
Thank you very much.
===========LIST1: 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>org.sample</groupId>
<artifactId>Switchclient</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>org.sample.switchclient.Switchclient</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>eu.somatik.serviceloader-maven-plugin</groupId>
<artifactId>serviceloader-maven-plugin</artifactId>
<version>1.0.6</version>
<configuration>
<services>
<param>javax.websocket.ContainerProvider</param>
</services>
</configuration>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.glassfish.tyrus.bundles</groupId>
<artifactId>tyrus-standalone-client-jdk</artifactId>
<version>1.12</version>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
</project>
===========LIST2: Switchclient.java===========
package org.sample.switchclient;
import java.net.URI;
import javax.websocket.ClientEndpoint;
import javax.websocket.ContainerProvider;
import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;
#ClientEndpoint
public class Switchclient {
#OnMessage
public void onRemoteMessage (String message) {
System.out.println("Received msg: "+message);
}
public static void main(String[] args) {
WebSocketContainer container = null;
Session session = null;
try{
container = ContainerProvider.getWebSocketContainer();
session = container.connectToServer (Switchclient.class, URI.create("ws://localhost:8080/Switchserver/"));
}catch (Exception e) {
e.printStackTrace();
}
}
}
Basically, Tyrus requires Java EE. It's the reason you have to list a lot of dependencies in pom.xml. If you use Java SE and want to keep your project small, use another different WebSocket client library that depends on only Java SE. For example, nv-websocket-client (mine).
Just add the following dependency to pom.xml,
<dependency>
<groupId>com.neovisionaries</groupId>
<artifactId>nv-websocket-client</artifactId>
<version>1.13</version>
</dependency>
then try:
import com.neovisionaries.ws.client.*;
public class Switchclient
{
public static void main(String[] args) throws Exception
{
WebSocket websocket = new WebSocketFactory()
.createSocket("ws://localhost:8080/Switchserver/")
.addListener(new WebSocketAdapter() {
#Override
public void onTextMessage(WebSocket ws, String message) {
System.out.println("Received msg: " + message);
}
})
.connect();
// Don't forget to call disconnect() after use.
// websocket.disconnect();
}
}
I'm not sure what exactly caused the problem since I kept trying and problems kept jumping out during the past day. But finally here is it:
Client dependencies:
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-client-api</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.tyrus.bundles</groupId>
<artifactId>tyrus-standalone-client</artifactId>
<version>1.12</version>
</dependency>
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-container-grizzly-client</artifactId>
<version>1.12</version>
</dependency>
at a first glance it seems javax.websocket-client-api should be enough but finally cyber said that ContainerProvider is not impediment.
Then, all built OK. (with different java codes from my original post, I tried a lot on including the source codes, but codes themselves don't matter that match here while the environment setup matters. They mostly based on the examples of Tyrus 1.9 user guide however.)
And the run from the NetBeans by maven was OK, but when I went to use "java -jar Switchclient.jar", same/similar problem jumped out saying problem with "Endpoint".
Finally (as a last try) I copied all those tar files included in the classpath (witch was generated by maven-jar-plugin by specifying "<addClasspath>true<addClasspath>" into one directory, and also copied the generated jar file in, then it worked:
[root#cet7 neededjars]# ls
grizzly-framework-2.3.22.jar tyrus-client-1.12.jar
grizzly-http-2.3.22.jar tyrus-container-grizzly-client-1.12.jar
grizzly-http-server-2.3.22.jar tyrus-core-1.12.jar
javax.websocket-api-1.1.jar tyrus-spi-1.12.jar
javax.websocket-client-api-1.1.jar tyrus-standalone-client-1.12.jar
Switchclient-1.1-SNAPSHOT.jar
[root#cet7 neededjars]# java -jar Switchclient-1.1-SNAPSHOT.jar
Received message: Hello world
That's it, dirty and but worked. and I'm at a new start. Again, I'm really new to java (one of those non-hard-tech guys, just pick it up in case of need); and it showed me the complicity of the community based development, especially in the case the technology is relatively new. dependencies and pitfall everywhere. That's is one part of the nature I guess...

Categories