◀ 4.

Struts 1 の Action クラス完全解説 (execute / ActionForm / forward)

▶
この記事の要点
  • Struts 1 の Action は execute(ActionMapping, ActionForm, request, response) を入口として要求を処理する
  • 戻り値は ActionForward 。struts-config.xml の <forward> 名で遷移先を解決
  • 複数機能を 1 クラスに → DispatchAction / LookupDispatchAction
  • Struts 1 は 2013 年 EOL。新規開発は Spring MVC @Controller / @RestController に移行
  • 移行はURL・フォーム・遷移・認証を照合して段階的に。DelegatingActionProxyは非推奨の旧Spring連携であり、現在の標準的な導入手順ではない

Actionは要求処理の入口です。業務処理と画面遷移を分けて読み、ActionForm・mapping・JSPまで一組で確認します。Struts 1は保守終了済みで、本記事の旧コードは既存システムの理解と移行のための参考です。

Struts 1のフォーム確認、Action生成と再利用、execute実行の関係

Struts 1 Action の基本形

次は構造を示す断片です。LoginForm・UserService・User・メッセージ定義・imports等は別に必要です。認証後のセッション管理やCSRF対策、認可を省略しており、安全なログイン実装として丸ごとコピーしないでください。

public class LoginAction extends Action {

    @Override
    public ActionForward execute(
            ActionMapping mapping,
            ActionForm form,
            HttpServletRequest request,
            HttpServletResponse response) throws Exception {

        // 1. ActionForm をキャスト
        LoginForm loginForm = (LoginForm) form;
        String userId = loginForm.getUserId();
        String pass   = loginForm.getPassword();

        // 2. 業務ロジック
        UserService svc = new UserService();
        User user = svc.authenticate(userId, pass);

        if (user == null) {
            ActionMessages errors = new ActionMessages();
            errors.add("login", new ActionMessage("error.login.failed"));
            saveErrors(request, errors);
            return mapping.findForward("failure");
        }

        // 3. セッション格納
        request.getSession().setAttribute("loginUser", user);

        // 4. 遷移先を返す
        return mapping.findForward("success");
    }
}

struts-config.xml の設定

<?xml version="1.0" encoding="UTF-8"?>
<struts-config>

    <!-- form bean 定義 -->
    <form-beans>
        <form-bean name="loginForm" type="com.example.LoginForm"/>
    </form-beans>

    <!-- action 定義 -->
    <action-mappings>
        <action path="/login"
                type="com.example.LoginAction"
                name="loginForm"
                scope="request"
                validate="true"
                input="/WEB-INF/jsp/login.jsp">
            <forward name="success" path="/WEB-INF/jsp/home.jsp"/>
            <forward name="failure" path="/WEB-INF/jsp/login.jsp"/>
        </action>
    </action-mappings>

</struts-config>

ActionForm

この例のvalidateは入力形式の検査で、本人確認ではありません。登録済み利用者の認証方針とパスワード長を勝手に置き換えないでください。またrequest/sessionスコープによってフォームの保持範囲が変わります。Actionのインスタンスの共有とフォームのスコープは別です。

public class LoginForm extends ActionForm {
    private String userId;
    private String password;

    public String getUserId() { return userId; }
    public void setUserId(String v) { this.userId = v; }
    public String getPassword() { return password; }
    public void setPassword(String v) { this.password = v; }

    @Override
    public ActionErrors validate(ActionMapping mapping, HttpServletRequest req) {
        ActionErrors errors = new ActionErrors();
        if (userId == null || userId.isEmpty()) {
            errors.add("userId", new ActionMessage("error.required.userid"));
        }
        if (password == null || password.length() < 8) {
            errors.add("password", new ActionMessage("error.password.length"));
        }
        return errors;
    }

    @Override
    public void reset(ActionMapping mapping, HttpServletRequest req) {
        this.userId = null;
        this.password = null;
    }
}

JSP 側

<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %>
<%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean" %>

