Google Code Prettify

顯示具有 Annotation 標籤的文章。 顯示所有文章
顯示具有 Annotation 標籤的文章。 顯示所有文章

2016年10月22日 星期六

JPA + Hibernate + Spring + JUnit (annotation)

ORM 在 JPA 出現後,逐漸要被整合起來,而不是像之前,每個不同的 framework 有自己的一套 xml 及 annotation。因為 xml 設定也正逐漸被 annotation 取代,底下這個例子將僅使用 annotation,並且在可能的情況下儘量用 JPA 的 annotation,只有當 JPA 不敷使用時,才使用 framework 提供的 annotation。

在資料庫 (Oracle) 中有一個 table Users,這裡將使用 JUnit 建立一筆資料。
  • JPA config
 1 package idv.steven.invitation.database;
 2 
 3 import javax.sql.DataSource;
 4 
 5 import org.apache.commons.dbcp.BasicDataSource;
 6 import org.springframework.context.annotation.Bean;
 7 import org.springframework.context.annotation.Configuration;
 8 import org.springframework.orm.jpa.JpaTransactionManager;
 9 import org.springframework.orm.jpa.JpaVendorAdapter;
10 import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
11 import org.springframework.orm.jpa.vendor.Database;
12 import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
13 import org.springframework.transaction.PlatformTransactionManager;
14 import org.springframework.transaction.annotation.EnableTransactionManagement;
15 
16 @Configuration
17 @EnableTransactionManagement
18 public class JpaConfig {
19     @Bean
20     public DataSource dataSource() {
21         BasicDataSource ds = new BasicDataSource();
22         ds.setDriverClassName("oracle.jdbc.OracleDriver");
23         ds.setUrl("jdbc:oracle:thin:@steven-nb:1521:DemoDB");
24         ds.setUsername("steven");
25         ds.setPassword("P@ssw0rd");
26         ds.setInitialSize(5);
27         ds.setMaxActive(10);
28         
29         return ds;
30     }
31     
32     @Bean
33     public JpaVendorAdapter jpaVendorAdapter() {
34         HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
35         
36         adapter.setDatabase(Database.ORACLE);
37         adapter.setShowSql(true);
38         adapter.setGenerateDdl(false);
39         adapter.setDatabasePlatform("org.hibernate.dialect.Oracle10gDialect");
40         
41         return adapter;
42     }
43     
44     @Bean
45     public PlatformTransactionManager jpaTransactionManager() {
46         JpaTransactionManager tm = new JpaTransactionManager();
47         tm.setEntityManagerFactory(this.entityManagerFactory().getObject());
48         
49         return tm;
50     }
51         
52     @Bean
53     public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
54         LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
55         emf.setDataSource(this.dataSource());
56         emf.setPackagesToScan(new String[] {"idv.steven.invitation.database.dao", "idv.steven.invitation.database.entity" });
57         emf.setJpaVendorAdapter(this.jpaVendorAdapter());
58         
59         return emf;
60     }
61 }
  1. 任何標識有 @Configuration 的類別,即為 spring 的設定類別 (取代了之前的 xml 設定檔)。
  2. @EnableTransactionManagement 表示,將由 spring 來管理 transaction。
  3. 這個類別就相當於採用 xml 設定時的 persistence.xml (參考: Hibernate + Spring + JPA (configure))。
  4. 第 34 行,在 spring 的 docs 中有這段說明「JpaVendorAdapter implementation for Hibernate EntityManager. Developed and tested against Hibernate 3.6, 4.2/4.3 as well as 5.x. Hibernate 4.2+ is strongly recommended for use with Spring 4.0+.」。
  5. 第 56 行,掃描 JPA 有關套件,將相關的類別載入。
  • 系統設定
 1 package idv.steven.invitation;
 2 
 3 import org.springframework.context.annotation.ComponentScan;
 4 import org.springframework.context.annotation.Configuration;
 5 
 6 @Configuration
 7 @ComponentScan(basePackages = {"idv.steven.invitation.database"})
 8 public class SystemConfig  {
 9 
10 }
  1. 這是個系統設定類別,當然類別裡一般是可以使用 @Bean 載入許多類別,因為我們的例子很簡單,這裡只是用第 7 行的方式掃描指定的套件,以載入上面的 JpaConfig 類別。
  • Entity
 1 package idv.steven.invitation.database.entity;
 2 
 3 import java.io.Serializable;
 4 
 5 import javax.persistence.Column;
 6 import javax.persistence.Entity;
 7 import javax.persistence.Id;
 8 import javax.persistence.Table;
 9 import javax.persistence.UniqueConstraint;
