Building a REST API with JAXB, Spring Boot and Spring Data
Join the DZone community and get the full member experience.
Join For Freeif someone asked you to develop a rest api on the jvm, which frameworks would you use? i was recently tasked with such a project. my client asked me to implement a rest api to ingest requests from a 3rd party. the project entailed consuming xml requests, storing the data in a database, then exposing the data to internal application with a json endpoint. finally, it would allow taking in a json request and turning it into an xml request back to the 3rd party.
with the recent release of apache camel 2.14 and my success using it , i started by copying my apache camel / cxf / spring boot project and trimming it down to the bare essentials. i whipped together a simple hello world service using camel and spring mvc. i also integrated swagger into both. both implementations were pretty easy to create ( sample code ), but i decided to use spring mvc. my reasons were simple: its rest support was more mature, i knew it well, and spring mvc test makes it easy to test apis.
camel's swagger support without web.xml
as part of the aforementioned spike, i learned out how to configure camel's rest and swagger support using spring's javaconfig and no web.xml. i made this into a sample project and put it on github as
camel-rest-swagger
.
this article shows how i built a rest api with java 8, spring boot/mvc, jaxb and spring data (jpa and rest components). i stumbled a few times while developing this project, but figured out how to get over all the hurdles. i hope this helps the team that's now maintaining this project (my last day was friday) and those that are trying to do something similar.
xml to java with jaxb
the data we needed to ingest from a 3rd party was based on the ncpdp standards. as a member, we were able to download a number of xsd files, put them in our project and generate java classes to handle the incoming/outgoing requests. i used the maven-jaxb2-plugin to generate the java classes.
<plugin>
<groupid>org.jvnet.jaxb2.maven2</groupid>
<artifactid>maven-jaxb2-plugin</artifactid>
<version>0.8.3</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<args>
<arg>-xtostring</arg>
<arg>-xequals</arg>
<arg>-xhashcode</arg>
<arg>-xcopyable</arg>
</args>
<plugins>
<plugin>
<groupid>org.jvnet.jaxb2_commons</groupid>
<artifactid>jaxb2-basics</artifactid>
<version>0.6.4</version>
</plugin>
</plugins>
<schemadirectory>src/main/resources/schemas/ncpdp</schemadirectory>
</configuration>
</execution>
</executions>
</plugin>
the first error i ran into was about a property already being defined.
[info] --- maven-jaxb2-plugin:0.8.3:generate (default) @ spring-app ---
[error] error while parsing schema(s).location [ file:/users/mraible/dev/spring-app/src/main/resources/schemas/ncpdp/structures.xsd{1811,48}].
com.sun.istack.saxparseexception2; systemid: file:/users/mraible/dev/spring-app/src/main/resources/schemas/ncpdp/structures.xsd;
linenumber: 1811; columnnumber: 48; property "multipletimingmodifierandtimingandduration" is already defined.
use <jaxb:property> to resolve this conflict.
at com.sun.tools.xjc.errorreceiver.error(errorreceiver.java:86)
i was able to workaround this by upgrading to maven-jaxb2-plugin version 0.9.1. i created a controller and stubbed out a response with hard-coded data. i confirmed the incoming xml-to-java marshalling worked by testing with a sample request provided by our 3rd party customer. i started with a
curl
command, because it was easy to use and could be run by anyone with the file and curl installed.
curl -x post -h 'accept: application/xml' -h 'content-type: application/xml' \
--data-binary @sample-request.xml http://localhost:8080/api/message -v
this is when i ran into another stumbling block: the response wasn't getting marshalled back to xml correctly. after some research, i found out this was caused by the lack of
@xmlrootelement
annotations on my generated classes. i posted a question to stack overflow titled
returning jaxb-generated elements from spring boot controller
. after banging my head against the wall for a couple days, i figured out
the solution
.
i created a bindings.xjb file in the same directory as my schemas. this causes jaxb to generate
@xmlrootelement
on classes.
<?xml version="1.0"?>
<jxb:bindings version="1.0"
xmlns:xsd="http://www.w3.org/2001/xmlschema"
xmlns:jxb="http://java.sun.com/xml/ns/jaxb"
xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"
xsi:schemalocation="http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_0.xsd">
<jxb:bindings schemalocation="transport.xsd" node="/xsd:schema">
<jxb:globalbindings>
<xjc:simple/>
</jxb:globalbindings>
</jxb:bindings>
</jxb:bindings>
to add namespaces prefixes to the returned xml, i had to modify the maven-jaxb2-plugin to add a couple arguments.
<arg>-extension</arg>
<arg>-xnamespace-prefix</arg>
and add a dependency:
<dependencies>
<dependency>
<groupid>org.jvnet.jaxb2_commons</groupid>
<artifactid>jaxb2-namespace-prefix</artifactid>
<version>1.1</version>
</dependency>
</dependencies>
then i modified
bindings.xjb
to include the package and prefix settings. i also moved
<xjc:simple/>
into a global setting. i eventually had to add prefixes for all schemas and their packages.
<?xml version="1.0"?>
<bindings version="2.0" xmlns:xsd="http://www.w3.org/2001/xmlschema" xmlns="http://java.sun.com/xml/ns/jaxb"
xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"
xmlns:namespace="http://jaxb2-commons.dev.java.net/namespace-prefix"
xsi:schemalocation="http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_0.xsd
http://jaxb2-commons.dev.java.net/namespace-prefix http://java.net/projects/jaxb2-commons/sources/svn/content/namespace-prefix/trunk/src/main/resources/prefix-namespace-schema.xsd">
<globalbindings>
<xjc:simple/>
</globalbindings>
<bindings schemalocation="transport.xsd" node="/xsd:schema">
<schemabindings>
<package name="org.ncpdp.schema.transport"/>
</schemabindings>
<bindings>
<namespace:prefix name="transport"/>
</bindings>
</bindings>
</bindings>
i learned how to add prefixes from the namespace-prefix plugins page .
finally, i customized the code-generation process to generate joda-time's
datetime
instead of the default
xmlgregoriancalendar
. this involved a couple custom xmladapters and a couple additional lines in
bindings.xjb
. you can see the adapters and
bindings.xjb
with all necessary prefixes in
this gist
. nicolas fränkel's
customize your jaxb bindings
was a great resource for making all this work.
i wrote a test to prove that the ingest api worked as desired.
@runwith(springjunit4classrunner.class)
@springapplicationconfiguration(classes = application.class)
@webappconfiguration
@dirtiescontext(classmode = dirtiescontext.classmode.after_class)
public class initiaterequestcontrollertest {
@inject
private initiaterequestcontroller controller;
private mockmvc mockmvc;
@before
public void setup() {
mockitoannotations.initmocks(this);
this.mockmvc = mockmvcbuilders.standalonesetup(controller).build();
}
@test
public void testgetnotallowedonmessagesapi() throws exception {
mockmvc.perform(get("/api/initiate")
.accept(mediatype.application_xml))
.andexpect(status().ismethodnotallowed());
}
@test
public void testpostpainitiationrequest() throws exception {
string request = new scanner(new classpathresource("sample-request.xml").getfile()).usedelimiter("\\z").next();
mockmvc.perform(post("/api/initiate")
.accept(mediatype.application_xml)
.contenttype(mediatype.application_xml)
.content(request))
.andexpect(status().isok())
.andexpect(content().contenttype(mediatype.application_xml))
.andexpect(xpath("/message/header/to").string("3rdparty"))
.andexpect(xpath("/message/header/sendersoftware/sendersoftwaredeveloper").string("hid"))
.andexpect(xpath("/message/body/status/code").string("010"));
}
}
spring data for jpa and rest
with jaxb out of the way, i turned to creating an internal api that could be used by another application. spring data was fresh in my mind after reading about it last summer. i created classes for entities i wanted to persist, using lombok's @data to reduce boilerplate.
i read the accessing data with jpa guide, created a couple repositories and wrote some tests to prove they worked. i ran into an issue trying to persist joda's datetime and found jadira provided a solution.
i added its usertype.core as a dependency to my pom.xml:
<dependency>
<groupid>org.jadira.usertype</groupid>
<artifactid>usertype.core</artifactid>
<version>3.2.0.ga</version>
</dependency>
... and annotated datetime variables accordingly.
@column(name = "last_modified", nullable = false)
@type(type="org.jadira.usertype.dateandtime.joda.persistentdatetime")
private datetime lastmodified;
with jpa working, i turned to exposing rest endpoints. i used accessing jpa data with rest as a guide and was looking at json in my browser in a matter of minutes. i was surprised to see a "profile" service listed next to mine, and posted a question to the spring boot team. oliver gierke provided an excellent answer .
swagger
spring mvc's integration for swagger
has greatly improved since i
last wrote about it
. now you can enable it with a
@enableswagger
annotation. below is the
swaggerconfig
class i used to configure swagger and read properties from
application.yml
.
@configuration
@enableswagger
public class swaggerconfig implements environmentaware {
public static final string default_include_pattern = "/api/.*";
private relaxedpropertyresolver propertyresolver;
@override
public void setenvironment(environment environment) {
this.propertyresolver = new relaxedpropertyresolver(environment, "swagger.");
}
/**
* swagger spring mvc configuration
*/
@bean
public swaggerspringmvcplugin swaggerspringmvcplugin(springswaggerconfig springswaggerconfig) {
return new swaggerspringmvcplugin(springswaggerconfig)
.apiinfo(apiinfo())
.genericmodelsubstitutes(responseentity.class)
.includepatterns(default_include_pattern);
}
/**
* api info as it appears on the swagger-ui page
*/
private apiinfo apiinfo() {
return new apiinfo(
propertyresolver.getproperty("title"),
propertyresolver.getproperty("description"),
propertyresolver.getproperty("termsofserviceurl"),
propertyresolver.getproperty("contact"),
propertyresolver.getproperty("license"),
propertyresolver.getproperty("licenseurl"));
}
}
after getting swagger to work, i discovered that endpoints published with
@repositoryrestresource
aren't picked up by swagger. there is an
open issue
for spring data support in the swagger-springmvc project.
liquibase integration
i configured this project to use h2 in development and postgresql in production. i used spring profiles to do this and copied xml/yaml (for maven and application*.yml files) from a previously created jhipster project.
next, i needed to create a database. i decided to use
liquibase
to create tables, rather than hibernate's schema-export. i chose liquibase over
flyway
based of discussions in the
jhipster project
. to use liquibase with spring boot is dead simple: add the following dependency to pom.xml, then place changelog files in
src/main/resources/db/changelog
.
<dependency>
<groupid>org.liquibase</groupid>
<artifactid>liquibase-core</artifactid>
</dependency>
i started by using hibernate's schema-export and changing
hibernate.ddl-auto
to "create-drop" in
application-dev.yml
. i also commented out the liquibase-core dependency. then i setup a postgresql database and started the app with "mvn spring-boot:run -pprod".
i generated the liquibase changelog from an existing schema using the following command (after downloading and installing liquibase).
liquibase --driver=org.postgresql.driver --classpath="/users/mraible/.m2/repository/org/postgresql/postgresql/9.3-1102-jdbc41/postgresql-9.3-1102-jdbc41.jar:/users/mraible/snakeyaml-1.11.jar" --changelogfile=/users/mraible/dev/spring-app/src/main/resources/db/changelog/db.changelog-02.yaml --url="jdbc:postgresql://localhost:5432/mydb" --username=user --password=pass generatechangelog
i did find one bug - the generatechangelog command generates too many constraints in version 3.2.2 . i was able to fix this by manually editing the generated yaml file.
tip: if you want to drop all tables in your database to verify liquibase creation is working in postgesql, run the following commands:
psql -d mydb
drop schema public cascade;
create schema public;
after writing minimal code for spring data and configuring liquibase to create tables/relationships, i relaxed a bit, documented how everything worked and added a loggingfilter . the loggingfilter was handy for viewing api requests and responses.
@bean
public filterregistrationbean loggingfilter() {
loggingfilter filter = new loggingfilter();
filterregistrationbean registrationbean = new filterregistrationbean();
registrationbean.setfilter(filter);
registrationbean.seturlpatterns(arrays.aslist("/api/*"));
return registrationbean;
}
accessing api with resttemplate
the final step i needed to do was figure out how to access my new and fancy api with resttemplate . at first, i thought it would be easy. then i realized that spring data produces a hal -compliant api, so its content is embedded inside an "_embedded" json key.
after much trial and error, i discovered i needed to create a resttemplate with hal and joda-time awareness.
@bean
public resttemplate resttemplate() {
objectmapper mapper = new objectmapper();
mapper.configure(deserializationfeature.fail_on_unknown_properties, false);
mapper.registermodule(new jackson2halmodule());
mapper.registermodule(new jodamodule());
mappingjackson2httpmessageconverter converter = new mappingjackson2httpmessageconverter();
converter.setsupportedmediatypes(mediatype.parsemediatypes("application/hal+json"));
converter.setobjectmapper(mapper);
stringhttpmessageconverter stringconverter = new stringhttpmessageconverter();
stringconverter.setsupportedmediatypes(mediatype.parsemediatypes("application/xml"));
list<httpmessageconverter<?>> converters = new arraylist<>();
converters.add(converter);
converters.add(stringconverter);
return new resttemplate(converters);
}
the
jodamodule
was provided by the following dependency:
<dependency>
<groupid>com.fasterxml.jackson.datatype</groupid>
<artifactid>jackson-datatype-joda</artifactid>
</dependency>
with the configuration complete, i was able to write a
messagesapiitest
integration test that posts a request and retrieves it using the api. the api was secured using basic authentication, so it took me a bit to figure out how to make that work with resttemplate. willie wheeler's
basic authentication with spring resttemplate
was a big help.
@runwith(springjunit4classrunner.class)
@contextconfiguration(classes = integrationtestconfig.class)
public class messagesapiitest {
private final static log log = logfactory.getlog(messagesapiitest.class);
@value("http://${app.host}/api/initiate")
private string initiateapi;
@value("http://${app.host}/api/messages")
private string messagesapi;
@value("${app.host}")
private string host;
@inject
private resttemplate resttemplate;
@before
public void setup() throws exception {
string request = new scanner(new classpathresource("sample-request.xml").getfile()).usedelimiter("\\z").next();
responseentity<org.ncpdp.schema.transport.message> response = resttemplate.exchange(gettesturl(initiateapi),
httpmethod.post, getbasicauthheaders(request), org.ncpdp.schema.transport.message.class,
collections.emptymap());
assertequals(httpstatus.ok, response.getstatuscode());
}
@test
public void testgetmessages() {
httpentity<string> request = getbasicauthheaders(null);
responseentity<pagedresources<message>> result = resttemplate.exchange(gettesturl(messagesapi), httpmethod.get,
request, new parameterizedtypereference<pagedresources<message>>() {});
httpstatus status = result.getstatuscode();
collection<message> messages = result.getbody().getcontent();
log.debug("messages found: " + messages.size());
assertequals(httpstatus.ok, status);
for (message message : messages) {
log.debug("message.id: " + message.getid());
log.debug("message.datecreated: " + message.getdatecreated());
}
}
private httpentity<string> getbasicauthheaders(string body) {
string plaincreds = "user:pass";
byte[] plaincredsbytes = plaincreds.getbytes();
byte[] base64credsbytes = base64.encodebase64(plaincredsbytes);
string base64creds = new string(base64credsbytes);
httpheaders headers = new httpheaders();
headers.add("authorization", "basic " + base64creds);
headers.add("content-type", "application/xml");
if (body == null) {
return new httpentity<>(headers);
} else {
return new httpentity<>(body, headers);
}
}
}
to get spring data to populate the message id, i created a custom
restconfig
class to expose it. i learned how to do this from
tommy ziegler
.
/**
* used to expose ids for resources.
*/
@configuration
public class restconfig extends repositoryrestmvcconfiguration {
@override
protected void configurerepositoryrestconfiguration(repositoryrestconfiguration config) {
config.exposeidsfor(message.class);
config.setbaseuri("/api");
}
}
summary
this article explains how i built a rest api using jaxb, spring boot, spring data and liquibase. it was relatively easy to build, but required some tricks to access it with spring's resttemplate. figuring out how to customize jaxb's code generation was also essential to make things work.
i started developing the project with spring boot 1.1.7, but upgraded to 1.2.0.m2 after i found it supported log4j2 and configuring spring data rest's base uri in application.yml. when i handed the project off to my client last week, it was using 1.2.0.build-snapshot because of a bug when running in tomcat .
this was an enjoyable project to work on. i especially liked how easy spring data makes it to expose jpa entities in an api. spring boot made things easy to configure once again and liquibase seems like a nice tool for database migrations.
if someone asked me to develop a rest api on the jvm, which frameworks would i use? spring boot, spring data, jackson, joda-time, lombok and liquibase. these frameworks worked really well for me on this particular project.
Published at DZone with permission of Matt Raible, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments