2024年4月2日 星期二

SQL Server 查詢對上(或下)一行的計算(例如相加或相減等)

這篇要來記錄一下用 SQL Server 來查詢出
對上(或下)一行的計算(例如相加或相減等),


例如:
我們現在有一個 Table ,有 id 和 amount 兩個 INT 的欄位,
放了三筆資料,如下所示:

id amount
3 45
2 25
1 10

我們希望在 id 由大到小排序的情況,得出 amount 一行與上一行的差值,也就是結果應該要像這樣:

increase
id current amount previous amount increase
3 45 25 20
2 25 10 15
1 10 0 10

上面的表列出了某一行的 amount (current amount) 、上一行的 amount (previous amount) 還有
之前的差值 (increase)。

下面直接上 SQL 語法:

-- 先建立測試用 Table
DECLARE @tempTable TABLE (
	id int not null,
	amount int not null,
	PRIMARY KEY (id)
)
-- 塞入測試資料
INSERT INTO @tempTable(id, amount) VALUES(1, 10)
INSERT INTO @tempTable(id, amount) VALUES(2, 25)
INSERT INTO @tempTable(id, amount) VALUES(3, 45)
-- 檢查一下
SELECT *
FROM @tempTable
ORDER BY id DESC;
-- 進行差值計算,
-- 利用 ROW_NUMBER() 來為每行標上序號值 (rowNumber),
-- 得到的 Table 自己跟自己用 JOIN,JOIN 規則是 Table 的某行 (假設序號是 i) 和上一行 (序號是 i - 1) 做 JOIN
WITH romNumberTable AS (
	SELECT *, ROW_NUMBER() OVER (ORDER BY id DESC) AS rowNumber
	FROM (
		SELECT id, amount
		FROM @tempTable
	) romNumberTable
)
SELECT T1.id, T1.amount AS 'current amount', ISNULL(T2.amount, 0) AS 'previous amount', T1.amount - ISNULL(T2.amount, 0) AS 'increase'
FROM romNumberTable T1 LEFT JOIN romNumberTable T2 ON T1.id = T2.id + 1
ORDER BY id DESC

說明:
我們利用 ROW_NUMBER 將排序好的 Table 標上序號,得到了像這樣的 Table:

id amount rowNumber
3 45 1
2 25 2
1 10 3

然後 Table 自己跟自己做 JOIN,JOIN 規則就是 rowNumber 和 rowNumber - 1 做 JOIN,
這樣就可以將 Table 的某行跟上一行 JOIN 再一起了,JOIN 後的結果會像這樣:

id - current amount - current rowNumber - current id - previous amount - previous rowNumber - previous
3 45 1 2 25 2
2 25 2 1 10 3
1 10 3


可以注意到最後一行因為沒有上一行所以 JOIN 不到東西,所以我們使用 LEFT JOIN 來保留沒有東西 JOIN 的那行資料。

成功 JOIN 後,就可以自由地做我們想要做的計算了。

參考資料:

  1. SQL如何计算当前行减去上一行的值

2024年2月29日 星期四

AngularJS - 自製分頁 Filter

紀錄一下 AngularJS 自製分頁 Filter 的方法

HTML:

<div ng-app="app" ng-controller="controller as ctrl">
  <div ng-repeat="item in ctrl.itmeList | paging:ctrl.page:ctrl.pageSize">
    {{item}}
  </div>
  
  <div>  
    Page: <input type="number" ng-model="ctrl.page"/>
  </div>
  <div>  
    Page size: <input type="number" ng-model="ctrl.pageSize"/> Page
  </div>
</div>

Javascript:

var app = angular.module("app", []);
app.controller("controller", [function() {
	var self = this;
  self.itmeList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  self.page = 1;
  self.pageSize = 3;
}]);

app.filter('paging', function() {
	    return function(input, page, pageSize) {
	        return input.slice(page * pageSize - pageSize, page * pageSize);
	    }
});

JsFiddle 範例:

參考資料:

  1. Filters

Javascript 濾掉 Object 中特定屬性的方法

紀錄一下 Javascript 濾掉 Object 中特定屬性的方法,
可以利用 reduce() 來達成,實作程式碼範例如下:

