こんにちわ、みけです。
だんだん寒くなってきて、一日の半分ほどは寝てるような状態です。
ここ数週間(!!!)、JavaFXをサーブレットから叩くアプリケーションを作っているのですが、
最初は単純にwarファイルを作って、
jetty-runnerで走るようにするだけのものにしていたのですが、
どうしてもJavaFXの方のスレッドまわりでうまく動かない部分があって、
デバッグが若干面倒になってきたので、
実行可能warを作ることにしました。
(デバッグポートをつなげて、IDE上でデバッグしてもいいのですが、手順が若干面倒だったので)
実行可能warの作り方
実行可能warの作り方については、qiitaにたくさんのっています。
若干不満な点
僕は主にgradleを使っているので、2.の方は参考になるのですが、
下記の点で気に入らなかったので、
自分なりに修正を必要としました。
- providedスコープで提供されるjarをIDEではcompileスコープにしないとIDEからmainの実行ができないこと(providedはIDEでの実行時にはclasspathに含まれないので
ClassNotFoundException
が出ます) @WebServlet
や@WebFilter
などのServlet3.0以降のアノテーションスキャンがmainからの実行では利用できないこと(元々はデバッグをやりたいために実行可能warを作ろうとしている)
jarのスコープ
gradleでdependencyのスコープを変更するのは比較的簡単です。
apply plugin: 'war' apply plugin: 'idea' ext { jdkLevel = 1.8 encoding = 'UTF-8' } repositories { mavenCentral() } configurations { jetty } dependencies { jetty 'org.eclipse.jetty:jetty-runner:9.2.0.v20140526' testCompile 'junit:junit:4.11' } // (1)クラスパスを各種タスクで通す tasks.withType(Compile) { sourceCompatibility = jdkLevel targetCompatibility = jdkLevel options.encoding = encoding classpath += configurations.jetty } javadoc { options.encoding = encoding classpath += configurations.jetty } test { classpath += configurations.jetty } // (2) IDEではコンパイルスコープになるように設定する idea { project { languageLevel = jdkLevel } module { scopes.COMPILE.plus += [configurations.jetty] } }
jettyというconfigurationを追加して、そこでjetty関連のjarを指定します。
(1)jetty configurationから下記のタスクに対してclasspathを通します。
(2)jetty configurationからIDEのコンパイルスコープにjarを追加します。
これによって、gradleでのビルド時にはclasspathが通っていて、かつ、
eclipse?知らない子ですね。
eclipse { classpath { plusConfigurations += [configurations.jetty] } }
アノテーションスキャン
実行可能warを作るときに、デバッグ実行すると、
プロジェクト内で@WebServlet
などのアノテーションを付与した
サーブレットがことごとくしかとされます。
広いインターネッツ、それへの対処法が全く見当たらず、
これの謎をとくのに、2日くらいかかりました。
僕が最初に書いたMainクラスはこんな感じでした。
Main.java
public class Main implements AutoCloseable, Runnable { private static final ClassLoader LOADER = Main.class.getClassLoader(); private static final URL MAIN_URL = Main.class.getProtectionDomain() .getCodeSource().getLocation(); public static void main(String[] args) throws Exception { // web.xmlはダミーでおいてあるだけで、空っぽのファイル Optional<URL> url = Optional.ofNullable(LOADER.getResource("WEB-INF/web.xml")); try(Main main = new Main(url)) { main.run(); } public Main(Optional<URL> warFile) { this.server = new Server(8080); String war = warFile.map(u -> MAIN_URL.toExternalForm()).orElse("src/main/webapp"); Configuration[] configurations = { new AnnotationConfiguration(), new WebInfConfiguration(), new WebXmlConfiguration(), new MetaInfConfiguration(), new FragmentConfiguration(), new EnvConfiguration(), new PlusConfiguration(), new JettyWebXmlConfiguration() }; context = new WebAppContext(); context.setWar(war); context.setContextPath("/"); context.setConfigurations(configurations); } @Override public void run() { server.setHandler(context); try { server.start(); server.join(); } catch (Exception e) { throw new RuntimeException(e); } } @Override public void close() throws Exception { server.stop(); } }
HelloServlet.java
@WebServlet(name = "hello", urlPatterns = "hello") public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setStatus(200); PrintWriter writer = resp.getWriter(); writer.append("hello").flush(); } }
で、これをwarにしてから実行する分には、
問題なく表示されます。
しかし、IDEから実行する場合にはもれなくHelloServlet
が
アノテーションスキャンの対象から外れて、
HelloServlet
のurlにアクセスしても404が返されます。
stackoverflowで探す
世の中、困ったらstackoverflowということで、ググったらありました。
Can't get Jetty to scan for annotated classes
曰く、
context.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern","out/production/project-name/.*");
の一文を追加しろとのこと。
で、追加するのですが、相変わらず404を返し続けます。
なんか、stackoverflow様でも解決できてないですね(´・ω・`)
Servlet3.0の仕様
同様の質問に対して、もう一つ別のページではServlet3.0の仕様を答えていたりします。
Embedded Jetty with annotated servlet patterns?
曰く、
WEB-INF/classes
かWEB-INF/libs
のいずれかに入っている場合にスキャンされる
というわけで、どうにもならなくなってきたので、
コード読んでどうやってるかを理解することにしました。
jettyのソース
アノテーションのスキャンはここで行われています。
AnnotationConfiguration#scanForAnnotations(WebAppContext)
ここでは三つの場所からアノテーションをスキャンしています。
このうち、コンテナとWEB-INF/libsはjarファイルでないと実行時例外が出るので、
WEB-INF/classesをスキャンする際に、
ついでに自分のクラスファイルをスキャンしてもらうようにします。
で、Main.java
にこれを追加します。
context.getMetaData() .setWebInfClassesDirs( Arrays.asList(Resource.newResource(MAIN_URL)));
ただし、これは自分がwarファイルになっていると、
自分自身のクラスはWEB-INF/classesに配置されているので、
要らない子です。
デバッグ実行の場合はこの設定を追加して、
warファイルで実行する場合はこの設定を追加しないようにします。
if (!warFile.isPresent()) {
context.getMetaData()
.setWebInfClassesDirs(
Arrays.asList(Resource.newResource(MAIN_URL)));
}
これで、IDEデバッグにも対応した実行可能warファイルを出力するプロジェクトが出来ました。
最終的なMain.java
Main.java
public class Main implements AutoCloseable, Runnable { private static final ClassLoader LOADER = Main.class.getClassLoader(); private static final URL MAIN_URL = Main.class.getProtectionDomain() .getCodeSource().getLocation(); public static void main(String[] args) throws Exception { // web.xmlはダミーでおいてあるだけで、空っぽのファイル Optional<URL> url = Optional.ofNullable(LOADER.getResource("WEB-INF/web.xml")); try(Main main = new Main(url)) { main.run(); } public Main(Optional<URL> warFile) { this.server = new Server(8080); String war = warFile.map(u -> MAIN_URL.toExternalForm()).orElse("src/main/webapp"); Configuration[] configurations = { new AnnotationConfiguration(), new WebInfConfiguration(), new WebXmlConfiguration(), new MetaInfConfiguration(), new FragmentConfiguration(), new EnvConfiguration(), new PlusConfiguration(), new JettyWebXmlConfiguration() }; context = new WebAppContext(); context.setWar(war); context.setContextPath("/"); if (!warFile.isPresent()) { context.getMetaData() .setWebInfClassesDirs( Arrays.asList(Resource.newResource(MAIN_URL))); } context.setConfigurations(configurations); } @Override public void run() { server.setHandler(context); try { server.start(); server.join(); } catch (Exception e) { throw new RuntimeException(e); } } @Override public void close() throws Exception { server.stop(); } }
最終的なbuild.gradle
build.gradle
apply plugin: 'idea' apply plugin: 'war' ext { jdkLevel = 1.8 encoding = 'UTF-8' } version = '0.1' repositories { mavenCentral() } configurations { jetty } dependencies { jetty 'org.eclipse.jetty:jetty-runner:9.2.0.v20140526' testCompile 'junit:junit:4.11' } tasks.withType(Compile) { sourceCompatibility = jdkLevel targetCompatibility = jdkLevel options.encoding = encoding classpath += configurations.jetty } javadoc { options.encoding = encoding classpath += configurations.jetty } test { classpath += configurations.jetty } idea { project { languageLevel = jdkLevel } module { scopes.COMPILE.plus += [configurations.jetty] } } war { manifest { attributes 'Main-Class': 'app.sample.Main' } from { configurations.jetty.collect { it.isDirectory()? it: zipTree(it) } } { exclude 'META-INF/*.SF' exclude 'META-INF/*.DSA' exclude 'META-INF/*.RSA' } from fileTree(dir: 'build/classes/main', include: '**/Main.class') }
以上