MavenでSpringを含む依存jarごと一つのjarにするときハマった件

タイトルが長い。

さらに詳しく言うなら、
MavenでSpringを含む依存jarごと一つのjarにしてインターネットに出られないサーバでそのjarを実行したときハマった件
です。

まず前提として、Springさんはクラスパス内にapplicationContext.xmlとかで定義したxsdファイルが見つからない場合、そのURLを見に行ってパースするようです。
このxsdファイルは、例えば「http://www.springframework.org/schema/beans/spring-beans-4.0.xsd」で言うと、spring-beans-xxx.jarの中のMETA-INF/spring.schemasの中で

http\://www.springframework.org/schema/beans/spring-beans-2.0.xsd=org/springframework/beans/factory/xml/spring-beans-2.0.xsd
http\://www.springframework.org/schema/beans/spring-beans-2.5.xsd=org/springframework/beans/factory/xml/spring-beans-2.5.xsd
http\://www.springframework.org/schema/beans/spring-beans-3.0.xsd=org/springframework/beans/factory/xml/spring-beans-3.0.xsd
http\://www.springframework.org/schema/beans/spring-beans-3.1.xsd=org/springframework/beans/factory/xml/spring-beans-3.1.xsd
http\://www.springframework.org/schema/beans/spring-beans-3.2.xsd=org/springframework/beans/factory/xml/spring-beans-3.2.xsd
http\://www.springframework.org/schema/beans/spring-beans-4.0.xsd=org/springframework/beans/factory/xml/spring-beans-4.0.xsd
http\://www.springframework.org/schema/beans/spring-beans.xsd=org/springframework/beans/factory/xml/spring-beans-4.0.xsd

のように記載されており、ここにある通り、その実体は同じくspring-beans-xxx.jarの中のorg.springframework.beans.factory.xmlパッケージの中に存在しています。

次に、依存jarごと一つのjarにまとめる場合、よく使うのはmaven-assembly-pluginです。
ということで実際にmaven-assembly-pluginでjarを作って、インターネットに出られないサーバで実行してみます。

WARN  org.springframework.util.xml.SimpleSaxErrorHandler:48 - Ignored XML validation warning
org.xml.sax.SAXParseException: schema_reference.4: 1)ドキュメントが見つからなかった、2)ドキュメントを読み取れなかった、3)ドキュメントのルート要素が<xsd:schema>ではなかったため、スキーマ・ドキュメント'http://www.springframework.org/schema/beans/spring-beans-4.0.xsd'の読取りに失敗しました。

_人人 人人_
> 突然の死 <
 ̄Y^Y^Y^Y ̄

なんぞこれーということでちょっとjarを展開してみます。
META-INFの下にはspring.schemasがありましたが、中身を見てみるとspring-beansの記述が無く、
spring-contextなどの記述がある状態。
いくつかのSpringのjarに依存していたので、どうやらそれぞれのspring.schemasが上書きに次ぐ上書きを繰り返し、最後に上書きされたやつが成果物に含まれている模様。
というわけで色々調べていると、「maven-shade-plugin」というプラグインならうまいこと出来るみたいなので、maven-assembly-pluginの部分と置き換えてみます。

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-shade-plugin</artifactId>
	<version>2.2</version>
	<configuration>
		<finalName>shade-sample</finalName>
		<transformers>
			<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
				<mainClass>com.tm8r.ShadeTest</mainClass>
			</transformer>
			<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
				<resource>META-INF/spring.handlers</resource>
			</transformer>
			<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
				<resource>META-INF/spring.schemas</resource>
			</transformer>
		</transformers>
	</configuration>
	<executions>
		<execution>
  			<phase>package</phase>
			<goals>
			  <goal>shade</goal>
			</goals>
		</execution>
	</executions>
</plugin>

こんな感じ。
「org.apache.maven.plugins.shade.resource.AppendingTransformer」を指定したtransformerを定義してやると、resourceに指定したファイルが複数あったとき、そのファイルに追記をしていくような挙動をしてくれる模様。

というわけでこれでpackageを実行してやると、spring.schemasとspring.handlersに対して、依存jarに含まれる同名ファイルの内容が全て記述されたものが成果物に含まれる形になりました。
めでたしめでたし。

GuavaのSetsでSetの差分をとったりする

