I have Gradle project with Spring Boot and AspectJ.
Want to load aspectjweaver and spring-instrument javaagents dynamically and directly from WEB-INF/libs (where Spring Boot locate all dependencies)
Gradle dependencies:
AgentLoader:
public class AgentLoader {
private static final Logger LOGGER = LoggerFactory.getLogger(AgentLoader.class);
public static void loadJavaAgent() {
if (!isAspectJAgentLoaded()) {
LOGGER.warn("Aspect agent was not loaded!");
}
}
public static boolean isAspectJAgentLoaded() {
try {
Agent.getInstrumentation();
} catch (NoClassDefFoundError e) {
return false;
} catch (UnsupportedOperationException e) {
LOGGER.info("Dynamically load AspectJAgent");
return dynamicallyLoadAspectJAgent();
}
return true;
}
public static boolean dynamicallyLoadAspectJAgent() {
String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
int p = nameOfRunningVM.indexOf('#');
String pid = nameOfRunningVM.substring(0, p);
try {
VirtualMachine vm = VirtualMachine.attach(pid);
String jarFilePath = AgentLoader.class.getClassLoader().getResource("WEB-INF/libs/aspectjweaver-1.9.6.jar").toString();
vm.loadAgent(jarFilePath);
jarFilePath = AgentLoader.class.getClassLoader().getResource("WEB-INF/libs/spring-instrument-5.3.2.jar").toString();
vm.loadAgent(jarFilePath);
vm.detach();
} catch (Exception e) {
LOGGER.error("Exception while attaching agent", e);
return false;
}
return true;
}
}
But found out that return value of getResource() in null
What is the best solution to handle this issue?
Nikita, today is your lucky day. I just had a moment and was curious how to make my code snippet from https://www.eclipse.org/aspectj/doc/released/README-187.html, which obviously you found before, work in the context of Spring Boot. I just used my Maven Spring Boot playground project. Depending on which Java version you are using, you either need to make sure that tools.jar from JDK 8 is defined as a system-scoped dependency and also copied into the executable Spring uber JAR, or you need to make sure that the Java attach API is activated in Java 9+. Here is what I did for Java 8:
Maven:
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
<!-- (...) -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>spring.aop.DemoApplication</mainClass>
<!-- Important for tools.jar on Java 8 -->
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
The <includeSystemScope> option is necessary because otherwise Boot does not know how to find the attach API classes. Just do something equivalent in Gradle and you should be fine.
Java:
You need to know that in order to attach an agent, it must be a file on the file system, not just any resource or input stream. This is how the attach API works. So unfortunately, you have to copy it from the uber JAR to the file system first. Here is how you do it:
public static boolean dynamicallyLoadAspectJAgent() {
String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
int p = nameOfRunningVM.indexOf('#');
String pid = nameOfRunningVM.substring(0, p);
try {
VirtualMachine vm = VirtualMachine.attach(pid);
ClassLoader classLoader = AgentLoader.class.getClassLoader();
try (InputStream nestedJar = Objects.requireNonNull(classLoader.getResourceAsStream("BOOT-INF/lib/aspectjweaver-1.9.4.jar"))) {
File targetFile = new File("aspectjweaver.jar");
java.nio.file.Files.copy(nestedJar, targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
vm.loadAgent(targetFile.getAbsolutePath());
}
try (InputStream nestedJar = Objects.requireNonNull(classLoader.getResourceAsStream("BOOT-INF/lib/spring-instrument-5.1.9.RELEASE.jar"))) {
File targetFile = new File("spring-instrument.jar");
java.nio.file.Files.copy(nestedJar, targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
vm.loadAgent(targetFile.getAbsolutePath());
}
vm.detach();
}
catch (Exception e) {
LOGGER.error("Exception while attaching agent", e);
return false;
}
return true;
}
Besides, in my case the files were unter BOOT-INF/lib, not WEB-INF/lib.
Update: You said you have this follow-up problem somewhere along the line (reformatted for readability):
failed to access class
org.aspectj.weaver.loadtime.Aj$WeaverContainer
from class
org.aspectj.weaver.loadtime.Aj
(
org.aspectj.weaver.loadtime.Aj$WeaverContainer is in
unnamed module of
loader 'app';
org.aspectj.weaver.loadtime.Aj is in
unnamed module of
loader org.springframework.boot.loader.LaunchedURLClassLoader #3e9b1010
)
at org.aspectj.weaver.loadtime.Aj.preProcess(Aj.java:108)
This means that Aj is unable to find its own inner class Aj.WeaverContainer. This indicates that they are loaded at different points in time and in different classloaders. When remote-debugging into my sample Boot application starting from an executable JAR, I see that the application classloader is actually the LaunchedURLClassLoader's parent, i.e. the class loaded in the parent is trying to access another class only available to its child classloader, which is impossible in Java. It only works the other way around.
Maybe it helps not to import and reference AspectJ weaver classes from inside the agent loader. Try commenting out the loadJavaAgent() and isAspectJAgentLoaded() methods and also remove import org.aspectj.weaver.loadtime.Agent;. Then in your application just directly call AgentLoader.dynamicallyLoadAspectJAgent() and see if this helps. I have some more aces up my sleeves with regard to agent loading, but let's keep it as simple as possible first.
Related
After quite some trials, I find that it is not possible to use the multinode module at all. Since the multinode depends on entity-store module and vice versa.
Thus including the multinode module into Gradle config of entity-store causes circular dependency.
Anyhow, I am still trying some hacks. Essentially the major issue I find is the creation of the S3BlobVault, since it is easy to (re)create the S3DataReaderWriterProvider from outside the Xodus project, the major issue is the S3BlobVault which needs an instance of the PersistentEntityStoreImpl which means it(the S3BlobVault) needs to be instantiated within/inside the PersistentEntityStoreImpl which is quite not possible due to circular dependency issue.
At the very least I did modify the PersistentEntityStoreImpl and added:
public void setBlobVault(BlobVault blobVault) {
this.blobVault = blobVault;
}
Then in my code(app), I added
final PersistentEntityStoreImpl store = PersistentEntityStores.newInstance(environment);
S3BlobVault s3BlobVault = createS3BlobVault(store, environment.getLocation());
store.setBlobVault(s3BlobVault);
Creating the vault like this:
private S3BlobVault createS3BlobVault(PersistentEntityStoreImpl store, String location) {
try {
S3AsyncClient s3 = S3AsyncClient.builder()
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("", "")))
.endpointOverride(new URI("https://s3.wasabisys.com"))
.region(Region.US_EAST_1).build();
S3BlobVault blobVault = null;
// Can't use code below (outside of package)
// try {
// final PersistentSequenceBlobHandleGenerator.PersistentSequenceGetter persistentSequenceGetter =
// new PersistentSequenceBlobHandleGenerator.PersistentSequenceGetter() {
// #Override
// public PersistentSequence get() {
// return getSequence(getAndCheckCurrentTransaction(), BLOB_HANDLES_SEQUENCE);
// }
// };
// blobVault = new S3BlobVault(store,
// new PersistentSequenceBlobHandleGenerator(persistentSequenceGetter), s3, "xodus", location, ".blobs", null);
// } catch (UnexpectedBlobVaultVersionException e) {
// blobVault = null;
// }
if(blobVault == null) {
blobVault = new S3BlobVault(store,
BlobHandleGenerator.IMMUTABLE, s3, "xodus", location, ".blobs", null);
}
return blobVault;
} catch (Exception e) {
throw ExodusException.toExodusException(e);
}
}
I still ended with the error:
Caused by: java.io.FileNotFoundException: s3:xodus\blobs\version (The filename, directory name, or volume label syntax is incorrect)
at java.io.FileOutputStream.open0(Native Method)
at java.io.FileOutputStream.open(FileOutputStream.java:270)
at java.io.FileOutputStream.<init>(FileOutputStream.java:213)
at java.io.FileOutputStream.<init>(FileOutputStream.java:162)
at jetbrains.exodus.entitystore.FileSystemBlobVaultOld.<init>(FileSystemBlobVaultOld.java:106)
at jetbrains.exodus.entitystore.FileSystemBlobVaultOld.<init>(FileSystemBlobVaultOld.java:71)
at jetbrains.exodus.entitystore.PersistentEntityStoreImpl.createDefaultFSBlobVault(PersistentEntityStoreImpl.java:424)
... 95 more
In your project, you can add dependency on the multinode jar and create PersitentEntityStore in such a way:
final S3BlobVault blobVault = createBlobVault(...);
final Environment env = Environments.newInstance("location");
final PersistentEntityStoreImpl store = PersistentEntityStores.newInstance(PersistentEntityStoreConfig.DEFAULT, env, blobVault, "entityStore name");
Probably, this would work. At least, if you pass the blob vault for creation of PersistentEntityStore then you wouldn't require the circular dependency you mentioned.
Dependency on the multinode module is enough to use functionality of the entity-store module.
Though, I have to emphasize that any functionality in the multinode module is incomplete, not announced, not documented, and is a subject to change. It may be removed completely in future versions.
Xodus build 1.3.91 shares S3 functionality as an experimental feature. There is no references in documentations for it also some tests failed for S3 file store. We do not recommend to use it in production code until there is no mentions in release notes and no section in documentation for it.
Result of using S3 as a store for Xodus at the moment is unpredictable.
I have a Gherkin executor where I execute my feature files. What I would like to do would be to add a StepDefinition file from an other jar. The user would be able to use my project with the step def that I have already wrote but he would also be able to add custom definitions from his own jar file.
Currently I have a JavaClassLoader where I load my class from my jar and I use it in my main
public class JavaClassLoader<C> extends ClassLoader {
public C LoadClass(String directory, String classpath, Class<C> parentClass) throws ClassNotFoundException {
File pluginsDir = new File(System.getProperty("user.dir") + directory);
for (File jar : pluginsDir.listFiles()) {
try {
ClassLoader loader = URLClassLoader.newInstance(
new URL[] { jar.toURL() },
getClass().getClassLoader()
);
Class<?> clazz = Class.forName(classpath, true, loader);
Class<? extends C> newClass = clazz.asSubclass(parentClass);
// Apparently its bad to use Class.newInstance, so we use
// newClass.getConstructor() instead
Constructor<? extends C> constructor = newClass.getConstructor();
return constructor.newInstance();
} catch (ClassNotFoundException e) {
// There might be multiple JARs in the directory,
// so keep looking
continue;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
throw new ClassNotFoundException("Class " + classpath
+ " wasn't found in directory " + System.getProperty("user.dir") + directory);
}
}
JavaClassLoader<AbstractStepDefs> loader = new JavaClassLoader<AbstractStepDefs>();
loader.LoadClass("/", "stepDef.dynamicClass", AbstractStepDefs.class);
The problem is that Cucumber isn't able to read the methods that I wrote in my other jar. Is there a way to use a step def file that isn't in the project?
Is there a way to use a step def file that isn't in the project?
Yes, and no. The yes part is closer to "sort of". git supports a couple of ways of referencing subprojects within other projects. The common "subprojects" are maintained in their own project and then pulled into using projects. Look here for a discussion. I looked into doing submodules once. I even had it working. But I couldn't convince the TeamCity owners to support it. It works but you have to be careful about how you use it. There are haters.
What I ended up doing was to create a shared "global" project that contained page files for the common login and navigation pages. This global project also contained all the startup and shutdown support for different browsers and for remote execution on SauceLabs.
The step definitions had to be repeated (yuck; I prefer DRY) but these are small as they mostly just call the page file methods. All of these web pages are defined in the global project in their own class files. The common housekeeping code is defined in class WebDriverManager.
#Given("^I navigate to the public ACME WebPage and select Login$")
public void iNavigateToTheAcmePublicWebPage() {
pageFactory.AcmePublicWebPage().navigateTo(
WebDriverManager.instance().getAcmeUrl());
pageFactory.AcmePublicWebPage().closeNotificationPopUp(); //If there is one
pageFactory.AcmePublicWebPage().selectLoginLink();
}
#When("^I close the browser$")
public void iCloseTheBrowser() {
WebDriverManager.instance().closeBrowser();
}
I have reduced most, but not all duplication. Most junior test automation engineers don't have to worry about the heavy lifting as long as I maintain the global git project and notify them when they need to download a new global jar from TeamCity.
Java fails to launch when the classpath is too long. The length limit is particularly short on Windows.
Gradle seem uninterested in fixing the issue on their side (even though it's sort of their responsibility since they're the ones launching Java), so we ended up substituting the JavaExec task out with our own alternative.
The alternative works like this:
public class WorkingJavaExec extends JavaExec {
private static final String MATCH_CHUNKS_OF_70_CHARACTERS =
"(?<=\\G.{70})";
private final Logger logger = LoggerFactory.getLogger(getClass());
#Override
public void exec() {
FileCollection oldClasspath = getClasspath();
File jarFile = null;
try {
if (!oldClasspath.isEmpty()) {
try {
jarFile =
toJarWithClasspath(oldClasspath.getFiles());
setClasspath(getProject().files(jarFile));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
super.exec();
} finally {
setClasspath(oldClasspath);
if (jarFile != null) {
try {
Files.delete(jarFile.toPath());
} catch (Exception e) {
logger.warn("Couldn't delete: " + jarFile, e);
}
}
}
}
public static File toJarWithClasspath(Set<File> files)
throws IOException {
File jarFile = File.createTempFile("long-classpath", ".jar");
try (ZipOutputStream zip =
new ZipOutputStream(new FileOutputStream(jarFile))) {
zip.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
try (PrintWriter writer =
new PrintWriter(
new OutputStreamWriter(
zip, StandardCharsets.UTF_8))) {
writer.println("Manifest-Version: 1.0");
String classPath = files.stream().map(
file -> file.toURI().toString())
.collect(Collectors.joining(" "));
String classPathEntry = "Class-Path: " + classPath;
writer.println(Arrays.stream(
classPathEntry.split(MATCH_CHUNKS_OF_70_CHARACTERS))
.collect(Collectors.joining("\n ")));
}
}
return jarFile;
}
}
Using this is cumbersome, though, because everywhere someone might run JavaExec, I have to replace it with WorkingJavaExec. New developers also don't know that there is this pitfall in Gradle in the first place, so they don't even know it's something they have to work around.
In reading the internals of Gradle, I saw that JavaExec internally uses a JavaExecAction to do the actual exec.
I thought that maybe by replacing this, we could fix the problem as if Gradle had fixed it themselves, and maybe it would then also apply to other tasks, such as Test. But I haven't been able to find any examples anywhere. (Even in other large projects, which you would expect to have hit the same issue!)
Is it possible to substitute JavaExecAction, and if so, how?
I'm not sure you can "substitute" JavaExecAction because it is set during JavaExec task instanciation, but I think you can solve this problem in a nicer way, using a custom Plugin as follow:
class FixClasspathLimitPlugin implements Plugin<Project> {
#Override
void apply(Project project) {
// after project has been evaluated, hack into all tasks of type JavaExec declared.
project.afterEvaluate {
project.tasks.stream().filter { task -> task instanceof JavaExec }.forEach {
println "Reconfiguring classpath for : $it"
JavaExec javaExec = (JavaExec) it;
FileCollection oldClasspath = javaExec.getClasspath()
// insert an Action at first position, that will change classpath
javaExec.doFirst { task ->
((JavaExec) task).setClasspath(getProject().files(toJarWithClasspath(oldClasspath.getFiles())));
}
// optional - reset old classpath
javaExec.doLast { task ->
((JavaExec) task).setClasspath(oldClasspath)
}
}
}
}
public static File toJarWithClasspath(Set<File> files)
throws Exception {
// same method implementation as given in your question
}
This way, you won't have to replace JavaExec in all build scripts written by your team, you will only have to ensure that these scripts apply your plugin.
And if you use a custom distribution of Gradle and use wrapper in you enterprise, you can even include this plugin in this distribution as an Init Script, as explained here: https://docs.gradle.org/current/userguide/init_scripts.html#sec:using_an_init_script
Put a file that ends with .gradle in the GRADLE_HOME/init.d/ directory, in the Gradle distribution. This allows you to package up a custom Gradle distribution containing some custom build logic and plugins. You can combine this with the Gradle wrapper as a way to make custom logic available to all builds in your enterprise.
This way, the plugin will be applied in a "transparent" way.
Concerning the Test task: it does not use JavaExecAction, I think, but a similar solution could be applied, using a similar plugin.
You can use the jar task to add the class path to the manifest for you:
jar {
baseName = "my-app"
version = "1.0.0"
manifest {
attributes("Class-Path": configurations.compile.collect { it.getName() }.join(' '))
}
}
And then you can reference that jar when launching:
task run(type:JavaExec) {
classpath = jar.outputs.files
main = "myapp.MainClass"
}
That works around the command line path limit. You might also want to copy the dependency JARs to the output folder, so they will be available at runtime.
task copyDependencies(type: Copy, dependsOn: [ "build" ]) {
from configurations.runtime
into "./build/libs"
}
build.finalizedBy(copyDependencies)
Helpful?
The following code adds jar file to the build path, it works fine with Java 8. However, it throws exception with Java 9, the exception is related to the cast to URLClassLoader. Any ideas how this can be solved? an optimal solution will edit it to work with both Java 8 & 9.
private static int AddtoBuildPath(File f) {
try {
URI u = f.toURI();
URLClassLoader urlClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
Class<URLClassLoader> urlClass = URLClassLoader.class;
Method method = urlClass.getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);
method.invoke(urlClassLoader, u.toURL());
} catch (NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException | MalformedURLException | IllegalAccessException ex) {
return 1;
}
return 0;
}
You've run into the fact that the system class loader is no longer a URLClassLoader. As indicated by ClassLoader::getSystemClassLoader's return type, this was an implementation detail, albeit one that a non-negligible amount of code relied upon.
Judging by the comments, you are looking for a way to dynamically load classes at run time. As Alan Bateman points out, this can not be done in Java 9 by appending to the class path.
You should instead consider creating a new class loader for that. This has the added advantage that you'll be able to get rid of the new classes as they are not loaded into the application class loader. If you're compiling against Java 9, you should read up on layers - they give you a clean abstraction for loading an entirely new module graph.
I have stumbled over this issue a while ago. As many, I had used a method similar to that in the question
private static int AddtoBuildPath(File f)
to dynamically add paths to the classpath at runtime. The code in the question is probably bad style in multiple aspects: 1) assuming that ClassLoader.getSystemClassLoader() returns an URLClassLoader is an undocumented implementation detail and 2) using reflection to make addURL public is maybe another one.
Cleaner way to dynamically add classpaths
In case that you need to use the additional classpath URLs for class loading through „Class.forName“, a clean, elegant and compatible (Java 8 to 10) solution is the following:
1) Write your own class loader by extending URL classloader, having a public addURL method
public class MyClassloader extends URLClassLoader {
public MyClassloader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public void addURL(URL url) {
super.addURL(url);
}
}
2) Declare a (singleton/app wide) object of your classloader
private final MyClassloader classLoader;
and instanciate it via
classLoader = new MyClassloader(new URL[0], this.getClass().getClassLoader());
Note: The system class loader is the parent. Classes loaded though classLoader know those who can be loaded through this.getClass().getClassLoader() but not the other way around.
3) Add additional classpaths whenever needed (dynamically):
File file = new File(path);
if(file.exists()) {
URL url = file.toURI().toURL();
classLoader.addURL(url);
}
4) Instanciate objects or your app though your singleton classloader via
cls = Class.forName(name, true, classLoader);
Note: Since class loaders try a delegation to the parent class loader prior loading a class (and the parent to its parent), you have to make sure that the class to load is not visible to the parent class loader to make sure that it is loaded through the given class loader. To make this clearer: if you have ClassPathB on your system class path and later add ClassPathB and some ClassPathA to your custom classLoader, then classes under ClassPathB will be loaded through the system classloader and classes under ClassPathA are not known to them. However, if you remove ClassPathB from you system class path, such classes will be loaded through your custom classLoader, and then classes under ClassPathA are known to those under ClassPathB.
5) You may consider passing your class loader to a thread via
setContextClassLoader(classLoader)
in case that thread uses getContextClassLoader.
If you're just looking to read the current classpath, for example because you want to spin up another JVM with the same classpath as the current one, you can do the following:
object ClassloaderHelper {
def getURLs(classloader: ClassLoader) = {
// jdk9+ need to use reflection
val clazz = classloader.getClass
val field = clazz.getDeclaredField("ucp")
field.setAccessible(true)
val value = field.get(classloader)
value.asInstanceOf[URLClassPath].getURLs
}
}
val classpath =
(
// jdk8
// ClassLoader.getSystemClassLoader.asInstanceOf[URLClassLoader].getURLs ++
// getClass.getClassLoader.asInstanceOf[URLClassLoader].getURLs
// jdk9+
ClassloaderHelper.getURLs(ClassLoader.getSystemClassLoader) ++
ClassloaderHelper.getURLs(getClass.getClassLoader)
)
By default the final fields in the $AppClassLoader class cannot be accesed via reflection, an extra flag needs to be passed to the JVM:
--add-opens java.base/jdk.internal.loader=ALL-UNNAMED
I was given a spring boot application that runs in Java 8. I had the task to upgrade it to Java 11 version.
Issue faced:
Caused by: java.lang.ClassCastException: jdk.internal.loader.ClassLoaders$AppClassLoader (in module: java.base) cannot be cast to java.net.URLClassLoader (in module: java.base)
Way around used:
Create a class:
import java.net.URL;
/**
* This class has been created to make the code compatible after migration to Java 11
* From the JDK 9 release notes: "The application class loader is no longer an instance of
* java.net.URLClassLoader (an implementation detail that was never specified in previous releases).
* Code that assumes that ClassLoader.getSytemClassLoader() returns a URLClassLoader object will
* need to be updated. Note that Java SE and the JDK do not provide an API for applications or
* libraries to dynamically augment the class path at run-time."
*/
public class ClassLoaderConfig {
private final MockClassLoader classLoader;
ClassLoaderConfig() {
this.classLoader = new MockClassLoader(new URL[0], this.getClass().getClassLoader());
}
public MockClassLoader getClassLoader() {
return this.classLoader;
}
}
Create Another class:
import java.net.URL;
import java.net.URLClassLoader;
public class MockClassLoader extends URLClassLoader {
public MockClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public void addURL(URL url) {
super.addURL(url);
}
}
Now set it in the current thread from your main class (Right at the beginning of your application)
Thread.currentThread().setContextClassLoader(new ClassLoaderConfig().getClassLoader());
Hope this solution works for your!!!
Shadov pointed to a thread at the oracle community. There is the correct answer:
Class.forName("nameofclass", true, new URLClassLoader(urlarrayofextrajarsordirs));
The caveats mentioned there are also important:
Caveats:
java.util.ServiceLoader uses the thread's ClassLoader context Thread.currentThread().setContextClassLoader(specialloader);
java.sql.DriverManager does honors the calling class' ClassLoader, -not- the Thread's ClassLoader. Create Driver directly using Class.forName("drivername", true, new URLClassLoader(urlarrayofextrajarsordirs).newInstance();
javax.activation uses the thread's ClassLoader context (important for javax.mail).
Referring to Edi's Solution this worked for me:
public final class IndependentClassLoader extends URLClassLoader {
private static final ClassLoader INSTANCE = new IndependentClassLoader();
/**
* #return instance
*/
public static ClassLoader getInstance() {
return INSTANCE;
}
private IndependentClassLoader() {
super(getAppClassLoaderUrls(), null);
}
private static URL[] getAppClassLoaderUrls() {
return getURLs(IndependentClassLoader.class.getClassLoader());
}
private static URL[] getURLs(ClassLoader classLoader) {
Class<?> clazz = classLoader.getClass();
try {
Field field = null;
field = clazz.getDeclaredField("ucp");
field.setAccessible(true);
Object urlClassPath = field.get(classLoader);
Method method = urlClassPath.getClass().getDeclaredMethod("getURLs", new Class[] {});
method.setAccessible(true);
URL[] urls = (URL[]) method.invoke(urlClassPath, new Object[] {});
return urls;
} catch (Exception e) {
throw new NestableRuntimeException(e);
}
}
}
Running within Eclipse, you need to set VM Arguments to JUnit Launch/Debug Configuration.
Running with maven via command line you have two options:
Option 1
Add following lines to pom.xml :
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.16</version>
<configuration>
<argLine>--add-opens java.base/jdk.internal.loader=ALL-UNNAMED</argLine>
</configuration>
</plugin>
Option 2
run mvn test -DargLine="-Dsystem.test.property=--add-opens java.base/jdk.internal.loader=ALL-UNNAMED"
There's also this guys article that helped me.
I could not find the article but... here: https://github.com/CGJennings/jar-loader
Here's a part of guide inside there there's a jar at release you could read his guide & setup it up.
I just tried it myself download the jar file which include the class file
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.jar.JarFile;
public final class classname{
public static void premain(String agentArgs, Instrumentation instrumentation) {
loadedViaPreMain = true;
agentmain(agentArgs,instrumentation);
}
public final static void addToClassPath(File jarfile)throws IOException{inst.appendToSystemClassLoaderSearch(new JarFile(jarfile));}
public final static void agentmain(String agentArgs, Instrumentation instrumentation) {
if (instrumentation == null){throw new NullPointerException("instrumentation");}
if (inst == null) {inst = instrumentation;}
}
private static Instrumentation inst;
private static boolean loadedViaPreMain = false;
}
I just try it out myself package these code as a package then start the application class with -javaagent:plugin......jar option then call this function.It doesn't change my classpath.I am probably missing some details here.
Hope you can make it work though.
i found this, and worked for me.
String pathSeparator = Syste .getProperty("path.separator");
String[] classPathEntries = System.getProperty("java.class.path") .split(pathSeparator);
from the web site https://blog.codefx.org/java/java-11-migration-guide/#Casting-To-URL-Class-Loader
I m trying to externalize the configuration of aop.xml so I removed the aop.xml from META-INF and made it available in the server for manual configuration by sys admins.
When I try to use an external aop.xml using
-Dorg.aspectj.weaver.loadtime.configuration="file:D:\Workspace\tomcat7\shared\lib\aop.xml"
I get java.lang.RuntimeException: Cannot register non aspect: aspectclass.... mainly because the aj casses are not loaded by AppClassLoader yet at that time. And the next time it tries to register the aspects from the WebAppClassLoader ( after all the classes are loaded), it works fine, but i still get the exceptions logged from the 1st attempt to register it.
The exception is caught and logged at ClassLoaderWeavingAdaptor.java line 307.
when the following line is called:
success = registerAspects(weaver, loader, definitions);
the exception is caught and logged.
try {
registerOptions(weaver, loader, definitions);
registerAspectExclude(weaver, loader, definitions);
registerAspectInclude(weaver, loader, definitions);
success = registerAspects(weaver, loader, definitions);
registerIncludeExclude(weaver, loader, definitions);
registerDump(weaver, loader, definitions);
} catch (Exception ex) {
trace.error("register definition failed", ex);
success = false;
warn("register definition failed", (ex instanceof AbortException) ? null : ex);
}
the exception is thrown excactly in the following line in BcelWeaver.java
if (type.isAspect()) {
......
} else {
// FIXME AV - better warning upon no such aspect from aop.xml
RuntimeException ex = new RuntimeException("Cannot register non aspect: " + type.getName() + " , " + aspectName);
if (trace.isTraceEnabled()) {
trace.exit("addLibraryAspect", ex);
}
throw ex;
}
How can I prevent the classLoader from logging the error to the console, when the aspects are not loaded yet. I was thinking of commenting the line that logs the exception from the source file and rebuilding the aspectjweaver jar file, but was looking for a better solution without modifying the aspectj source.
I am not sure that there is an easy way out of your problem. As I said I haven't worked with AspectJ before but I believe this is a mis-behaviour of the weaver.
Problem description: During boot the agent tries to apply weaving other not only to the WebAppClassLoader but to the whole classloader chain (once per classloader) i.e. to: sun.misc.Launcher$AppClassLoader, sun.misc.Launcher$ExtClassLoader, org.apache.catalina.loader.StandardClassLoader (the tomcat's classloader). When you use the META-INF/aop.xml approach it disables weaving for the above classloaders because "a configuration file is not available" (if you enable verbose mode you can see those messages in console). When you use the file configuration approach, a configuration is available for all the classloaders in the chain. Since it does find a configuration file, the agent parses the definitions, it does not find the aspects' class and shows the error.
The weird thing is that, as described in the configuration documentation if you use the WeavingURLClassLoader approach for load time weaving, "... it also allows the user to explicitly restrict by class loader which classes can be woven". So this is actually a feature (!) that the classloader approach can have but the agent approach doesn't. (Unfortunately I was not able to use this approach)
The good (and the bad) news: The good news is that you can easily create your own agent that will ignore the weaving for the aforementioned classloaders. The bad news is that restricting weaving per classloader is not enough because if you have other applications in the same server, Tomcat would still use the WebAppClassLoader to load them so you would still get error messages for those applications. (Perhaps you could extend the classes below to filter packages/classes as well, in that case).
Below you can find two class for the modified agent. To use them you would need to do the following:
Un-jar the aspectjweaver.jar to a folder
Under org/aspectj/weaver/loadtime create a new folder filter to match the package name and put there the two new classes after you compile them.
Edit the META-INF/MANIFEST.MF file and change the line
Premain-Class: org.aspectj.weaver.loadtime.Agent to
Premain-Class: org.aspectj.weaver.loadtime.filter.FilterAgent
Re-jar and you have your new agent ready.
When starting the JVM you can now pass a new system property with a comma separated list of the classloaders you would like to ignore i.e. -Dorg.aspectj.weaver.loadtime.filter=sun.misc.Launcher$AppClassLoader,sun.misc.Launcher$ExtClassLoader,org.apache.catalina.loader.StandardClassLoader ( I have set CATALINA_OPTS to do that).
The classes are a modified copy of the original agent's classes Agent and ClassPreProcessorAgentAdapter. The only code I have added is the part that parses the above system property if it exists and to ignore calls for the classloaders we are not interested in.
Use at your own risk :) I hope that helps
package org.aspectj.weaver.loadtime.filter;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
public class FilterAgent {
private static Instrumentation s_instrumentation;
// Use our own version of ClassFileTransformer that would filter out selected classloaders
private static ClassFileTransformer s_transformer = new ClassPreprocessorFilteredAdapter();
/**
* JSR-163 preMain Agent entry method
*
* #param options
* #param instrumentation
*/
public static void premain(String options, Instrumentation instrumentation) {
/* Handle duplicate agents */
if (s_instrumentation != null) {
return;
}
s_instrumentation = instrumentation;
s_instrumentation.addTransformer(s_transformer);
}
public static Instrumentation getInstrumentation() {
if (s_instrumentation == null) {
throw new UnsupportedOperationException("Java 5 was not started with preMain -javaagent for AspectJ");
}
return s_instrumentation;
}
}
//-----------------------------------------------------------------------------------
package org.aspectj.weaver.loadtime.filter;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.HashMap;
import java.util.Map;
import org.aspectj.weaver.loadtime.Aj;
import org.aspectj.weaver.loadtime.ClassPreProcessor;
public class ClassPreprocessorFilteredAdapter implements ClassFileTransformer {
/**
* Concrete preprocessor.
*/
private static ClassPreProcessor s_preProcessor;
private static Map<String, String> ignoredClassloaderNames = new HashMap<String, String>();
static {
try {
s_preProcessor = new Aj();
s_preProcessor.initialize();
String ignoredLoaders = System.getProperty("org.aspectj.weaver.loadtime.filter", "");
if (ignoredLoaders.length() > 0) {
String[] loaders = ignoredLoaders.split(",");
for (String s : loaders) {
s = s.trim();
ignoredClassloaderNames.put(s, s);
System.out.println("---> Will filtered out classloader: " + s);
}
}
} catch (Exception e) {
throw new ExceptionInInitializerError("could not initialize JSR163 preprocessor due to: " + e.toString());
}
}
/**
* Invokes the weaver to modify some set of input bytes.
*
* #param loader the defining class loader
* #param className the name of class being loaded
* #param classBeingRedefined is set when hotswap is being attempted
* #param protectionDomain the protection domain for the class being loaded
* #param bytes the incoming bytes (before weaving)
* #return the woven bytes
*/
#Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {
if (classBeingRedefined != null) {
System.err.println("INFO: (Enh120375): AspectJ attempting reweave of '" + className + "'");
}
String loaderName = loader.getClass().getName();
if (shouldIgnoreClassLoader(loaderName)) {
return bytes;
}
return s_preProcessor.preProcess(className, bytes, loader, protectionDomain);
}
private boolean shouldIgnoreClassLoader(String loaderName) {
boolean result = false;
String ignoredLoader = ignoredClassloaderNames.get(loaderName);
if (ignoredLoader != null) {
result = true; // if the loader name exists in the map we will ignore weaving
}
return result;
}
}
If you need the feature to exclude classloaders from weaving with the agent approach, there is a developer build available providing a new command line switch -Daj.weaving.loadersToSkip to do that. The topic is being discussed on a thread of the AspectJ users mailing list. The feature will probably make it into AspectJ 1.7.4, but is not available in 1.7.3 yet.
Update:
The feature did make it into AspectJ 1.7.4 even though it is not explicitly mentioned in the release notes, but listed under resolved issues for that release.
What I ended up doing is changing the LOG Level for the error message from ERROR to DEBUG, as I don't see this as an ERROR ( at least in my case ). this case I can still see the error when I enable the DEBUG level. so I modified the source file below and rebuild my aspectjweaver-1.7.1.jar
try {
registerOptions(weaver, loader, definitions);
registerAspectExclude(weaver, loader, definitions);
registerAspectInclude(weaver, loader, definitions);
success = registerAspects(weaver, loader, definitions);
registerIncludeExclude(weaver, loader, definitions);
registerDump(weaver, loader, definitions);
} catch (Exception ex) {
//(CHANGE 1) trace.error("register definition failed", ex);
trace.debug( "register definition failed" + ex.getMessage());
success = false;
// (CHANGE 2) warn("register definition failed", (ex instanceof AbortException) ? null : ex);
debug("register definition failed" + ((ex instanceof AbortException) ? null : ex));
}