DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Actuator Enhancements: Spring Framework 6.2 and Spring Boot 3.4
  • How Spring Boot Starters Integrate With Your Project
  • A Practical Guide to Creating a Spring Modulith Project
  • Structured Logging in Spring Boot 3.4 for Improved Logs

Trending

  • Endpoint Security Controls: Designing a Secure Endpoint Architecture, Part 2
  • Navigating Double and Triple Extortion Tactics
  • Memory-Optimized Tables: Implementation Strategies for SQL Server
  • Designing for Sustainability: The Rise of Green Software
  1. DZone
  2. Coding
  3. Frameworks
  4. Spring Boot Classloader and Class Overriding

Spring Boot Classloader and Class Overriding

Want to temporarily override library classes with your own custom ones, but Spring Boot's classloader is making it tough? Here is one solution.

By 
Dawid Kublik user avatar
Dawid Kublik
·
Jan. 11, 18 · Tutorial
Likes (22)
Comment
Save
Tweet
Share
137.4K Views

Join the DZone community and get the full member experience.

Join For Free

this article explains the spring boot classloader ( launchedurlclassloader ) and a way to temporarily override library classes with your custom ones.

just a little fix

let’s say you found a bug in some third-party jar your app uses. as a good scout, you fixed it and created a pull request with a solution. the pull request was merged, but the fix is critical for you and you can’t wait till next library release. is using a library snapshot the only way? wouldn’t it be great if there was a solution to temporarily override only a few particular classes?

as an imaginary example ( follow the code ), let’s say you found a bug in the springbootbanner class and already have a solution to fix the banner’s colors — springbootbanner fixed .

( i know we can easily define custom banners in spring boot, it’s just a useful example — it will be super easy to spot if the ‘fix’ is working or not )

so what can we do to have the solution work immediately? let’s just take the class (with the package) and paste it into our project ( src/main/java ).

now let’s run the app from the ide and everything seems to work:

working fix in an ide

great! but the joy is premature… if you build the app and run it:

./gradlew build
cd build/libs
java -jar spring-boot-loader-play-0.0.1-snapshot.jar


the original banner is still being displayed, and this is not about a terminal not supporting ansi colors.

the banner class ( springbootbanner ) was simply not overridden.

not working fix when running jar

the difference is that when you launch the app from an ide, you have two kinds of artifacts: classes and jars. classes are loaded before jars, so even though you have two versions of a class (your fix in /src/main/java and original in spring-boot-2.0.0.m7.jar lib), only the fix will be loaded. (classloaders don’t care about duplicates — the class that is found first is loaded).

spring boot classloader

with jars, the situation is harder. it’s a spring boot fat jar with the structure below:

+--- spring-boot-loader-play-0.0.1-snapshot.jar
     +--- meta-inf
     +--- boot-inf
     |    +--- classes                            # 1 - project classes
     |    |     +--- org.springframework.boot
     |    |     | \--- springbootbanner.class    # this is our fix
     |    |     | 
     |    |     +--- pl.dk.loaderplay
     |    |          \--- springbootloaderapplication.class
     |    |
     |    +--- lib                                # 2 - nested jar libraries
     |          +--- javax.annotation-api-1.3.1
     |          +--- spring-boot-2.0.0.m7.jar     # original banner class inside
     |          \--- (...)
     |
     +--- org.springframework.boot.loader         # spring boot loader classes
          +--- jarlauncher.class
          +--- launchedurlclassloader.class
          \--- (...)


so actually, it contains three types of entries:

  1. project classes
  2. nested jar libraries
  3. spring boot loader classes

both project classes ( boot-inf/classes ) and nested jars ( boot-inf/lib ) are handled by the same classloader, which, in turn, resides in the root of the jar ( org.springframework.boot.loader.launchedurlclassloader ).

one might expect that launchedurlclassloader will load the class content before the lib content, but the loader seems not the have that preference.