10 
11 @Entity
12 @Table(
13     name="USERS",
14     uniqueConstraints = {
15         @UniqueConstraint(columnNames = "EMAIL")
16     })
17 public class Users implements Serializable {
18     private static final long serialVersionUID = 3965333920543479036L;
19     @Id
20     private String email;
21     private String password;
22     private String phone1;
23     private String phone2;
24     private String nickname;
25     private String city;
26     private String gender;
27     private String birthday;
28     private String industry;
29     private String occupation;
30     private String description;
31     
32     … getter & setter method
33     
34 }
  1. ORM 中 entity 與實體 table 的對映,getter & setter method 我在這裡就省略不佔篇幅,第 14~16 行指出 EMail 欄位為 unique (這個 table 以 email 欄位為 primary key)。
  • DAO
1 package idv.steven.invitation.database.dao;
2 
3 import idv.steven.invitation.database.entity.Users;
4 
5 public interface UsersDAO {
6     public int create(Users user);
7 }
  1. 定義 DAO 介面,這是可以省略,直接定義下面的類別也可以,這是為了設計上的彈性。 
 1 package idv.steven.invitation.database.dao;
 2 
 3 import javax.persistence.EntityManager;
 4 import javax.persistence.PersistenceContext;
 5 
 6 import org.springframework.stereotype.Repository;
 7 import org.springframework.transaction.annotation.Transactional;
 8 
 9 import idv.steven.invitation.database.entity.Users;
10 
11 @Repository
12 public class UsersDAOImpl implements UsersDAO {
13     @PersistenceContext
14     private EntityManager manager;
15 
16     @Transactional
17     public int create(Users user) {
18         manager.persist(user);
19         return 1;
20     }
21 }
  1. 第 13 行,由 JpaConfig 類別裡載入 EntityManager,這裡不能用 @Inject 或 @Autowired 而要用 @PersistenceContext,因為這裡不是直接載入一個 Bean,而是由 EntityManagerFactory bean 產生一個 EntityManager。
  2. 第 16 行指出這個 method 的 transaction 要交由 spring 控管,所以在這個 method 一開始 spring 會產生一個 transaction,在離開 method 前根據成功或失敗決定 commit 或 rollback。
  • 單元測試 JUnit
 1 package idv.steven.invitation.database.dao;
 2 
 3 import static org.junit.Assert.*;
 4 
 5 import javax.inject.Inject;
 6 
 7 import org.junit.After;
 8 import org.junit.Before;
 9 import org.junit.Test;
10 import org.junit.runner.RunWith;
11 import org.springframework.test.context.ContextConfiguration;
12 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
13 
14 import idv.steven.invitation.SystemConfig;
15 import idv.steven.invitation.database.JpaConfig;
16 import idv.steven.invitation.database.entity.Users;
17 
18 @RunWith(SpringJUnit4ClassRunner.class)
19 @ContextConfiguration(classes = { JpaConfig.class, SystemConfig.class })
20 //@Transactional
21 public class UserDAOTest {
22     @Inject
23     private UsersDAO dao;
24 
25     @Before
26     public void setUp() throws Exception {
27     }
28 
29     @After
30     public void tearDown() throws Exception {
31     }
32 
33     @Test
34     public void testCreate() {
35         Users user = new Users();
36         user.setEmail("hi.steven@gmail.com");
37         user.setNickname("Steven");
38         user.setBirthday("19750315");
39         user.setCity("台南市");
40         user.setGender("M");
41         user.setPassword("password");
42         
43         int n = dao.create(user);
44         assertEquals(1, n);
45     }
46 }
  1. 第 19 行,載入相關設定檔。
  2. 第 20 行,如果在單元測試的類別前加上 @Transactional,那麼在單元測試結束後,JUnit 會將資料 rollback。
  3. 第 35~41 將要加入資料庫的值設定在 Users 的物件。
  4. 第 43 行,呼叫 DAO 將資料 insert 到資料庫,Hibernate 會產生這樣的 sql --- insert into USERS (BIRTHDAY, CITY, DESCRIPTION, GENDER, INDUSTRY, NICKNAME, OCCUPATION, PASSWORD, PHONE1, PHONE2, EMAIL) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)。
  • see also
第一個 spring boot web 程式 (使用 embedded tomcat)




