こんにちは、開発ユニットbcです。

みなさんご存知かとは思いますが、Seasar2のサポートが2016年9月26日を以って終了します。
弊社の運営するSUPERDELIVERYでも一部の機能でSeasar2を現在も利用しています。

今回はSeasar2からの移行を検討している方の参考になればと思い、以前に行った移行の事例を紹介させていただきます。

2014年にSUPERDELIVERYの商品登録機能をリニューアルした際、弊社ではSeasar2からSpring Framework(4系)へ移行することを選択しました。
選択の理由はプロダクトの開発が停滞していないことと、Seasar2からの移行のしやすさでした。
なぜ移行がしやすいかというと、Seasar2の各機能を代替する機能がSpring Frameworkにも備わっているからです。

ではまずはSeasar2とSpring Frameworkの構成の比較から。

構成の比較

移行前 移行後
Controller S2Struts Spring MVC
DI Seasar2 Spring Framework
DAO S2Dao Spring Data

このようにSeasar2の機能の移行先がSpring Frameworkにもあることが分かります。
では、商品一覧表示の機能を例にしてどのように移行するかを見ていきましょう。

移行前と移行後の実装を比較

移行前のシステムでは、コンポーネント毎に設定ファイルへ記述するスタイルで開発していたので、
新たに機能の追加がある場合は以下のファイルの追加が必要でした。
  1. XXXAction.java
  2. XXXActionImpl.java
  3. XXXService.java
  4. XXXServiceImpl.java
  5. XXXDao.java
  6. XXX.dicon
  7. struts-config-XXX.xml
■ProductSearchAction.java
public interface ProductSearchAction {
  String execute();
}
■ProductSearchActionImpl.java
public class ProductSearchActionImpl implements ProductSearchAction {
  private ProductSearchService productSearchService;
  private List<Product> products;
  private ProductSearchForm searchForm;
	
  public String execute() {
  products = productSearchService.findProducts(searchForm.createProductSearchCondition());
  return "success";
  }

  public List<Product> getProducts() {
  return products;
  }

  public void setProductSearchService(ProductSearchService productSearchService) {
  this.productSearchService = productSearchService;
  }

  public void setSearchForm(ProductSearchForm searchForm) {
  this.searchForm = searchForm;
  }
}
■ProductSearchService.java
public interface ProductSearchService {
List<Product> findProducts(ProductSearchCondition condition);
}
■ProductSearchServiceImpl.java
public class ProductSearchServiceImpl {
private ProductDao productDao;

public List<Product> findProducts(ProductSearchCondition condition) {
return productDao.findByStatusCode(condition.getStatusCode());
}

public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}
}
■ProductDao.java
public interface ProductDAO {
  @Query("status_code = ?")
  List<Product> findByStatusCode(Integer statusCode);
  void insert(Product product);
  void update(Product product);
  void delete(Product product);
}
■action.dicon
<components>
... <component name="productSearchAction" class="jp.ne.raccoon.action.product.impl.ProductSearchActionImpl" instance="request"> </component>
... </components>
■service.dicon
<components>
  ...
  <component
    class="jp.ne.raccoon.service.product.impl.ProductSearchServiceImpl">
  </component>
  ...
</components>
■dao.dicon
<components>
  ...
  <component
    class="jp.ne.raccoon.dao.ProducDao">
  </component>
  ...
</components>
■struts-config-product.xml
<struts-config>
  <action-mappings>
... <action path="/product/search" type="jp.ne.raccoon.action.product.ProductSearchAction" parameter="execute"> <forward name="success" path="/product/search.html" /> </action>
... </action-mappings> </struts-config>
  1. DIはdiconファイルにコンポーネントを個別に定義して、Seasar2のセッターインジェクションを利用していました。
  2. リクエストのマッピングは、struts-config.xmlにaction-mappingsをaction毎に個別に設定していました。
 
