TL;DR
- expose java static calls as Karaf shell native commands
- override OSGi Headers at deploy time
- override OSGi Headers after deploy time with OSGi Fragments
Expose java static calls as Karaf shell native commands
As part of my job as software engineer that has to collaborate with support guys and customers, I very often find myself in the need of extracting additional information from a system I don't have access to.
Usual approaches, valid in all kind of softwares, are usually extracting logs, invoking interactive commands to obtain specific outputs or in what is the most complex case deploy some PoC unit that is supposed to verify a specific behavior.
JBoss Fuse, adn Karaf, the platform it's based onto do alredy a great job in exposing all those data.
You have:
- extensive logs and integration with Log4j
- extensive list of jmx operation (you can eventually invoke over http with jolokia)
- a large list of shell commands
But sometimes this is not enough. If you have seen my previous post about how to use Byteman on JBoss Fuse, you can imagine all the other cases:
- you need to print values that are not logged or returned in the code
- you might need to short-circuit some logic to hit a specific execution branch of your code
- you want to inject a line of code that wasn't there at all
Byteman is still a very good option to, but Karaf has a facility we can use to run custom code.
Karaf, allows you to write code directly in its shell; and allows you to record these bits of code as macro you can re-invoke. This macro will look like a native Karaf shell command!
Let's see a real example I had to implement:
verify if the jvm running my JBoss Fuse instance was resolving a specific DNS as expected.
The standard JDK has a method you can invoke to resolve a dns name:InetAddress.gettAllByName(String)
Since that command is simple enough, meaning it doesn't requires a complex or structured input, I thought I could turn it into an easy to reuse command:
# add all public static methods on a java class as commands to the namespace "my_context": # bundle 0 is because system libs are served by that bundle classloader addcommand my_context (($.context bundle 0) loadClass java.net.InetAddress)
That funky line is explained in this way:
addcommand
is the karaf shell functionality that accepts new commandsmy_context
is the namespace/prefix you will attach you command to. In my case, "dns" would have made a good namespace.($.context bundle 0)
invokes java code. In particular we are invoking the$.context
instances, that is a built-in instance exposed by Karaf shell to expose the OSGi framework, whose type isorg.apache.felix.framework.BundleContextImpl
, and we are invoking its method calledbundle
passing it the argument0
representing the id of the OSGi classloader responsible to load the JDK classes. That call returns an instance oforg.apache.felix.framework.Felix
that we can use to load the specific class definition we need, that isjava.net.InetAddress
.
As the inline comment says, an invocation of addcommand
, exposes all the public static method on that class. So we are now allowed to invoke those methods, and in particular, the one that can resolve dns entries:
JBossFuse:karaf@root> my_context:getAllByName "www.google.com" www.google.com/74.125.232.146 www.google.com/74.125.232.145 www.google.com/74.125.232.148 www.google.com/74.125.232.144 www.google.com/74.125.232.147 www.google.com/2a00:1450:4002:804:0:0:0:1014
This functionality is described on Karaf documentation page.
Override OSGi Headers at deploy time
If you work with Karaf, you are working with OSGi, love it or hate it.
A typical step in each OSGi workflow is playing (or fighting) with OSGi headers.
If you are in total control of you project, this might be more or less easy, depending on the releationship between your deployment units. See Christian Posta post to have a glimpse of some less than obvious example.
Within those conditions, a very typical situation is the one when you have to use a bundle, yours or someone else's, and that bundle headers are not correct.
What you end up doing, very often is to re-package that bundles, so that you can alter the content of its MANIFEST
, to add the OSGi headers that you need.
Karaf has a facility in this regard, called the wrap
protocol.
You might alredy know it as a shortcut way to deploy a non-bundle jar on Karaf but it's actually more than just that.
What it really does, as the name suggest, is to wrap. But it can wrap both non-bundles and bundles!
Meaning that we can also use it to alter the metadata of an already packaged bundle we are about to install.
Let's give an example, again taken fron a real life experience.
Apache HttpClient is not totally OSGi friendly. We can install it on Karaf with the wrap:
protocol and export all its packages.
JBossFuse:karaf@root> install -s 'mvn:org.apache.httpcomponents/httpclient/4.2.5' Bundle ID: 257 JBossFuse:karaf@root> exports | grep -i 257 257 No active exported packages. This command only works on started bundles, use osgi:headers instead JBossFuse:karaf@root> install -s 'wrap:mvn:org.apache.httpcomponents/httpclient/\ 4.2.5$Export-Package=*; version=4.2.5' Bundle ID: 259 JBossFuse:karaf@root> exports | grep -i 259 259 org.apache.http.client.entity; version=4.2.5 259 org.apache.http.conn.scheme; version=4.2.5 259 org.apache.http.conn.params; version=4.2.5 259 org.apache.http.cookie.params; version=4.2.5 ...
And we can see that it works with plain bundles too:
JBossFuse:karaf@root> la -l | grep -i camel-core [ 142] [Active ] [ ] [ ] [ 50] mvn:org.apache.camel/camel-core/2.12.0.redhat-610379 JBossFuse:karaf@root> install -s 'wrap:mvn:org.apache.camel/camel-core/2.12.0.redhat-610379\ $overwrite=merge&Bundle-SymbolicName=paolo-s-hack&Export-Package=*; version=1.0.1' Bundle ID: 269 JBossFuse:karaf@root> headers 269 camel-core (269) ---------------- ... Bundle-Vendor = Red Hat, Inc. Bundle-Activator = org.apache.camel.impl.osgi.Activator Bundle-Name = camel-core Bundle-DocURL = http://redhat.com Bundle-Description = The Core Camel Java DSL based router Bundle-SymbolicName = paolo-s-hack Bundle-Version = 2.12.0.redhat-610379 Bundle-License = http://www.apache.org/licenses/LICENSE-2.0.txt Bundle-ManifestVersion = 2 ... Export-Package = org.apache.camel.fabric; uses:="org.apache.camel.util, org.apache.camel.model, org.apache.camel, org.apache.camel.processor, org.apache.camel.api.management, org.apache.camel.support, org.apache.camel.spi"; version=1.0.1, ...
Where you can see Bundle-SymbolicName
and the version of the exported packages are carrying the values I set.
Again, the functionality is described on Karaf docs and you might find useful the wrap protocol reference.
Override OSGi Headers after deploy time with OSGi Fragments
Last trick is powerful, but it probably requires you to remove the original bundle if you don't want to risk having half of the classes exposed by one classloader and the remaining ones (those packages you might have added in the overridden Export
) in another one.
There is actually a better way to override OSGi headers, and it comes directly from an OSGi standard functionality: OSGi Fragments.
If you are not familiare with the concept, the definition taken directly from OSGi wiki is:
A Bundle fragment, or simply a fragment, is a bundle whose contents are made available to another bundle (the fragment host). Importantly, fragments share the classloader of their parent bundle.
That page gives also a further hint about what I will describe:
Sometimes, fragments are used to 'patch' existing bundles.
We can use this strategy to:
- inject .jars in the classpath of our target bundle
- alter headers of our target bundle
I have used the first case to fix a badly configured bundle that was looking for a an xml configuration descriptor that it didn't include, and that I have provided deploying a light Fragment Bundle that contained just that.
But the use case I want to show you here instead, is an improvement regarding the way to deploy Byteman on JBoss Fuse/Karaf.
If you remember my previous post, since Byteman classes needed to be available from every other deployed bundle and potentially need access to every class available, we had to add Byteman packages to the org.osgi.framework.bootdelegation
property, that instructs the OSGi Framework to expose the listed packages through the virtual system bundle (id = 0).
You can verify what is currently serving with headers 0
, I won't include the output here since it's a long list of jdk extension and framework classes.
If you add your packages, org.jboss.byteman.rule,org.jboss.byteman.rule.exception
in my case, even these packages will be listed in the output of that command.
The problem with this solution is that this is a boot time property. If you want to use Byteman to manipulate the bytecode of an already running instance, you have to restart it after you have edited this properties.
OSGi Fragments can help here, and avoid a preconfiguration at boot time.
We can build a custom empty bundle, with no real content, that attaches to the system bundle and extends the list of packages it serves.
<Export-Package> org.jboss.byteman.rule,org.jboss.byteman.rule.exception </Export-Package> <Fragment-Host> system.bundle; extension:=framework </Fragment-Host>
That's an excerpt of maven-bundle-plugin plugin configuration, see here for the full working Maven project, despite the project it's really just 30 lines of pom.xml
:
JBossFuse:karaf@root> install -s mvn:test/byteman-fragment/1.0-SNAPSHOT
Once you have that configuration, you are ready to use Byteman, to, for example, inject a line in java.lang.String
default constructor.
# find your Fuse process id PROCESS_ID=$(ps aux | grep karaf | grep -v grep | cut -d ' ' -f2) # navigate to the folder where you have extracted Byteman cd /data/software/redhat/utils/byteman/byteman-download-2.2.0.1/ # export Byteman env variable: export BYTEMAN_HOME=$(pwd) cd bin/ # attach Byteman to Fabric8 process, no output expected unless you enable those verbose flags sh bminstall.sh -b -Dorg.jboss.byteman.transform.all $PROCESS_ID # add these flags if you have any kind of problem and what to see what's going on: -Dorg.jboss.byteman.debug -Dorg.jboss.byteman.verbose # install our Byteman custom rule, we are passing it directly inline with some bash trick sh bmsubmit.sh /dev/stdin <<OPTS # smoke test rule that uses also a custom output file RULE DNS StringSmokeTest CLASS java.lang.String METHOD <init>() AT ENTRY IF TRUE DO traceln(" works: " ); traceOpen("PAOLO", "/tmp/byteman.txt"); traceln("PAOLO", " works in files too " ); traceClose("PAOLO"); ENDRULE OPTS
Now, to verify that Byteman is working, we can just invoke java.lang.String
constructor in Karaf shell:
JBossFuse:karaf@root> new java.lang.String works:
And as per our rule, you will also see the content in /tmp/byteman.txt
Inspiration for this third trick come from both the OSGi wiki and this interesting page from Spring guys.
If you have any comment or any other interesting workflow please leave a comment.