04 3月 2013

buildnumber-maven-plugin

專案開始進入測試階段時,每次提供給測試人員或客戶測試時,若有問題要回報時,我比較希望除了問題的描述之外,還能有目前測試的版本版次的資訊,因為用不同的版本,不一定能測出相同的問題。
當然每次release時加上tag是個好習慣,但在專案後半期會大量進行build->testing的循環,每次build都要加上tag可能不是很容易。
所以我們簡單一點想,如果在build完產生的war或jar這類的檔案,能自行包含這些資訊是不是比較好些?

buildnumber-maven-plugin提供了三個變數讓我們取得這些資訊

  • buildNumber : repository的版本,SVN就是目前的版本,像是22013,而Git就是Head的revision.
  • scmBranch : repository的Branch名稱,SVN的值會像是brunch/developer,而Git則不支援,會顯示UNKNOWN
  • timestamp : 此次Build的時間

我們可以在pom.xml中利用上列三個變數,加諸在檔名或是MANIFEST.MF中,以便識別這個檔案的相關版本資料。
再來就簡單示範如何使用這個plugin

首先一定要加入scm的設定,讓plugin辨別是要怎麼讀取Buildnumber與Branch.

<scm>
	<connection>scm:git:git://github.com/ElliotChen/buildDemo.git</connection>
</scm>

第二步就是加入buildnumber-maven-plugin,並要求在validate這個phase時執行create這個command,也就是在validate這個phase之後,我們就可以使用上列三個變數了。

<plugin>
	<groupId>org.codehaus.mojo</groupId>
	<artifactId>buildnumber-maven-plugin</artifactId>
	<version>1.2</version>
	<executions>
		<execution>
			<phase>validate</phase>
			<goals>
				<goal>create</goal>
			</goals>
		</execution>
	</executions>
</plugin>


再來可以在MANIFEST.MF中加入相關的資訊,下面是在war檔案裡加入相關資訊的方法。

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-war-plugin</artifactId>
	<version>2.3</version>
	<configuration>
		<archive>
			<manifestEntries>
				<buildNumber>${buildNumber}</buildNumber>
				<branch>${scmBranch}</branch>
				<buildTime>${timestamp}</buildTime>
			</manifestEntries>
		</archive>
	</configuration>
</plugin>

這樣輸出的MANIFEST.MF中就會有包含有下列的資訊

buildTime: 1362382356069
branch: UNKNOWN
buildNumber: 7b9aa85d

這樣可能還不夠,因為包在war檔裡沒人看得見,所以系統裡應該有個簡單的讀取MANIFEST.MF的功能,然後以JSP,或是其他方式做為顯示的媒介。
提供一個簡單的Servlet給大家參考

public class SystemInfoServlet extends HttpServlet {
	protected void doPost(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		HashMap<Object, Object> map = new HashMap<Object, Object>();
		ServletContext application = getServletConfig().getServletContext();

		Manifest manifest = new Manifest(application.getResourceAsStream("/META-INF/MANIFEST.MF"));
		Attributes attr = manifest.getMainAttributes();
		for (Object key : attr.keySet()) {
			map.put(key, attr.get(key));
		}

		request.setAttribute("infoMap", map);
		request.getRequestDispatcher("/SystemInfo.jsp").forward(request,response);
	}
}

JSP裡只要用下列的程式就能呈現MANIFEST.MF資料

<body>
	<c:forEach items="${infoMap}" var="info">
		${info.key} : ${info.value} <br/>
	</c:forEach>
</body>

另外,若是在Eclipse中有安裝Maven Plugin,那它會在每次pom.xml修改時自動產生MANIFEST.MF,但是因為沒有執行validate的關係,所以上列三項參數不會正常顯示在MANIFEST.MF裡;但是在Build完成時是會正確崁入war檔裡的,所以如果看到Eclipse Project中的MANIFEST.MF與你想像不同時,別擔心,這是正常的。

21 1月 2013

那些我被放生的專案

由於最近被問了幾次,我不確定大家聽到什麼,所以整理一下先前的情形囉。

先請看一看我2012年六月的行事曆

