Open wtwhite opened 4 months ago
There may be a better way, at least for Spring Boot. It may be possible to implement all agent-related code (namely, the HTTP provenance pickup endpoint (a method sporting @RequestMapping
inside a @Controller
in Spring Boot terms), and the servlet filter (implemented as an Interceptor
with Spring)) in the agent after all, leaving the main app with no dependency on provenance-injector
at all:
@EnableAutoConfiguration
(on by default), which causes it to scan all dependency jars for a text file called META-INF/spring.factories
that lists @Configuration
classes that define beans with @Bean
-annotated methods.@Configuration
class of our own probably won't let us add a controller component directly, since AFAICT controllers are not "ordinary" Spring beans but are treated specially in certain ways, and can probably only be defined with @Controller
annotations -- but it should be possible to @Import
a component:As of Spring Framework 4.2,
@Import
also supports references to regular component classes, analogous to theAnnotationConfigApplicationContext.register
method.
If this works, it would be ideal: The app would have no dependency on this repo, and could be built to either:
Testing with a dummy Spring Boot app shows that the previous comment's META-INF/spring.factories
+ @Import(SomeController.class)
works, at least for controllers ๐
Specifically, I:
org.springframework.boot:spring-boot-starter-parent:2.5.5
and depends on org.springframework.boot:spring-boot-starter-web
per the instructions@Controller
component class named wtwhitetest.DummyController
with a single method annotated with @RequestMapping("/dummy")
and returning ResponseEntity.ok().body("blah");
wtwhitetest.spring.DummyControllerInAgentAutoConfiguration
class labeled with @Configuration
and @Import(wtwhitetest.DummyController.class)
src/main/resources/META-INF/spring.factories
containing org.springframework.boot.autoconfigure.EnableAutoConfiguration=wtwhitetest.spring.DummyControllerInAgentAutoConfiguration
mvn package
(instead of the Spring Boot plugin)WEB-INF/lib/test-spring-boot-controller-in-agent-1.0-SNAPSHOT.jar
and inserted it into the Spring Boot main app war file with zip -r -Z store with_exploded_embedded_provenance-agent_jar_and_dummy_controller_embedded_jar.war WEB-INF
-Z store
java -jar with_exploded_embedded_provenance-agent_jar_and_dummy_controller_embedded_jar.war
curl -i http://localhost:8080/dummy
and observed that:
Interceptor
defined in the main app was still called ๐Still TODO:
Interceptor
s, which require more setup via WebMvcConfigurerAdapter
Interceptor
s defined in the embedded jar work ๐<scope>provided</scope>
:
/dummy
controller can create a NoopProvenanceTracker
without problems.class
files from the main web app
NoopProvenanceTracker.class
from the app war and rerunning; the app starts up, but the first /dummy
request results in Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.NoClassDefFoundError: nz/ac/wgtn/veracity/provenance/injector/tracker/NoopProvenanceTracker]
I also confirmed that each part of the configuration (META-INF/spring.factories
, the empty @Configuration
class, the @Import
on it) is necessary for the /dummy
endpoint to work.
Conclusion: We can entirely remove all dependencies on this repo from a Spring Boot app ๐
For some app types, like Spring Boot apps, it currently seems necessary to include some or all of the
provenance-injector
class files on the classpath of the main app itself, e.g., with a Gradle command like:This is certainly necessary if we want to access classes/interfaces like
ProvenanceAgent
directly (non-reflectively) from within main app controllers/interceptors.However, running
java -javaagent:/path/to/provenance-agent.jar -jar mainapp.jar
then causes these classes to appear in the classpath twice (once via-javaagent
, once via a dependency jar nested inside the main app jar/war).My testing shows that, with Spring Boot, the classes provided via
-javaagent
take precedence both at agent startup time and when accessing them from inside the main app. That is, this currently seems to work -- but it's fragile, since it depends on which class loaders Spring Boot uses to load the application, and the details of how they work. It would be easy for the jar loaded with-javaagent
to get out of sync with the one nested inside the main app jar, and if Spring's class loader logic changed, it could lead to a hard-to-understand bug.We can't simultaneously:
NoClassDefFoundError
if-javaagent
is omitted from the command line.Dropping any of the 3 requirements above leads to a working solution. Currently we drop (2).
Possible solutions
Live with 2 copies of the agent classes in the classpath
What we're currently doing. A footgun.
Access all agent classes reflectively
Simple enough, but ugly, slow and prone to break at runtime instead of compile time if we change the agent implementation.
Shrink the duplicated classes as far as possible
The idea would be to make 2 implementations of a factory singleton named something like
ProvenanceAgentFactoryToBeReplacedAtRuntimebyJavaAgent
and containing just aProvenanceTracker makeProvenanceTracker()
a method -- one that the main web app bundles as a nested jar (perhaps returning aNoopProvenanceTracker
), the other that the agent includes, and which returns a genuine tracker. But this doesn't work well since the main app will need to duplicate (at least stub implementations of) all types mentioned in the interface (Invocation
,Activity
,Entity
, ...).(I considered going full
ServiceLoader
instead of using the same class name in two places, but it doesn't really help and only adds complexity.)Exclude java agent jar from the main web app
implementation
tocompileOnly
inbuild.gradle
to force Gradle to excludeprovenance-agent.jar
from the jar file it builds (changing it toprovidedRuntime
as suggested means it still gets included in the jar, just underWEB-INF/lib-provided
instead ofWEB-INF/lib
).The downside is that forgetting
-javaagent
on the command line will lead to an uglyNoClassDefFoundError
at startup.Include only "exploded" java agent classes in the main web app, ditch
-javaagent
JDK 11 (and maybe earlier, but not JDK 8) have a
Launcher-Agent-Class
manifest entry that can enable an agent to be started automatically (i.e., without specifying-javaagent
), though it can't read nested jar files.implementation
tocompileOnly
inbuild.gradle
as beforeMETA-INF/MANIFEST.MF
:java -jar mainapp.jar
๐Testing confirms that this works on a manually modified .war file ๐
This way we can't forget
-javaagent
, and we have just one copy of the agent classes in the classpath, so there are no footguns. Modifying the Gradle build (or (automatically) modifying the jar/war file it produces post-build) is some work and complexity though.