えー今更Guava-?キモーイ!Guavaの記事が許されるのは2012年までだよねー!キャハハハ!
みたいな反応が無いかgkbrしながらも、ちょっといじる機会があったので。
いやGuava全然まだ開発されてるんですけども。

AとBのSetがあってAにあってBにないもの、BにあってAにないものを抽出したい、そんなとき使えるのがSetsのdifferenceメソッドさんです。

Set<String> testSet1 = new HashSet<>();
testSet1.add("a");
testSet1.add("b");
testSet1.add("c");
Set<String> testSet2 = new HashSet<>();
testSet2.add("b");
testSet2.add("c");
testSet2.add("d");
testSet2.add("e");
Set<String> diff1 = Sets.difference(testSet1, testSet2);
Set<String> diff2 = Sets.difference(testSet2, testSet1);

このようにすると、以下のような結果が返ってきます。

diff1:[a]
diff2:[d, e]

べんり!

両方に含まれるものを抽出する場合はintersectionメソッドさんを使います。

Set<String> intersection = Sets.intersection(testSet1, testSet2);

結果は以下の通り。

intersection:[b, c]

捗る。やっぱりGuavaさんすてき。

Ansibleのtips的なあれやそれ

Ansibleをごにょごにょしたので覚え書き程度に。

シンボリックリンクをはる

/usr/local/destに対して/usr/local/srcのシンボリックリンクをはる場合、以下のようにします。

- name: create symlink
  action: file src=/usr/local/src dest=/usr/local/dest state=link

Ansible実行サーバからリモートサーバにrsyncする

1.3.xまではlocal_actionを用いてrsyncコマンドを実行する感じでしたが、
1.4.xからはsynchronizeというモジュールが追加されたのでこれで実現できるみたいです。
rsync_pathはrootで実行したいとか、別のユーザーで実行したいといった場合は以下のように指定してあげるとよいみたいです。

- name: sync files
  action: synchronize src=/usr/local/src dest=/usr/local/dest recursive=yes rsync_path='sudo rsync'

同一コマンドを複数のファイルに対して行う

with_itemsを使えばらくちんです。
{{ item }}と書いた部分に対してwith_itemsで指定したファイルが入ります。

- name: change permission
  action: file path={{ item }} owner=tm8r group=tm8r state=directory recurse=yes
  with_items:
   - /usr/local/hoge/
   - /usr/local/fuga/

すてきですねAnsible。

HipChatで送信したファイルを消す

HipChat、つかってますか!

最近使い始めたのですけど、人にファイルを送信したくてファイルをD&Dしたらuploading…って出てファッ!?となりました。
どうやらS3に上がるっぽいですね。こわい。

・Files uploaded to HipChat are stored on Amazon's S3 servers.
・All uploaded files are accessible via an obscure URL which is shared with people in the chat when the file is uploaded.
・Users are not required to be logged in to view uploaded files - they are visible to anyone who has the link. (This means the links can be shared easily with anyone you want to be able to view the file).

http://help.hipchat.com/knowledgebase/articles/64477-are-files-uploaded-to-hipchat-secure-private-

URLはランダムで生成されるのでピンポイントで誰のものを見るみたいなことは出来なさそうですが、現状特に認証とかはないみたいなのでちょっと怖い。

ので削除する方法を探したところ、ブラウザから消せるっぽいです。
How do I delete chat history and files? – Help Center

ルームでアップロードしたファイルを消す場合は以下、
https://www.hipchat.com/rooms
1:1でアップロードしたファイルを消す場合は以下
https://www.hipchat.com/people
にアクセスし、表示されたヒストリーで該当のファイルの行をマウスオーバーすると「×」マークが出るので、これを選択すると削除できます。

わかりづらい。

tomcatで一つのアプリを複数のバーチャルホストで配信する

普通こんなことしないとは思うんですけど。

tomcatにアプリを二つ以上乗っけて、かつ一方のアプリは複数のドメインで配信する方法です。

たとえば、tm8r.jpはwebbappsにあるアプリが処理、
tm8r.com、tm8rhoge.com、tm8rfuga.comはwebapps2にあるアプリが処理したい場合。

<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="false">
</Host>
<Host name="tm8r.com" appBase="webapps2"
unpackWARs="true" autoDeploy="false">
  <Alias>tm8rhoge.com</Alias>
  <Alias>tm8rfuga.com</Alias>