<html:form action="/login">
    ユーザーID: <html:text property="userId"/><br/>
    <html:errors property="userId"/><br/>

    パスワード: <html:password property="password"/><br/>
    <html:errors property="password"/><br/>

    <html:submit value="ログイン"/>
</html:form>

DispatchAction(1 Action 複数メソッド)

methodで処理を選べても、認可が付くわけではありません。下のshow例ではidの未指定・非数値・対象不在を処理する必要があります。delete等の更新処理では、許可するHTTPメソッド・CSRF検証・対象ごとの操作権限を別途確認し、URLのパラメータだけで削除を許可しないでください。下のコードのdeleteは処理を省略した断片です。

機能ごとに Action クラスを作ると膨大になるため、DispatchAction で 1 クラスに複数機能を集約:

public class UserAction extends DispatchAction {

    public ActionForward list(ActionMapping mapping, ActionForm form,
            HttpServletRequest req, HttpServletResponse res) throws Exception {
        req.setAttribute("users", new UserService().findAll());
        return mapping.findForward("list");
    }

    public ActionForward show(ActionMapping mapping, ActionForm form,
            HttpServletRequest req, HttpServletResponse res) throws Exception {
        long id = Long.parseLong(req.getParameter("id"));
        req.setAttribute("user", new UserService().findById(id));
        return mapping.findForward("detail");
    }

    public ActionForward delete(ActionMapping mapping, ActionForm form,
            HttpServletRequest req, HttpServletResponse res) throws Exception {
        // ...
        return mapping.findForward("list");
    }
}
<!-- struts-config.xml: parameter で method を切り替え -->
<action path="/user"
        type="com.example.UserAction"
        parameter="method"
        scope="request">
    <forward name="list"   path="/WEB-INF/jsp/user/list.jsp"/>
    <forward name="detail" path="/WEB-INF/jsp/user/detail.jsp"/>
</action>

<!-- 呼び出し: /user.do?method=list / /user.do?method=show&id=1 -->

Action Chain (forward の連鎖)

1 Action の結果を別の Action に流す:

<action path="/login" type="com.example.LoginAction">
    <!-- 成功時に /home に forward (chain) -->
    <forward name="success" path="/home.do" redirect="false"/>
</action>

<action path="/home" type="com.example.HomeAction">
    <forward name="default" path="/WEB-INF/jsp/home.jsp"/>
</action>

注意: Action Chain は Struts 1.x で多用されたが、デバッグが困難でアンチパターン扱い。共通業務処理はサービス層 (Service)への分離を検討します。forwardとredirectでは要求属性の保持が異なるため、既存の遷移を削除・置換する前に入力エラー時と成功時の動作を照合してください。

Spring MVC への移行

Struts 1 は 2013 年 4 月にサポート終了。セキュリティ保守のない状態を放置せず、保守される環境への移行を計画してください。ただし移行先がSpring MVCに限られるわけではなく、Apacheも置き換えるだけの互換製品はないと案内しています。以下はSpring MVCを選んだ場合の構造比較です:

// Struts 1 → Spring MVC
@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;  // DI で注入

    @GetMapping
    public String list(Model model) {
        model.addAttribute("users", userService.findAll());
        return "user/list";   // → /WEB-INF/jsp/user/list.jsp
    }

    @GetMapping("/{id}")
    public String show(@PathVariable Long id, Model model) {
        model.addAttribute("user", userService.findById(id));
        return "user/detail";
    }

    @PostMapping("/save")
    public String save(@Valid @ModelAttribute UserForm form,
                       BindingResult result,
                       RedirectAttributes ra) {
        if (result.hasErrors()) return "user/edit";
        userService.save(form);
        ra.addFlashAttribute("msg", "保存しました");
        return "redirect:/user";
    }
}

Struts 1 vs Spring MVC

比較は概念の違いです。Springのサポート状況や必要なServlet/Java/名前空間は使用する版で確認します。上のController断片のuser/listという戻り値からJSPパスへの解決にはViewResolver等の設定が必要で、コメントだけでそのパスになるわけではありません。UserFormの制約・検証依存・サービス・認証設定も別です。

