Google Code Prettify

2017年12月22日 星期五

在 linux 中無法切換身份? (su: Permission denied)

Linux 預設,只有 root 可以切換到別的身份 (帳號),如果要取消這個限制,怎麼做? 如下:

1. 變更授權規則
    編輯 /etc/pam.d/su 檔,找到 auth required 這個項目,原來的值可能如下:
auth            required        pam_wheel.so trust group=wheel
    將它改成如下:
auth            required        pam_wheel.so use_uid
2. 將要能切換到別的身份的帳號加入 wheel 群組
    假設想要 user1 和 user2 可以相互切換,那麼執行如下指令:
usermod -a -G wheel user1
usermod -a -G wheel user2 
再測試看看,應該沒問題了。

2017年8月10日 星期四

Gradle: 編譯、測試 web 專案

Java 專案的編譯、測試、部署,從十幾年前很流行的 ant,到現在則是以 Maven 及 Gradle 最流行,這篇先簡單的介紹怎麼用 gradle 將 web 專案打包成 war 檔。
  • 安裝 Gradle
請到 Gradle 的官網下載 Gradle,並依上面步驟安裝設定好環境。
  • build.gradle


如果是用 eclipse 開發,在專案的根目錄下新增一個文字檔,檔名為 build.gradle,這個檔名不是強迫非取這個名稱不可,只不過這是預設的檔名,這一篇希望快速的用 gradle 來編譯、測試及包裝 war 檔,一切都採預設值。接下來還有一些目錄也都是 gradle 預設的,以後有空會說明怎麼使用非預設的目錄,但是這邊我們把程式都放到預設目錄,現在說明這些目錄的用處 (目錄當然要自己建立啦~)。
  1. src/main: 網站相關的檔案,像是 java、jsp、html、*.properties、*.xml ... 都放在這個目錄下的子目錄。
  2. src/main/java: 顧名思義,這裡是放 java 程式碼的地方。
  3. src/main/webapp: 這個目錄就相當於一般我們用 eclipse 產生一個 web project 時,eclipse 自動幫我們產生的 WebContent,這個目錄在部署後就會相當於網站的根目錄。
  4. src/test/java: 測試程式放這個目錄下,只要裡面有測試程式,每次執行 gradle 時,在 compile 並包裝成 war 這些工作完成後,gradle 還會來執行這些測試程式產生測試報告。
接下來直接看一個簡單的 build.gradle 吧 ~
 1 apply plugin: 'java'
 2 apply plugin: 'war'
 3 
 4 sourceCompatibility = 1.8
 5 version = '1.0'
 6 
 7 sourceSets {
 8     main {
 9         java {
10             srcDirs = ['src/main/java']
11         }
12         resources {
13             srcDirs = ['config']
14         }
15     }
16     test {
17         java {
18             srcDirs = ['src/test/java']
19         }
20     }
21 }
22 
23 buildDir = 'out'
24 
25 repositories {     
26      maven { url "http://maven.springframework.org/milestone" }
27      maven { url "http://repo.maven.apache.org/maven2" }
28      maven { url "http://repo1.maven.org/maven2/" }
29      maven { url "http://amateras.sourceforge.jp/mvn/" }
30      mavenCentral()
31 }
32 
33 configurations.all {
34     exclude group: 'org.freemarker', module: 'freemarker'
35     exclude group: 'javax.el', module: 'el-api'
36     exclude group: 'xerces', module: 'xercesImpl'
37     exclude group: 'xml-apis', module: 'xml-apis'
38     exclude group: 'jboss', module: 'javassist'
39     exclude group: 'org.apache.tomcat.embed', module: 'tomcat-embed-core'
40     exclude group: 'org.apache.tomcat.embed', module: 'tomcat-embed-el'
41     exclude group: 'org.apache.tomcat.embed', module: 'tomcat-embed-websocket'
42     exclude group: 'org.apache.tomcat', module: 'tomcat-jdbc'
43     exclude group: 'org.apache.tomcat', module: 'tomcat-juli'
44     exclude group: 'org.apache.ant', module: 'ant'
45     
46     resolutionStrategy {
47         force group: 'org.apache.ant', name: 'ant', version: '1.10.1'
48     }
49 }
50 
51 dependencies {
52     def tomcatVersion = '9.0.0.M22'
53     def springVersion = '4.3.10.RELEASE'
54     def springBootVersion = '1.5.4.RELEASE'
55     def hibernateVersion = '5.2.10.Final'
56     def hibernateValidatorVersion = '6.0.1.Final'
57     
58     compile fileTree(dir: 'library', include: ['*.jar'])
59     
60     testCompile group: 'junit', name: 'junit', version: '4.+'
61     
62     //javax
63     compile group: 'javax.inject', name: 'javax.inject', version: '1'
64     compile group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
65     compile group: 'javax.activation', name: 'activation', version: '1.1.1'
66     
67     //spring framework
68     compile group: 'org.springframework', name: 'spring-context', version: "$springVersion"
69     compile group: 'org.springframework', name: 'spring-test', version: "$springVersion"
70 
71     //spring boot
72     compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: "${springBootVersion}"
73     
74     //Others
75     compile group: 'io.jsonwebtoken', name: 'jjwt', version: '0.7.0'
76 }
簡要說明如下:
  1. apply plugin: 引入插件,這裡引入 java 及 war 兩個插件,這樣就可以編譯、測試並包裝 war 檔了 (一般 console、desktop 程式不需要引入 war 這個插件)。
  2. sourceCompatibility: 指出編譯要用的 JDK 版本,這裡指出是要用 1.8 版。
  3. version: 設定我們自己的這個專案的版本編號
  4. sourceSets: 裡面比較值的注意的是 resources,設定檔我們是放在專案的根目錄下的 config 目錄裡,所以 12~14 是必要的,不然 gradle 不會知道要把這個目錄下的檔案也包裝進 war。至於 main/java、test/java 這兩個設定因為值都是 gradle 的預設值,在這裡是可省略的。
  5. buildDir: 指出編譯後的產出要放到那個目錄下。
  6. repositories: 指出 gradle 要到那些網站抓 jar 檔。
  7. configurations.all: gradle 抓 jar 檔的時候,會把相依的 jar 檔一併抓下來,不然,我們要找到所有相依 jar 檔會浪費非常大量年輕寶貴的生命,但是,有時候我們不想要其中的某些 jar 檔時,可以在這裡以 exclude group 將它排除,或是 gradle 抓下來的 jar 版本不是我們想要的,可以用 force group 強制限定版本。
  8. dependencies: 這是一般人最熟悉的部份,把我們專案需要的 jar 檔寫在這裡,有些 framework 會有非常多的 jar 檔,版號又一樣,那麼,可以先用 def 定義版號的變數,省去每次換版本的麻煩。
  9. compile fileTree: 有一些 jar 檔不是到網路上的 jar repository 裡抓的,是放在自己的電腦某處,像這裡,這類的 jar 檔是放在專案根目錄的 library 目錄下,所以要這個指令指出來。
  10. testCompile group: 指出測試時要用的 framework,這裡使用的是 JUnit 4.+ 版。