var obj = {"1": 1, "2": 2, "3": 3};
obj = Object.keys(obj).filter(function(key) {
	return key == 1 || key == 3; //濾出值為 1 和 3 的 key
}).reduce(function(newObj, key){
  newObj[key] = obj[key];
	return newObj;
}, {});
console.log(obj); // {1: 1, 3: 3}

Code Point, Surrogate Pair 和 HTML Unicode 編碼

在這篇文章中,我要來紀錄一下 Code Point (字符代碼、碼位、編碼位置) 、Surrogate Pair (代理對)、HTML Unicode 編碼之間的關係,並且示範一下在 Java 中儲存的 UTF-16 字元要如何轉成可在 HTML 中正確顯示的 Unicode 編碼。

首先,先說明一下以下名詞的意義:

  1. Code Point (字符代碼、碼位、編碼位置):
    Code Point 是一個字符的 Unicode 編碼代碼。
    參考 Unicode字元平面對映,Unicode 目前分成 17 組 平面 (Plane),每平面有 65536 (2 的 16 次方),編碼範圍從 0x0000 到 0x10FFFF ,每一個碼位代表某一個字符 (目前還沒有有全部使用到),例如
    "我" 的 Code Point 是 25105 (也就是 0x6211)
    "👍" 的 Code Point 是 128077 (也就是 0x1F44D)
  2. Surrogate Pair (代理對):
    Surrogate Pair 是一種儲存表示字符的一種方式,專門用來儲存 UTF-16 無法用 16 Bit 表示的字符,
    以 "👍" 為例,
    對於 Code Point 是 0xFFFF 以下的字符 UTF-16 會用 16 Bit (2 Byte) 去儲存,
    而對於 "👍" 這種 Code Point 大於 0xFFFF 的字符則會使用 Surrogate Pair 的方式用 32 Bit (4 Byte) 來儲存。

    規則如下,括弧中是以 "👍" 做的例子:
    先將字符的 Code Point 減去 0x10000 (0x1F44D - 0x10000= 0xF44D),
    把得到的值高位部份用 0 填充得到一個 20 Bit 的結果 (0000 1111 0100 0100 1101),
    再將前 10 Bit 高位加上 110110 成為 High Surrogate (1101 1000 0011 1101 = 0xD83D = 55357),
    將後 10 Bit 高位加上 110111 成為 Low Surrogate (1101 1100 0100 1101 = 0xDC4D = 56397),
    最後 UTF-16 會用 High Surrogate + Low Surrogate 為一組的方式 (稱為 Surrogate Pair) 用 32 Bit (4 Byte) 來表示這樣一個字符 ,以  "👍"  來說就是 0xD83D 0xDC4D。

    可以注意到,因為 1101 10 和 1101 11 已作為辨識 High/Low Surrogate 的用途,
    所以 1101 1000 0000 0000 (0xD800) ~ 1101 1111 1111 1111 (0xDFFF) 之間的值不會對應到 任何一個 UTF-16 字符,
    而在 Unicode 中,0xD800 ~ 0xDCFF 之中的 Code Point 也是設計給 UTF-16 的 Surrogate 使用的區塊,
    也就沒有字符的 Code Point 在 0xD800 ~ 0xDCFF 之中。
    可以參考 Unicode字元平面對映

    以 Java 來說,字符是用 UTF-16 來儲存的,
    所以 "👍" 跟 "\ud83d\udc4d" 是等價的。

  3. HTML Unicode 編碼 (或稱 HTML Character Entity 、HTML 字符實體編碼):
    在 HTML 中可以使用 Unicode 表示特殊字符,可以直接使用 10 進位及 16 進位以 Code Point 表示,以 "👍" 為例,可以用 &#128077; 或 &#x1f44d 來表示,
    注意這裡不需使用 Surrogate Pair ,因為 HTML Unicode 是支援大於 0xFFFF 的 Code Point 字符的。

    同樣的,使用 CSS 的時候,也不需要使用 Surrogate Pair,例如:
    <style>
        div:after {
          content: "\1f44d";
        }
      </style>
    

    不過要注意的是,如果是使用 Javascript 的話,還是需要使用 Surrogate Pair 的表達方式,例如一樣以 "👍" 為例:
    console.log("\ud83d\udc4d");
    