Calendar
再看看我五月底到六月底兩個專案git的歷史資料

Pa

Pb

不清楚我要表達什麼?這很正常,讓我稍微整理一下
1.三義Yulon,Zyxel, MXIC這三個公司地點都不在台北,也分別代表三個案子
2.我Git的commit log自5月24日起到6月22日,只有5月26日,5月30日,6月1日這三天沒有Commit log,其他都都有喔,而且這三天裡,只有5月26日是假日,其他都還是工作日
3.再看看commit的時間,在晚上八點之後到隔日八點之間的至少有15天,凌晨2~4的日子也不少
這代表我在這個月內,獨自面對兩個看來是高強度的遠距離案子,當然強度如何可能要看一下Commit的程式多寡與難度,所以只好說〞看來是〞。

那這些又能代表什麼?嗯,當然只能看下去囉
我先說明這三個案子
1.三義是個維護案,就是原來系統的功能再修修改改,量不多,壓力也不大
2.Zyxel則是某個廠商案子的第二階段,這案子自3月開始接觸,預計開發時程為4、5兩個月 。以下簡稱A案。
3.MXIC則是個導入案,我們只負責其中資料交換的部份。類似ETL,但沒有邏輯的部份。4月中談下來的,預計時間大約是5月~7月初要結束。以下簡稱B案。

再來我用時間的順序來看一下狀況
三月:
A案自3月起開始與客戶討論,初步大約是九個功能,中間廠商會做SA與PM的角色,討論時僅能說個大概,詳細部份要等4月份才能提供;所以我評估這個案子可能需要2個人,時間2個月應該風險不高,最後的報價大概支付2個月帳面薪水還能有一半的收益。
不過因為預計要有兩個人,所以我當然會去要人,我希望不要是新手,而且至少在4月前能有1~2個星期先磨合,不要到了專案開始才給我人,這樣風險會很高,我得到的回答是要人沒有問題,到時候一定找得到人,不過再提了2次後,我想應該不會有人了,所以我只好提,這個案子開始後,我應該沒有時間去做別的東西,面試或其他的工做大概都幫不上忙。
四月:
3號左右收到了文件,稍微放下了心,功能的要求比我想像中要來得容易,很多我認為要做後台管理的東西都可以免了,我跟廠商的PM談了一下各階段的東西,預計是2星期左右做Prototype,再來就是開發的階段,中間客戶提了項技術性的要求,我必需去改某OpenSource的Source Code加入AJAX的支援以便配合我慣用的頁面模式,多要求了3天做Prototype,不過最後還是在2星期內提給客戶一個全部頁面都能點擊操作的系統,只是內容資料都是假的;
再來後的的就比較輕鬆,當時認為如果客戶該提供的東西都沒有Delay的話,還可能會提早1~2個星期完成。所以當提出要去談另一個案子,我覺得還可以,由其客戶預計是五月才開始,七月初要上線,那依規劃的情形下,我六月份可以有很多的時間來處理,應該完全不用擔心。
五月:
中間持續有些討論,廠商的權限設計很有趣....,不過在5月18日我也把東西給A案廠商測,測試大約一個星期左右也都符合他們內部份的規格後,我開始在看B案要怎麼做比較好。
我當時認為,A案廠商都驗收過了,再來應該不會有什麼大問題.....
六月:
A案廠商交給客戶看了,提了很多問題,不過裡面有3/4我認為那是規格上沒有註明也沒有說明的部份,這情形當然要算CR,廠商的PM也承認很多不是我們的問題;第一個假日,我將他們提的東西改完後分為2部份,一部份是我該修的,一部份是我不該修的,各自Package一個版本,交給公司內部的人去運用;另一方面,B案的PM也與我討論什麼時間要到客戶那,初步訂了6月18日要到客戶那進行我那部份的Demo;


A案的客戶在修正之後,又持續提了很多東西,幾乎佔用了我大多的時間,特別又一直要我待在新竹,但如果在新竹,我B案無法做任何事,再來後面提的東西基本都是CR,我不認為我們需要為這些負責,CR本來就該另估時程並計價,在沒完成計價估時的情形下,開發人員不該擅自處理。