程式寫好,build.gradle 也準備好了,就在專案的目錄下打入 gradle build 指令,build 是 java 插件提供的一個任務 (task),這個任務會以正確的順序編譯、測試和打包。最後結果產生後應該會在 buildDir 指定的目錄下,看到如下的結果:
  • classes: java compile 後產生的 class 檔,都放在這個目錄下。
  • libs: war 檔放在這裡。
  • resources: 上面 build.gradle 中指定的資源檔會放在這個目錄下。
  • reports、test-results: 這裡放的是測試報告,reports 放的是每個測試的結果,test-results 則有每個測試詳細資訊,有錯誤時可以來這看看是什麼錯誤。


2017年7月27日 星期四

變更 property 檔裡的設定值

Java 的程式設定習慣放在 property 檔,這些檔會放置在 classpath 的某處,大部份時候程式都只需要讀取檔案裡的資料,要變更資料是人工變更,但是,萬一程式想變更怎麼辦? 底下式可以辦到 …

紅色部份比較常見,就是讀入 application.properties 裡的資料到 prop 裡,綠色部份是變更想變更的值後在存回去。
Properties prop = new Properties();

URL url = this.getClass().getClassLoader().getResource("application.properties");
URI uri = url.toURI();
File file = new File(uri);
   
InputStream is = new FileInputStream(file);
prop.load(is);
is.close();
   
FileOutputStream os = new FileOutputStream(file);
prop.setProperty("myKey", myValue);
prop.store(os, null);
os.close();
上面這樣就搞定了嗎? 通常應該沒有這麼好過 … Orz...
因為程式中如果有用到這個 property 檔的相關變數應該也要被更新,在 spring framework 中,載入 property 檔,是在 @Configuration 所在的類別再增加如下註釋。
@PropertySource("classpath:application.properties")
在需要使用裡面的設定的類別裡如下載入。
@Value("${myKey}")
private String myValue;
因為我們已經將 property 檔委由 spring framework 管理,要重新載入,就要從 spring 的環境著手。
@Inject
private StandardEnvironment environment;
...
MutablePropertySources propertySources = environment.getPropertySources();
PropertySource resourcePropertySource = propertySources.get("class path resource [application.properties]");
        
URL url = this.getClass().getClassLoader().getResource("application.properties");
URI uri = url.toURI();
File file = new File(uri);
  
InputStream is = new FileInputStream(file);
Properties prop = new Properties();
prop.load(is);
is.close();
        
propertySources.replace("class path resource [application.properties]", new PropertiesPropertySource("class path resource [application.properties]", prop));




2017年7月23日 星期日

Spring Application Event

spring framework 的 bean 一般熟知的就是 IoC 的依賴注入 (dependency injection),這裡介紹一個較少用到的功能,bean 與 bean 之前傳遞事件! 既然是談到事件,最好就先了解一下 Observer Pattern,spring framework 實作了這個 pattern,讓程式中有兩個 bean,當 bean A 狀態有變化時,可以通知 bean B,說明如下:
  • Event
 1 @Data
 2 public class MyBeanEvent extends ApplicationEvent {
 3     private String msg;
 4 
 5     public MyBeanEvent(Object source) {
 6         super(source);
 7         
 8         this.msg = (String) source;
 9     }
10 }
首先定義一個事件類別,這個類別需繼承 ApplicationEvent 類別,我們自己定義的事件類別需有建構子 (constructor),即程式中 5 ~ 9 行,我們將傳遞過來的物件存入變數 msg 裡。
  • Publisher
