SpringSource Tool Suite を使って Apache CXF による REST サービスを作成する
NetBeansのサンプルアプリであれば、ものの数分で、RESTful なWebアプリケーションができあがるところなのだが、はまるポイントが盛りだくさん。。。何とか動かせるようになるまで、足かけ2日もかかってしまった。
プロジェクトの作成
まず、Spring Template Project の Spring MVC プロジェクトから、Spring MVC を外して、Apache CXF を入れ込むという手順を踏むことにした。
というのは、適当なプロジェクトテンプレートがなくて、Maven の組み込みとか考えるとよくわからないので、Spring MVCプロジェクトから不要なものを削るのが早いのかなぁと。
Maven依存関係の設定
まず、Spring MVC のプロジェクトを作成 して、pom.xml をいじくる。
基本的に、以下のサイトを参考にさせてもらいました。
dependencies 要素に、以下を追加。Derby と JPA を使って、XMLを返すところまでを今回は目指す。
<!-- TODO Apache CXF -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxrs</artifactId>
<version>${cxf.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http</artifactId>
<version>${cxf.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-databinding-aegis</artifactId>
<version>${cxf.version}</version>
</dependency>
<!-- TODO Derby -->
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derbyLocale_ja_JP</artifactId>
<version>10.8.1.2</version>
</dependency>
<!-- TODO JPA -->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>3.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>3.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-instrument</artifactId>
<version>3.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.5.4</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>2.1_3</version>
</dependency>
repositories 配下に以下を追加
<!-- TODO JPA -->
<repository>
<id>EclipseLink Repo</id>
<url>http://www.eclipse.org/downloads/download.php?r=1&nf=1&file=/rt/eclipselink/maven.repo</url>
<snapshots><enabled>false</enabled></snapshots>
</repository>
Entity の作成
Derby のデモデータ toursDB から CITIES テーブルと、COUNTRIES を取得するサービスをつくってみる。
Spring MVC から Apache Derby を JPA経由で使用するアプリケーションを作成
あたりを参考に、上記テーブルに対応する以下の Entity クラスを自動生成。
City クラス
- @XmlRootElement は、JAXB にて、XML に変換させるためのアノテーション
package info.typea.sample.restservice.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="city")
@Entity
@Table(name="CITIES")
public class City implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name="CITY_ID")
private int cityId;
private String airport;
@Column(name="CITY_NAME")
private String cityName;
private String country;
private String language;
//bi-directional many-to-one association to Country
@ManyToOne
@JoinColumn(name="COUNTRY_ISO_CODE")
private Country countryBean;
public City() {
}
public int getCityId() {
return this.cityId;
}
public void setCityId(int cityId) {
this.cityId = cityId;
}
public String getAirport() {
return this.airport;
}
public void setAirport(String airport) {
this.airport = airport;
}
public String getCityName() {
return this.cityName;
}
public void setCityName(String cityName) {
this.cityName = cityName;
}
public String getCountry() {
return this.country;
}
public void setCountry(String country) {
this.country = country;
}
public String getLanguage() {
return this.language;
}
public void setLanguage(String language) {
this.language = language;
}
public Country getCountryBean() {
return this.countryBean;
}
public void setCountryBean(Country countryBean) {
this.countryBean = countryBean;
}
}
Country.java
- @XmlTransient は、JAXBにて、XMLに変換するときに無視させるアノテーション。循環参照になってしまい、実行時例外となる。
package info.typea.sample.restservice.entity;
import java.io.Serializable;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
@XmlRootElement(name="country")
@Entity
@Table(name="COUNTRIES")
public class Country implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name="COUNTRY_ISO_CODE")
private String countryIsoCode;
private String country;
private String region;
//bi-directional many-to-one association to City
@OneToMany(mappedBy="countryBean")
private Set cities;
public Country() {
}
public String getCountryIsoCode() {
return this.countryIsoCode;
}
public void setCountryIsoCode(String countryIsoCode) {
this.countryIsoCode = countryIsoCode;
}
public String getCountry() {
return this.country;
}
public void setCountry(String country) {
this.country = country;
}
public String getRegion() {
return this.region;
}
public void setRegion(String region) {
this.region = region;
}
@XmlTransient
public Set getCities() {
return this.cities;
}
public void setCities(Set cities) {
this.cities = cities;
}
}
Cities.java
- 対応するテーブルはないが、City をまとめて XML とするためのクラスを用意しておく。
package info.typea.sample.restservice.entity;
import java.util.List;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="cities")
public class Cities {
private List cities;
public Cities() {
}
public Cities(List cities) {
this.cities = cities;
}
public List getCities() {
return cities;
}
public void setCities(List cities) {
this.cities = cities;
}
}
DAOの作成
上記の City を取得するためのDAOを作成。
CityDao.java (インターフェース)
package info.typea.sample.restservice.dao;
import info.typea.sample.restservice.entity.City;
import java.util.List;
public interface CityDao {
public City findById(String cityId);
public List findAll();
}
CityDaoImpl.java (実装クラス)
- “toursdb_persistence_unit” は、永続化ユニット名。persistence.xml にて指定する
package info.typea.sample.restservice.dao;
import info.typea.sample.restservice.entity.City;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.Query;
public class CityDaoImpl implements CityDao {
public City findById(String cityId) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("toursdb_persistence_unit");
EntityManager em = emf.createEntityManager();
Query query = em.createQuery("select c from City c where c.cityId = :cityId");
query.setParameter("cityId", Integer.parseInt(cityId));
return (City)query.getSingleResult();
}
public List findAll() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("toursdb_persistence_unit");
EntityManager em = emf.createEntityManager();
@SuppressWarnings("unchecked")
List<city> list = em.createQuery("select c from City c").getResultList();
return list;
}
}
サービスの作成
REST サービスのインターフェースを作成
CityResource.java (インターフェース)
- /{アプリケーション名}/city/{city id} で city id に一致する情報を取得
- /{アプリケーション名}/city/all ですべての情報を取得
- インターフェースにアノテートしておけば、実装クラスにも効く
package info.typea.sample.restservice.rs;
import info.typea.sample.restservice.entity.Cities;
import info.typea.sample.restservice.entity.City;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
@Path("/city")
public interface CityResource {
@GET
@Path("/{cityId}")
@Produces("{application/xml}")
public City getCity(@PathParam("cityId") String cityId);
@GET
@Path("/all")
@Produces("{application/xml}")
public Cities getCities();
}
CityResourceImpl.java (実装クラス)
- DAO に処理を委譲
- DAOのアクセッサは、DI用
package info.typea.sample.restservice.rs;
import info.typea.sample.restservice.dao.CityDao;
import info.typea.sample.restservice.entity.Cities;
import info.typea.sample.restservice.entity.City;
public class CityResourceImpl implements CityResource {
private CityDao cityDao;
public City getCity(String cityId) {
return cityDao.findById(cityId);
}
public Cities getCities() {
return new Cities(cityDao.findAll());
}
public CityDao getCityDao() {
return cityDao;
}
public void setCityDao(CityDao cityDao) {
this.cityDao = cityDao;
}
}
プロバイダーの作成
一番はまったのがここ。
を参考に、JAXB が、Java をXMLに変換するときに利用する、MessageBodyWriter を作成する。
Spring の Bean定義にて、org.apache.cxf.jaxrs.provider.JAXBElementProvider を利用してあげれば、@XmlRootElement 付きのクラスは自動でXML に変換してくれるんではないかと踏んでいたのだが、どうも意図したとおりに動いてくれないので、対応する。
その辺の記述は、この本に結構詳しく書いてあるので、後ほどじっくり検証することとしておき(非常にわかりやすい良書)、とりあえず動く程度のコードで先に進む。
CityWriter.java
- JAXBContext の初期化にコストがかかるみたいなので、DIするようにする
package info.typea.sample.restservice.provider;
import info.typea.sample.restservice.entity.City;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
@Provider
public class CityWriter implements MessageBodyWriter<City> {
private JAXBContext jaxbContext;
public long getSize(City city,
Class<?> type,
Type genericType,
Annotation[] annotation,
MediaType mediaType) {
return -1;
}
public boolean isWriteable(Class<?> type,
Type genericType,
Annotation[] annotation,
MediaType mediaType) {
return type.equals(City.class);
}
public void writeTo(City city, Class<?> type,
Type genericType,
Annotation[] annotation,
MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException, WebApplicationException {
try {
httpHeaders.add("Content-Type", "application/xml");
Writer writer = new OutputStreamWriter(entityStream, "UTF-8");
Marshaller mshr = jaxbContext.createMarshaller();
mshr.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
mshr.marshal(city, writer);
} catch (JAXBException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public JAXBContext getJaxbContext() {
return jaxbContext;
}
public void setJaxbContext(JAXBContext jaxbContext) {
this.jaxbContext = jaxbContext;
}
}
CitiesWriter.java
package info.typea.sample.restservice.provider;
import info.typea.sample.restservice.entity.Cities;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
@Provider
public class CitiesWriter implements MessageBodyWriter<Cities> {
private JAXBContext jaxbContext;
public long getSize(Cities cities,
Class<?> type,
Type genericType,
Annotation[] annotation,
MediaType mediaType) {
return -1;
}
public boolean isWriteable(Class<?> type,
Type genericType,
Annotation[] annotation,
MediaType mediaType) {
return type.equals(Cities.class);
}
public void writeTo(Cities cities, Class<?> type,
Type genericType,
Annotation[] annotation,
MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException, WebApplicationException {
try {
httpHeaders.add("Content-Type", "application/xml");
Writer writer = new OutputStreamWriter(entityStream, "UTF-8");
Marshaller mshr = jaxbContext.createMarshaller();
mshr.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
mshr.marshal(cities, writer);
} catch (JAXBException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public JAXBContext getJaxbContext() {
return jaxbContext;
}
public void setJaxbContext(JAXBContext jaxbContext) {
this.jaxbContext = jaxbContext;
}
}
クラス全体
一応これくらいか。全体の構成はこんな感じ
設定系
Web.xml
- Spring MVC の appServlet (org.springframework.web.servlet.DispatcherServlet) を削除して、代わりにCXFServlet の記述を追加
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>CXFServlet</servlet-name>
<servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>CXFServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
Bean定義 (root-context.xml)
- JAXBContext のコンストラクタに、対応するクラスを引数として渡してあげる必要がある
- cxf がらみの import、サンプルによってはいろいろなxmlをimportしているが、現在はこれだけでよさそう
- NG 1.、NG 2. としてコメントになっているのは、MessageBodyWriterをつくらずとも XML 変換できるだろうと試した設定。できなかった。。。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jaxrs="http://cxf.apache.org/jaxrs"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://cxf.apache.org/jaxrs
http://cxf.apache.org/schemas/jaxrs.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-2.0.xsd">
<!-- Root Context: defines shared resources visible to all other web components -->
<import resource="classpath:META-INF/cxf/cxf.xml" />
<jaxrs:server id="cityResourceService" address="/">
<jaxrs:serviceBeans>
<ref bean="cityResource" />
</jaxrs:serviceBeans>
<jaxrs:providers>
<ref bean="cityWriter"/>
<ref bean="citiesWriter"/>
</jaxrs:providers>
</jaxrs:server>
<bean id="jaxbContext" class="javax.xml.bind.JAXBContext" factory-method="newInstance">
<constructor-arg>
<list>
<value>info.typea.sample.restservice.entity.City</value>
<value>info.typea.sample.restservice.entity.Cities</value>
</list>
</constructor-arg>
</bean>
<bean id="cityDao" class="info.typea.sample.restservice.dao.CityDaoImpl"/>
<bean id="cityResource" class="info.typea.sample.restservice.rs.CityResourceImpl">
<property name="cityDao" ref="cityDao"/>
</bean>
<bean id="cityWriter" class="info.typea.sample.restservice.provider.CityWriter" >
<property name="jaxbContext" ref="jaxbContext" />
</bean>
<bean id="citiesWriter" class="info.typea.sample.restservice.provider.CitiesWriter" >
<property name="jaxbContext" ref="jaxbContext" />
</bean>
<!-- NG 1. http://cxf.apache.org/docs/jax-rs-data-bindings.html#JAX-RSDataBindings-ConfiguringJAXBprovider
<bean id="jaxbProvider" class="org.apache.cxf.jaxrs.provider.JAXBElementProvider">
<property name="marshallerProperties" ref="propertiesMap"/>
</bean>
<util:map id="propertiesMap" map-class="java.util.Hashtable">
<entry key="jaxb.formatted.output">
<value type="java.lang.Boolean">true</value>
</entry>
</util:map>
-->
<!-- NG 2. http://cxf.apache.org/docs/jax-rs-data-bindings.html#JAX-RSDataBindings-ConfiguringJAXBprovider
<bean id="jaxb" class="org.apache.cxf.jaxrs.provider.JAXBElementProvider">
<property name="singleJaxbContext" value="true"/>
<property name="extraClass">
<list>
<value>info.typea.sample.restservice.entity.City</value>
</list>
</property>
</bean>
-->
</beans>
persistence.xml
- 永続化ユニット名はここで定義
- Derby の URLもここで定義
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="toursdb_persistence_unit" transaction-type="RESOURCE_LOCAL">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<class>info.typea.sample.restservice.entity.Airline</class>
<class>info.typea.sample.restservice.entity.City</class>
<class>info.typea.sample.restservice.entity.Country</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>
<property name="eclipselink.logging.level" value="FINEST"/>
<property name="eclipselink.target-database" value="Derby" />
<!-- transaction-type="RESOURCE_LOCAL" -->
<property name="javax.persistence.jdbc.driver" value="org.apache.derby.jdbc.EmbeddedDriver" />
<property name="javax.persistence.jdbc.url" value="jdbc:derby:C:\Users\piroto\toursdb" />
<property name="javax.persistence.jdbc.user" value="app" />
<property name="javax.persistence.jdbc.password" value="" />
<!-- <property name="eclipselink.ddl-generation" value="create-tables" /> -->
<property name="eclipselink.ddl-generation" value="none" />
<property name="eclipselink.ddl-generation.output-mode" value="database" />
</properties>
</persistence-unit>
</persistence>
設定ファイルの構成
設定ファイルの構成はこんな感じ
いざ実行
City ID = 4 のデータを取得してみる
http://localhost:8080/sample_rest_service/city/4
とれてきてます!
http://localhost:8080/sample_rest_service/city/all/
で、すべて取得
とれてきてます!!!
つかれたので、今日はここまで。
次は、クライアント Spring MVC アプリをつくって、jQuery からRESTサービスをたたいて、CRUD を実現するとこまでやろ。
Android はしばらく休みだな。

MessageBodyWriter を特に作成しなくても、「Java による RESTful システム構築」6.2.1 「JAXBについて」あたりと同じようにコーディングしたら、XMLに変換された。アノテーションの付けかたが悪かったかな?