最後,用 Java 示範一下上述的觀念,可以配合著看來更好的理解實際上的例子:
使用 JDK 11,
Maven Dependency :

<!-- https://mvnrepository.com/artifact/commons-lang/commons-lang -->
	<dependency>
	    <groupId>commons-lang</groupId>
	    <artifactId>commons-lang</artifactId>
	    <version>2.4</version>
	</dependency>
	
    <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
	<dependency>
	    <groupId>org.apache.commons</groupId>
	    <artifactId>commons-lang3</artifactId>
	    <version>3.14.0</version>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-text -->
	<dependency>
	    <groupId>org.apache.commons</groupId>
	    <artifactId>commons-text</artifactId>
	    <version>1.11.0</version>
	</dependency>

Java:

package test;

import java.util.HashMap;
import java.util.Map;

public class UTF16Test {

	public static void main(String[] args) {
		//👍
		//Code Point (字符代碼、碼位、編碼位置): 0x1f44d = 128077 (10進位)
		//UTF-16 Surrogate Pair: \ud83d \udc4d = 55357 56397 (10進位)
		//High Surrogate: \ud83d
		//Low Surrogate: \udc4d
		//HTML Character Entity (HTML 字符實體編碼): &128077
		String str = "👍";
		
		//確認是 Code Point = 128077 是否就是 "👍"
		System.out.println(Character.toString(128077).equals(str)); // true
		
		//比較 "👍" 是否與 "\ud83d\udc4d" 等價
		System.out.println("\ud83d\udc4d".equals(str)); // true
		
		//"👍" 字串長度為2,因為是用2個 16 Bit 表示,一個 16 Bit 被當成1個 Char
		System.out.println(str.length()); // 2
		System.out.println(str.toCharArray().length); // 2
		
		//"👍" 只含有一個 Code Point,所以字串的 Code Point 陣列長度是 1
		System.out.println(str.codePoints().toArray().length); // 1
		
		//org.apache.commons.lang.StringEscapeUtils.escapeHtml(String) 
		//無法正確地將 "👍" 編碼成 HTML Character Entity,應該算是 Bug ?
		//輸出是 &#55357;&#56397;
		//這種編碼是無法在 HTML page 裡正常顯示的
		System.out.println(org.apache.commons.lang.StringEscapeUtils.escapeHtml(str)); // &#55357;&#56397;
		
		//org.apache.commons.text.StringEscapeUtils 不會對 👍 進行編碼處理,
		//所以輸出不變一樣是 "👍" ,
		//不過不編碼一樣是不能在 HTML page 裡正常顯示的
		System.out.println(org.apache.commons.text.StringEscapeUtils.escapeHtml4(str)); //👍
		
		//示範如何正確地將 "👍" 轉成可以在 HTML page 正常顯示的 HTML Character Entity,
		//也就是 &#128077;
		System.out.println(convertSurrogatePairToHtmlCode(str)); //&#128077;
		
		//示範如何從 Code Point 計算 Surrogate Pair (包括 High Surrogate 和 Low Surrogate)
		//以 "👍" 為例, Surrogate Pair 就是 \ud83d \udc4d = 55357 56397 (10進位)
		Map<String, Integer> surrogatePair = calculateSurrogatePairFromCodePoint(str.codePointAt(0));
		int highSurrogate = surrogatePair.get("highSurrogate");
		int lowSurrogate = surrogatePair.get("lowSurrogate");
		System.out.println(highSurrogate); // 55357
		System.out.println(lowSurrogate); // 56397
		System.out.println(Character.isHighSurrogate((char) highSurrogate));
		System.out.println(Character.isLowSurrogate((char) lowSurrogate));
		
		//示範如何從 Surrogate Pair 計算 Code Point
		//以 "👍" 為例, Code Point 就是 0x1f44d = 128077 (10進位)
		int codePoint = calculateCodePointFromSurrogatePair(highSurrogate, lowSurrogate);
		System.out.println(codePoint); // 128077
		System.out.println(Character.isSupplementaryCodePoint(codePoint)); // true		
	}
	