【日本電影 - 你的名字】
這部動畫電影無疑是今年日本最夯的電影,劇中以慧星撞地球及男女主角靈魂(身體)互換為背景,述說著兩人在茫茫人海中尋找生命中註定要在一起的人,確實是相當能吸引青年男女的劇情。下圖是慧星分裂後的瞬間,以拋物線來說,那個分裂出來的小塊應該會拋向遠方,在劇中則落在男女主角所在之處,這一點引來了一些批評,當然這個是個缺失,但無損整個故事的精彩。

2014年8月9日 星期六

剖析固定長度欄位的訊息字串

在進行系統整合時,系統間不管是透過網路、檔案傳送訊息資料,常會遇到訊息字串的各個欄位是固定長度的,這種情況下程式怎麼寫會比較簡潔易懂? 這裡是我提供的一個方法,使用 annotation 將訊息字串欄位的設定值直接寫在程式上,而不是寫在設定檔裡,這樣應該會比較直覺。
假設有個程式,要顯示中華民國總統的任期和歷史評價,其訊息格式如下:




首先,我們定義一個 annotation,可以設定欄位的開始位置和長度,如下:
 1 package idv.steven.annotation;;
 2 
 3 import java.lang.annotation.ElementType;
 4 import java.lang.annotation.Retention;
 5 import java.lang.annotation.RetentionPolicy;
 6 import java.lang.annotation.Target;
 7 
 8 @Retention(RetentionPolicy.RUNTIME)
 9 @Target(ElementType.FIELD)
10 public @interface MsgField {
11     public int start();
12     public int length();
13 }
接著定義一個介面,所有訊息的 POJO 都要實作這個介面,這個介面的作用只是為了統一各個訊息的類型。
1 package idv.steven.msgspec;
2 
3 public interface FixedFieldMsg {
4 
5 }
接著依訊息的需要定義 POJO,這裡當然就是依上方的訊息規格來定義,並且於類別裡的各個欄位上設定開始位置、長度,因為訊息有四個欄位,當然這個類別也有相對應的四個欄位。
 1 package idv.steven.pojo;
 2 
 3 import idv.steven.MsgField;
 4 
 5 public class President implements FixedFieldMsg {
 6     @MsgField(start=0,length=4)
 7     private String startYear;
 8     @MsgField(start=4,length=4)
 9     private String endYear;
10     @MsgField(start=8,length=10)
11     private String name;
12     @MsgField(start=18,length=30)
13     private String evaluate;
14     
15     /**
16      * @return 任期開始的年份
17      */
18     public String getStartYear() {
19         return startYear;
20     }
21     public void setStartYear(String startYear) {
22         this.startYear = startYear;
23     }
24     
25     /**
26      * @return 任期結束的年份
27      */
28     public String getEndYear() {
29         return endYear;
30     }
31     public void setEndYear(String endYear) {
32         this.endYear = endYear;
33     }
34     
35     /**
36      * @return 姓名
37      */
38     public String getName() {
39         return name;
40     }
41     public void setName(String name) {
42         this.name = name;
43     }
44     
45     /**
46      * @return 評價
47      */
48     public String getEvaluate() {
49         return evaluate;
50     }
51     public void setEvaluate(String evaluate) {
52         this.evaluate = evaluate;
53     }
54 }
下面就是剖析訊息的程式了…
 1 package idv.steven;
 2 
 3 import java.lang.reflect.Field;
 4 import java.lang.reflect.InvocationTargetException;
 5 import java.lang.reflect.Method;
 6 import java.util.Arrays;
 7 
 8 import idv.steven.annotation.MsgField;
 9 import idv.steven.msgspec.FixedFieldMsg;
