Spring Frameworkでアプリ作るときのちょっとしたメモ

ニコニコ動画からランキング取得して独自のRSSを作り出すWebAPIとTumblrへのポストを行うアプリをJavaで作ってみた。
その時の構成と、どのように設定して動かしたかをメモ書き程度に記載。

構成

java EE6な時代にspringを採用したのは、FreeMaker、MyBatis、AspectJと簡単に連携できるためである。

MyBatisは発行するSQLを完全に制御したいから使ったのだが、チューニングが必要な複雑なクエリが出来上がらない前提であればJPAglassfishならeclipse link)で良かったかも。JOINさえしなければ、簡単なCRUDなら何も書かなくていいJPAが圧倒的有利だった。
FreeMakerはなんとなく使用。JSFはメモリ食いすぎ。
H2Databaseは軽量な上、組み込みモードだと圧倒的パフォーマンスらしいのでglassfish上で動作させて使用。glassfishの管理コンソールからデータソースを登録するだけでアプリから簡単に使用出来る。
AspectJは必須wプロキシなAOPなんて軟弱なものじゃ物足りないw
ログはslf4jでまとめてlogbackに出力。
quartzについてはEJBタイマーを使っても良かったのだが、全部springに任せる構成を簡単に実現できたため使用した。


MyBatisの設定は/WEB-INF/mybatis.xmlに記述。DBのアンダースコア区切りをJavaのキャメルケースに変換してマッピングする設定は個人的に必須なので下記の設定をしたxmlファイルを作成。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<settings>
		<setting name="mapUnderscoreToCamelCase" value="true"/>
	</settings>
</configuration>

glassfish管理のトランザクションを使うための設定と、マッパーを勝手にスキャンする設定、上記のMyBatisの設定ファイルを読ませる設定のために、springのbean構成を追記。

<tx:jta-transaction-manager />

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
	<property name="basePackage" value="com.neco_labo.db" />
	<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>


<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
	<property name="jndiName"><value>jdbc/myh2db</value></property>
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="configLocation" value="/WEB-INF/mybatis.xml" />
		<property name="transactionFactory">
			<bean class="org.apache.ibatis.transaction.managed.ManagedTransactionFactory" />
		</property>
</bean>

なんか突っ込みどころありそうな書き方だけど、気にしない!


MyBatisはアノテーションを使ってSQLを記述。
@Paramでパラメータ名を教えてあげないとダメな部分があるのがなんだかなぁ。別にJPA使っても良かったのか。

package com.neco_labo.db;

import java.util.List;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import com.neco_labo.web.db.PostedVideo;

public interface PostedVideoMapper {

	@Select("SELECT * FROM posted_video")
	List<PostedVideo> getAll();

	@Select("SELECT * FROM posted_video WHERE type = #{type} AND id = #{id}")
	PostedVideo select(@Param("type")final String type, @Param("id")final String id);

	@Select("SELECT * FROM posted_video WHERE type = #{type}")
	List<PostedVideo> selectByType(final String type);

	@Insert("INSERT INTO posted_video (type,id,title,tag,rate,pub_date,insert_date,update_date) values (#{type},#{id},#{title},#{tag},#{rate},#{pubDate},#{insertDate},#{updateDate})")
	void insert(PostedVideo record);

	@Update("UPDATE posted_video SET rate = #{rate},update_date = #{updateDate} WHERE type = #{type} AND id = #{id}")
	void updateRate(PostedVideo record);

}

さらに、AspectJでクラスロード時にせっせとバイトコード書き換えてくれるように設定。
コンポーネントスキャンで@Aspectがついてるクラスを勝手にbean登録してくれる設定って素晴らしい!CDI(weld)もデフォルトでそれに近いけどバイトコード書き換えは出来ないし。

<aop:aspectj-autoproxy/>

<context:component-scan base-package="com.neco_labo">
	<context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect" />
</context:component-scan>

<context:load-time-weaver/>

context:load-time-weaverを指定したらglassfishの起動時のパラメータに-javaagent:spring-instrument.jarを追加。glassfishの管理コンソールで簡単に設定可能!
もちろん/lib配下にjarを置かないとダメ。H2Databasesのjarファイルもここへ。

アスペクトの記述クラスは、@Aspectをつけるだけでおk。先ほどそう設定したからな。その他、@Pointcutと@Aroundなどすべてアノテーションだけで完結。
AspectJのウィービング設定は"クラスパスが通っている場所"にMETA-INFを作り、その配下にaop.xmlとして配置。ポイントはweaverにアスペクト自身の場所も含めないといけない(なんでだろ)ようだ。

<?xml version="1.0"?>
<aspectj>
	<aspects>
		<aspect name="com.neco_labo.aspect.JapanLocaleAspect" />
		<aspect name="com.neco_labo.aspect.CDATAAspect" />
	</aspects>
	<weaver>
		<include within="com.example.io..*" />
		<include within="com.neco_labo.aspect.*" />
	</weaver>
</aspectj>

view周りのUI層はこんなかんじで。
FreeMakerのテンプレートのサフィックスをhtmlにして、FreeMakerのタグを[]で囲うようにすることによってEclipseで扱いやすく!あと、文字コードをきっちり指定しないと文字化けするらしい。

<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>

<mvc:annotation-driven />

<bean id="viewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
		<property name="cache" value="true"/>
		<property name="prefix" value=""/>
		<property name="suffix" value=".html"/>
		<property name="contentType" value="text/html;charset=UTF-8"/>
		<property name="exposeSpringMacroHelpers" value="true"/>
</bean>

<bean id="freemarkerConfig"
	class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
	<property name="templateLoaderPath" value="/WEB-INF/view/" />
	<property name="defaultEncoding" value="UTF-8" />
	<property name="freemarkerSettings">
		<props>
			<prop key="tag_syntax">square_bracket</prop>
		</props>
	</property>

</bean>

あとはquartzを動かす設定。

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
	<property name="triggers">
		<list>
			<ref bean="myJobTrigger" />
		</list>
	</property>
</bean>

<bean id="myJobTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
  <property name="jobDetail" ref="myJob" />
  <property name="cronExpression" value="0 0 */6 * * ?" />
</bean>

<import resource="job-config.xml"/>

</beans>

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:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

<bean id="myJob" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
  <property name="targetObject" ref="MyJobExecutable" />
  <property name="targetMethod" value="execute"  />
  <property name="concurrent" value="false" />
</bean>
</beans>


ニコニコ動画からデータを引っ張ってきたり、RSSの出力をするのにはApache Httpclient 4, jackson, rome, jdom, jsoupを使用。
Apache Httpclientは普通のWebブラウザとほぼ同じようにWebサイトにアクセスできるので便利。リダイレクト、クッキー、gzipなどが透過的に扱えるため採用。
レスポンスに応じて、json=>jackson, xml=>jdom, html=>jsoupでパースして処理を行う独自ラッパーを作成して使用している。jdomはW3Cの仕様に準拠していない代わりに速くて簡単。
romeは各種RSSフォーマットで出力するのに便利なライブラリ。だが、CDATAセクションに対応していなかったり、ロケールがUSでハードコードなため(怒)、AspectJで(以下略)

そんなこんなで今日も動いています。
http://api.neco-labo.com/rss/harurun0403/