不過只要我沒去新竹,A案廠商就會一直打電話來,說我一定要去新竹不然客戶不高興等等,我堅持的是如果是我們的BUG,那當然我們負責到底,但是後面提的都不在規格裡,為什麼我們一定要配合他們給客戶看,當然沒提的是,我還在忙另一個案子。

中間雖然公司的人被電話煩到一度曾氣說案子不做了,但最後還是讓執行的人討論,所以我還是做了下去。


就這樣撐著撐到6月12日,當時給的9個功能在客戶那也都過了,不過另一個晴天霹靂出現,A案的客戶提出兩個全新的功能,一個是較複雜的查詢,另一個是要與舊系統整合,13號我到新竹看了實際的需求,初步的評估可能需要4~6個工作日。
這下慘了,我很需要後面幾個完整的工作日來進行我B案的部份,要不B案會沒辦法如期交付,而且還訂好時間要到客戶那....

6月14日,我上午先到三義瞭解客戶的需求,下午我還是進台北辦公室跟公司的人談A案與B案的事情,我認為A案是CR,而不知道還有多少洞要補,AB兩個案子要救應該要先救B案,至少A案在當時給的東西是完全不在當時簽約的規格內。
不過公司的人接電話接到煩了,A案廠商會晚上很晚還打電話罵為什麼我不去新竹,所以看來公司的人看來是不知道怎麼擋了,也不說話了,
〞那公司有沒有其他人可以先幫助B案?〞,我想有人先頂一下吧?不過沒有回應。
我最後想想,〞那是不是幫忙打電話給B案的客戶,說明因其他專案的問題所以可能延後交付?〞
公司的人還是沈默沒回應,我笑了笑,〞算了,我自己處理〞,我很明白,我完全被放生了。

所以接下來就一連串的加班工作,我把B案完全放下,利用假日加上三個左右的工作天完全在加班趕A案的東西,還好B案他們自己延到20號,不過那天是颱風天,所以又延到22號,我在沒有出天窗的情形下過關了。。。。

總結一下
1。我在專案裡要不到人,急的時候完全沒有支援。
2。CR的部份,公司的人完全不會處理,我不只一次說,不在規格內的東西,時程應該是可以討論的,但這是我最失敗的地方,應該所有東西都要抓回來讓我自己談,我把公司的人想得太好了。

對了,我完全沒有加班費,開車去新竹也沒有交通補助,大家說我是不是佛心來的呢?

24 12月 2012

JSDT jQuery - Eclipse Plugin

在Eclipse中要編寫jQuery的程式通常要另開Browser以便查詢API,之前有jqueryWTP可以協助editor裡的autocomplete,但久未更新,換了新版的Eclipse也就無法使用了。

今天終於又看到了一個新的Plugin,而且還支援jQuery 1.8!!所以記錄一下,主要是安裝完後的設定不是那麼直覺。

JSDT jQuery,請參考http://code.google.com/a/eclipselabs.org/p/jsdt-jquery/

第一步,在Eclipse Marketplace中以jquery尋找plugin,就可以找到JSDT jQuery 

Jquery01

第二步,安裝後選取要使用的project的properties,在JavaScript -> Include Path裡按下 "Add JavaScript Library" 

Jquery02

第三步,選取jQuery Library,這選項是Plugin加上去的

Jquery03

第四步,就是選取jQuery的版本,最新的1.8已經有支援了!

Jquery04

 

接下來在Properties裡就能看到多了一個jQuery的Library

Jquery05

 

無論是在JSP或是JS檔案裡,都可以輕鬆看到jQuery的API囉!

Jquery06

26 7月 2012

Query By Example In Spring-Data-JPA

Query By Example (QBE) 是個常用的查詢模式,Hibernate有Example Query來實踐,但JPA沒有,這對我常用的開發模式而言是個不小的困擾,所以基本上我都把JPA放在一邊,直接用Hibernate。
其實QBE的概念並不難實現,只要分析傳入的Domain Object,哪些property有值,加入查詢條件即可, 趁著覆習Spring Data的同時,就做個簡單的實作好了。