1 @Component
2 public class MyPublisher {
3     @Inject
4     private ApplicationContext context;
5     
6     public void publish(String msg) {
7         context.publishEvent(new MyBeanEvent(msg));
8     }
9 }
發佈訊息的類別如上,使用 spring 的 ApplicationContext 的 publishEvent method 即可發佈出訊息物件,所有的傾聽者 bean 就會收到訊息物件。
  • Listener
1 @Component
2 public class MyListener implements ApplicationListener<MyBeanEvent> {
3 
4     @Override
5     public void onApplicationEvent(MyBeanEvent event) {
6         System.out.println("listen: " + event.getMsg());
7     }
8 }
傾聽類別要實作 ApplicationListener 介面,並指出要傾聽的事件類別,如上面的第 2 行,這個介面會要求傾聽類別實作一個 onApplicationEvent method,當 publisher 發出事件時,這個 method 即會收到該事件,這裡很簡單的將收到的事件內容印出來 (line 6)。
  • Config
1 @Configuration
2 @ComponentScan("idv.steven.info.tse")
3 public class MyEventConfig {
4 
5 }
要測試 publisher 送出的訊息是否會被 listener 收到,當然要先讓 spring 將它們載入,因為 publisher 和 listener 兩個 bean 是被在 idv.steven.info.tse package 下,所以第 2 行就去掃描該 package。
  • Main
1 public class MyTestMain {
2 
3     public static void main(String[] args) {
4         AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyEventConfig.class);
5         MyPublisher publisher = context.getBean(MyPublisher.class);
6         publisher.publish("Hello World");
7         context.close();
8     }
9 }
主程式很簡單,使用 spring 的 AnnotatioinConfigApplicationContext 載入 MyEventConfig,MyEventConfig 會去掃描指定的 package 將標有 @Component 相關的 bean 載入,載入後就如第 5 行,主程式取得 publisher 物件,然後在第 6 行發佈出 "Hello World" 的訊息,執行的結果就可以看到 console 印出如下訊息:
listen: Hello World



2017年3月30日 星期四

在 WebSphere 8.5.5.x 上遇到的 jar 檔版本和衝突問題

最近將自己負責的一個系統升級,升級的項目主要有以下幾項:
  1. JDK: 5.0 升到 8.0
  2. IBM WebSphere Application Server: 6.0 升到 8.5.5.x
  3. spring framework: 2.5.6 升到 4.3.7,且全部的 xml 設定都改為用 annotation。
  4. hibernate: 3.3.x 升到 5.2.4,且程式都用 JPA 改寫。
  5. Ajax: 由 DWR (Direct Web Remoting) 換為 jQuery
當然是有花了一些時間,也請了幾個同事協助修改以加快進度,好不容易改完,要部署到 WAS 時,遇到很多 jar 檔的問題,這裡記錄幾個,下面是安裝過程遇到的錯誤訊息及解決辦法。

遇到這些錯誤從錯誤訊息基本上都可以找到蛛絲馬跡,我用紅色標起來的部份,可以看到因為 jar 檔版本升級或被廠商修改過,程式內容已經不一樣了,造成型別錯誤、找不到該有的 method 等,用這些關鍵字先 google 一下,網路上多半會有人已經找到有問題的 jar 檔,如果 google 不到,就是考驗經驗、實力和耐心的開始了 ...
  • xercesImpl
ServletWrappe E com.ibm.ws.webcontainer.servlet.ServletWrapper init SRVE0271E: 未捕捉到應用程式中Servlet [application] 產生的init() 異常狀況:java.lang.IncompatibleClassChangeError
        at org.dom4j.tree.AbstractElement.setAttributes(AbstractElement.java:505)
        at org.dom4j.io.SAXContentHandler.addAttributes(SAXContentHandler.java:916)
        at org.dom4j.io.SAXContentHandler.startElement(SAXContentHandler.java:249)
        at org.apache.xerces.parsers.AbstractSAXParser.startElement(Unknown Source)
        at org.apache.xerces.impl.XMLNSDocumentScannerImpl.scanStartElement(Unknown Source)
        at org.apache.xerces.impl.XMLNSDocumentScannerImpl$NSContentDispatcher.scanRootElementHook(Unknown Source)
        at org.apache.xerces.impl.XMLDocumentFragmentScannerImpl$FragmentContentDispatcher.dispatch(Unknown Source)
        at org.apache.xerces.impl.XMLDocumentFragmentScannerImpl.scanDocument(Unknown Source) 
怎麼解決? 基本也只能從訊息中猜測,是不是 jar 檔版本太舊沒有更新? 並嘗試更新幾個 jar 檔試看看,最後是把 xercesImpl-2.8.1.jar 升級到 2.9.1 以上版本就可解決了。
  • javax.el-api