launchedurlclassloader extends java.net.urlclassloader , which is created with a set of urls that will be used for classloading. the url might point to a location like a jar archive or classes folder. when classloading, all of the resources specified by urls will be traversed in the order the urls were provided, and the first resource containing the searched class will be used.

so how are the urls are provided to launchedurlclassloader? the jar archive is parsed from top to bottom, and when an archive is found, it’s added to the url list.

in our example:

"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/spring-boot-starter-2.0.0.m7.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/spring-boot-autoconfigure-2.0.0.m7.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/spring-boot-2.0.0.m7.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/spring-boot-starter-logging-2.0.0.m7.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/javax.annotation-api-1.3.1.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/spring-context-5.0.2.release.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/spring-aop-5.0.2.release.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/spring-beans-5.0.2.release.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/spring-expression-5.0.2.release.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/spring-core-5.0.2.release.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/snakeyaml-1.19.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/logback-classic-1.2.3.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/log4j-to-slf4j-2.10.0.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/jul-to-slf4j-1.7.25.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/spring-jcl-5.0.2.release.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/logback-core-1.2.3.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/slf4j-api-1.7.25.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/lib/log4j-api-2.10.0.jar!/"
"jar:file:/users/dk/spring-boot-loader-play/build/libs/spring-boot-loader-play-0.0.1-snapshot.jar!/boot-inf/classes!/"


as we can see, /boot-inf/classes is the last entry on the list — far after /boot-inf/lib/spring-boot-2.0.0.m7.jar . so, in the search for springbootbanner.class, the version from the latter will be used — not an outcome we hoped for.

on our quest to figure out what we can do about it, it’s worth zooming into how classloaders work in hierarchies.

basically, classloaders form hierarchies — with every child loader having a reference to its parent. with launchedurlclassloader being the youngest descendant in spring boot's case, we end up with a simple hierarchy like this:

+--- sun.misc.launcher$extclassloader     # loading classes /jre/lib/ext/
     +--- sun.misc.launcher.launcher$appclassloader    # loading classes from the root of the jar - spring-boot-loader-play-0.0.1-snapshot.jar
          +--- org.springframework.boot.loader.launchedurlclassloader    # loading classes from /boot-inf/lib/ & /boot-inf/classes/

in spring boot, when the class is about to be loaded, we always start with launchedurlclassloader, but the “parent first” rule applies. this means that the child loader will try to load a given class only if the parent doesn’t find it.

first idea: appclassloader

if launchedurlclassloader delegates classloading to appclassloader, then why not to use it to load our class before it’s loaded by launchedurlclassloader ?

you might be tempted to simply do:

thread.currentthread().getcontextclassloader()
        .getparent()
        .loadclass("org.springframework.boot.springbootbanner");


but this won’t work. yes, thread.currentthread().getcontextclassloader().getparent() gets us the correct appclassloader, but this one is designed to work with standard jars — where classes (with packages) are placed in the root of the jar. so where appclassloader has no problems handling the org.springframework.boot.loader classes (see the jar directory tree above), it will not find classes in boot-inf/classes .

meanwhile...

thread.currentthread().getcontextclassloader()
        .getparent()
        .loadclass("boot-inf/classes/org/springframework/boot/springbootbanner");


...won’t work either. yes, the class will be found, but the package will not match the path.

it appears there is no other way than copying springbootbanner from boot-inf/classes/org/ into the root of the jar. if we do that, there is no need to call appclassloader directly to load the class, as it will always have precedence before launchedurlclassloader .

this is easily done with this gradle build:

bootjar {
    with copyspec {
        from "$builddir/classes/java/main/org"
        into 'org'
    }
}


we can launch the app and find that springbootbanner was copied to the jar root and appclassloader was used to load it, but it won’t work. the problem is that springbootbanner depends on other classes — loaded by the child launchedurlclassloader . one thing we forgot about classloader hierarchy is that classes loaded by parents don’t see classes loaded by children.
the “load by appclassloader” idea seems to be a dead end — but worry not. we will use that knowledge with our second attempt!