先建個ExpressionParam來儲存分析Domain Object Class後的結果,readMethod就是用來取值,而attribue則是用來取得Criteria的Path

public class ExpressionParam<T> {
	private String name;
	private Method readMethod;
	private SingularAttribute<T, ?> attribute;
	
	public ExpressionParam(String name, Method readMethod, SingularAttribute<T, ?> attribute) {
		super();
		this.name = name;
		this.readMethod = readMethod;
		this.attribute = attribute;
	}
	.....
}

再來就是配合Spring-Data的Specification Query,實作一個ExampleSpecification,其中作個簡單的Cache機制,以免同樣的Domain Object Class要一直重覆分析有哪些ReadMethod。

public class ExampleSpecification<T> implements Specification<T> {
	private static final Logger logger = LoggerFactory.getLogger(ExampleSpecification.class);
	protected static final Map<Class<?>, List<ExpressionParam<?>>> classCache = Collections.synchronizedMap(new WeakHashMap<Class<?>, List<ExpressionParam<?>>>());
	
	EntityManager entityManager;
	T example;
	
	public ExampleSpecification(final EntityManager entityManager, final T example) {
		this.entityManager = entityManager;
		this.example = example;
	}
	
	@Override
	public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
			CriteriaBuilder cb) {
		List<Predicate> predicates = new ArrayList<Predicate>();
		
		EntityType<T> entity = entityManager.getMetamodel().entity((Class<T>)example.getClass());
		List<ExpressionParam<?>> params = parseReadMethod(entity);
		for (ExpressionParam<?> param : params) {
			try {
				Object value = param.getReadMethod().invoke(example);
				if (null != value && StringUtils.isNotEmpty(value.toString())) {
					predicates.add(cb.equal(root.get((SingularAttribute<T, ?>)param.getAttribute()), value));
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return predicates.isEmpty()?cb.conjunction() : cb.and(predicates.toArray(new Predicate[predicates.size()]));
	}

	protected List<ExpressionParam<?>> parseReadMethod(EntityType<T> entityType) {
		Class<T> clazz = (Class<T>) entityType.getClass();
		if (classCache.containsKey(clazz)) {
			return classCache.get(clazz);
		}
		logger.info("First Parsing Read Method for Class[{}]", clazz);
		
		List<ExpressionParam<?>> methods = new ArrayList<ExpressionParam<?>>();
		classCache.put(clazz, methods);
		PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(example.getClass());
		
		Set<SingularAttribute<T, ?>> atts = entityType.getDeclaredSingularAttributes();
		for (SingularAttribute<T,?> sat : atts) {
			if (PersistentAttributeType.MANY_TO_ONE == sat.getPersistentAttributeType()
					|| PersistentAttributeType.ONE_TO_ONE == sat.getPersistentAttributeType()) {
				continue;
			}
			String name = sat.getName();
			Method readMethod = null;
			for (PropertyDescriptor pd : pds) {
				if (pd.getName().equals(name)) {
					readMethod = pd.getReadMethod();
					break;
				}
			}
			
			logger.debug("Property {} - Method {}", name, readMethod);
			if (null != readMethod) {
				methods.add(new ExpressionParam<T>(name, readMethod, sat));
			}
		}
		
		return methods;
	}
}

由於Spring Data的Repository產生機制不容易修改(其實是我還沒找到好的切入點....)所以只好在Service Layer來達到QBE的作用,可以參考下面UserService的做法。

@Service
@Transactional(readOnly=true)
public class UserService {
	@PersistenceContext
	private EntityManager entityManager;
	
	@Autowired
	private UserJpaDao userDao;
	
	public List findByExample(User example) {
		ExampleSpecification es = new ExampleSpecification(entityManager, example);
		return userDao.findAll(es);
	}
}

再來看一下Test的實際應用

public class UserJpaDaoTest {
	@Autowired
	private UserService userService;
	@Test
	public void test() {
		User example = new User();
		example.setName("Bob");
		
		this.userService.findByExample(example);
	}

}

順便提一下,不才小弟又要找新工作了,若有覺得適合小弟的可以聯絡交流一下。