webapp        E com.ibm.ws.webcontainer.webapp.WebApp initializeExtensionProcessors SRVE0280E: 在 Factory [com.ibm.ws.jsp.webcontainerext.ws.WASJSPExtensionFactory@f87c2d7d] 中,延伸規格處理器無法起始設定:java.lang.ClassCastException: org.apache.el.ExpressionFactoryImpl incompatible with javax.el.ExpressionFactory
        at javax.el.ExpressionFactory.newInstance(ExpressionFactory.java:210)
        at javax.el.ExpressionFactory.newInstance(ExpressionFactory.java:118)
        at com.ibm.ws.jsp.configuration.JspConfiguration.<init>(JspConfiguration.java:63)
        at com.ibm.ws.jsp.configuration.JspConfigurationManager.createJspConfiguration(JspConfigurationManager.java:202)
        at com.ibm.ws.jsp.taglib.TagLibraryCache.loadWebInfTagFiles(TagLibraryCache.java:444)
        at com.ibm.ws.jsp.taglib.TagLibraryCache.<init>(TagLibraryCache.java:117)
        at com.ibm.ws.jsp.webcontainerext.AbstractJSPExtensionProcessor.<init>(AbstractJSPExtensionProcessor.java:215)
        at com.ibm.ws.jsp.webcontainerext.ws.WASJSPExtensionProcessor.<init>(WASJSPExtensionProcessor.java:81)
        at com.ibm.ws.jsp.webcontainerext.ws.WASJSPExtensionFactory.createProcessor(WASJSPExtensionFactory.java:267)
        at com.ibm.ws.jsp.webcontainerext.AbstractJSPExtensionFactory.createExtensionProcessor(AbstractJSPExtensionFactory.java:76)
        at com.ibm.ws.webcontainer.webapp.WebApp.initializeExtensionProcessors(WebApp.java:1408)
這個猜測是 JSP 上的 EL (Expression Language) 相關的 jar 檔和 WAS 的 jar 檔衝突了,於是找到 java.el-api-2.2.4.jar 將它刪除。事實上,在 war 檔中不應該含有 servlet-api.jar, jsp-api.jar, el-api.jar, j2ee.jar, javaee.jar 這些 jar 檔,否則都會有錯誤。
  • xml-apis
Caused by: javax.xml.parsers.FactoryConfigurationError: Provider javax.xml.parsers.DocumentBuilderFactory could not be instantiated: java.util.ServiceConfigurationError: javax.xml.parsers.DocumentBuilderFactory: Provider org.apache.xerces.jaxp.DocumentBuilderFactoryImpl not a subtype
        at javax.xml.parsers.DocumentBuilderFactory.newInstance(Unknown Source)
        at com.ibm.websphere.product.utils.SimpleXMLParser.getDocumentBuilderWithEntityResolver(SimpleXMLParser.java:760)
        at com.ibm.websphere.product.utils.SimpleXMLParser.<init>(SimpleXMLParser.java:198)
        at com.ibm.websphere.product.utils.SimpleXMLParser.<init>(SimpleXMLParser.java:176)
        at com.ibm.websphere.product.metadata.im.IMMetadata.parseInstalledXmlFile(IMMetadata.java:422)
        at com.ibm.websphere.product.metadata.im.IMMetadata.parseInstallRegistryFiles(IMMetadata.java:364)
這個錯誤看來是和 xml 相關的 jar 檔有關,但是和 xml 有關的 jar 檔很多,很難確定是那一個,會找到 xml-apis-2.0.2.jar,是 google 得到的答案,因為這個 jar 檔和 javax.xml 裡的相關 API 衝突了,刪除這個檔案即可。
  • geronimo-stax-api_1.0_spec-1.0.1.jar
Exception occurred while the JNDI NamingManager was processing a javax.naming.Reference object. [Root exception is javax.xml.stream.FactoryConfigurationError: Provider javax.xml.stream.XMLInputFactory could not be instantiated: java.util.ServiceConfigurationError: javax.xml.stream.XMLInputFactory: Provider com.ctc.wstx.stax.WstxInputFactory not a subtype]
這也是和 xml 有關的 jar 檔衝突,移除 geronimo-stax-api_1.0_spec-1.0.1.jar 即可。
  • persistence-api-1.0.2.jar
Caused by: java.lang.NoSuchMethodError: javax.persistence.spi.PersistenceUnitInfo.getValidationMode()Ljavax/persistence/ValidationMode;
    at org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor.getValidationMode(PersistenceUnitInfoDescriptor.java:88)
    at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.mergeSettings(EntityManagerFactoryBuilderImpl.java:454)
    at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.<init>(EntityManagerFactoryBuilderImpl.java:199)
    at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.<init>(EntityManagerFactoryBuilderImpl.java:163)
看起來是 JPA 相關的 jar 檔版本有衝突,我想用的是 hibernate-jpa-2.1-api-1.0.0.Final.jar,但是實際上用到舊的,找了一下發現是 persistence-api-1.0.2.jar 也有相關的 class,刪除即可。
[see also] 如何在 WebSphere 中解決 jar 包衝突