	//將文字轉成可以正常顯示的 HTML 編碼
	public static String convertSurrogatePairToHtmlCode(String str) {
		String htmlCode = "";
		
		StringBuilder htmlCodeStringBuilder = new StringBuilder();
		
		str.codePoints().forEach((int codePoint) -> {
			if (Character.isSupplementaryCodePoint(codePoint)) {
				htmlCodeStringBuilder.append(String.format("&#%d;", codePoint));
			} else {
				htmlCodeStringBuilder.append(org.apache.commons.text.StringEscapeUtils.escapeHtml4(Character.toString(codePoint)));
			}
		});
		
		htmlCode = htmlCodeStringBuilder.toString();
		return htmlCode;
	}
	
	//計算一個 Code Point 的 Surrogate Pair (代理對),
	//Surrogate Pair 含有兩部份,分別是 High Surrogate 和 Low Surrogate
	public static Map<String, Integer> calculateSurrogatePairFromCodePoint(int codePoint) {
		Map<String, Integer> surrogatePair = new HashMap<>();
		
		int s = codePoint - 0x10000;
		int highSurrogate = (s >> 10) + 0xd800;
		int lowSurrogate = (s & 0x3ff) + 0xdc00;
		
		surrogatePair.put("highSurrogate", highSurrogate);
		surrogatePair.put("lowSurrogate", lowSurrogate);
		
		return surrogatePair;
	}
	
	//計算一組 High Surrogate 和 Low Surrogate 代表的 Code Point 
	public static int calculateCodePointFromSurrogatePair(int highSurrogate, int lowSurrogate) {

		return ((highSurrogate - 0xd800) << 10) + (lowSurrogate - 0xdc00) + 0x10000;
	}
}

推薦一個線上有人寫的小工具,作者解釋了 Surrogate Pair 並寫了線上幫忙互轉 Code Point 和 Surrogate Pair 的功能:
Surrogate Pair Calculator etc.

參考資料:

  1. The Surrogate Pair Calculator etc.
  2. Unicode Surrogate Pairs
  3. Emoji character sequence &#55357;&#56391; breaks old XML process
  4. How to find the surrogate pair of a symbol in java

2024年2月15日 星期四

Java EE Spring JDBC tmeplate 執行 PostgreSQL 的 Function

這篇要來紀錄如何使用 Java EE Spring JDBC template 來執行
PostgreSQL 的 Function。

首先我們先建立一個測試用的 Table:

