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&amp;nf=1&amp;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;
    }
}

プロバイダーの作成

一番はまったのがここ。

[Java]JAX-RSで好きなオブジェクトを返す

を参考に、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;
    }
}

クラス全体

一応これくらいか。全体の構成はこんな感じ

jax_rs01

設定系

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>

設定ファイルの構成

設定ファイルの構成はこんな感じ

jax_rs02

いざ実行

City ID = 4 のデータを取得してみる

http://localhost:8080/sample_rest_service/city/4

jax_rs03

とれてきてます!

http://localhost:8080/sample_rest_service/city/all/

で、すべて取得

jax_rs04

とれてきてます!!!

つかれたので、今日はここまで。

次は、クライアント Spring MVC アプリをつくって、jQuery からRESTサービスをたたいて、CRUD を実現するとこまでやろ。

Android はしばらく休みだな。

Follow me!

SpringSource Tool Suite を使って Apache CXF による REST サービスを作成する” に対して1件のコメントがあります。

  1. pppiroto (Hiroto YAGI) より:

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

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です