ぺーぺーSEのブログ

備忘録・メモ用サイト。

JBossEAP6とJerseyの組合せでjar内のJAX-RSリソースが見えない件

そもそもなんでJBossにJerseyなんだ?なんて聞かないで。大人の事情なんです。

問題

warファイルをJBossEAPにデプロイしたんだが、WEB-INF/lib配下のjarファイルに入ってるエンティティプロバイダクラス(@Providerついてるクラス)が有効になんねぇ。。。
Tomcatだと動くのに(´・ω・`)

環境

  • JDK1.7u45
  • JBossEAP6.1.1
  • Jersey1.17.1(Spring連携)
  • warファイルでデプロイ


設定

■web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xmlns="http://java.sun.com/xml/ns/javaee" 
         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
         id="WebApp_ID" version="3.0">
  <display-name>RESTApplication-sample</display-name>
  
  <!-- Springのbean定義ファイル -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
  </context-param>
  <!-- log4jの設定ファイル -->
  <context-param>
    <param-name>log4jConfigLocation</param-name>
    <param-value>/WEB-INF/classes/log4j.xml</param-value>
  </context-param>
  <context-param>
    <param-name>webAppRootKey</param-name>
    <param-value>REST Application</param-value>
  </context-param>
  
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <listener>
    <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
  </listener>
  <listener>
    <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
  </listener>
  
  <!-- Spring連携用のJerseyサーブレット(Spring連携用じゃなくてもいい) -->
  <servlet>
    <servlet-name>Jersey Spring Web Application</servlet-name>
    <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
    <!-- ↓①↓ -->
    <init-param>
      <param-name>com.sun.jersey.config.property.packages</param-name>
      <param-value>org.codehaus.jackson.jaxrs</param-value>
    </init-param>
    <!-- ↑①↑ -->
    <init-param>
      <param-name>com.sun.jersey.config.property.WebPageContentRegex</param-name>
      <param-value>/.*[\\.]html</param-value>
    </init-param>
  </servlet>
  
  <servlet-mapping>
    <servlet-name>Jersey Spring Web Application</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>
  
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
  </welcome-file-list>
</web-app>

①の設定で「org.codehaus.jackson.jaxrs」パッケージ以下のエンティティプロバイダ(@Providerついてるやつ)とかリソースクラスをロードしてくれるはずなんだけど。。。


jboss-deployment-structure.xml

<jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.2">
  <deployment>
    <exclude-subsystems>
      <subsystem name="jaxrs" />
    </exclude-subsystems>
  </deployment>
</jboss-deployment-structure>

RESTEasy無効化の設定。


■standalone.xml
以下の行をコメントアウト

<extension module="org.jboss.as.jaxrs"/>
<subsystem xmlns="urn:jboss:domain:jaxrs:1.0"/>

RESTEasy無効化の設定。

原因

com.sun.jersey.spi.container.servlet.ServletContainer(com.sun.jersey.spi.spring.container.servlet.SpringServletの親)のinit()メソッドのちょっと奥で生成されているcom.sun.jersey.core.spi.scanning.PackageNamesScannerのコンストラクタが下記。

public PackageNamesScanner(final ClassLoader classloader, final String[] packages) {
  this.packages = packages;
  this.classloader = classloader;

  this.scanners = new HashMap<String, UriSchemeScanner>();
  add(new JarZipSchemeScanner());
  add(new FileSchemeScanner());
  add(new VfsSchemeScanner());  // ②
  add(new BundleSchemeScanner());

  for (UriSchemeScanner s : ServiceFinder.find(UriSchemeScanner.class)) {  // ③
    add(s);
  }
}

private void add(final UriSchemeScanner ss) {
  for (final String s : ss.getSchemes()) {
    scanners.put(s.toLowerCase(), ss);
  }
}
// 〜省略〜
private void scan(final URI u, final ScannerListener cfl) {
  final UriSchemeScanner ss = scanners.get(u.getScheme().toLowerCase());
  if (ss != null) {
    ss.scan(u, cfl);
  } else {
    throw new ScannerException("The URI scheme " + u.getScheme() +
          " of the URI " + u +
          " is not supported. Package scanning deployment is not" +
          " supported for such URIs." +
          "\nTry using a different deployment mechanism such as" +
          " explicitly declaring root resource and provider classes" +
          " using an extension of javax.ws.rs.core.Application");
  }
}

scannersというMapにKeyがスキーム、ValueがUriSchemeScannerの継承クラスでaddしてる。
このscannersがJerseyサーブレットで必要なリソース(エンティティプロバイダとか)を文字通りスキャンする。
で、②のVfsSchemeScannerってのがJavadocによるとJBoss用のスキャナなんだが、こいつが以下の通り。

package com.sun.jersey.core.spi.scanning.uri;
// 〜省略〜
public class VfsSchemeScanner implements UriSchemeScanner {

    public Set<String> getSchemes() {
        return new HashSet<String>(Arrays.asList("vfsfile", "vfszip", "vfs"));
    }

    // UriSchemeScanner

    public void scan(final URI u, final ScannerListener sl) {
        if (!u.getScheme().equalsIgnoreCase("vfszip")) {
            new FileSchemeScanner().scan(
                    UriBuilder.fromUri(u).scheme("file").build(),
                    sl);
        } else {
            final String su = u.toString();  // ④
            final int webInfIndex = su.indexOf("/WEB-INF/classes");
            if (webInfIndex != -1) {
                final String war = su.substring(0, webInfIndex);
                final String path = su.substring(webInfIndex + 1);

                final int warParentIndex = war.lastIndexOf('/');
                final String warParent = su.substring(0, warParentIndex);

                // Check is there is a war within an ear
                // If so we need to load the ear then obtain the InputStream
                // of the entry to the war
                if (warParent.endsWith(".ear")) {
                    final String warName = su.substring(warParentIndex + 1, war.length());
                    try {
                        JarFileScanner.scan(new URL(warParent.replace("vfszip", "file")).openStream(), "",
                                new ScannerListener() {
                            public boolean onAccept(String name) {
                                return name.equals(warName);
                            }

                            public void onProcess(String name, InputStream in) throws IOException {
                                // This is required so that the underlying ear
                                // is not closed
                                in = new FilterInputStream(in) {
                                    public void close() throws IOException {};
                                };
                                try {
                                    JarFileScanner.scan(in, path, sl);
                                } catch (IOException ex) {
                                    throw new ScannerException("IO error when scanning war " + u, ex);
                                }
                            }
                        });
                    } catch (IOException ex) {
                        throw new ScannerException("IO error when scanning war " + u, ex);
                    }
                } else {
                    try {
                        JarFileScanner.scan(new URL(war.replace("vfszip", "file")).openStream(), path, sl);
                    } catch (IOException ex) {
                        throw new ScannerException("IO error when scanning war " + u, ex);
                    }
                }
            } else {
                try {
                    JarFileScanner.scan(new URL(su).openStream(), "", sl);
                } catch (IOException ex) {
                    throw new ScannerException("IO error when scanning jar " + u, ex);
                }
            }
        }
    }
}

④の変数suには「vfs:/[デプロイ先のパス]/SampleApp.war/WEB-INF/lib/jackson-jaxrs-1.9.2.jar/org/codehaus/jackson/jaxrs/」みたいなのが入ってる。
これ見るとscanメソッドサーブレットinitで後々実行されるんだけど、「/WEB-INF/classes」配下しか見てなくね?(´・ω・`)
「/WEB-INF/lib」配下も見てほしい。。。てか「vfsfile」「vfszip」「vfs」スキームってなんやねん!