兩個 PostgreSQL Function,如以下 SQL 語法:
CREATE TABLE IF NOT EXISTS public.testTable
(
    id integer NOT NULL,
	title text NOT NULL,
	PRIMARY KEY(id) 
)
並且塞入一些資料:
INSERT INTO public.testTable(id, title) VALUES(1, '111');
INSERT INTO public.testTable(id, title) VALUES(2, '222');
INSERT INTO public.testTable(id, title) VALUES(3, '333');
然後建立三個 Function,以下三個 Function 的作用基本一樣,都是接受傳入的 param_id 變數,然後進行
SELECT * FROM testTable WHERE id = param_id
的查詢,不同的是傳回的 type 不同,呼叫 Functoin 的方式也有所不同,
可以注意到的是,有兩個 Function 的名稱一樣 (都取名為 queryUsingReturnedRefcursor),不過 Function 簽名不同 (傳入參數不同),這是為了演示 PostgreSQL 能夠支持同名 Function 故意取名的,類似 Java 的 Function Overload:
  1. queryUsingReturnedSetof(param_id integer):
    CREATE OR REPLACE FUNCTION queryUsingReturnedSetof(param_id integer) RETURNS SETOF testTable AS $$
      SELECT * FROM testTable WHERE id = param_id;
    $$ LANGUAGE sql;
    
    說明:
    此 Function 傳回值 type 為 SETOF,相當於傳回 Table 的資料行集合,呼叫 Function 的方式為如下 SQL 語法:
    SELECT * FROM queryUsingReturnedSetof(2);
  2. queryUsingReturnedRefcursor(param_id integer):
    CREATE OR REPLACE FUNCTION queryUsingReturnedRefcursor(param_id integer) RETURNS refcursor AS $$
    DECLARE ref refcursor;
    BEGIN
      OPEN ref FOR SELECT * FROM testTable WHERE id = param_id;
      RETURN ref; 
    END;
    $$ LANGUAGE plpgsql;
    
    說明:
    此 Function 傳回值 type 為 refcursor,為一個 cursor ,不是 Table 的資料行集合,直接呼叫的話只會得到一個 cursor ,必須要配合 FETCH 指令才能遍歷每一行的資料,例如以下語法:
    SELECT queryUsingReturnedRefcursor(2); FETCH ALL IN "<unnamed portal 6>";
    其中,<unnamed portal 6> 是被回傳的 cursor 名稱,不過在實際上每次執行 Function 後回傳的 cursor name 是會變化的,每次都不一樣,
    而執行完 Function 後, cursor 就消失了,導致執行 FETCH 時會有
    ERROR: cursor "<unnamed portal xxx>" does not exist 
    的錯誤,所以上面那樣的語法沒辦法正確地得到查詢結果資料行,
    要解決上面的問題,可以看下一個 Functoin 的說明,
    其想法是把想要的 cursor name 傳進 Function 以指定回傳的 cursor name。

    不過如果是用 Java 去呼叫的話,是可以成功執行並得到資料行的,
    之後可以看到我們可以用 Java 去指定 cursor name。
  3. queryUsingReturnedRefcursor(ref refcursor, param_id integer):
    CREATE OR REPLACE FUNCTION queryUsingReturnedRefcursor(ref refcursor, param_id integer) RETURNS refcursor AS $$
    BEGIN
      OPEN ref FOR SELECT * FROM testTable WHERE id = param_id;
      RETURN ref; 
    END;
    $$ LANGUAGE plpgsql;
    
    說明:
    此 Function 傳回值 type 為 refcursor,跟上一個 Fucntion 一樣,只差在多傳進一個指定的 cursor name,這樣我們就可以用已知的 cursor name 來取得資料行,呼叫方式如下:
    SELECT queryUsingReturnedRefcursor('xxx_cur', 2); FETCH ALL IN "xxx_cur";
    其中 xxx_cur 可以隨意更換成想要的 cursor name。
接下來是 Java EE Spring 的部份了。
下圖為測試專案的檔案結構,紅框處的是主要專案檔案:






因為重要的地方主要是 DAO 使用 JdbcTemplate 的部份,
為了簡單起見,所以這裡只展示 DAO 的部份,也就是 com.dao.MyDAO.java,
其他 Java EE Spring、Data Source 、使用到的 lib (使用 Maven POM 檔設定) 等的設定可以參考文章最後的源碼下載分享

MyDAO.java :
package com.dao;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcCall;
import org.springframework.stereotype.Repository;

@Repository
public class MyDAO {

	@Autowired
	@Qualifier("postgresql_JdbcTemplate")
	private JdbcTemplate postgresql_JdbcTemplate;
	
	public List<Map> queryUsingReturnedSetof(int id) {
		List<Map> dataList = postgresql_JdbcTemplate.query("SELECT * FROM queryUsingReturnedSetof(?);", new RowMapper<Map>() {		
	        @Override
	        public Map mapRow(ResultSet rs, int rowNumber) throws SQLException {
	        	Map<String, Object> map = new HashMap<>();
	        	
	        	map.put("id", rs.getInt("id"));
	        	map.put("title", rs.getString("title"));
	        	
	            return map;
	        }
	    }, id);
		
		//for test
		for (Map data : dataList) {
			System.out.println(data.get("id") + ", " + data.get("title"));
		}
		
		return dataList;
	}
	
