FilterでWeb要求・応答をログ出力してみる

今、リクエストを受信した後に対外接続を行うシステムを作ってますが、がつがつと外部に繋ぐのも気が引けるという事で、、そのシミュレータとして簡単なWebAPも作りました。
APサーバは、WebLogic11G。作っているAPの仕様上、POSTをサーブレット処理してます。
で、結局Oracleのページに乗っていたグッドなサンプルをそのままパクってみました(汗)
Oracle Containers for J2EE サーブレット開発者ガイド
Request処理は解るのですが、Responseの方はFilter内でラップするというちょっと自分では考え付かない内容でした。。
上記に載っている、FilterServletOutputStreamとGenericResponseWrapperは、深く考えずに同一パッケージ内にそのままクラスとして作ります。
で、Filterクラスを作ってログを出力します。よく使っているlog4j2でやってます。

package jp.esoro.http;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.OutputStream;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class PostFilter implements Filter {
	protected static Logger logger = LogManager.getLogger();
	
	@Override
	public void destroy() {
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
 		try {
 			//要求
			HttpServletRequest req = (HttpServletRequest) request;
			
			BufferedReader body = new BufferedReader(req.getReader());
			String ret = body.readLine();
			//要求ロギング
			logger.info(req.getRemoteAddr()+"->"+req.getLocalAddr()+":"+req.getLocalPort()+req.getContextPath() + ":" + req.getMethod() + " RCV:"+ret);

			OutputStream out = response.getOutputStream();
			
			GenericResponseWrapper wrapper = 
			         new GenericResponseWrapper((HttpServletResponse) response); 
			//この後、サーブレット側処理に移ります
			chain.doFilter(request, wrapper);	
			
			//ラップしたwrapperからサーブレットの処理結果を取得
			byte[] data = wrapper.getData();
 			//OutputStreamに書き込んで応答
			out.write(data);
			out.close();
			//Shift-JISでString変換
			String result = new String(data, "Shift-JIS");
			//応答ロギング
			logger.info(req.getRemoteAddr()+"<-"+req.getLocalAddr()+ ":"+req.getLocalPort()+ req.getContextPath() + ":" + req.getMethod() + " SND:"+result);

		} catch (IOException e) {
			logger.error(e.getMessage());
		}
	}
	@Override
	public void init(FilterConfig arg0) throws ServletException {
	}
}

で、web.xmlに上記Filterをセットします

 	<!-- Filter -->
	<filter>
		<filter-name>PostFilter</filter-name>
		<filter-class>jp.esoro.http.PostFilter</filter-class>
	</filter>
	<filter-mapping>
        <filter-name>PostFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

最初サーブレットはアノテーション@WebServletで書きましたが、どうも11Gだとダメっぽいのでweb.xmlにservletをMappingする事にしました。シミュレータは本当に簡単なAPなので。。
開発環境は他のプロジェクトで使うWebLogic12cのままで11Gを別途用意しなかったんですが、実行環境が違うとデプロイに失敗して結構メンドクサイです。コンパイラーJavaのバージョンをちゃんと確認してなかったからなんですけど、、

WLSのデータソース設定せずにJPA

今筆者が作っているWEBアプリケーションのDB周り実装です。
インフラ的には、主にWLS12+OracleDBとなっていますが、社内特定部署に利用が限定されたアプリケーションなので、DBセッションは「節約したい」という微妙な要件です。さらに、WLSデータソース設定はしたくないとの希望です。
せっかくEE使うんで、JPA前提でやる事にしますが、WEBに出てる殆どのネタがWLSのデータソースを使っているんですよね。
セッションプールは別途OracleUCPの独自実装があるので、persistance.xmlで指定せずに、UCPのデータソースをプログラム上からエンティティマネージャに指定するというイメージなんですが、WEBで調べながら実際に動くまで結構時間掛かりました。。

まず、EclipseのプロジェクトファセットにJPAを追加するとpersistence.xmlが出来ますが、データソースをここでは設定しないのでエンティティクラス定義のみ書きます。

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
	<persistence-unit name="DBresource" transaction-type="RESOURCE_LOCAL">
		<class>jp.xxx.zzz.model.user.LoginUser</class>
他クラス定義省略
   	<exclude-unlisted-classes>true</exclude-unlisted-classes>
	</persistence-unit> 