</Host>

こんな感じでAliasを定義してやれば実現出来るもよう。
はじめてつかった。

tm8r.jpは明示的に定義されてないのでlocalhostの設定を参照し、
それ以外の明示的に指定されているドメインはtm8r.comの方を参照します。

tm8rhoge.comとtm8rfuga.comをそれぞれHostに書いてappBaseにwebapp2を指定しても実現できるっちゃできるんですが、書いた分だけアプリがデプロイされてしまうのでこんな感じに。
もっといい方法あったりするのだろうか。とりあえず実現できたのでよいとする。

jsoupでHTMLをパースする

オヒサシブリデース。
ついにはてなブログに移行しました。インポート楽ちんでよいですね。

ここのところあんまり新しいことしてなかったので書くことなかったんですが、
久々に触ったことなかったライブラリに触ったので覚書までに。

HTMLをパースする案件がありまして、以下のパーサを触ったんですが、
割と精度も使い勝手もパフォーマンスもよかったjsoupを紹介してみます。

  1. jsoup
  2. jericho
  3. HtmlCleaner
  4. Validator.nu
  5. HTMLEditorKit
  6. TagSoup
  7. HTML Parser
  8. NekoHtml
  9. JTidy

準備

mavenでビルドするのを想定して、まずはpom.xmlに以下を追記します。

<dependency>
	<groupId>org.jsoup</groupId>
	<artifactId>jsoup</artifactId>
	<version>1.7.3</version>
</dependency>

公式サイトによると1.7.3が最新みたいです。
jsoup Java HTML Parser, with best of DOM, CSS, and jquery

実装してみる

まずはHTMLをパースして、タグ名、属性名、属性値、テキストを表示するものを実装してみます。

public class JsoupTest {
  private static final String PREFIX = "<html><head></head><body>";
  private static final String SUFFIX = "</body></html>";

  private static final String BACKQUOTE = "`";
  private static final String DOUBLEQUOTE = "\"";

  public static void main(String[] args) {
    String target = "<a href=\"#\">hoge</a>";
    // パーサがバッククォートをタグ内のクォートとして認識しないので置換(IE対策)
    String replacedText = StringUtils.replace(target, BACKQUOTE, DOUBLEQUOTE);

    // 閉じられてないタグのみを渡した場合にパーサが勝手にHTMLとして整形し、渡したものが除去されるので自前で整形する
    replacedText = PREFIX + replacedText + SUFFIX;
    Document doc = Jsoup.parse(replacedText);
    Elements elements = doc.getAllElements();
    for(Element element : elements) {
      System.out.println("tagname:" + element.tagName() + ", text:" + element.ownText());
      for (Attribute attr : element.attributes()) {
        System.out.println("key:" + attr.getKey() + ", value:" + attr.getValue() + "\n-");
      }
      System.out.println("-----");
    }
  }
}

実行結果は以下の通り。

tagname:#root, text:
-----
tagname:html, text:
-----
tagname:head, text:
-----
tagname:body, text:
-----
tagname:a, text:hoge
key:href, value:#
-
-----

らくちんですね!

ちなみにhtmlタグとかheadタグとかをつけてるのは、
仮にこのあたりのタグがなかったとき、jsoupが勝手にそのへんのタグを補完してくれるからです。
基本的にはこの保管に任せておいて問題ないのですが、
たとえば以下のような文字列を渡した場合。

String target = "<a href=\"#\"aaa";

以下のような結果になってしまします。

<html>
 <head></head>
 <body></body>
</html>

_人人 人人_
> 突然の死 <
 ̄Y^Y^Y^Y ̄

というわけで自前でつけてます。
htmlタグとかheadタグとかが入力内容に入ってくるのであれば別の方法を考えないといけないかと思いますが、今回は入ってこない想定です。

まあこれはこれで以下みたいな変なHTMLが生成されたりはするんですが。

<html>
 <head></head>
 <body>
  <a href="#" aaa<="" ody=""></a>
 </body>
</html>

なくなるよりはいいかなという感じで今後の修正に期待します。

特定のタグを抽出する

jsoupには特定の条件に合致するタグを取得するメソッドが用意されています。
コードを以下のように書き換えてみます。