移行後は以下のような実装になりました。
■ProductSearchController.java
@Controller
public class ProductSearchController {
@Autowired private ProductSearchService productSearchService; @RequestMapping(value = "/search", method = RequestMethod.GET) public String execute( @Valid @ModelAttribute("searchForm") ProductSearchForm searchForm, BindingResult bindingResult, Model model) {
model.addAttribute(
"products",
productSearchService.findProducts(searchForm.createProductSearchCondition()));
return "product/search"; } }
■ProductSearchServiceImpl.java (※interfaceは移行前と同じなので割愛してます。)
@Service
public class ProductSearchServiceImpl implements ProductSearchService {
@Autowired private ProductRepository productRepository; public String findProducts(ProductSearchCondition condition) { return productRepository.findByStatusCode(condition.getStatusCode()); } }
■ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> {
  List<Product> findByStatusCode(Integer statusCode);
}

移行前と比べると以下のように実装を置き換えることができました。
Action → Controller
Service → Service
Dao → Repository
diconファイル → @Controller @Service等のアノテーション
struts-config → @RequstMapping
  1. 設定ファイルを必要とせず、Springのアノテーションで コンポーネントの定義とDIを行うようになりました。
  2.  @RequestMappingを利用することでリクエストメソッドの指定やパラメーターの受け取り方等がより柔軟な設定を書けるようになりました。
  3. Daoのinsert/update/deleteのメソッドは、継承元に定義されているのでメソッドを用意する必要はなく、@Queryに相当する物はfindByXXXの部分の命名規則によってwhere句が生成されます。※where status_code = ? が生成されます。
これだけで、新機能の追加に必要な作業がだいぶ軽減されました。
順調に移行できそうでしたが、問題点もありました。

問題点

一つ目が、sqlファイルの外部化です。
S2Daoの資産として、参照系の動的なsqlは外部ファイル化していました。 
Springのプロダクトでは対応している物が見当たらず、可能ならば慣れ親しんだ形で移行したいという思いもあって、Mirageというライブラリを導入することにしました。 

■ProductDao_findProducts.sql
//日付の範囲指定と商品の状態を検索条件で指定できることを例としてます。
select
  product_code
from
  product
/*BEGIN*/ where /*IF dateFrom != null*/ and created_at >= /*dateFrom*/ /*END*/ /*IF dateTo != null*/ and created_at < /*dateTo*/ + 1 /*END*/ /*IF productStatus !=null*/ and product_status = /*productStatus*/
 /*END*/
/*END*/
■ProductDao.java
@Component
@Transactional(readOnly = true)
public class ProductDaoImpl implements ProductDao {
  @Autowired
  private SqlManager sqlManager;

  public Product findProducts(SearchCondition condition) {
//上記sqlファイルを指定 SqlResource sqlResource = new ClasspathSqlResource("ProductDao_find_products.sql"); return sqlManager.getResultList(Product.class, sqlResource, condition); } }
mirageで実装したDaoも@ComponentでSpringのコンポーネントとして定義しました。
必要に応じてspringDataとmirageを使い分けるようにして、移行前の資産も活かすことができました。

二つ目がSeasar2とSpringでのコンポーネントのライフサイクルの違いでした。
S2Strutsの時は、Actionはリクエスト単位で作成していましたが、SpringMVCのControllerでは、デフォルトがシングルトンなため、リクエストやセッション等の短いライフサイクルのコンポーネントのDIができないということでした。
@Controller
@Scope(value = WebApplicationContext.SCOPE_REQUEST) public class XXXController { }
@Scopeの設定でライフサイクルを変えることで対応はできますが、全部のControllerに書くのは・・・ということで、以下のようなアノテーションを作成してライフサイクルをリクエストにしたいControllerに指定するようにしました。
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
@Controller
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
public @interface RequestScopeController {
}
利用する側のController
@RequestScopeController
public class XXXController {
}
これで、リクエストやセッションのライフサイクルのコンポーネントをDIしても問題なく動作するようになりました。

まとめ  

局所的ではありますが、以上がSeasar2から移行した時の内容です。いかがだったでしょうか。
Seasar2のサポート終了をきっかけに新しいことに挑戦していきたいですね。