2017年1月28日 星期六

spring data: PagingAndSortingRepository

在「spring data: CrudRepository 自動生成」有說明過 spring data 的 CrudRepository,提供了許多實用的預設 method,方便程式員進行資料庫的 CRUD 操作,這篇說明另一個好用的介面 PagingAndSortingRepository,只要宣告一個繼承這個介面的介面,即可以輕鬆擁有查詢翻頁的功能,也就是在網頁中,如果查詢結果很多會分成很多頁面時,可以使用這個介面提供的預設 method,它會傳回指定頁次的結果。
假設網站管理者要查詢網站中的使用者,並進行管理,就可以用到這個介面。
public interface UsersDAO extends PagingAndSortingRepository<Users, String> {
    public List<Users> findByEmail(String email);
    //public Page<Users> findAll(Pageable p);
}
上面的 UserDAO 繼承了 PagingAndSortingRepository 介面,於是我們不需要寫任何程式,就可以使用 findAll 等預設的介面,上面 findAll 只是我寫出來讓大家知道 spring data 中是怎麼宣告,實際上是不需要寫出來,測試程式如下:
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = { WebInitializer.class, RootConfig.class, JpaConfig.class, MvcConfig.class })
@ActiveProfiles("TEST")
public class UserDAOTest {
    @Autowired
    private UsersDAO dao;
    
    @Transactional
    @Test
    public void testFindAll() {
        Pageable p = new PageRequest(0, 5);
        Page<Users> all = dao.findAll(p);
        all.forEach(u -> System.out.println(u.toString()));
    }
} 
看一下紅色部份,dao 直接可以呼叫 findAll,參數傳入的是指定資料結果每 5 筆資料一頁,要傳回的是第 1 頁 (0 代表第一頁、1 代表第二頁…)。至於前面的 @WebAppConfiguration,這只是因為這個程式是 spring MVC 程式,所以在初始化時,會帶入一些 web 相關的物件,所以要這麼寫才不會有錯,如果不是 web 程式,就不需要這個 annotation。另外一提的是,不管 CrudRepository 或 PagingAndSortingRepository,spring data 都提供了預設 method 以外的一個規範,只要規循該規範定義 method,spring data 就可以自動產生相關的程式,例如上面的 findByEmail,這個 method 就可以用來搜尋特定 email 的使用者,spring data 還可以根據我們宣告的傳回值判斷傳回的是一個或多個結果,測試程式如下:
    @Transactional
    @Test
    public void testFindByEmail() {
        Users user = dao.findByEmail("hi.steven@gmail.com");
        assertNotNull(user);
        System.out.println(user.toString());
    }
spring data 提供的規範有那些? 可以參考「Professional Java for Web Applications - Featuring WebSockets, Spring Framework, JPA Hibernate and Spring Security」第 641 頁,內容如下:
Is and Equals are implied in the absence of other keywords, but you may explicitly use them. For example, findByIsbn is equivalent to findByIsbnIs and findByIsbnEquals.
Not and IsNot negate any other keyword except for Or and And. Is and Equals are still implied in the absence of other keywords, so findByIsbnIsNot is equivalent to findByIsbnIsNotEqual.
After and IsAfter indicate that the property is a date and/or time that should come after the given value, whereas Before and IsBefore indicate that the property should come
before the given value. Example: findByDateFoundedIsAfter(Date date).
Containing, IsContaining, and Contains indicate that the property’s value may start and end with anything but should contain the given value. This is similar to StartingWith, IsStartingWith, and StartsWith, which indicate that the property should start with the specified value. Likewise, EndingWith, IsEndingWith, and EndsWith indicate that the property should end with the specified value.
Example:
findByTitleContains(String value) is equivalent to the SQL criteria WHERE title = '%value%'.
Like is similar to Contains, StartsWith, and EndsWith, except that value you provide should already contain the appropriate wildcards (instead of Spring Data adding them for you). This gives you the flexibility to specify more advanced patterns. NotLike simply negates Like. Example: findByTitleLike(String value) could be called with value "%Catcher%Rye%" and would match “The Catcher in the Rye” and “Catcher Brings Home Rye Bread.”
Between and IsBetween indicate that the property should be between the two specified values. This means that you must provide two parameters for this property criterion. You can use Between on any type that may be compared mathematically in this manner, such as numeric and date types. Example: findByDateFoundedBetween(Date start, Date end).
Exists indicates that something should exist. Its meaning may vary wildly between storage mediums. It is roughly equivalent to the EXISTS keyword in JPQL and SQL.
True and IsTrue indicate that the named property should be true, whereas False and IsFalse indicate that the named property should be false. These keywords do not require method parameters because the value is implied by the keywords themselves. Example: findByApprovedIsFalse().
GreaterThan and IsGreaterThan indicate that the property is greater than the parameter value. You can include the parameter value in the bounds with GreaterThanEqual or IsGreaterThanEqual. The inverse of these keywords are LessThan, IsLessThan, LessThanEqual, and IsLessThanEqual.
In indicates that the property value must be equal to at least one of the values specified. The parameter matching this criterion should be an Iterable of the same type of the property.
Example: findByAuthorIn(Iterable<String> authors).
Null and IsNull indicate that the value should be null. These keywords also do not require method parameters because the value is implied.
Near, IsNear, Within, and IsWithin are keywords useful for certain NoSQL database types but have no useful meaning in JPA.
Regex, MatchesRegex, and Matches indicate that the property value should match the String regular expression (do not use Pattern) specified in the corresponding method parameter.






