たまたまWebで読んだStruts2の紹介記事に興味を持ち、面白そうなので触ってみました。初心者の初心者による初心者の為の備忘録ですから、突っ込みどころが満載かと思いますがご容赦のほどを、、、
正直、DIとかAOPとかいう用語を最近はじめて知りましたw ちょっと前まで組み込みやらシステムコールを直接叩くような真似ばかりやっていたので、理論に裏付けられた実践的な業務プログラムなど書いたこともありませんw こういうのも一度やってみようかと勉強がてらちまちま書いています。最初は「依存注入なんて言われてもねぇ、、、何の役に立つの?」程度のものでしたが、やってみると確かにこれは便利そうだというのが見え(たような気がし)てきましたw
Webの世界にはASP.NETやサーバサイドJavaやらいろいろあるようですが、.netは仕事で触っているので馴染みの薄いJavaにしてみました。
Struts2を触るのは今回が初めてなので、いろいろなサイトで情報を掻き集めては実験実証しているのですが、本来なら参照したサイトの一覧なりを載せるべきところ、あまりにも多くのサイトを参考にしたのとStruts1とStruts2との情報が脳内で混乱してしまいまとめられませんでした。とういうことで勝手ながら省略させて頂きます、いろいろ情報を発信してくださった方々に感謝致します。
私のPCにはCentOS5.4(64bit)が入っているので、まずはjdk6とEclipse3.5(Galileo)それにtomcat6が動作するようにしておきます。Eclipseは予め必要なpluginと本体がパッケージ化されているJ2EE版をダウンしPleiadesで日本語化しました。最近のEclipseって凄く進化してるんですね驚きました。
動的Webプロジェクトで"Sample1"(とか何でもいい)というプロジェクトを作成しました:
srcディレクトリはビルド後にclassesとなってWEB-INF配下に収まるようです。src_debugというのはテスト用のコードがWARファイルに入らないように分離するためのディレクトリです。JUnitで単体テストみたいなことをするのに、テスト用コードはディプロイしたくない場合はどうするの?という疑問にディレクトリを分けてWARに出力しなければよい、、、みたいな記事があったので真似してみました。実はまだ中身は空ですw
実験的意味合いの強いものですから、他にもいろいろと冗長的なことをやってます。
Eclipseの動的Webプロジェクトでテスト用のプロジェクトを作成しWEB-INF/web.xmlにstruts2の使用とspring2の使用を設定します:
<?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_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>Sample1</display-name>
<filter>
<filter-name>requestContextFilter</filter-name>
<filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>requestContextFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext*.xml</param-value>
</context-param>
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
何のことやらよくわからない呪文ですがこういうもんみたいです。最初のフィルターはspring用ですが、これを指定するとspringのscopeにsessionとか使えるそうです。
次にstrutsにspringを連携させるようにsrc/struts.xmlを設定します:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.enable.DynamicMethodInvocation" value="false" />
<constant name="struts.devMode" value="false"/>
<constant name="struts.objectFactory" value="spring" />
<constant name="struts.action.extension" value="action" />
<constant name="struts.ui.theme" value="simple" />
<include file="struts-actions1.xml" />
</struts>
includeを使ってstrutsの定義を分割しています:
<struts>
<package name="mytest" namespace="/" extends="struts-default">
<!-- デフォルトinterceptorを定義 -->
<default-interceptor-ref name="defaultStack"/>
<!-- アクションリスト -->
<action name="start" class="defaultAction">
<result name="success">/WEB-INF/jsp/sample.jsp</result>
</action>
:
</package>
</struts>
これでstrutsのAction beanをspringで生成させられるようになるそうです。
次に必要なライブラリですが、これが依存関係がさっぱりわからず、コンパイルやtomcat起動ログを見ながら適宜足りないものを補足したりと試行錯誤。最終的には下記の通りになりました:
aopalliance-1.0.jar
asm-3.2.jar
cglib-2.2.jar
commons-dbcp-1.2.2.jar
commons-digester-1.8.jar
commons-fileupload-1.2.1.jar
commons-io-1.3.2.jar
commons-logging-1.0.4.jar
commons-pool-1.5.3.jar
freemarker-2.3.15.jar
mysql-connector-java-5.1.10-bin.jar
ognl-2.7.3.jar
spring-aop.jar
spring-beans-2.5.6.jar
spring-context-2.5.6.jar
spring-core-2.5.6.jar
spring-jdbc.jar
spring-tx.jar
spring-web-2.5.6.jar
struts2-convention-plugin-2.1.8.jar
struts2-core-2.1.8.jar
struts2-spring-plugin-2.1.8.jar
xwork-core-2.1.6.jar
目的によって不要なものが過分にあるかと思います、赤字のものが最低限必要なんじゃないかと思いますが、、、必要に応じて参照させるなりWEB-INF/libにコピーするなりします。
例えばstruts.xmlに:
<action name="start" class="defaultAction">
<result name="success">/WEB-INF/jsp/sample.jsp</result>
</action>
のようにstrutsのActionを登録し、applicationContext.xmlに:
<bean id="defaultAction" class="webapp.actions.SuccessAction" scope="prototype"/>
とspringのbeanを登録すると、strutsで"start"Actionが処理される時にspringがwebapp.actions.SuccessActionのインスタンスを生成してくれるといった動きをします。こうして2つの設定ファイルに対に登録する、、、実に面倒ですねwww
XMLファイルに設定をするというのは、ある意味コードと依存関係などの設定を分離出来るという利点はありますが、数が増えるとXMLの管理も大変なことになりそうです。そんなXML地獄を軽減するのにアノテーションが使えます。まずはspring側の設定をアノテーションで置き換えてみます:
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component("defaultAction")
@Scope("prototype")
public class SuccessAction {
:
上記のようにJavaのコードに直接設定し、applicationContext.xmlを下記のように書き換えます:
<context:component-scan base-package="webapp" /><bean id="defaultAction" class="webapp.actions.SuccessAction" scope="prototype"/>
こうするとbeanの行が不要になり、指定したパッケージ配下を探しに行ってくれるようです。
あるクラスが必要と(依存)する別のクラスを自動的(勝手)に生成して代入してくれる。そんな理解しかしてませんが、springがそういう面倒なことを受け持ってくれるそうです。bean定義に:
<bean id="dataSource"・・・>
</bean>
<bean id="xxxx" >
<property name="dataSource" ref="dataSource" />
</bean>
のように書くことで設定したり、アノテーションでコード中に記載したりすることで後はspringにお任せします:
import org.springframework.beans.factory.annotation.Autowired;
public class MyBean {
@Autowired(required=true)
private DataSource dataSource = null;
アノテーションを使えばsetterメソッドすら書かなくてもいいようです。
これまではStrutsのActionをSpringで生成させていましたが、StrutsでActionを生成させる場合にアノテーションを使えばactionすらXMLに登録しなくてもいけるようです。但し、Actionクラスはaction、actions、struts2とかのパッケージ名の下に置く必要があるとかいうルールがあるようです:
import org.apache.struts2.convention.annotation.Action;
import org.apache.struts2.convention.annotation.ParentPackage;
import org.apache.struts2.convention.annotation.Result;
@ParentPackage(value="mytest")
@Result(name="success", location="/WEB-INF/jsp/sample.jsp")
public class MyAction {
@Action(value="action1")
public String MyActionMethod() {
:
return "success";
}
上記のようにデフォルトのexecuteメソッド以外でもActionメソッドに設定可能です。また結果の飛び先も複数設定可能ですし、Actionメソッドに設定することでクラスに設定している属性を上書きできるようです:
@Action(value="action1", results={
@Result(name = "success", location="/WEB-INF/jsp/sample.jsp"),
@Result(name = "failure", location="/WEB-INF/jsp/error.jsp")
})
public String MyActionMethod() {
:
if (xxxxx)
return "success";
else
return "failure";
}
結果のタイプも設定できますから、ファイルのダウンロードもアノテーションで指定可能です:
private InputStream inputStream = null;
private long contentLength = 0;
private String filename = null;
:
public InputStream getInputStream() { return this.inputStream; }
public long getContentLength() { return this.contentLength; }
public String getFileName() { return this.filename; }
:
@Action(value="action2", results={
@Result(name = "failure", location="/WEB-INF/jsp/error.jsp"),
@Result(name = "success", type = "stream",
params = {
"inputName", "inputStream",
"bufferSize", "1024",
"contentType", "application/octet-stream; charset=UTF-8",
"contentLength", "${contentLength}",
"contentDisposition", "attachment; filename = ${fileName}"
})
})
public String download() {
:
}
またインターセプターを設定(又はデフォルトを上書き)するにもアノテーションで指定可能です:
@Action(value="action3", interceptorRefs={
@InterceptorRef(value="scope",
params={"session","page","key","sesinfo","type","start","autoCreateSession","true"}),
@InterceptorRef(value="defaultStack")}
)
public String MyActionMethod() {
struts.xmlに設定するものも多くはアノテーションで設定できるようですね。尚、各種設定の中にparamsでオプションを指定するところがありますが、paramsには{パラメータ1, 値1, パラメータ2, 値2,・・・}のように設定していくようです。
ブラウザからのリクエストに対し、Strutsではインターセプターが順番に実行され、その後でActionに到達する仕組みになっいます。そこで、ユーザがログインしているか否かを(一部を除く)全てのAction実行前にインターセプターでチェックして、ログインしていなければ強制的にログインActionにリダイレクトさせる実験をしてみます。ますは、自前のインターセプターを作成します:
package webapp.interceptors;
import javax.servlet.http.HttpSession;
import org.apache.struts2.ServletActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
public class Login extends AbstractInterceptor {
private static final long serialVersionUID = 8515533499615457714L;
@Override
public String intercept(ActionInvocation invocation) throws Exception {
HttpSession session = ServletActionContext.getRequest().getSession();
// (1) ログインしていれば、次のインターセプターへ
webapp.models.dto.User dtoUser = (webapp.models.dto.User) session.getAttribute("LOGIINUSER");
if (dtoUser != null)
return invocation.invoke();
// (2) ログインしていない場合はLoginActionへリダイレクトされる
return "login";
}
このインターセプターはセッション変数にログインユーザのオブジェクトが登録されているか否かを見るだけの単純なものです。次にこのインターセプターを通るようにstruts.xmlを設定します:
<struts>
<package name="mytest" namespace="/" extends="struts-default">
<!-- インターセプター -->
<interceptors>
<interceptor name="login" class="webapp.interceptors.Login" />
<interceptor-stack name="myDefaultStack">
<interceptor-ref name="login" />
<interceptor-ref name="defaultStack" />
</interceptor-stack>
</interceptors>
<!-- デフォルトinterceptorを再定義 -->
<default-interceptor-ref name="myDefaultStack"/>
<global-results>
<result name="login" type="redirectAction">login</result>
</global-results>
既定のインターセプタースタックに今回のインターセプターを追加し、ログインしていない場合はloginアクションにリダイレクトされるように設定しました。loginアクションのコードは以下に抜粋:
@ParentPackage(value="mytest")
@InterceptorRef(value="defaultStack")
public class LoginAction implements ModelDriven<webapp.models.dto.User> {
:
@Action(value="login", results={
@Result(name="success", location="/WEB-INF/jsp/login.jsp")}
)
public String execute() throws Exception {
return "success";
}
:
@Action(value="loginauth", results={
@Result(name="success", location="/WEB-INF/jsp/top.jsp"),
@Result(name="input", location="/WEB-INF/jsp/login.jsp")}
)
public String loginauth() throws Exception {
:
// ログインに成功したら、古いセッションを破棄し新しいセッションを開始し
// 新しいセッションにログインユーザのオブジェクトを登録する
ServletActionContext.getRequest().getSession(true).invalidate();
HttpSession newsession = ServletActionContext.getRequest().getSession(true);
newsession.setAttribute("LOGIINUSER", target);
return "success";
else
// ログインのやり直し
return "input";
}
当然、loginアクションが実行される際にはログイン状態でないわけですから、このアクションに関しては実行前にloginインターセプターを通らないようにアノテーションで個別にインターセプタースタックの設定をしておきます。その後、ログイン画面からユーザがログインするとloginauthで認証しトップ画面に遷移させます。
かなり大雑把ではありますが、ザクっと紹介してみました。バリデーションなども機会があれば触ってみたいと思います。
尚、本記事に関して間違いなどあればご指摘頂ければ幸いです。