	public List<Map> queryUsingReturnedRefcursor(int id) {
		String customCursorName = "xxx_cur";
		
		SimpleJdbcCall simpleJdbcCall = new SimpleJdbcCall(postgresql_JdbcTemplate)
				                        .withProcedureName("queryUsingReturnedRefcursor")
				                        .declareParameters(new SqlParameter("id", Types.INTEGER))
				                        .withoutProcedureColumnMetaDataAccess()
				                        .returningResultSet(customCursorName, new RowMapper<Map>() {
				                	        @Override
				                	        public Map mapRow(ResultSet rs, int rowNumber) throws SQLException {
				                	        	Map<String, Object> map = new HashMap<>();
				                	        	
				                	        	map.put("id", rs.getInt("id"));
				                	        	map.put("title", rs.getInt("title"));
				                	        	
				                	            return map;
				                	        }
				                	    });
		
		Map returnedMapContainsCursorName = simpleJdbcCall.execute(new MapSqlParameterSource().addValue("id", id));
		List<Map> dataList = (List) returnedMapContainsCursorName.get(customCursorName);
		
		//for test
		for (Map data : dataList) {
			System.out.println(data.get("id") + ", " + data.get("title"));
		}
		
		return dataList;
	}
}

說明:
queryUsingReturnedSetof(int id) 對應到 PostgreSQL 中的 queryUsingReturnedSetof(param_id integer) 這個 Function。

queryUsingReturnedRefcursor(int id) 對應到 PostgreSQL 中的 queryUsingReturnedRefcursor(param_id integer) 這個 Function,可以看到它可以讓我們指定一個回傳的 refCursor name,
simpleJdbcCall.execute() 並不直接回傳 data list 資料,而是一個 Map,
就像上面我們直接用 Sql 語法查詢那樣回傳的 refCursor 的資料,
我們需要再用指定的 refCursor name 當作 Key 從 Map 中才能獲取真正的 data list 資料。

源碼下載分享:

2024年1月1日 星期一

SpringMVC 動態設定 Properties 給 $Value 用的方法 - 使用 PropertySourcesPlaceholderConfigurer

這篇要來記錄在 Spring MVC 中,
要如何動態地設定 Properties 給 $Value 用的方法。

我們都知道在 spring MVC 中,如果設定了 PropertySourcesPlaceholderConfigurer ,
我們可以把 property file 中的 Key-Value 值讀進來,
之後在 @Component, @Controller, @Service, @Bean 等 Spring 為我們裝配的物件中,
就可以使用 @Value 來取得 property value,例如:

PropertySourcePlaceholderConfigurer 的設定,有兩種方法,使用 xml 或 java config:

  1. 使用 xml:
  2. mvc-config.xml:

    <bean id="propertyConfigurer" class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
    	<property name="locations">
    		<list>
    			<value>classpath:xxx1.properties</value>
    			<value>classpath:xxx2.properties</value>
    		</list>
    	</property>
    	<property name="ignoreUnresolvablePlaceholders" value="true" />
    </bean>
    
  3. 使用 java config:
  4. import java.io.IOException;
    import java.util.Properties;
    
    import org.springframework.beans.BeansException;
    import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
    import org.springframework.core.env.Environment;
    
    @Configuration
    @PropertySource("classpath:xxx1.properties")
    @PropertySource("classpath:xxx2.properties")
    public class MyPropertyConfiguration {
    	
    	@Bean
    	public static PropertySourcesPlaceholderConfigurer setPropertySourcesPlaceholderConfigurer() {
    		PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
    		propertySourcesPlaceholderConfigurer.setIgnoreUnresolvablePlaceholders(true);
    		return propertySourcesPlaceholderConfigurer;
    	}
    }
    
    

Java 中就可以使用 @Value:

@Component
public class Test
	...
	@Value("${xxxPropertyKey}")
	String xxxPropertyKey;
	...
}

但是這樣子因為 property file 的內容本身是寫死的,
如果今天我們想要注入的 @Value 值是專案啟動時動態決定的,
例如從 Database 中讀取而來、從環境變數 (Environment) 中取得、從網路上讀取 (例如 AWS 的 Parameter Store 服務) 等等,
就不能這樣設定了。

我們必須自己實作 PropertySourcesPlaceholderConfigurer 並覆寫掉 (Override) 它的
postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
方法,在裡面實作增添修改 property key-value 值的程式邏輯,
實作的範例如下:

Java:

import java.io.IOException;
import java.util.Properties;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.env.Environment;

@Configuration
@PropertySource("classpath:xxx1.properties")
@PropertySource("classpath:xxx2.properties")
public class MyParameterStorePropertyConfiguration {
	