項目Struts 1Spring MVC
サポート2013/04 EOL継続中
設定struts-config.xmlアノテーション
FormActionForm (継承必須)POJO + @ModelAttribute
DI標準Action生成は直接生成。旧Spring連携等は別設定・版の確認が必要標準搭載
REST独自に応答は作れるが、現代的なREST用アノテーションの構造とは異なる@RestController で標準対応
テストAction等の単体はServletのモックで検査可能。JSP・配備全体は統合検査が別途必要MockMvc で容易

段階的移行: DelegatingActionProxy

下の例は古い連携設定の読解用で、新しい移行計画の標準手順ではありません。 Spring 3.2.18のDelegatingActionProxyはSpring 3.0以降非推奨です。現行Springへこのクラス名を足すだけでは連携できません。実際のライブラリの版・クラスの有無・WebApplicationContextとContextLoaderPlugInの設定を確認してください。

下のBeanにはuserServiceのsetterが必要ですが、上のLoginAction断片にはありません。二つの例をそのまま結合しても動く完成例ではありません。委譲先のBean名はpathとモジュールprefixに対応し、/adminモジュールの/loginなら/admin/loginを照合します。プロキシの存在を認証・認可・保守継続の保証と扱わないでください。

<!-- struts-config.xml: Action を DelegatingActionProxy 経由に -->
<action path="/login"
        type="org.springframework.web.struts.DelegatingActionProxy">
    <forward name="success" path="/WEB-INF/jsp/home.jsp"/>
</action>

<!-- applicationContext.xml: Bean として Action を定義 -->
<bean name="/login" class="com.example.LoginAction">
    <property name="userService" ref="userService"/>
</bean>

FAQ

Q: Struts 2 ではどうなる?
A: Struts 2 は別物(旧 WebWork)。Action は POJO で execute() が String を返す形に。Struts 1 とは互換性なし。

Q: 既存の Struts 1 アプリはすぐ捨てるべき?
A: 公開系なら最優先。社内系でも段階的移行を計画。CVE-2014-0114 等の重大脆弱性あり。

Q: ActionServlet は何をしている?
A: Struts 1 の FrontController。web.xml で *.do をマップし、URL から ActionMapping を解決して Action を呼ぶ。Spring MVC では DispatcherServlet がそれに相当。

要求ごとの値をActionのフィールドに置かない

Struts 1.3.10の標準RequestProcessorは、生成したActionをtypeのクラス名で保持し再利用します。同じprocessor内の異なるpathでも同一typeなら同じActionでした。Actionに現在のrequest・form・userIdを保存すると別要求の値を共有し得るため、executeの引数やローカル変数で扱います。独自processorの生成規則やサービス自体のスレッド安全性は別に確認してください。

移行時に比較する動作

  • 既存URL・HTTPメソッド・文字コード・パラメータ名。古いURLからの利用を勝手に切らない。
  • 入力検証で戻る画面、エラーメッセージ、フォーム値の保持。入力エラーと認証エラーを分ける。
  • forward/redirect、要求属性・session属性、認証後とログアウト後の状態。
  • 対象データの取得・更新・操作権限。Controllerが応答しただけでDB更新や認可の一致と扱わない。

検証範囲と一次資料

Struts 1.3.10のAction生成、同じtypeの再利用、生成後のexecute失敗はServletコンテナなしの単体で確認しました。これは上のログイン・JSP・Spring移行全体の動作確認ではありません。既存アプリの要求と応答を比較する統合検査が別途必要です。

Apache Struts 1.3.10公式ソース配布、Spring 3.2.18のDelegatingActionProxy API、Struts 1保守終了と移行案内。

子ページ

子ページはありません

同階層のページ
  1. 導入方法
  2. struts-config.xmlの説明
  3. ActionForm
  4. Action
  5. エラー一覧
  6. JSP タグ
  7. カスタムタグ(tablib)