【美影 - 關鍵少數】
這部電影是由真人真事改編,時代是1960年,當時美國還是個種族歧視、性別歧視非常嚴重的國家,許多學校不但不准黑人入學,也不准女性入學,在 NASA 卻有一群優秀的黑人女性科學家,在雙重壓迫下,為美國的太空競賽默默的作出重大的貢獻。

劇中的科學家白人全部是男性,黑人全部是女性,這是一個很怪的狀況,但是電影中沒有解釋原因,因為在種族、性別的雙重歧視下,白人女性、黑人男性應該更有機會進入 NASA,為什麼偏偏是最被歧視的黑人女性成為裡面的關鍵少數?
撇開上面的疑惑不談,電影中相當精典的橋段,如上圖,主角在黑板上寫下她的發現。在電腦進入 NASA 協助運算前,NASA 竟然養了一大群數學高手充當人肉計算機,這些人除了頭腦要好,看來體力也要不錯,不然沒能力在梯子上爬上爬下,恐怕也沒有表現的機會。

2017年1月25日 星期三

JPA + Spring: one-to-many (II)


在看過之前一對多多對多的例子後,現在把兩個合在一起,現在要把 Appointment 類別完全依它與其它 Entity 類別的關係如上圖的建立好,Appointment 修改如下:
 1 @Entity
 2 @Table(
 3     name="APPOINTMENT",
 4     uniqueConstraints = {
 5         @UniqueConstraint(columnNames = "SYSID")
 6     })
 7 @Data
 8 public class Appointment implements Serializable {
 9     private static final long serialVersionUID = -3781642274581820116L;
10     
11     @Id
12     @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SEQ_APPOINTMENT")
13     @SequenceGenerator(name="SEQ_APPOINTMENT", sequenceName="SEQ_APPOINTMENT", initialValue=1, allocationSize=1)
14     private Long sysid;
15     
16     @ManyToOne(fetch = FetchType.LAZY)
17     @JoinColumn(name = "EMAIL", insertable = false, updatable = false)
18     private Users users;
19     
20     @Column(name = "NAME", length = 30)
21     private String name;
22     
23     @ManyToOne(fetch = FetchType.LAZY)
24     @JoinColumn(name = "RESTAURANT_SYSID", insertable = false, updatable = false)
25     private Restaurant restaurant;
26     
27     @Column(name = "DESCRIPTION", length = 500)
28     private String description;
29     
30     @Column(name = "PHOTO")
31     private byte[] photo;
32     
33     @Column(name = "PEOPLE")
34     private Integer people;
35     
36     @Column(name = "WILLPAY")
37     private Integer willPay;
38     
39     @Column(name = "PAYKIND", length = 2)
40     private String payKind;
41     
42     @Column(name = "CANCEL", length = 1)
43     private String cancel;
44     
45     @Column(name = "HTIME")
46     private Date htime;
47     
48     @Column(name = "CTIME")
49     private Date ctime;
50 
51     @Override
52     public String toString() {
53         return "Appointment [sysid=" + sysid + ", name=" + name + ", description=" + description + ", photo="
54                 + Arrays.toString(photo) + ", people=" + people + ", willPay=" + willPay + ", payKind=" + payKind
55                 + ", cancel=" + cancel + ", htime=" + htime + ", ctime=" + ctime + "]";
56     }
57     
58     @ManyToMany(fetch = FetchType.LAZY)
59     @JoinTable(name = "PARTICIPANT",
60         joinColumns = { @JoinColumn(name = "APPOINTMENT_SYSID") },
61         inverseJoinColumns = { @JoinColumn (name = "EMAIL") })
62     private List<Users> particpants;  
63 }
  • 第 16~18 行,定義 Appointment 和 Users 的多對一關係。
  • 第 23~25 行,定義 Appointment 和 Restaurant 的多對一關係,特別注意第 24 行的 RESTAURANT_SYSID,這是 table APPOINTMENT 中關係到 table RESTAURANT 的欄位。
  • 第 58~62 行,定義 Appointment - Talking - Users 即參與者的多對多關係。
一個 Entity 即使定義了這麼複雜的關係,還是可以執行,不會有問題的,但是,後面會說明這樣定義的一些限制,先來看一下怎麼在 table APPOINTMENT 中新增一筆資料。
    @Test
    public void testCreate() throws IOException {
        //讀檔
        Path path = Paths.get("E:/55047183.jpg");
        byte[] photo = Files.readAllBytes(path);

        //寫入DB
        Appointment appointment = new Appointment();
        
        Users user = daoUsers.findOne("hi.steven@gmail.com");
        
        Restaurant restaurant = daoRestaurant.find(2L);
        
        appointment.setUsers(user);
        appointment.setName("除夕圍爐");
        appointment.setRestaurant(restaurant);
        appointment.setDescription("除夕找人聊天");
        appointment.setPhoto(photo);
        appointment.setPeople(2);
        appointment.setWillPay(500);
        appointment.setPayKind("A");
        appointment.setCancel("N");
        appointment.setHtime(Calendar.getInstance().getTime());
        appointment.setCtime(Calendar.getInstance().getTime());
        
        int count = daoApp.create(appointment);
        assertEquals(1, count);
    }