	//如果原本這邊就有做讀 xxx1.properties 和 xxx2.properties 的 PropertySourcesPlaceholderConfigurer 設定,
	//不能因為多做了 setMyParameterStorePropertySourcesPlaceholderConfigurer_2() 就把 setMyParameterStorePropertySourcesPlaceholderConfigurer_1() 拿掉,
	//不然 xxx1.properties 和 xxx2.properties 的 property 都會被
	//setMyParameterStorePropertySourcesPlaceholderConfigurer_2() 移掉,因為
	//setMyParameterStorePropertySourcesPlaceholderConfigurer_2() 中已經用新的 Properties 取代了原本從 xxx1.properties 和 xxx2.properties 讀到的 property
	@Bean
	public static PropertySourcesPlaceholderConfigurer setMyParameterStorePropertySourcesPlaceholderConfigurer_1() {
		PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
		propertySourcesPlaceholderConfigurer.setIgnoreUnresolvablePlaceholders(true);
        //如果不想用 @PropertySource,也可以直接用程式設定,像下面這樣
        //propertySourcesPlaceholderConfigurer.setLocations(new ClassPathResource("xx1.properties"), new ClassPathResource("xx2.properties"));
		return propertySourcesPlaceholderConfigurer;
	}
	
	@Bean
	public static PropertySourcesPlaceholderConfigurer setMyParameterStorePropertySourcesPlaceholderConfigurer_2(Environment environment) {
    	//可視需要看要不要取得環境變數 (Environment)
		PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer() {
			
			@Override
			public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
				Properties properties = new Properties();				

				//動態地設定 property,key-value 值可能從其他地方來,而非寫死的 property file,
				//例如從 Database 中讀取而來、從環境變數 (Environment) 中取得、從網路上讀取 (例如 AWS 的 Parameter Store 服務) 等等
				//do your logic code here to get xxxKey1, xxxValue1, xxxKey2, xxxValue2, ......
				properties.put(xxxKey1, xxxValue1);
				properties.put(xxxKey2, xxxValue2);
				//......
				
				setProperties(properties);
				super.postProcessBeanFactory(beanFactory);
			}
		};
		propertySourcesPlaceholderConfigurer.setIgnoreUnresolvablePlaceholders(true);
		
		
		return propertySourcesPlaceholderConfigurer;
	}
}

參考資料:

  1. 注入設定檔
  2. Spring PropertyPlaceholderConfigurer load from DB
  3. Spring @Configuration作用

2023年12月26日 星期二

Java 日期時間(DateTime, TimeZone, Offset 等)操作筆記 - 使用 DateTimeFormatter, OffsetDateTime, LocalDateTime, TemporalAdjusters 等

紀錄下 Java 的各種日期時間操作

使用 JDK 11

package test;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.Calendar;
import java.util.Date;

public class DateTimeFormatterTest {