10 import idv.steven.msgspec.President;
11 
12 /**
13  * 剖析固定欄位長度的訊息字串成為一個物件
14  * @author Steven
15  */
16 public class Parser {
17     /**
18      * 將訊息字串轉換為訊息物件
19      * @param msgText 訊息字串
20      * @param msgObject 訊息物件
21      * @return 訊息物件
22      * @throws IllegalArgumentException
23      * @throws IllegalAccessException
24      * @throws NoSuchMethodException
25      * @throws SecurityException
26      * @throws InvocationTargetException
27      * @throws ClassNotFoundException
28      */
29     public FixedFieldMsg parsing(byte[] msgText, FixedFieldMsg msgObject) 
30             throws IllegalArgumentException, IllegalAccessException, 
31             NoSuchMethodException, SecurityException, 
32             InvocationTargetException, ClassNotFoundException {
33         
34         Class<?> aClass = Class.forName(msgObject.getClass().getName());
35         Field[] fields = aClass.getDeclaredFields();
36         for(Field field:fields) {
37             MsgField msgField = field.getAnnotation(MsgField.class);
38             if (msgField != null) {
39                 byte[] subMsg = Arrays.copyOfRange(msgText, msgField.start(), msgField.start() + msgField.length());
40                 
41                 Method method = aClass.getMethod(getSetterName(field.getName()), String.class);
42                 method.invoke(msgObject, new String(subMsg));
43                 //field.set(message, new String(subMsg));
44             }
45         }
46         
47         return msgObject;
48     }
49     
50     private String getSetterName(String fieldName) {
51         return String.format("set%s%s", fieldName.substring(0, 1).toUpperCase(), fieldName.substring(1));
52     }
53 
54     public static void main(String[] args) {
55         String msgText = "20082016馬英九    一個自私的笨蛋、禍國殃民!     ";
56         
57         try {
58             Parser parser = new Parser();
59             President president = (President) parser.parsing(msgText.getBytes(), new President());
60             
61             System.out.println("任期: " + president.getStartYear() + " ~ " + president.getEndYear());
62             System.out.println("總統姓名: " + president.getName());
63             System.out.println("歷史評價: " + president.getEvaluate());
64         } catch (Exception e) {
65             e.printStackTrace();
66         }    
67     }
68 }
這個程式如果要實際應用,當然還要加上一些錯誤處理,以避免傳入的字串不合規定,在這裡為使程式邏輯更清楚,就先省略了,只談我們真正在意的部份。程式說明如下:
  1. parsing method 無疑是程式的重點,這裡將利用 Java 的反射 (reflect) 機制,簡潔的將訊息剖析及訊息轉換成物件合在一起,注意一下 Line 59,呼叫 parsing 時,第二個參數就傳入實際要轉換成的物件。
  2. Line 34: 透過這個方法,可以取得類別的相關訊息,要特別注意的是,這裡取得的不是物件 (object) 而是類別 (class)。
  3. Line 35: 取得類別中所有的欄位,不管它是 public、protected、private 或無修飾子。
  4. Line 37: 取得欄位上的 annotation 相關的訊息。
  5. Line 39: 以 annotation 設定的開始位置、長度,從訊息字串中取出子字串。
  6. Line 41: 依 Java 的習慣,POJO 的欄位會設定成 private,再透過 getter、setter method 來存取,我們依這樣的習慣寫這個程式,所以要取得 setter method 才能給欄位設定字串中取得的值。getMethod 一子第一個參數是 setter method 的名稱,第二個參數是傳給 setter method 的參數的類別種類。
  7. Line 42: 呼叫 setter method 將值設定給欄位。
  8. Line 43: 如果 Message 類別的欄位修飾子改成 public,我們也可以用這個方式給各個欄位值,這只是給各位參考,但不建議這麼做。
執行結果如下:
任期: 2008 ~ 2016
總統姓名: 馬英九    
歷史評價: 一個自私的笨蛋、禍國殃民!    

see also: Spring Batch: FixedLengthTokenizer

【日劇 - 倒數第二次戀愛】
以中年人為主角的日劇好像還不少,除了這一部戲外,天海祐希主演的「偽裝夫妻」,篠原涼子主演的「熟女正青春」也都蠻好看的。但是,要說選角嘛~ 「倒數第二次戀愛」選的小泉今日子 (飾演 吉野千明)、中井貴一 (飾演 長倉和平) 真的最讓人覺得完全沒得挑剔,劇中兩人的鬥嘴真的就是活生生的中年人,非常合拍。
除了主角生動的演出外,配角也相當不錯,三井 (久保田磨希 飾) 是吉野千明的得力助手,出現的時間固然不多,表現似乎也沒有什麼特別,但是她的存在總能突顯吉野的一些人格特質,是不可或缺的人物。
女主角吉野的兩個閏密長得如三井一樣不亮眼,可是這兩個閏密的存在很能顯示出吉野中年生活的空虛,而且那看似平淡無奇的演出,就是生活中三個歐巴桑聚在一起的樣子。
這部戲沒有什麼特別的高潮,從頭到尾卻完全不會讓人覺得無聊,蠻神奇的編劇、蠻神奇的演出,要說有什麼讓人不滿意的,雙胞胎兄妹長倉萬理子 (內田有紀 飾)、長倉真平 (坂口憲二 飾) 的角色設定比較讓人覺得怪,一方面是兩個角色怪異的個性和這平淡的劇情有點格格不入,另一方面是造型看起來有那麼一點不舒服 ...