</persistence>

次にエンティティクラス(テーブル定義みたいなもの)

@Table(name="USER_MST")
@Entity
public class LoginUser implements Serializable{
	
	private static final long serialVersionUID = 1L;
	
	/** ユーザーID*/ @Id @NotNull @Size(max=20,min=7) 
	private String USER_ID;
	/** ユーザーパスワード*/
	private String USER_PASS;
	/** ユーザー種別*/ @NotNull
	private String USER_TYPE;
以下、項目とゲッタセッタは省略

その次が、データアクセスインターフェース(データ操作定義)

/**
 * ログインユーザーマスタ 
 * Data access object interface
 * */
public interface LoginUserDao {

    /***
     * データ1件取得
     * @return キー
     * */
    public LoginUser find(Object id);
    
    /***
     * データ新規作成
     * */
	public void create(LoginUser entitiy);

    /**
     * データ更新
     * */
    public void update(LoginUser entitiy);

    /**
     * データ全件取得
     * */
    public List<LoginUser> getAll();
    
    /**
     * データ件数取得
     * */
    public long  getAllCnt();
}

さらに上記インターフェースをJPA実装したデータアクセスオブジェクト(DAO)

/**
 * ログインユーザーマスタ
 * Data access object (JPA)
 * */
@Stateless
//@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class LoginUserDaoJpa implements LoginUserDao{
	protected static Logger logger = LogManager.getLogger();
	public EntityManager  em = null;

	/**
	 * コンストラクタ
	 * */
	public LoginUserDaoJpa() {
		super();
		em = DBEntityManager.getEntityManager();
	}
	/**
	 * データ検索
	 * @param id (key)
	 * @return LoginUser
	 * */
	@Override
	public LoginUser find(Object id) {
		return em.find(LoginUser.class, id);
	}
	/**
	 * データ新規登録
	 * @param ServerConf 
	 * */
	@Override
	public void create(LoginUser entity) {
 		EntityTransaction txn = em.getTransaction();
 		txn.begin();

    	if ( ! em.getTransaction().isActive() ){
    		em.getTransaction().begin();
    	}
		em.persist(entity);
        txn.commit();
	}

	/**
	 * ユーザーマスタの全取得
	 * @return ログインユーザーマスタのリスト
	 * */
	@SuppressWarnings("unchecked")
	@Override
	public  List<LoginUser>  getAll() {
		//後日追加
		em.clear();
		List<LoginUser> result = em.createQuery("SELECT c FROM LoginUser c WHERE c.STATUS_FLG < :SFLG ORDER BY c.USER_ID")
					.setParameter("SFLG", "9")
					.getResultList();
    	return result;
    } 	
	/**
	 * ユーザーマスタより業務メンバーの取得
	 * @return ログインユーザーマスタのリスト
	 * */
	@SuppressWarnings("unchecked")
	public  List<LoginUser>  getBizMember() {
		//後日追加
		em.clear();
		List<LoginUser> result = em.createQuery("SELECT c FROM LoginUser c WHERE c.USER_TYPE < '5' AND c.STATUS_FLG < :SFLG ORDER BY c.USER_ID")
					.setParameter("SFLG", "9")
					.getResultList();
    	return result;
    }
    /**
     * データ更新
     * @param ServerConf
     * */
	@Override
 	public void update(LoginUser entity) {
 		EntityTransaction txn = em.getTransaction();
 		txn.begin();

    	if ( ! em.getTransaction().isActive() ){
    		em.getTransaction().begin();
    	}
        em.merge(entity);
        txn.commit();
    }
 	/**
 	 * 件数取得
 	 * */
	public long  getAllCnt() {
    	return em.createQuery("SELECT c FROM UserMaster c").getMaxResults();
    }
}

上記DAOのコンストラクタで読んでいるEntityManagerを取得するシングルトンクラス

public class DBEntityManager {
	protected static Logger logger = LogManager.getLogger();
	private static Map<String, Object> configOverrides;
	