	public static void main(String[] args) {
		String timeStr = "2023-01-01T00:00:00+00:00";
		//使用 DateTimeFormatter
		DateTimeFormatter dtf_utc0 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssxxx")
													  .withZone(ZoneId.of("+0000"));
		//上例可以簡單寫成 DateTimeFormatter dtf = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
		OffsetDateTime offsetDateTime = OffsetDateTime.parse(timeStr, dtf_utc0);
		//轉換成 Date
		Date date = Date.from(offsetDateTime.toInstant());
		//轉換成 Calendar
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        
        //將 Calendar 轉成 Date,再從 Date 轉成 OffsetDateTime
        //atOffset(Offset) 只是用來把 Date 轉成 OffsetDateTime,
        //Offset 設多少不重要,因為都可以再用 DateTimeFormatter.format() 將其 format 成其他時區的日期文字 
        OffsetDateTime offsetDateTime2 = calendar.getTime() // 將 Calendar 轉成 Date
        								 .toInstant()
        								 .atOffset(ZoneOffset.of("+0800"));
        
        //用不同時區和 format 印出日期,可以注意 xxx 和 xx 的不同,xxx會印出冒號而xx不會
        DateTimeFormatter dtf_utc3 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssxx")
        							 				  .withZone(ZoneId.of("+0300"));
        System.out.println(dtf_utc3.format(offsetDateTime2)); //2023-01-01T03:00:00+0300
        
        ////////////////////////// - LocalDate, LocalTime 等串接練習 - //////////////////
        //只有日期沒有時間的 DateTimeFormatter
  		DateTimeFormatter dtf_utc0_dateOnly = DateTimeFormatter.ofPattern("yyyy-MM-dd")
  													           .withZone(ZoneId.of("+0000"));
  		//含有有日期及時間的 DateTimeFormatter
  		DateTimeFormatter dtf_losAngeles_withTime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
  				                                                     .withZone(ZoneId.of("America/Los_Angeles"));
  		//只有日期不含時間、時區的 LocalDate
  		LocalDate localDate = LocalDate.parse("2024-01-01", dtf_utc0_dateOnly);
  		System.out.println(localDate); //2024-01-01
  		//只有時間不含日期、時區的 LocalTime
  		LocalTime localTime = LocalTime.of(0, 0, 0);
  		System.out.println(localTime); //00:00
  		//LocalDateTime 含有日期和時間,但不含時區,可以用 LocalDate 加 LocalTime 組合而成
  		LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
  		System.out.println(localDateTime); //2024-01-01T00:00
  		//OffsetDateTime 含有日期、時間和時區,可以用 LocalDateTime 加 ZoneOffset 組合而成
  		OffsetDateTime offsetDateTime1 = OffsetDateTime.of(localDateTime, ZoneOffset.of("+0000"));
  		
  		System.out.println(dtf_losAngeles_withTime.format(offsetDateTime1)); //2023-12-31 16:00:00		
  		
  		//OffsetDateTime 也提供多種不同的建立 Method,
  		//例如其中的一個 OffsetDateTime.of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneOffset offset)
  		//它可以用以1來表示1月 (而不是用0來表示1月),如下例:
  		OffsetDateTime offsetDateTime_uct0 = OffsetDateTime.of(2024, 6, 6, 20, 0, 0, 0, ZoneOffset.of("+0000"));
  		System.out.println(offsetDateTime_uct0); //2024-06-06T20:00Z
  		//得到當日是星期幾,可能會依不同時區而不同
  		System.out.println(offsetDateTime_uct0.getDayOfWeek()); //THURSDAY
  		//可以把日期、時間提出來換上不同時區建立新的 OffsetDateTime,此時其 Instant 代表的毫秒數可能會改變 
  		System.out.println(OffsetDateTime.of(offsetDateTime_uct0.toLocalDateTime(), ZoneOffset.of("+0800"))); //2024-06-06T20:00+08:00
  		//可以用 OffsetDateTime.withOffsetSameInstant(ZoneOffset)在Instant代表毫秒數不變的情況下修改時區
  		OffsetDateTime offsetDateTime_uct8 = offsetDateTime_uct0.withOffsetSameInstant(ZoneOffset.of("+0800"));
  		System.out.println(offsetDateTime_uct8); //2024-06-07T04:00+08:00
  		//可以看到因為時區不同而顯示不同的星期
  		System.out.println(offsetDateTime_uct8.getDayOfWeek()); //FRIDAY
  		
  		//////////////////////////- TemporalAdjusters 練習 - //////////////////
  		//配合 OffsetDateTime.with() 和 TemporalAdjusters 可以得到特定條件的修改時間,
  		//例如此例使用 TemporalAdjusters.previousOrSame(DayOfWeek.FRIDAY)
  		//可以得到往前算的最近一次星期五(包含今天)
  		OffsetDateTime lastFirdayIncludeToday_utc0 = offsetDateTime_uct0.with(TemporalAdjusters.previousOrSame(DayOfWeek.FRIDAY));
  		System.out.println(lastFirdayIncludeToday_utc0); //2024-05-31T20:00Z
  		
  		OffsetDateTime lastFirdayIncludeToday_utc8 = offsetDateTime_uct8.with(TemporalAdjusters.previousOrSame(DayOfWeek.FRIDAY));
  		System.out.println(lastFirdayIncludeToday_utc8); //2024-06-07T04:00+08:00
	}
}

參考資料:

  1. DateTimeFormatter (Java Platform SE 8 )