2012 11 26

フレームワークを作るの5

サービス層だが、サンプルの BooksService が簡単な処理しか無いために、ここから更に何かを共通化するメリットが見出せない。 でも、無いと認めてしまうと、ここで終わってしまうので認めない。

で、取り上げるのがトランザクション制御。 更新系の処理しか関係無いが、正常終了の場合は commit で、例外をキャッチしたら rollback するのが基本形。 この構成を何とかできないかを考える。

すっきりするのは、サービス層のメソッドを呼ぶと、その結果に応じて勝手に commit または rollback してくれるパターンだろう。 いわゆるアスペクトとして、トランザクション制御を実装するのだな。 こうすると、コード上からはトランザクション制御は排除され、メソッドの境界がトランザクションの境界ということだけ意識すればよくなる。

具体的にどう実装するか。

共通のラッパークラスを作って、トランザクション制御を任せるか。

メソッド doTransaction を持つ Service インターフェースを定義し、サービスクラスはこのインターフェースを実装するものとする。

public interface Service<T> { public T doTransaction(); }

ラッパーはこの doTransaction メソッドの呼び出しに対してトランザクション制御を行う。

public class TransactionManager<T> { private final Connection conn; private final Service<T> service; public TransactionManager(final Connection conn, final Service<T> service) { this.conn = conn; this.service = service; } public T doTransaction() throws Exception { T ret = null; try { ret = service.doTransaction(); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } return ret; } }

でも、これだと、今のサービス層のクラスにまとめているメソッドをそれぞれ別のクラスに doTransaction として定義し直しが必要になるんだよな。 さすがにそれはちょっと、ねえ。

ラッパーがテンプレートになっても同じこと。 サービスクラスが外から渡されるか、継承した先で doTransaction を実装するかの違いだけで、BooksService をメソッド毎にバラバラに分解しなければいけないのは同じ。

簡単だと思ったが、意外に難しい。 ループを繰り返すほむらもこんな気持ちだったんだろうか。

で、しばらく考えていて、思い出したのが動的プロキシ。 確か Java魂 で読んだはずだと久しぶりに本を開いて、最初にこれを見た時のちょっとした感動まで思い出した。 「こんな便利なものがあったのか!」 って。 その後、あまり使う機会が無くて忘れてたんだよね。

動的プロキシの何が便利って、さっきのラッパーと違ってインターフェースで定義する全てのメソッドの呼び出しに対して制御できること。 欠点は、利点と表裏で必ずインターフェースを定義しなければならないことだが、これは大した障害にはならないだろう。 インターフェースはそもそも積極的に使うべきものだし、定義が難しいわけでもないし。

ということで動的プロキシ。

まずはトランザクション制御を実装するハンドラー。

package framework.service; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.sql.Connection; public class TransactionInvocationHandler implements InvocationHandler { private Connection conn; private Object serviceImpl; public TransactionInvocationHandler(Connection conn, final Object serviceImpl) { this.conn = conn; this.serviceImpl = serviceImpl; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object ret = null; try { ret = method.invoke(serviceImpl, args); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } return ret; } }

次にプロキシのファクトリー。

package framework.service; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.sql.Connection; import framework.exception.SystemException; public class ServiceFactory { public static <T> T getService(Connection conn, final T serviceImpl) { try { return (T) Proxy.newProxyInstance( serviceImpl.getClass().getClassLoader(), serviceImpl.getClass().getInterfaces(), new TransactionInvocationHandler(conn, serviceImpl)); } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException e) { throw new SystemException(e); } } }

こいつがサービスのインターフェースを備えたプロキシオブジェクトを返す。 使う側はプロキシだと意識すること無くメソッドを呼ぶが、内部では実装クラスのインスタンスに処理が移譲され、その結果例外が発生したかどうかでトランザクション制御がなされる。

サンプルの BooksService は、インターフェースとして再定義。

package service; import java.sql.SQLException; import java.util.List; import dto.BookDto; import framework.exception.BusinessException; public interface BooksService { public void insertBook(final BookDto book) throws SQLException; public BookDto selectBook(final String isbn) throws SQLException; public List<BookDto> selectAllBooks() throws SQLException; public void updateBook(final BookDto book) throws SQLException, BusinessException; public void deleteBook(final BookDto book) throws SQLException, BusinessException; }

このインターフェースを実装するクラスとして BooksServiceImpl を作る。

package service.impl; import java.sql.Connection; import java.sql.SQLException; import java.util.List; import service.BooksService; import dao.BooksDao; import dto.BookDto; import framework.exception.BusinessException; public class BooksServiceImpl implements BooksService { private final BooksDao dao; public BooksServiceImpl(Connection conn) { this.dao = new BooksDao(conn); } @Override public void insertBook(final BookDto book) throws SQLException { dao.insert(book); } @Override public BookDto selectBook(final String isbn) throws SQLException { return dao.selectByISBN(isbn); } @Override public List<BookDto> selectAllBooks() throws SQLException { return dao.selectAll(); } @Override public void updateBook(final BookDto book) throws SQLException, BusinessException { if (!dao.updateByISBN(book).equals(1)) { throw new BusinessException(book.getIsbn() + " : deleted or updated by someone else."); } } @Override public void deleteBook(final BookDto book) throws SQLException, BusinessException { if (!dao.deleteByISBN(book).equals(1)) { throw new BusinessException(book.getIsbn() + " : deleted or updated by someone else."); } } }

更新系のメソッドから、トランザクション制御のコードが消えてすっきりした。 まあ、元々簡単だったんだけど。

最後にアプリケーション層のサンプルコード。

package apl; import java.sql.Connection; import service.BooksService; import service.impl.BooksServiceImpl; import util.DBUtil; import dto.BookDto; import framework.service.ServiceFactory; public class Main { public static void main(String[] args) { try (Connection conn = DBUtil.getConnection()) { BooksService service = ServiceFactory.getService(conn, new BooksServiceImpl(conn)); BookDto book = service.selectBook("4-635-09016-7"); System.out.println(book.toString()); book.setPrice("3400"); service.updateBook(book); book = service.selectBook("4-635-09016-7"); System.out.println(book.toString()); } catch (Exception e) { e.printStackTrace(); } } }

変わったのは service の生成部分。 前は単純に new してたのが、今回はファクトリーから取得するようになった。 それ以外は同じ。

現在の構成は以下の通り。

apl Main.java service BooksService.java impl BooksServiceImpl.java dao BooksDao.java dto BookDto.java util DbUtil.java framework service ServiceFactory.java TransactionInvocationHandler.java dao CmnDao.java DtoConverter.java SqlDefLoader.java SqlDef.java exception BusinessException.java SystemException.java sqlDef.xml

サービス層で言えば、フレームワークを使う時に自分で書く必要があるのはインターフェース BooksService.java とその実装 BooksServiceImpl.java の2つ。

さて、使う側が実装するコードからトランザクションを消せたのはいいが、現状はそれだけなんだよな。 ファクトリーのサービス取得メソッドの引数に実装クラスを指定したんじゃファクトリーの存在意義が半減するし、何よりかっこ悪い。 また、一律トランザクション制御がかかるので、検索系のメソッドでも無駄に commit している。 全く無駄に。

次はこの辺りを、もう少し見れるものにする。