Sometimes you need to parse JSON files in a library. Then the question is pretty quickly: do I use a SPI (Service Provider Interface) to let the user choose the library he uses with my code or do I bind my library to one parser/writer?
Sometimes the answer is easy: when the need is basic (read/write) having a SPI and a default implementation is pretty good and let your users avoid to depend on one implementation. Said otherwise, you let your users reuse the implementation they probably already have in their application/classpath.
However sometimes you need a more advanced setup to get some customizations or features (like comments in JSON for instance which is very handy for configuration files) so either you bind your library to one implementation and your users end with N JSON implementations they don’t control and which potentially conflict together, or you enhance a lot your SPI risking to loose some features if your user chooses an implementation not supporting some of your needs, or you can also grab an implementation which work fine for you and just put it in your delivery. This last option is pretty nice cause then your code can use all the shortcuts an implementation can offer – like custom annotations.
Going further with this last option you can also potentially reuse some standard API like JSON-P or the coming JSON-B in your code and just hide it and not depend on the container for the runtime.
Of course there is no free lunch and it will make your delivery a bit more heavy but a good mapper is ~200k max so it is still a good option for JSON needs.
How to do it?
You have generally two options leading to the same technical solution: the shading with relocation.
Just before going in code, the two options are:
- You deliver a set of library: in this case you relocate the JSON implementation in one jar without your own code which is in another dependency/jar
- You include the implementation in your own dependency
Whichever packaging you have to deal with the solution is to just write code with the library you use. For instance if you use Apache Johnzon here what your code can look like:
import org.apache.johnzon.mapper.Mapper; import org.apache.johnzon.mapper.MapperBuilder; import java.io.InputStream; import java.util.SortedMap; public class ApplicationLoader { private final Mapper mapper = new MapperBuilder().build(); public ApplicationDescriptor read(final InputStream stream) { return mapper.readObject(stream, ApplicationDescriptor.class); } public static class ApplicationDescriptor { private String version; private SortedMap<String, ComponentConfiguration> components; // getters/setters @Override public String toString() { return "ApplicationDescriptor{version='" + version + "', components=" + components + '}'; } public static class ComponentConfiguration { private boolean eager; private String name; // getters/setters @Override public String toString() { return "ComponentConfiguration{eager=" + eager + ", name='" + name + "'}"; } } } }
Of course in this sample I used the Johnzon Mapper but using JSON-P API (ie using johnzon-core instead of johnzon-mapper as dependency) would have looked as normal as previous code.
Also note this code imports standard johnzon classes and not the shaded ones. This is because we will shade them in the same module our code is.
For that purpose we simply have as dependencies in our pom.xml the following artifacts:
<dependencies> <!-- compile scope since we will shade/relocate it --> <dependency> <groupId>org.apache.geronimo.specs</groupId> <artifactId>geronimo-json_1.0_spec</artifactId> <version>1.0-alpha-1</version> </dependency> <dependency> <groupId>org.apache.johnzon</groupId> <artifactId>johnzon-mapper</artifactId> <version>0.9.1-incubating</version> </dependency> </dependencies>
Now all our code is ready we just need to say to maven shade plugin that during the packaging we will copy the johnzon classes in com.github.rmannibucau.blog.johnzon.embedded.shaded.org.apache.johnzon package and the javax.json classes in com.github.rmannibucau.blog.johnzon.embedded.shaded.javax.json for instance – Johnzon relies on JSON-P for the parsing so we need the JSON-P specification API jar as well.
This looks like:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.4</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <relocations> <relocation> <pattern>org.apache.johnzon</pattern> <shadedPattern>com.github.rmannibucau.blog.johnzon.embedded.shaded.org.apache.johnzon</shadedPattern> </relocation> <relocation> <pattern>javax.json</pattern> <shadedPattern>com.github.rmannibucau.blog.johnzon.embedded.shaded.javax.json</shadedPattern> </relocation> </relocations> </configuration> </execution> </executions> </plugin>
Once you executed “mvn package” you can check you don’t have javax.json or org.apache.johnzon classes in your jar:
$ jar tf target/mylib-1.0-SNAPSHOT.jar | grep -v com | grep -v META-INF ...empty result....
but if you grep johnzon or json spec classes you find them – because we kept the name in the relocated package, it means javax/json is actually com/github/rmannibucau/blog/johnzon/embedded/shaded/javax/json for instance:
$ for i in 'javax/json' 'org/apache/johnzon'; do echo -n $i': '; jar tf target/embedded-johnzon-1.0-SNAPSHOT.jar | grep -v 'META-INF' | grep $i | wc -l; done javax/json: 32 org/apache/johnzon: 124
Using last johnzon version the shade is ~210K. Not relying on the Mapper but only JSON-P (ie johnzon-core) you can decrease it to 120K pretty easily. With few more work (exclusions/inclusions) you can even make it lighter but not sure it does worth it a lot at this point.
So finally your library can rely as much as it needs on johnzon advanced configuration and features and it is bundled with your application in a way avoiding any conflicts with other JSON-P implementations or JSON solutions.