在 Entity Appointment 中,並沒有定義 table APPOINTMENT 的 EMAIL、RESTAURANT_SYSID 兩個欄位,代替的是 Users 及 Restaurant 兩個 Entity,所以在新增資料時,就可以看到如上面紅色的程式碼,設定到 appointment 的是這兩個 Entity 的物件。
  • DAO
public List<Appointment> find(Users user) {
        TypedQuery<Appointment> query = manager.createQuery("select a from Appointment a where a.users = :user ", Appointment.class);
        query.setParameter("user", user);
        return query.getResultList();
    }
  • Unit Test
    @Transactional
    @Test
    public void testFindUser() throws IOException {
        Users user = daoUsers.findOne("hi.steven@gmail.com");
        
        List<Appointment> app = daoApp.find(user);

        for(Appointment a:app) {
            System.out.println(a.getParticpants().toString());
        }
    }
在 Entity Appointment 中沒有 email 欄位,要搜尋是那個使用者起的飯局,就要用該使用者的 Users 物件,如上 DAO 的 JPQL 所示。第二段的 Unit Test 程式也可以看到傳入 DAO 的是事先查詢出來的 Users 物件。從這裡可以看到,在設計 Entity 時,可能不一定要完全按照資料庫的關聯來建立,還要考慮到使用 DAO 時的方便性,以這個例子來說,或許可以 email 變數來代表 EMAIL 欄位,而不要用 Entity Users,雖然會無法由 Entity 中看出資料庫 table 間的關係,但是使用上比較方便的話,也是無妨。





【萬年五K的圍棋 - 星位-小馬步掛】
不管持黑子或白子,我佈局習慣下星位,這時候就會常遇到對手用小馬步掛來搶地盤,通常我就如下圖所示的下: (星位-小馬步掛-一間跳)

這樣下很簡單的黑白雙方各據一方,而且白子下 (4) 之後,黑子又可以有先手到別的戰場上去。問題是,有時候因為週邊情勢的關係,不希望白可以 (4) 佔有邊上領地,就會使用一間夾,如下: (星位-小馬步掛-一間夾)

這是其中一種最常遇到的變化,(7) 衝一手很重要,因為可以讓白棋留下缺陷。

2017年1月21日 星期六

JPA + Spring: 資料庫連線設定 (JNDI@Tomcat)

Tomcat 應該是 Java 世界使用最廣的 AP server? 即使正式營運主機上安裝的是商業的 AP server (IBM WebSphere、BEA WebLogic ...),程式員在開發程式時,也多半是在本機安裝 Tomcat 用來測試,這裡整理一下兩種建立資料庫連線的方式,一個是使用 Tomcat JNDI 的方式,另一個是使用 apache commons dbcp2,用這兩個方式都可以有 connection pool,在大部份的情況下,在正式營運環境會採用 JNDI,在本機開發則可能採用 dbcp2。
  • JDBC driver
將資料庫的 JDBC driver 放到 $Tomcat$/lib 目錄下,我使用的資料庫是 oracle,所以我將 ojdbc7.jar 放到 $Tomcat$/lib 目錄下。
  • 設定 server.xml


在 eclipse 中加入 Server 後,可以選擇要將那些專案加入 server 中,這裡選擇一個命名為 Party 的專案,按【Finish】後,在 server.xml 中就會產生如下內容:
<Context docBase="Party" path="/Party" reloadable="true" source="org.eclipse.jst.jee.server:Party" />
接著在裡面加入我們要的 JNDI 設定,如下:
<Context docBase="Party" path="/Party" reloadable="true" source="org.eclipse.jst.jee.server:Party">
    <Resource auth="Container" driverClassName="oracle.jdbc.OracleDriver" maxIdle="10" maxTotal="20" maxWaitMillis="-1" name="jdbc/DemoDB" password="passw0rd" type="javax.sql.DataSource" url="jdbc:oracle:thin:@192.168.0.102:1521:DemoDB" username="steven"/>
</Context>
這樣設定完,啟動 Tomcat,Tomcat 就會自動建立起資料庫的連線,接下要看看程式要怎麼使用。
  • web.xml
<resource-ref>
     <description>oracleDB</description>
     <res-ref-name>jdbc/DemoDB</res-ref-name>
     <res-type>javax.sql.DataSource</res-type>
     <res-auth>Container</res-auth>