launchedurlclassloader: resource order

it appears that parent loaders are not an option and we are stuck with the last loader in the hierarchy — launchedurlclassloader . you might remember that launchedurlclassloader loads classes traversing nested resources in the order they were provided to it. so let’s try to manipulate the order so that the /boot-inf/classes/ resource is first — not last on the list.

with org.springframework.boot.loader.jarlauncher , this seems to be an easy task, as it provides:

protected void postprocessclasspatharchives(list<archive> archives)


this method manipulates archives just before they are given to launchedurlclassloader .

so let’s write a custom launcher using this functionality:

public class classesfirstjarlauncher extends jarlauncher {

    @override
    protected void postprocessclasspatharchives(list<archive> archives)
            throws malformedurlexception {
        for (int i = archives.size() - 1; i >= 0; i--) {
            archive archive = archives.get(i);
            if (archive.geturl().getpath().endswith("/classes!/")) {
                archives.remove(archive);
                archives.add(0, archive);
                break;
            }
        }
    }

    public static void main(string[] args) throws exception {
        new classesfirstjarlauncher().launch(args);
    }
}


a quick reminder is that jarlauncher is the class launching your spring boot app. check any spring boot manifest.mf, and you will find something like:

manifest-version: 1.0
start-class: pl.dk.loaderplay.springbootloaderapplication
main-class: org.springframework.boot.loader.jarlauncher


main-class being the class with the main method launched when we do:

java -jar spring-boot-loader-play-0.0.1-snapshot.jar


jarlauncher must be loaded by appclassloader ( launchedurlclassloader is not even loaded itself yet), and to do that, it must be placed in the root of the jar. let’s use the trick we learned before:

bootjar {
    with copyspec {
        from "$builddir/classes/java/main/pl/dk/loaderplay/classesfirstjarlauncher.class"
        into 'pl/dk/loaderplay'
    }
}


what remained is to replace main-class in our manifest.mf . spring boot gradle plugin provides a way to do it:

bootjar {
    manifest {
        attributes 'main-class': 'pl.dk.loaderplay.classesfirstjarlauncher'
    }
}


unfortunately, when replacing main-class , the original spring boot loader/launcher classes are not copied to the root of the jar — and we still need them. this is how spring boot gradle plugin works, and i have not found a way around it. (it happens because the plugin’s bootzipcopyaction decision whether or not to copy the loader files is based upon whether the original jarlauncher was used or not).

so changing main-class by bootjar configuration is of no use to us. one can try to change it in some other way. for me, it was enough to leave the original main-class in the manifesto and simply specify start class when launching the app.

java -cp spring-boot-loader-play-0.0.1-snapshot.jar \
pl.dk.loaderplay.classesfirstjarlauncher


when doing so, the class was finally overridden:

working fix when running jar

quick summary

to summarize shortly:

goal

override...

spring-boot-loader-play-0.0.1-snapshot.jar
/boot-inf/lib/spring-boot-2.0.0.m7.jar/org/springframework/boot/springbootbanner.class


...with:

spring-boot-loader-play-0.0.1-snapshot.jar
/boot-inf/classes/org/springframework/boot/bootspringbootbanner.class


steps

  1. place the overriding springbootbanner in src/main/java
  2. create a custom launcher ordering the resources from which classes are loaded — classesfirstjarlauncher
  3. copy the launcher to root of the jar via bootjar gradle task
  4. launch the archive specifying the launcher class:
    java -cp spring-boot-loader-play-0.0.1-snapshot.jar \
    pl.dk.loaderplay.classesfirstjarlauncher

again, you may check the code here .

Spring Framework Spring Boot

Published at DZone with permission of Dawid Kublik, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Actuator Enhancements: Spring Framework 6.2 and Spring Boot 3.4
  • How Spring Boot Starters Integrate With Your Project
  • A Practical Guide to Creating a Spring Modulith Project
  • Structured Logging in Spring Boot 3.4 for Improved Logs

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!