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 Setcities; 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 Listcities; 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 ListfindAll(); }
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 ListfindAll() { 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に変換された。アノテーションの付けかたが悪かったかな?