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する事で対処としました。

VBSでFTP

あるWindowsサーバのログファイルを別サーバへ定期的にFTP送信したいとの事で、VBSで簡単なスクリプトを作りました。これまでは、バッチやスクリプトでFTP用のコマンドファイルを作り、FTP -S:ファイル名で実行するような感じだったのですが、最近は、スクリプトだけでできるようです。
下記例は、D:\log\tempというディレクトリに一つのzipファイルが置かれており、そのファイルを送信するという前提です。
ちなみにzipファイル自体の名前でstrSrcを指定するとCopyHereが勝手にZipの中身を解凍した状態で送信されます。。そういや、CopyHereでzipの圧縮や解凍もできるからそういう事なんでしょうか。。恐るべしCopyHere

'FTP送信指定
Const HostName = "192.168.1.101"
Const UserID = "userid"
Const UserPass = "password"
Const DestDir ="Upload"

Dim strDest,strSrc, UpFileName, UpFileSize, oSH, objDest

strSrc="D:\log\temp"

strDest = "ftp://" & UserID & ":" & UserPass & "@" & HostName & "/" & DestDir

Set oSH = CreateObject("Shell.Application")
Set objDest = oSH.NameSpace(strDest)

WScript.Echo "[" & strSrc & "]FTP転送対象"

For Each objItem In oSH.NameSpace(strSrc).Items
WScript.Echo "[" & strSrc & "]FTP転送ファイル [" & objItem.Name & "]," & objItem.Size
	UpFileName = objItem.Name
	UpFileSize = objItem.Size
    '&H04 + &H10は効かない・・・
	objDest.CopyHere objItem, &H04 + &H10
Next
'非同期なのでスリープで待つ・・・
Wscript.Sleep 15000

'リモートディレクトリの送信ファイル存在確認
Dim CheckOK, iWaitCnt
CheckOK = False
For iWaitCnt = 0 To 10
Set objDest = oSH.NameSpace(strDest)
    For Each objItems In objDest.Items
		If UpFileName = objItems.Name Then
    		'リモートファイルのサイズは取れないみたい・・・
       		WScript.Echo "[" & strSrc & "]FTP転送ファイル確認 [" & objItems.Name & "]," & objItems.Size
	   		CheckOK = True
	   		Exit For
		End If
    Next
	If CheckOK = True Then
		Exit For
	End If
    Wscript.Sleep 10000
    WScript.Echo "FTP転送待ち[" & iWaitCnt & "]" 
Next
If CheckOK = False Then
	WScript.Echo "FTP転送に失敗しました " & Err.Number
Else
	WScript.Echo "FTP転送正常終了"
End If

でも、このCopyHere、FTPでは非同期のようで送信終了タイミングが解らなくスリープ待ちせざるを得なかったり、上書き確認のダイアログがどうしても消せない等、ちょっとクセが強いです。。
今回の要件的にはまあ上書きの対処は見切りました。確実性が必要な処理だとこのやり方はしないかな~

要件定義書というシロモノについて

システムを作る際に、要件定義として何かしらドキュメントを書きますが様々なタイプがありますよね?
筆者が社内SEやってたときは、メイン読者は誰なのかを特に意識して書いてきました。それによってドキュメントの目的が違ってくるからです。それに読む人が理解できないものを書いてもしょうがないじゃないですか?
よく見かける「要件定義書」を成果物と設定し、ドキュメントそのものが目的物になっている場合って、何かピンとこないことが多いように思うんですが、ドキュメントって全ての読者向けに書いた、もしくはターゲットを特定しない場合は、ページ数が多くて読む気もならなかったり、ちょっとの誤字脱字を発見して読む気なくしたりして、誰にも伝わらない、もしくは各自の思いで解釈し後で痛い目にあうというなんて事も多々あるように思います。
という事でメイン読者を軸にすると主に4タイプに分類できるかなと思います。
1.経営者向け
 あまり書く機会は無かったけど、この場合の目的はビジネスがどう変わるか、予算がどのくらいかかりそうかが焦点かと。
 とにかく簡潔に少ないページ数でまとめる。図を多く使ってパワポで書く。
2.利用者(利用部門)向け
 この場合の目的は、業務がどう変わるか、システム化される範囲としない範囲の指定なんで、具体的な対象物の明記が必要。
 概要説明と解りやすそうな業務フロー(UML的に書いても理解されない可能性が高いです)
 また、外部設計を後でやるかやらないかにもよりますが、画面・帳票設計に近いもの付け加える。
3.開発者向け
 開発工数を見積もるという目的がメインと考えます。
 どういうレベルの工数見積もりを期待するかで、たくさん書くこともあれば、ちょっとの時もあり・・
 開発者の理解レベル次第では、2と同じにする事も多々あり・・
4.運用管理者向け
 この場合の目的は、どう構築して、その後維持管理していくかなんで上記とは別物ですね。いわゆる非機能要件に分類される内容がメインになると思われます。

現在の筆者のスタンスでは、自分では要件定義書的なものを書きません。
だって見積もりもしなければ、別の開発者に伝える必要もほとんどないし・・・
開発者という立場の場合、見積もりしない限り、手間が掛かるだけで必要性を見いだせないからです。
よく開発ベンダーに要件定義書を任せるというスタンスの所もあります。開発規模次第という面もありますが、出来ればしない方がいいですね。あれもこれもといろいろ盛られて、見積もり見てびっくりします。

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にしたらハマった・・