</resource-ref>
在專案的 web.xml 中,加入如上內容,裡面的 res-ref-name 指出了要使用的 JNDI 資源名稱,這裡當然就是上一步驟設定好的資料庫連線。
  • WebInitializer
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { MvcConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }
}
在 spring MVC 的專案中,會以一個繼承 AbstractAnnotationConfigDispatcherServletInitializer 的類別為起始入口,就像一般在寫 application 會以 main 為起始入口一樣,這裡有兩個重要設定,在 getRootConfigClasses 中指出程式啟動後,要優先執行的設定類別,在 getServletConfigClasses 中則指出處理 servlet 的類別。雖然上面的設定都只有一個類別,實際上那是陣列,也就是說可以有多個。這一篇的重點不在 spring MVC,所以後續只會說明 RootConfig。
  • 在啟始類別中建立資料庫連線
 1 @Configuration
 2 @ComponentScan(
 3         basePackages = {"idv.steven.invitation"},
 4         includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Configuration.class),
 5         excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)
 6     )
 7 @Log4j
 8 public class RootConfig {
 9     
10     @Bean
11     @Profile("PROD")
12     public JndiObjectFactoryBean jndiObjectFactoryBean() {
13         JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
14         jndiObjectFB.setJndiName("jdbc/DemoDB");
15         jndiObjectFB.setResourceRef(true);
16         jndiObjectFB.setProxyInterface(javax.sql.DataSource.class);
17         
18         return jndiObjectFB;
19     }
20     
21     @Bean
22     @Profile("TEST")
23     public DataSource dataSource() {
24         BasicDataSource ds = new BasicDataSource();
25         ds.setDriverClassName("oracle.jdbc.OracleDriver");
26         ds.setUrl("jdbc:oracle:thin:@192.168.0.102:1521:DemoDB");
27         ds.setUsername("steven");
28         ds.setPassword("passw0rd");
29         ds.setInitialSize(5);
30         
31         return ds;
32     }
33 }
  1. 如文章一開頭說的,在正式營運環境和開發環境有可能使用不同的資料庫連線方式,這裡就是這樣,建立了兩個不同的連線方式,一個是使用 JndiObjectFactoryBean,另一個使用 DataSource。
  2. 第 14 行設定要使用那個 JNDI 資源,當第 15 行的 setResourceRef 設定為 true 時,就寫成如上面的方式,如果設定為 false,就得寫成 java:comp/env/jdbc/DemoDB。
  3. 第 11、22行指出這兩個 bean 會在什麼環境裡啟動,至於在什麼環境要怎麼判斷? 這是在 web.xml 中設定的,如下,裡面設定了 spring.profiles.active 的值為 PROD,這表示程式啟動時,只有 JndiObjectFactoryBean 會被建立,DataSource 不會被建立,反之亦然。
<context-param>
      <param-name>spring.profiles.active</param-name>
      <param-value>PROD</param-value>
</context-param>
  • JPA config
 1 @Configuration
 2 @EnableTransactionManagement
 3 @EnableJpaRepositories (
 4     basePackages = { "idv.steven.invitation.database.dao" },
 5     entityManagerFactoryRef = "entityManagerFactory",
 6     transactionManagerRef = "jpaTransactionManager"
 7 )
 8 public class JpaConfig {
 9     @Autowired(required=false)
10     private DataSource dataSource;
11     
12     @Autowired(required=false)
13     private JndiObjectFactoryBean jndiObjectFB;
14     
15     @Bean
16     public JpaVendorAdapter jpaVendorAdapter() {
17         HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
18         
19         adapter.setDatabase(Database.ORACLE);
20         adapter.setShowSql(true);
21         adapter.setGenerateDdl(false);
22         adapter.setDatabasePlatform("org.hibernate.dialect.Oracle10gDialect");
23         
24         return adapter;
25     }
26     
27     @Bean
28     public PlatformTransactionManager jpaTransactionManager() {
29         JpaTransactionManager tm = new JpaTransactionManager();
30         tm.setEntityManagerFactory(this.entityManagerFactory().getObject());
31         
32         return tm;
33     }
34         
35     @Bean
36     public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
37         LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
38         if (dataSource == null)
39             emf.setDataSource((DataSource) jndiObjectFB.getObject());
40         else
41             emf.setDataSource(dataSource);
42         emf.setPackagesToScan(new String[] { "idv.steven.invitation.database.entity" });
43         emf.setJpaVendorAdapter(this.jpaVendorAdapter());
44         
45         return emf;
46     }
47 }
這個類別在「JPA + Hibernate + Spring + JUnit (annotation)」中已有說明,這裡只針對粗體的部份補充說明,第 9、12 行一定要有 required=false 的設定,因為兩個只會有一個被建立,如果沒有設定,預設是 required=true,那麼一定會有一個因為在 RootConfig 中沒有建立,在這裡無法被注入。第 38~41 行在設定 DataSource 時,當然要判斷一下現在那個有建立就用那一個。到這裡就搞定了 ...




Forza Gesù -- 一個小女孩成長的故事】
義大利教會合唱團的這首歌在網路上被廣傳,但是最受歡迎的是影片中影片那個版本,是主唱 2010 年 4 歲第一次領唱時的影片,到了 2016 年 10 歲時小女孩長大了,失去了 4 歲時那份混然天成的童真,就沒辦法感動那麼多人了 … 還是來看一下 2010 年的版本吧 ~