public class JsoupTest {
  private static final String PREFIX = "<html><head></head><body>";
  private static final String SUFFIX = "</body></html>";

  private static final String BACKQUOTE = "`";
  private static final String DOUBLEQUOTE = "\"";

  public static void main(String[] args) {
    String target = "<a href=\"#\" onmouseover=\"alert(0)\">aa</a><img onerror=\"alert(1)\">";
    // パーサがバッククォートをタグ内のクォートとして認識しないので置換(IE対策)
    String replacedText = StringUtils.replace(target, BACKQUOTE, DOUBLEQUOTE);

    // 閉じられてないタグのみを渡した場合にパーサが勝手にHTMLとして整形し、渡したものが除去されるので自前で整形する
    replacedText = PREFIX + replacedText + SUFFIX;
    Document doc = Jsoup.parse(replacedText);
    // aタグを抽出
    System.out.println("a:" + doc.getElementsByTag("a").size());
    // 属性名がhrefのものを含むタグを抽出
    System.out.println("href:" + doc.getElementsByAttribute("href").size());
    // 属性名がonで始まるものを含むタグを抽出
    System.out.println("on:" + doc.getElementsByAttributeStarting("on").size());
  }
}

それぞれのメソッドが何をしてくれるかはコメントの通りです。
この実行結果は以下のようになります。

a:1
href:1
on:2

べんり!

特定のタグ抽出メソッドを実装する

上で便利な抽出メソッドを紹介しましたが、もともと用意されていない抽出方法を実装したいこともあるかと思います。
たとえば、属性名と属性値を指定して、指定した属性名の値に指定した値が含まれているものを抽出するメソッドははありますが、属性名を限定せずに指定した文字列が属性値に含まれるものを抽出するメソッドはありません。
というわけで実装してみます。

public class CustomEvaluator {

  /**
   * 指定した文字列が属性値に含まれているものを抽出
   */
  public static final class ValueContainsEvalutor extends Evaluator {

    private String value;

    public ValueContainsEvalutor (String value) {
      Validate.notEmpty(value);
      this.value = value.trim().toLowerCase();
    }
    @Override
    public boolean matches(Element root, Element element) {
      for (org.jsoup.nodes.Attribute attribute : element.attributes().asList()) {
        if (attribute.getValue().toLowerCase().contains(value))
          return true;
      }
      return false;
    }
  }
}

次に、こいつを呼び出してみます。

    System.out.println("alert:"
        + Collector.collect(new CustomEvaluator.ValueContainsEvalutor("alert"), doc).size());

実行結果は以下のようになります。

alert:2

できた!

おわり

以上、jsoupのご紹介でした。
お手軽でよいですね!

一番ブラウザに近いパースをしてくれたのはValidator.nuだったんですが、
パフォーマンスがjsoupの方が優れてる感じだったので見送りました。
こちらの紹介は気が向いたら。向かなさそうですが。

jacksonでデシリアライズする際に未知のプロパティを無視する

大したことじゃないんですけど毎回忘れて調べてる気がするので覚書。
jacksonであるjsonをデシリアライズしたいんだけど、そこに未知のプロパティがあったとき、UnrecognizedPropertyExceptionが発生しちゃうので、それを回避するやつです。


たとえば以下のようなクラスがあったとして。

public class TestJsonBean {
	private String str;
	public String getStr() {
		return str;
	}
	public void setStr(String str) {
		this.str = str;
	}
}


こんなコードでデシリアライズを実行してみます。

ObjectMapper om = new ObjectMapper();
String testStr = "{\"str\":\"hoge\",\"unknownstr\":\"fuga\"}";
TestJsonBean bean = om.readValue(testStr,TestJsonBean.class);


すると、案の定以下のようなExceptionが発生します。

Exception in thread "main" org.codehaus.jackson.map.exc.UnrecognizedPropertyException: Unrecognized field "str3" (Class jp.ameba.TestJsonBean), not marked as ignorable


今回は未知のプロパティを無視して、存在するプロパティだけをデシリアライズしたいので、クラスに以下のようなアノテーションを追記します。

@JsonIgnoreProperties(ignoreUnknown=true)
public class TestJsonBean {
(略)
}


この状態で先ほどと同じコードでデシリアライズを実行すると、無事存在する「str」だけがデシリアライズされる結果になります。
かんたん!

スポンサーリンク