	static {
		Map<String, Object> map = new HashMap<String, Object>();		
		try {
			//ここのDBConnectionMgrとは、UCPを使用してデータソースを戻す独自実装です・・
			//独自実装内で各種JDBC設定がされています・・
			map.put("javax.persistence.nonJtaDataSource", DBConnectionMgr.getDataSource());
		} catch (SQLException e) {
			logger.error("DBConnectionPool JpaEntityManager put Error!", e);
		}
		configOverrides = Collections.unmodifiableMap(map);
	}
	/**
	 * EntityManagerを返します
	 * */
	public static EntityManager getEntityManager(){
		return Persistence.createEntityManagerFactory("DBresource", configOverrides).createEntityManager();
	}
}

上記を呼び出しているJava画面ソース

/**
 * ユーザーマスタ管理画面処理(ユーザー作成・更新・削除・パスワード変更共通)
 * 
 * */
@ManagedBean(name="UserManager")
@ViewScoped
public class UserManager implements Serializable {
	private static final long serialVersionUID = 1L;

    @EJB
    private LoginUserDao dao;
	
	List<LoginUser> List;
	
	@ManagedProperty("#{userDto}")
	LoginUserDto user;

	public LoginUserDto getUser() {
		return user;
	}

	public void setUser(LoginUserDto user) {
		this.user = user;
	}

	/** 以下画面項目 */
	public List<LoginUser> getList() {
    	List = dao.getAll();
		return List;
	}

	public void setList(List<LoginUser> list) {
		List = list;
	}
以下省略

最後に上記呼び出し箇所のXHMLの抜粋

  <h:dataTable var="item" value="#{UserManager.list}" columnClasses="userid,username" >
    <h:column>
      <f:facet name="header">#{words.Label_UserID}</f:facet>
      <h:outputText value="#{item.USER_ID}" />
    </h:column>
  </h:dataTable>

後で、テストしたところ、DB更新は出来ていてもクエリーでデータを取得すると反映していませんでした。永続化コンテキストからみの事象と思われますが、細かい仕様を調査するのは骨が折れるので、、、クエリー取得前にEntityManagerをclearする事で対処としました。

JAX-RSを使ってみる

今筆者が作っているサブシステムは、複数種類の定期ジョブとWeblogicで動作させる画面アプリと組合せなんですが、定期ジョブの実装方式として、JAX-RSを検討しました。

なんでジョブをJAX-RSで作るのか?という所なんですが、Weblogicデプロイモジュールに含めることでジョブ実行時のメモリ消費をインフラ設計範囲内に収める為(Coherenceキャッシュへのアクセスがある)と、ジョブ変更時のモジュール管理をサブシステムとして統一したいという理由です。

あと、ジョブ起動については既存のジョブ管理ツールに統一したいとの事なので、EJBスケジューリングは使えない前提です。というか、EJBスケジューリングの挙動検証がしきれてないので、お勧めするのを躊躇したという所なんですが。。

当然、そのままだと外部からアクセスできるので、セキュリティ対策としては要求元IPでフィルタする形で対応する事としてます。

簡単な汎用POSTリクエストモジュールを作成した後、JAX-RSとして動作する処理をJavaEEアプリ内に作成します。

Weblogicデフォルトではなぜか動かなかったので、
weblogic.jaxrs.server.portable.servlet.ServletContainerを使わず、
POMにjerseyを追加

<dependency>
  <groupId>com.sun.jersey</groupId>
  <artifactId>jersey-core</artifactId>
  <version>1.19</version>
</dependency>

web.xmlを上記に合わせる

  <servlet>
    <description>JAX-RS Tools Generated - Do not modify</description>
    <servlet-name>JAX-RS Servlet</servlet-name>
      <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class> 
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>JAX-RS Servlet</servlet-name>
    <url-pattern>/batch/*</url-pattern>
  </servlet-mapping>

JAX-RSソース(コンテキストルート/batch/JOB1のURLでPOST実行)

/**
 * バッチ処理
 * */
@Path("/JOB1")
public class BatchExecute{
    public String hello(@Context HttpServletRequest request) throws InterruptedException {
        return "Batch GET Hello!";
    }
    /**
     * ジョブ実行
     * */
    @POST
    public Response execute(@Context HttpServletRequest request) {
       //POSTパラメータ取得
       String Interval = request.getParameter("PARAM1");
      ・・・以下省略
    }
}

ジョブ管理の側はログファイルの検知をトリガーにするので、どのプロセスがログに書いたかはどうでもよく、これで行こうと思います

追伸 1年経って下記のような事もありました。。
weblogic.xmlのprefer-web-inf-classesをtrueにしたらハマった・・