출처 :
http://bleujin.tistory.com/153framework_src.zip의 com.bleujin.framework.db.sample에 기본 적인 예제가 있으며 특히 com.bleujin.framework.db.sample에 가장 기초적인 예제가 있다.
해당 예제들을 실행하기 위해서는 sample_mysql.sql(mssql.sql, oracle.sql 역시 모두 같은 역할을 한다.) 의 예제 테이블 등의 내용을 실행시킨다 . 모든 테스트를 실행하기 위해서는 com.bleujin.framework.db.sample.SampleAllTest.java 를 실행한다.
주요 클래스
DBManager - 가장 먼저 생성해야 할 클래스로 대부분 JDBC URL과 userID, password를 생성자로 받는다.
DB별로 혹은
MySQLDBManger, MySQLPoolDBManager
MSSQLDBManger, MSSQLPoolDBManager
OracleDBManger, OracleDBManager, OracleCacheDBManger, Oracle9iCacheDBManger
H2EmbedDBMangaer, HSQLDBManager 등등
DB별, 접속형태별(Pool여부, Cache여부)로 여러개의 구현체를 가지고 있다.
ex)
DBManager dbm = new MySQLPoolDBManager("jdbc:mysql://novision/test", "bleu", "redf") ;
IDBController - DBManger를 생성자로 받는 IQueryable Impl의 Factory 역할을 맡는다.
- 현재는 IDBController의 구현체로 DBController 한개만 있다.
- initSelf() 메소드로 dbm의 Pool 초기화 등의 작업을 실행하고 destroySelf() 메소드로 DBManager Pool을 해제시킨다. initSelf()는 생성후 꼭 실행시켜 주어야 한다. 보통의 경우 initSelf()는 프로그램 시작시에 한번, destorySelf()는 프로그램 종료시에 한번 실행한다. Static 변수로 참조하여 사용하는 경우가 일반적이지만 꼭 그럴필요는 없다.
ex)
DBController dc = new DBController(dbm) ;
dc.initSelf() ;
....
dc.destroySelf() ;
- DBManger와 IDBController를 생성시키고 initSelf()를 실행했다면 이제 쿼리를 실행시킬 준비는 모두 완료되었다.
가장 간단한 쿼리 실행은
Rows rows = dc.execQuery(String select) ;
int result = dc.execUpdate(String mdl) ; 이다.
IQueryable - 인터페이스이고 execQuery()와 execUpdate()의 주 메소드 2개를 가지고 있다. 모든 실행할수 있는 쿼리는 IQueryable를 상속받는다. 가장 많이 알려진 구현체로 UserCommand, UserCommandBatch, UserProcedure, UserProcedureBatch, UserPorcedures 등이 있다.
IUserCommand - 굳이 비슷한 걸 찾자면 JDBC의 PreparedStatement의 역할과 비슷하며 사용방법도 비슷하다.
ex)
IUserCommand cmd = dc.createUserCommand("select * ... where a = ? and b = ?") ;
cmd.addParam(1).addParam("abc") ;
// cmd.addParam(0, 1).addParam(1, "abc") ; // 위구문과 의미가 같다. param index는 0부터 시작한다.
// 혹은 아래와 같이 같은 변수를 여러개 사용하거나 편이성을 위해 named parameter를 사용할수 있다.
IUserCommand cmd = dc.createUserCommand("select * ... where a = :a and b = :b and c >= :a") ;
cmd.addParam("a", 1).addParam(b, "abc") ;
// 만약 특별한 형을 지정하고 싶다면. cmd.addParam("a", 1, Types.LONG) ; 와 같이 명시적으로 지정한다.
- IUserCommand 실행
Rows rows = cmd.execQuery() ; // select
int result = cmd.execUpdate() ; // mdl
- 실행계획 보기
//실행계획의 해당 DB의 기능을 이용하기 때문에 Format은 DB마다 다른데, MySQL은 표 형태로, MSSQL은 XML Graph형태로, Oracle은 문자열로 보여준다.
cmd.viewPlan(OutputSteam output) ;
- Page 설정
// 쿼리를 실행하기 전에 Page를 설정하여 해당 Page의 결과값만을 가져올수 있다.
cmd.setPage(Page.create(10, 2)) ; // 10개씩 했을때 2페이지 즉 11-20번째 row를 가져온다.
cmd.execQuery() ;
이전의 cursor 관련글에서도 확인하였듯이 가장 좋은 방법은 원하는 집함만을 억세스 해서 원하는 집합형태로 가져오는 쿼리를 사용하는 것이 Page 설정에 있어서의 가장 좋은 답이다. 그렇게 생각했기 때문에 예전의 버전에는 Page 관련 기능을 넣지 않았다. 다만 그것은 MySQL의 limit나 오라클의 rownum의 단순한 사용방법보다는 훨씬 더 고난이도이기 때문에 DB별로 자유자재로 할 수 있는 사람이 극히 드물고 또한 매우 번거롭다. 또한 그 효과를 볼수 있는 곳이 제한적이기 때문에 번거로움을 무시하고 모든 곳에 그와 같은 쿼리 방식을 사용하는것은 불필요하다는 아니지만 비효율적이다라는 생각이 들었다.
Rows - 쿼리를 실행할수 있는 Interface인 IQueryable의 하위 구현체는 IUserCommand, IUserProcedure, IUserCommmandBatch, IUserProcedureBatch, UserProceudres, TimeoutQuery ... 등등 아주 많은데 그중의 모든 Select 문의 결과값으로 ResultSet의 인터페이스를 구현한 Rows를 반환한다. ResultSet Interface를 구현하였기 때문에 기존의 메소드를 거의 모두 지원하며 몇가지 추가 유틸리티 메소드를 제공한다.
- 기존의 일반 JDBC에서 반환하는 ResultSet의 구현체와는 다르게 Rows는 Value Object 기반의 클라이언트 커서를 사용한다. 따라서 쿼리의 실행후 pstmt.close(), rs.close(), freeConnection()의 과정은 전부 내부에서 이루어지고 호출자에게 노출되지 않는다. 일단 IDBControler가 초기화된후 dc.getRows() 혹은 cmd.execQuery() 실행후 사용자는 Resource의 반환등에 전혀 신경쓸 필요 없으며 내부적으로 상황에 맞는 결과 캐쉬를 통해 실행속도를 보장한다. rows는 인터페이스 규약상 close()가 있지만 close()를 하지않아도 단순 ValueObject이므로 가비지 컬렉터에 의해 자동으로 정리가 된다.
- Rows는 거의 순수하게 Value 객체이기 때문에 Framework에서 자동으로 반환한 Resource와 상관없이 Clob 억세스와 쿼리 재실행 등이 가능하며 Serialized XML 형태로 언제든 변환이 가능하다.
rows.getNextPage() ; // 다음 페이지의 rows를 가져온다.
rows.getPrePage() ;; // 이전 페이지의 rows를 가져온다.
- Clob Access
JDBC에는 Clob과 Blob의 표준 datetype이 있지만 DB마다 지원여부와 사용방법이 조금씩 다르다. 또한 그 사용방법이 대부분 번거롭게 때문에 varchar의 글자수 제한으로 어쩔수 없이 Clob을 사용해야 하는 경우 매우 불편하다. 그래서 Datatype이 Clob이더라도 DB에 따라 자동 형변환을 통해 rows.getString()을 통해 Access한다. (Blob은 getBinaryStream()) 만약에 Clob에 들어가는 데이타가 100M가 넘는다면 이 방법을 사용하는 것을 신중히 생각해야 할지도 모른다. 그러나 텍스트 1M를 넣는 경우도 매우 흔치 않을뿐더러 그경우에도 Blob으로 다루는 방법을 선택하는게 좋다.
# insert 혹은 update시 parameter는 DB 벤더에 상관없이 setClob(String str), setBlob(InputStream input) 메소드를 사용하면 된다.
IUserCommandBatch - 특정 테이블에 10건의 insert를 해야 한다면 10개의 UserCommand를 실행시키는 것보다 10개의 Arrary를 parameter에 set한후 실행시키는 Batch를 사용하는게 좋다. 평균적으로 최소한 Batch는 건당 insert 속도가 최소 1ms 이하를 보장하며 이는 UserCommand의 기준 속도인 50ms보다 약 50배가 더 빠르다.
ex)
IUserCommandBatch cmd = dc.createUserCommandBatch("insert into update_sample values(?, ?)") ;
int max = 1000 ;
for (int i = 0; i < max; i++) {
cmd.addBatchParam(0, i) ;
cmd.addBatchParam(1, i + "th ..") ;
}
int count = cmd.execUpdate() ;
와 같이 설정하거나..
int[] a = new int[max];
String[] b = new String[max] ;
for (int i = 0; i < max; i++) {
a[i] = i ; b[i] = i + "th .." ;
}
cmd.addParam(a).addParam(b) ;
int count = cmd.execUpdate() ;
등과 같이 직접 Array를 인자로 설정한다.
- 하나의 Batch문에 에 가장 효율적인 건수는 JVM에서 관리되는 Array가 차지하는 메모리에도 밀접한 관련이 있긴 하지만 대략 10,000-100,000 사이가 가장 좋다. row의 평균사이즈를 150-250 byte로 했을때 대부분은 만건당 최대 10초이내의 효과를 보인다.
IUserProcedure - 가장 많은 오해를 받지만 가장 효율적이며 많이 사용되는 객체이다. IUserProcedure는 오라클이나 MSSQL의 Procedure와는 직접적인 관계는 없다. 프로그램에 직접 ANSI SQL를 사용했을 경우 이후 DB에 변동이 있다면 재앙수준의 변경을 요구한다. 특히 SQL이 String 변수로 관리되기 때문에 직접 모든 프로그램을 열어서 수정해야 한다. 컴파일 오류가 아닌 런타임 오류가 나기 때문에 수정에 대한 확신도 매우 어렵다.
이러한 문제로 몇년전부터는 IBatis등에서 XML 형태로 Key Value형태로 관리한후 Key 형태로 Access 하는 방식이 좀더 많이 퍼지게 되었다. 비슷하게 UserProcedure는 앞에서 말한 DB 벤더에 디펜던트한 Procedure와는 직접적 상관없이 그냥 IDName을 가진 Message 객체에 불과하다. 그 Message를 어떻게 해석할 것인가는 DB Manager에 달려 있으며
현재 Framework에 포함되어 있는 OracleDBManger는 패키지와 프로시저, MSSQL은 Procededure로, MySQL은 Procedure 혹은 Function으로 H2DBManager는 특정 XML 파일의 Key-Value 형식의 SQL name으로 해석하고 있다. 즉 UserProcedure는 DB Maanger를 어떻게 구현하는가에 따라 해석방법이 정해지며 일단 같은 name을 가진다면 DB 벤더의 프로시저 혹은 패키지 지원여부와 상관없이 동일한 결과값을 보장해야 한다.
ex)
IUserProcedure upt = dc.createUserProcedure("Sample@selectBy(:a)") ;
upt.addParam("a", 1);
Rows rows = upt.execQuery() ;
만약 DBManager가 MySQLPoolDBManager라면 위 구문을 Sample_selectBy라는 procedure라고 해석하며
해당 MySQL에는 아래와 같은 Procedure가 있다면 해당 프로시저를 실행한후 그 결과 Rows를 반환한다.
CREATE PROCEDURE sample_selectBy(v_a int)
BEGIN
SELECT * FROM update_sample WHERE a > v_a ;
END//
만약 DBManger가 오라클이라면 아래와 같이 Sample Package의 selectBy function을 실행시킨후 결과셋을 반환한다.
CREATE OR REPLACE PACKAGE BODY Sample
is
function selectBy (v_a number) return Types.cursorType
is
rtn_cursor Types.cursorType ;
begin
open rtn_cursor For
select * from update_sample where a > v_a ;
return rtn_cursor ;
end ;
................
End Sample ;
- 이와 같이 DB가 Procedure를 지원하는가 혹은 지원하지 않는가와 상관없이 IUserProcedure는 쓰이며 그 해석방법은 DBManager에게 달려있다. IUserCommand보다 IUserProcedure의 사용을 더 권장하는 이유는 DB 벤더의 장점을 최대한 활용할 수 있고 개발과 유지보수에 있어서의 장점때문이다. 물론 기타 성능이나 보안등의 자잘한 이유는 제껴두고서라도 말이다.
IUserProcedureBatch - IUserCommandBatch 처럼 UserProcedure의 Batch 버전이다. 다만 Proceudre 구조상 Batch라서 해서 UserProcedure와 비교해 커다란 성능차이는 없다. 다만 하나의 Procedure에 여러개의 SQL문을 담을 수 있다는 걸 생각하면 복잡한 배치처리 작업에나 쓸만하다.
UserProcedures - s가 뒤에 하나 붙어 있다. UserProcedure는 자신이 IQueryable의 구현체이면서 IQueryable의 구현체를 담을수 있는 composite 형태로 되어 있다. UserProcedures에 담긴 모든 MDL문은 자동으로 하나의 Transaction으로 처리가 된다. 그리고 물론 각각의 개별적인 실행보다는 조금 더 빠르다.
ex)
IUserCommand cmd1 = dc.createUserCommand("insert into update_sample values(?, ?)") ;
cmd1.addParam(1).addParam("abc") ;
IUserCommand cmd2 = dc.createUserCommand("delete from update_sample where a = ?") ;
cmd2.addParam(1);
UserProcedures upts = dc.createUserProcedures("Multi MDL") ;
upts.add(cmd1).add(cmd2) ;
int result = upts.execUpdate() ;
assertEquals(2, result) ;
- 만약 UserProcedures에 여러개의 Select Query를 집어넣고 execQuery()를 실행하면 어떻게 될까? 이 경우에 여러개의 결과값을 반환하게 되는데 Rows.getNextRows()를 통해 다음 결과셋을 얻을수 있다. UserProcedures에 담긴 IQueryable 객체는 add된 순서대로 실행된다.
ex)
IUserCommand cmd1 = dc.createUserCommand("select * from copy_sample") ;
cmd1.setPage(Page.create(10, 1)) ;
IUserCommand cmd2 = dc.createUserCommand("select 3 from dept_sample") ;
UserProcedures upts = dc.createUserProcedures("Multi Query") ;
upts.add(cmd1).add(cmd2) ;
Rows first = upts.execQuery() ; // first query result ;
assertEquals(true, first.getRowCount() == 10) ;
Rows second = first.getNextRows() ; // second query result ;
assertEquals(3, second.firstRow().getInt(1)) ;
XAUserProcedure - 앞의 UserProcedures는 하나의 DB에 대한 Transaction 보장이며 이기종 DB 즉 멀티 DB에 대한 멀티 MDL문의 분산 트랜잭션은 XAUserProcedure를 통해 할수 있다.
MSSQL의 접속관리자인 mdc와 오라클의 접속관리자인 odc가 아래와 같이 이미 생성되어 있다고 하자.
// prepare
MSSQLDBManager mManager = new MSSQLDBManager("jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=test", "bleu", "redf");
DBController mdc = new DBController(mManager) ;
mdc.initSelf() ;
OracleDBManager oManager = new OracleDBManager("jdbc:oracle:thin:@novision:1521:al", "al", "redf") ;
DBController odc = new DBController(oManager) ;
odc.initSelf() ;
// tx1 : MSSQL용 쿼리 집합을 만든다.
TxTransaction tx1 = new MSSQL2000TxTransaction(mManager, "mquery") ;
tx1.add((Queryable)mdc.createUserCommand("insert into copy_tblc values('111', 111)")) ;
// tx2 : oracle용 쿼리 집합을 만든다.
TxTransaction tx2 = new Oracle9iTxTransaction(oManager, "oquery") ;
tx2.add((Queryable)odc.createUserCommand("insert into copy_tblc values('111', 111)")) ;
// add tx : 두개의 쿼리 집합을 XAUserProcedure에 넣는다.
TxTransaction[] txs = new TxTransaction[]{tx1, tx2} ;
XAUserProcedure upts = new XAUserProcedure(txs) ;
// 실행한다.
upts.execUpdate() ;
// clear : 더이상 사용되지 않을 DB 접속정보라면 정리한다.
mdc.destroySelf() ;
odc.destroySelf() ;
- 보통의 XaTransaction 보다 훨씬 더 간편하게 실행할 수 있다. 결국 인터페이스의 간결화로 호출자는 자세한 XaTransaction 과정에 대해 알 필요가 없다.
MSSQL에서 XaTransaction을 사용하기 위해서는
* Be sure that you have copied your sqljdbc.dll file from C:\Program Files\Microsoft SQL Server 2000 Driver for JDBC\SQLServer JTA\ to your SQL Server's "binn" directory (most likely C:\Program Files\Microsoft SQL Server\MSSQL\Binn).
* Then open your instjdbc.sql script from C:\Program Files\Microsoft SQL Server 2000 Driver for JDBC\SQLServerJTA\ within Query Analyzer and run the script. This will install the extended stored procedures into SQL Server so that it can reference the sqljdbc.dll file.
의 과정을 실행해야 하며 MS DTC가 기동중이어야 한다. MySQL은 얼마전까지 XaTransaction을 지원하지 않았고 현재도 InnoDB storage engine에서만 지원하는 걸로 알고 있다.
CombinedQuery - CombinedQuery는 UserProcedure와 조금 비슷한데 단지 UserProcedures와는 달리 MDL문과 Query문을 동시에 실행시킬수 있다는 점이 다르다.
ex)
IUserCommand ins = dc.createUserCommand("insert into update_sample values(?,?)") ;
ins.addParam(1).addParam("abc") ;
IUserCommand sel = dc.createUserCommand("select * from update_sample") ;
CombinedUserProcedures upts = dc.createCombinedUserProcedures("combined") ;
upts.add(ins, "ins", IQueryable.UPDATE_COMMAND).add(sel, "sel", IQueryable.QUERY_COMMAND) ;
// 타입이 다른 ins문과 sel문을 동시에 실행시킨다.
upts.execUpdate() ;
Map result = upts.getResultMap() ; // for access sel'result
int rowcount = (Integer)result.get("ins") ;
Rows rows = (Rows)result.get("sel") ;
assertEquals(1, rowcount) ;
assertEquals("abc", rows.firstRow().getString("b")) ;
- 역시 하나의 Transaction으로 처리된다. 주로 마이그레이션 작업이나 session scope temporary table을 가지고 작업할때 쓰인다. 혹은 Session Level Configuration 명령문과 select문을 혼합할때 쓰이기도 한다. 즉 특정경우에 Set Ansi_null Off 시키고자 할때 session 명령문은 Update로 Select문은 Query로 실행시킨다. 앞어 거듭 말했듯 Rows는 클라이언트 커서이기 때문에 한건을 insert 하고 해당 건을 select하고 해당 건을 지우는 3개의 명령문을 combinedProcedure에 넣고 실행시켜도 제대로 된 Rows 결과물을 볼 수 있다.
ExtraServant - ExtraServant는 IQueryable의 구현 클래스가 아니며 이름 그대로 IDBController의 하인 역할를 수행한다. 예컨데 모든 쿼리의 실행시간을 System.out으로 확인하고 싶다고 하자.
ex)
dc.addServant(new StdOutServant(StdOutServant.All)) ;
를 실행시켜 주면 해당 dc로부터 생성된 모든 IQueryable 객체는 실행후 StdOutServant를 통해 쿼리 이름과 실행시간을 System.out에 출력한다. 그와 동시에 300ms 이상의 모든 쿼리는 따로 기록해 두고 싶다고 한다면.
ex)
dc.addServant(new TraceOfLateProcServant(300)) ;
를 추가시키면 된다.
dc는 1개 이상의 ExtraServant를 가지며 모든 쿼리는 해당 쿼리 작업을 완료후 Chain된 ExtranServant를 통해 임의의 추가 행동을 지정할 수 있다. ExtraServant는 별도의 Thread로 관리되며 ExtraServant의 동작시간과 행동은 기존의 작업에 영향을 주지 않는다.
별도의 IDBController를 새로 만들어서 Connection만 공유한채 Servant 정보는 달리 유지시킬수 있다.
ex)
DBController newDc = new DBController("newDC", dc.getDBManager()) ;
newDc.addServant(new StdOutServant(StdOutServant.All)) ; // 모든 IQueryable를 화면에 프린트하는 Servant를 추가한다.
newDc.addServant(new StdOutServant(StdOutServant.All)) ; // 한개 더 추가한다.
newDc.initSelf() ;
newDc.getRows("select 1 from copy_sample") ;
newDc.destroySelf() ;
위의 경우 newDC는 기존 dc.getDBManger의 Owner가 아니기 때문에 destroySelf()를 실행시켜도 해당 DBManger의 Pool은 파괴되지 않는다.
- ExtraServant는 다양한 상황에 새롭게 상속받아 구현함으로써 다양한 역할을 수행할 수 있다. 이를테면 특정 사용자가 접속했을때 MSN으로 메시지를 보내주는 Servant를 구현할 수도 있고 특정 쿼리들이 실행되었을때 Log를 남기는 Servant를 구현할수도 있다.
Handler - Rows는 여러종류의 Handler를 사용하여 다양한 타입으로 변신이 가능하다.
Rows rows = dc.getRows("select * from copy_sample order by no1", 10, 1) ;
첫번째 row를 Map 형태로 바꿀 수도 있고
ex)
Map results = (Map)rows.toHandle(new MapHandler()) ;
assertEquals(2, results.size()) ; // column 수 확인
모든 row를 Map의 List 형태로 바꿀 수도 있고.
List<Map> results = (List<Map>)rows.toHandle(new MapListHandler()) ;
assertEquals(10, results.size()) ; // row수 확인
모든 row를 Bean List 형태로 바꿀 수도 있고
List<TestBean> results = (List<TestBean>)rows.toHandle(new BeanListHandler(TestBean.class)) ;
TestBean row = results.get(0);
assertEquals(1, row.getNo1()) ;
assertEquals("01", row.getNo2()) ;
assertEquals(2, results.get(1).getNo1()) ;
assertEquals("02", results.get(1).getNo2()) ;
특정 컬럼의 값만을 얻어올수도 있다.
Object value = rows.toHandle(new ScalarHandler(2)) ; // 첫번째 row의 2번째 컬럼의 값
assertEquals("1", value.toString()) ;
6년전에 해당 Framework를 처음 개발했을때 목적은 4가지이다.
1. 풀링을 구현하되 프레임워크 사용자가 실수로라도 Connectionless를 만들 수 있는 방법을 원천적으로 차단한다.
-> Connectio은 외부에서 보이지 않으며 Client Cursor 사용으로 결과셋 전달후 Resource는 자동으로 바로 반환한다. 즉 사용자는 JDBC의 Resource에 대해 알 필요도 없으며 알 수도 없다.
2. 프로그램에 SQL문을 심지 않는다.
-> UserProcedure의 적극적인 사용으로 Query문은 모두 중앙집중 관리 되며 이는 개별적인 DB 벤더의 장점은 최대한 활용함과 동시에 DB 벤더에 종속되지 않는다는 상이한 목표를 동시에 추구한다. IQueryable의 모든 구현체는 단순히 Message역할만을 수행하기 때문에 JDBC의 PreparedStatement와 달리 정리해줘야 할 Resource가 아니다.
3. LOB 핸들링을 쉽게 한다.
-> 벤더마다 그리고 버전마다 Lob Handling에 있어서의 사용방법이 다른걸 간단한 인터페이스로 통합시고 사용자는 byte 단위의 Access Handling을 하지 않아도 되게 한다.
4. 사용방법은 쉬워야 함과 동시에 여러가시 발생할 수 있는 상황에 쉽게 적응 가능해야 한다. 동시에 불필요한 퍼포먼스 감소를 일으켜서는 안된다.
이후.. 한동안 추가 기능이 없다가 최근에
- MySQL을 기존의 MSSQL, Oracle과 더블어 기본 테스트 항목및 지원DB로 함.
- 인터페이스의 소폭 수정
- viewPlan(Query의 실행계획보기)
- Paging (쿼리의 Page 설정, nextPage, prePage Access)
- NamedParameter
등등의 잡다한 기능을 넣었다. 가장 큰 이유는 Paging 때문인데 JDBC에 Client cursor에 Page 관련 메소드가 있는데 제대로 implement한 JDBC가 거의 없어서 그냥 만들었다. -ㅅ-. Page는 DB 마다 처리 방법이 다르지만 해당 DB에 가장 효율적인 방법을 사용하려고 했다.
사용자는 framework_core_fat.jar를 추가하고 해당 DB의 JDBC jar만 추가시켜서 사용하면 된다.
아래와 같은 Apache Common Jar를 사용한다.
<fatjar.jarsource file="lib\jericho-html-2.5.jar" relpath=""/> // 얘는 parser framework에 쓰임
<fatjar.jarsource file="lib\jmock-core-1.2.0.jar" relpath=""/>
<fatjar.jarsource file="lib\junit.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-pool-1.1.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-beanutils-1.8.0.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-collections-3.1.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-dbcp-1.1.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-digester.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-fileupload-1.1.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-io-1.2.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-lang-2.3.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-logging-1.0.4.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-net-1.3.0.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-validator-1.1.4.jar" relpath=""/>