解法

幸いなことに③でscannersマップを上書きできる余地を残してくれてる!
classes配下に「META-INF/services」ディレクトリを作って、その中に「com.sun.jersey.core.spi.scanning.uri.UriSchemeScanner」ファイルを作って

UriSchemeScannerを継承して作ったクラスのフルネーム(「org.sample.scanning.uri.VfsSchemeScannerFix」とか)

を書いておけば②の「VfsSchemeScanner」を別のクラスに差し替え可能だ!
③の詳細は「ServiceLoader」を調べてね。
参考:http://itpro.nikkeibp.co.jp/article/COLUMN/20061215/257003/
で、VfsSchemeScannerの代わりを下記に作成。


■org.sample.scanning.uri.VfsSchemeScannerFix

package org.sample.scanning.uri;

import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import com.sun.jersey.core.spi.scanning.JarFileScanner;
import com.sun.jersey.core.spi.scanning.ScannerException;
import com.sun.jersey.core.spi.scanning.ScannerListener;
import com.sun.jersey.core.spi.scanning.uri.UriSchemeScanner;

public class VfsSchemeScannerFix implements UriSchemeScanner {

  @Override
  public Set<String> getSchemes() {
    return new HashSet<String>(Arrays.asList("vfs"));
  }

  @Override
  public void scan(final URI u, final ScannerListener sl)
      throws ScannerException {

    if (u.getScheme().equalsIgnoreCase("vfs")) {

      String su = u.toString();
      int jarIdx = su.lastIndexOf(".jar");
      String jar = su.substring(0, jarIdx + 4);
      String path = su.substring(jarIdx + 5);

      try {
        JarFileScanner.scan(
            new URL(jar.replace("vfs", "file")).openStream(), path,
            sl);
      } catch (IOException ex) {
        throw new ScannerException("IO error when scanning jar " + u,
            ex);
      }
    }

  }
}

一応動いてる。

参考



SAStrutsをJBossAS7で動かそう(体験談)
http://tech-sketch.jp/2012/09/sastrutsjbossas7.html

[#JERSEY-763] VfsSchemeScanner cannot be run in Jboss-7 environment - Java.net JIRA
https://java.net/jira/browse/JERSEY-763

Quick Fix Jersey VfsSchemeScanner can not Recognize vfs:/ Scheme Issue
http://my.opera.com/Vancoole/blog/quick-fix-jersey-vfsschemescanner-can-not-recognize-vfs-scheme-issue