JMSTemplate 的教學


前言(一堆廢話)

嘿,大家好久不見!我發現距離上次在 Blogger 發文,竟然已經過了快半年了。想想還真有點不好意思,不過這次我可是帶著新東西來跟大家分享的!

這次要聊的是 Java JMSTemplate。為什麼突然要學這個呢?主要是因為我最近換了新公司,而他們那邊剛好需要用到 訊息佇列 (Message Queue) 的技術,所以我就趁這個機會好好學了一下。

(老實說,我其實都工作六年了,這東西早就該會了啦!)

Message Queue 是什麼和目的

這部分我覺得我讀的書的寫的很棒,因此想說直接引用過來。

假設現在是星期五 16:55 ,在幾分鐘就可以開始期待已久的休假了。再過幾分鐘你的飛機就要起飛了,但是在這幾分鐘你需要先確定主管和同事了解你的工作進度,這樣他們才有機會於下週一接續你的工作。這時候該怎麼辦?
這時候避免打擾到他們最好的做法就是留紙條或發送 E-mail ,如此一來他們就可以在下週一或有空的時候看到 E-mail 接續工作。
這段很完美的解釋了 Message Queue,的概念,將訊息傳到佇列,之後根據佇列的先進先出讓服務吸收消化,最後調整服務一次接收多少 Message 。

如此一來可以帶來以下幾個好處:

  • 解耦合:
    • 發送者可以完全不管接收者狀態與是否可以承受,只需要將訊息丟到 Queue 就好了
    • 系統不同的區塊可以獨立開發(其實就跟現今流行的微服務類似概念)
  • 非同步處理:發送者可以完全不需要等待另一方處理就可以繼續處理下一個 request。這樣講有點抽象,可以想像麥當勞點餐系統,櫃檯的人員不需要完全確認廚房是否收到單就可以接下一個客人了。
  • 提高可靠性:很多提供 Message Queue 的元件會提供持久化的功能,他會將 message 存起來,這樣一來就算遇到某個 Message 處理有問題,也可以很快查到並且做後續 support。
  • 水平擴展:我們可以很輕易地增加消費 Message 的處理人員,這樣就可以提高 Queue 的消化速度,已應付高流量又要快速處理的需求。
  • 等等

上述只有放我自己認為很重要的優點,但其實還有很多的優點。

名詞

那優點介紹完之後我們來聊一下在 Message Queue 常會用到的名詞,

  • 佇列(Queue):就像是排隊一樣,先進先出。
  • 生產者(Producer):就是指生成 Message 的服務
  • 消費者(Consumer):就是指多個
  • 一對一(one to one, O2O):一個生產者生產的訊息對應一個消費者
  • 點對點模式(Point to Point, P2P):一對一就是點對點模式
  • 一對多(one to many, O2M):一個生產 Message 給多個使用 Message 的人
  • 發布/訂閱模式(Pub/Sub):當一個生產 Message 的人對應多個使用 Message 的人就是 Pub/Sub 模式
  • 發布者(Pub):由於一個 Message 會被多人使用,此時就不是生產者了,而是發布者
  • 訂閱者(Sub):由於多個人使用同一個 Message,因此此時使用 Message 的人就會被算是訂閱者。

環境

接下來是環境,我們預計在本機使用 ActiveMQ 當作我們練習 Message Queue 和 Pub/Push 的平台。

  1. 首先第一件事就是下載 ActiveMQ: https://activemq.apache.org/components/artemis/download/
  2. 下載之後解壓縮
  3. 到一個你你想要放 activeMQ 實例的地方建立資料夾
  4. 回到剛剛解壓縮的資料夾並進到 bin 資料夾
  5. 下以下的命令來建立 ActiveMQ 的實例: ./artemis create 你剛剛建立資料夾的路徑
  6. 此時應該會要求輸入使用者帳號密碼,這邊可以自己設定,以這篇文章來說我是設成 admin 和 admin
  7. 完成之後到存放 ActiveMQ 的資料夾可以看到多了一些資料夾和文件,由於這篇 blogger 主要想要聚焦在程式碼上,因此這邊就不多解釋檔案的內容。
  8. 進到 bin 資料夾後執行 ./artemis run 之後就會開始出現 log
  9. 在啟動完成的 log 的最後一行會看到一個網址,以我這邊看到的是:http://localhost:8161/console 這個是用來看 ActiveMQ 狀態的網址,後續可以用來檢查 JMSTemplate 傳送 Message 之後的狀態

到這邊環境就算是完成了,接下來就是正題:Java 開如何配合 ActiveMQ 使用。

如何使用 JMSTemplate

這個段落我會先說明各個方法,最後附上一個 Sample project 並且說明如何透過該 project 來玩。

Lib

由於我的情境是會在 Spring 中使用 Message Queue ,因此這邊會直接導入 Spring 相關的 lib。以下是我使用到的 Lib 。

  • org.springframework.boot:spring-boot-starter-artemis
  • org.springframework.boot:spring-boot-starter-web
    可以看到除了基本的 spring starters-web 以外就只需要再多一個 spring-boot-starter-artemis 就好了

application.yml

接下來就是設定 spring 連線到 ActiveMQ 了,以下是我的相關設定和說明:

spring:
  artemis:
    user: admin #帳號
    password: admin #密碼
    broker-url: tcp://localhost:61616 #連線資訊

都設定完成之後我們就可以開始來玩 JMSTemplate 了。

P2P 模式

發送 message

在 JMSTemplate 中我覺得最簡單易用的方法是:

public void send(
    String destinationName,
    org.springframework.jms.core.MessageCreator messageCreator
)

這個方法使用起來很簡單,我們直接看一段範例 code:

@Autowired
private JmsTemplate jmsQueueTemplate;

public void someFunction() {
    jmsQueueTemplate.send("test3", new MessageCreator() {
        @Override
        public Message createMessage(Session session) throws JMSException {
            return session.createTextMessage("要放入的訊息");
        }
    });
}

下面是透過 lambda 簡化之後的 code。

@Autowired
private JmsTemplate jmsQueueTemplate;

public void someFunction() {
    jmsQueueTemplate.send("test3",
        session -> session.createTextMessage("要放入的訊息"));
}

這段程式碼傳入參數有兩個,分別是 destinationName 和 messageCreator,下列是相關說明:

  • destinationName: 主要用於設定類似主題的東西,但是在 ActiveMQ 並不像是 Kafka 用 Topic ,在 ActiveMQ 使用的是 Address ,詳細的內容或許可以找時間再寫一篇 blogger 來聊這一塊,這邊只要先記得他是類似主題的概念。
  • messageCreator:就如同變數名稱,他是用於建立 Message 的,他會傳入 session,之後我們可以透過 session 的createTextMessage 方法傳入我們要放的訊息。
    寫到這邊就算是完成最簡單的 sendMessage 了,但這時候會發現這樣的做法其實不太符合實際開發,我們通常會希望傳入一個 Dto 到方法裡,並且自動轉成 json 傳進 Message Queue ,因此這邊要再來介紹另外一個很像的方法: convertAndSend

下面是他的方法:

public void convertAndSend(
    String destinationName,
    Object message
)

我們可以看到原本的 MessageCreator 變成了 Object message ,沒錯,Object message 就是用來放傳入的 pojo 的,因此我們趕快試試看

@Autowired
private JmsTemplate jmsQueueTemplate;

public void someFunction() {
    var somePojo = new SomePojo(); //可以自己建立一個 pojo
    jmsQueueTemplate.send("test3", somePojo);
}

這時候運行並且執行到的時候會跳出 org.springframework.jms.support.converter.MessageConversionException: Cannot convert object of type [com.crater.learnjms.dto.SendMessageDto] to JMS message. Supported message payloads are: String, byte array, Map<String,?>, Serializable object.
沒錯,恭喜你跳進第一個坑,在沒有設定 Bean 的前提下,jmsTemplate 預設會使用 SimpleMessageConverter 那該怎麼辦?
難道我只能使用 Map 之類的方式傳送了嗎?
這點不用擔心,有沒有注意到 JMSTemplate 有 Template 這個詞,代表他是一個樣板模式,我們可以建立自己的 JmsTemplate 來解決這個問題。
讓我們先轉到 spring 的 @Configuration 。

建立自己的 JmsTemplate

這邊我們直接透過程式碼和註解來說明要做的事情:

Configuration
public class Config {

    @Bean
    public MappingJackson2MessageConverter messageConverter() {
        var messageConverter = new MappingJackson2MessageConverter();
//         轉換器會先將物件序列化成 JSON 字串。接著,它會自動在 JMS 訊息中加入一個額外的**屬性 (Property),這個屬性的名稱就是_type,而它的值則是該 Java 物件的完整類別路徑
        messageConverter.setTypeIdPropertyName("_typeId");
        return messageConverter;

    }

    @Bean
    public JmsTemplate jmsQueueTemplate(@Qualifier("jmsConnectionFactory") ConnectionFactory connectionFactory,
                                        MappingJackson2MessageConverter messageConverter) {
        var jmsTemplate = new JmsTemplate(connectionFactory);
        jmsTemplate.setPubSubDomain(false); //宣告不是要用 Pub/Sub 如果是 P2P 的話可以不用特別寫,只是為了帶出這個東西所以寫出來
        jmsTemplate.setMessageConverter(messageConverter); //將剛剛建立的MappingJackson2MessageConverter 放進去
        return jmsTemplate;
    }

完成之後改成使用剛建立的 JmsTemlate 就可以正常使用了。

接收 Jms 訊息

這個部分很簡單,一樣直接用 code 說明就可以了

@JmsListener(destination = "test2") //用來接收特定 jms 的 message,他不會一次收一堆 Message。 調整的方式可以透過 spring 的設定更改
public void receiveMessage(SendMessageDto receivedDto) {

    // 直接使用反序列化後的 DTO 物件
    System.out.println("成功接收並轉換 DTO: " + receivedDto);
}

Pub/Sub 模式

在開始之前可以多聊一個設定: spring.jms.pub-sub-domain,當這個設定放在 application.yml 並且設定成 true,但這個設定會讓整個專案變成 Pub/Sub 模式,並且這個設定相對簡單,因此我們講比較困難一點點點點的。

Config

我們一樣先去新增對 Pub/Sub 模式的 JmsTemplate,這部分一樣直接用 code 說明

// 用於發送到 Pub/Sub 的 JmsTemplate
@Bean
public JmsTemplate jmsTopicTemplate(@Qualifier("jmsConnectionFactory") ConnectionFactory connectionFactory,
                                    MappingJackson2MessageConverter messageConverter) {
    var jmsTemplate = new JmsTemplate(connectionFactory);
    jmsTemplate.setPubSubDomain(true);
    jmsTemplate.setMessageConverter(messageConverter);
    return jmsTemplate;
}

// 用於監聽 Pub/Sub 的 Factory
@Bean
public JmsListenerContainerFactory<?> topicListenerFactory(
        @Qualifier("jmsConnectionFactory") ConnectionFactory connectionFactory,
        DefaultJmsListenerContainerFactoryConfigurer configurer) {
    var factory = new DefaultJmsListenerContainerFactory();
    configurer.configure(factory, connectionFactory);
    factory.setPubSubDomain(true);
    return factory;
}

Send

這部分和 P2P 的做法一樣

@Override
public void sendMessage(SendMessageDto sendMessageDto) {
    jmsTopicTemplate.convertAndSend("test9", sendMessageDto);
    System.out.println("Message sent");
}

接收 message 的部分

這部分和 P2P 的接收一樣,只差在 containerFactory 需要指定前面 Config 建立的 Bean

@JmsListener(destination = "test9", containerFactory = "topicListenerFactory")
public void receiveMessageFromTopic1(SendMessageDto receivedDto,
                                     @Header(value = "target", required = false) String target) {
    System.out.println("成功接收並轉換 DTO: " + receivedDto);
    System.out.println("接收到的 'resource' 屬性為: " + target);
}

@JmsListener(destination = "test9", containerFactory = "topicListenerFactory")
public void receiveMessageFromTopic2(SendMessageDto receivedDto, //@JmsListener 監聽器收到一則訊息時,轉換器會先讀取訊息的屬性。它會尋找一個名叫 _type 的屬性。一旦找到,它就會讀取這個屬性的值(也就是那個類別路徑),然後轉換
                                     @Header(value = "target", required = false) String target) {
    System.out.println("2成功接收並轉換 DTO: " + receivedDto);
    System.out.println("2接收到的 'resource' 屬性為: " + target);
}

結語

以上就是這次JMSTemplate 的教學啦,之後會再 push 一個Sample project 到 git 上,到時候會把網址更新到這邊。

如果大家對於這份教學有覺得可以更好或任何意見的話都可以留言喔。

前言(一堆廢話)

嘿,大家好久不見!我發現距離上次在 Blogger 發文,竟然已經過了快半年了。想想還真有點不好意思,不過這次我可是帶著新東西來跟大家分享的!

這次要聊的是 Java JMSTemplate。為什麼突然要學這個呢?主要是因為我最近換了新公司,而他們那邊剛好需要用到 訊息佇列 (Message Queue) 的技術,所以我就趁這個機會好好學了一下。

(老實說,我其實都工作六年了,這東西早就該會了啦!)

Message Queue 是什麼和目的

這部分我覺得我讀的書的寫的很棒,因此想說直接引用過來。

假設現在是星期五 16:55 ,在幾分鐘就可以開始期待已久的休假了。再過幾分鐘你的飛機就要起飛了,但是在這幾分鐘你需要先確定主管和同事了解你的工作進度,這樣他們才有機會於下週一接續你的工作。這時候該怎麼辦?
這時候避免打擾到他們最好的做法就是留紙條或發送 E-mail ,如此一來他們就可以在下週一或有空的時候看到 E-mail 接續工作。
這段很完美的解釋了 Message Queue,的概念,將訊息傳到佇列,之後根據佇列的先進先出讓服務吸收消化,最後調整服務一次接收多少 Message 。

如此一來可以帶來以下幾個好處:

  • 解耦合:
    • 發送者可以完全不管接收者狀態與是否可以承受,只需要將訊息丟到 Queue 就好了
    • 系統不同的區塊可以獨立開發(其實就跟現今流行的微服務類似概念)
  • 非同步處理:發送者可以完全不需要等待另一方處理就可以繼續處理下一個 request。這樣講有點抽象,可以想像麥當勞點餐系統,櫃檯的人員不需要完全確認廚房是否收到單就可以接下一個客人了。
  • 提高可靠性:很多提供 Message Queue 的元件會提供持久化的功能,他會將 message 存起來,這樣一來就算遇到某個 Message 處理有問題,也可以很快查到並且做後續 support。
  • 水平擴展:我們可以很輕易地增加消費 Message 的處理人員,這樣就可以提高 Queue 的消化速度,已應付高流量又要快速處理的需求。
  • 等等

上述只有放我自己認為很重要的優點,但其實還有很多的優點。

名詞

那優點介紹完之後我們來聊一下在 Message Queue 常會用到的名詞,

  • 佇列(Queue):就像是排隊一樣,先進先出。
  • 生產者(Producer):就是指生成 Message 的服務
  • 消費者(Consumer):就是指多個
  • 一對一(one to one, O2O):一個生產者生產的訊息對應一個消費者
  • 點對點模式(Point to Point, P2P):一對一就是點對點模式
  • 一對多(one to many, O2M):一個生產 Message 給多個使用 Message 的人
  • 發布/訂閱模式(Pub/Sub):當一個生產 Message 的人對應多個使用 Message 的人就是 Pub/Sub 模式
  • 發布者(Pub):由於一個 Message 會被多人使用,此時就不是生產者了,而是發布者
  • 訂閱者(Sub):由於多個人使用同一個 Message,因此此時使用 Message 的人就會被算是訂閱者。

環境

接下來是環境,我們預計在本機使用 ActiveMQ 當作我們練習 Message Queue 和 Pub/Push 的平台。

  1. 首先第一件事就是下載 ActiveMQ: https://activemq.apache.org/components/artemis/download/
  2. 下載之後解壓縮
  3. 到一個你你想要放 activeMQ 實例的地方建立資料夾
  4. 回到剛剛解壓縮的資料夾並進到 bin 資料夾
  5. 下以下的命令來建立 ActiveMQ 的實例: ./artemis create 你剛剛建立資料夾的路徑
  6. 此時應該會要求輸入使用者帳號密碼,這邊可以自己設定,以這篇文章來說我是設成 admin 和 admin
  7. 完成之後到存放 ActiveMQ 的資料夾可以看到多了一些資料夾和文件,由於這篇 blogger 主要想要聚焦在程式碼上,因此這邊就不多解釋檔案的內容。
  8. 進到 bin 資料夾後執行 ./artemis run 之後就會開始出現 log
  9. 在啟動完成的 log 的最後一行會看到一個網址,以我這邊看到的是:http://localhost:8161/console 這個是用來看 ActiveMQ 狀態的網址,後續可以用來檢查 JMSTemplate 傳送 Message 之後的狀態

到這邊環境就算是完成了,接下來就是正題:Java 開如何配合 ActiveMQ 使用。

如何使用 JMSTemplate

這個段落我會先說明各個方法,最後附上一個 Sample project 並且說明如何透過該 project 來玩。

Lib

由於我的情境是會在 Spring 中使用 Message Queue ,因此這邊會直接導入 Spring 相關的 lib。以下是我使用到的 Lib 。

  • org.springframework.boot:spring-boot-starter-artemis
  • org.springframework.boot:spring-boot-starter-web
    可以看到除了基本的 spring starters-web 以外就只需要再多一個 spring-boot-starter-artemis 就好了

application.yml

接下來就是設定 spring 連線到 ActiveMQ 了,以下是我的相關設定和說明:

spring:
  artemis:
    user: admin #帳號
    password: admin #密碼
    broker-url: tcp://localhost:61616 #連線資訊

都設定完成之後我們就可以開始來玩 JMSTemplate 了。

P2P 模式

發送 message

在 JMSTemplate 中我覺得最簡單易用的方法是:

public void send(
    String destinationName,
    org.springframework.jms.core.MessageCreator messageCreator
)

這個方法使用起來很簡單,我們直接看一段範例 code:

@Autowired
private JmsTemplate jmsQueueTemplate;

public void someFunction() {
    jmsQueueTemplate.send("test3", new MessageCreator() {
        @Override
        public Message createMessage(Session session) throws JMSException {
            return session.createTextMessage("要放入的訊息");
        }
    });
}

下面是透過 lambda 簡化之後的 code。

@Autowired
private JmsTemplate jmsQueueTemplate;

public void someFunction() {
    jmsQueueTemplate.send("test3",
        session -> session.createTextMessage("要放入的訊息"));
}

這段程式碼傳入參數有兩個,分別是 destinationName 和 messageCreator,下列是相關說明:

  • destinationName: 主要用於設定類似主題的東西,但是在 ActiveMQ 並不像是 Kafka 用 Topic ,在 ActiveMQ 使用的是 Address ,詳細的內容或許可以找時間再寫一篇 blogger 來聊這一塊,這邊只要先記得他是類似主題的概念。
  • messageCreator:就如同變數名稱,他是用於建立 Message 的,他會傳入 session,之後我們可以透過 session 的createTextMessage 方法傳入我們要放的訊息。
    寫到這邊就算是完成最簡單的 sendMessage 了,但這時候會發現這樣的做法其實不太符合實際開發,我們通常會希望傳入一個 Dto 到方法裡,並且自動轉成 json 傳進 Message Queue ,因此這邊要再來介紹另外一個很像的方法: convertAndSend

下面是他的方法:

public void convertAndSend(
    String destinationName,
    Object message
)

我們可以看到原本的 MessageCreator 變成了 Object message ,沒錯,Object message 就是用來放傳入的 pojo 的,因此我們趕快試試看

@Autowired
private JmsTemplate jmsQueueTemplate;

public void someFunction() {
    var somePojo = new SomePojo(); //可以自己建立一個 pojo
    jmsQueueTemplate.send("test3", somePojo);
}

這時候運行並且執行到的時候會跳出 org.springframework.jms.support.converter.MessageConversionException: Cannot convert object of type [com.crater.learnjms.dto.SendMessageDto] to JMS message. Supported message payloads are: String, byte array, Map<String,?>, Serializable object.
沒錯,恭喜你跳進第一個坑,在沒有設定 Bean 的前提下,jmsTemplate 預設會使用 SimpleMessageConverter 那該怎麼辦?
難道我只能使用 Map 之類的方式傳送了嗎?
這點不用擔心,有沒有注意到 JMSTemplate 有 Template 這個詞,代表他是一個樣板模式,我們可以建立自己的 JmsTemplate 來解決這個問題。
讓我們先轉到 spring 的 @Configuration

建立自己的 JmsTemplate

這邊我們直接透過程式碼和註解來說明要做的事情:

Configuration
public class Config {

    @Bean
    public MappingJackson2MessageConverter messageConverter() {
        var messageConverter = new MappingJackson2MessageConverter();
//         轉換器會先將物件序列化成 JSON 字串。接著,它會自動在 JMS 訊息中加入一個額外的**屬性 (Property),這個屬性的名稱就是_type,而它的值則是該 Java 物件的完整類別路徑
        messageConverter.setTypeIdPropertyName("_typeId");
        return messageConverter;

    }

    @Bean
    public JmsTemplate jmsQueueTemplate(@Qualifier("jmsConnectionFactory") ConnectionFactory connectionFactory,
                                        MappingJackson2MessageConverter messageConverter) {
        var jmsTemplate = new JmsTemplate(connectionFactory);
        jmsTemplate.setPubSubDomain(false); //宣告不是要用 Pub/Sub 如果是 P2P 的話可以不用特別寫,只是為了帶出這個東西所以寫出來
        jmsTemplate.setMessageConverter(messageConverter); //將剛剛建立的MappingJackson2MessageConverter 放進去
        return jmsTemplate;
    }

完成之後改成使用剛建立的 JmsTemlate 就可以正常使用了。

接收 Jms 訊息

這個部分很簡單,一樣直接用 code 說明就可以了

@JmsListener(destination = "test2") //用來接收特定 jms 的 message,他不會一次收一堆 Message。 調整的方式可以透過 spring 的設定更改
public void receiveMessage(SendMessageDto receivedDto) {

    // 直接使用反序列化後的 DTO 物件
    System.out.println("成功接收並轉換 DTO: " + receivedDto);
}

Pub/Sub 模式

在開始之前可以多聊一個設定: spring.jms.pub-sub-domain,當這個設定放在 application.yml 並且設定成 true,但這個設定會讓整個專案變成 Pub/Sub 模式,並且這個設定相對簡單,因此我們講比較困難一點點點點的。

Config

我們一樣先去新增對 Pub/Sub 模式的 JmsTemplate,這部分一樣直接用 code 說明

// 用於發送到 Pub/Sub 的 JmsTemplate
@Bean
public JmsTemplate jmsTopicTemplate(@Qualifier("jmsConnectionFactory") ConnectionFactory connectionFactory,
                                    MappingJackson2MessageConverter messageConverter) {
    var jmsTemplate = new JmsTemplate(connectionFactory);
    jmsTemplate.setPubSubDomain(true);
    jmsTemplate.setMessageConverter(messageConverter);
    return jmsTemplate;
}

// 用於監聽 Pub/Sub 的 Factory
@Bean
public JmsListenerContainerFactory<?> topicListenerFactory(
        @Qualifier("jmsConnectionFactory") ConnectionFactory connectionFactory,
        DefaultJmsListenerContainerFactoryConfigurer configurer) {
    var factory = new DefaultJmsListenerContainerFactory();
    configurer.configure(factory, connectionFactory);
    factory.setPubSubDomain(true);
    return factory;
}

Send

這部分和 P2P 的做法一樣

@Override
public void sendMessage(SendMessageDto sendMessageDto) {
    jmsTopicTemplate.convertAndSend("test9", sendMessageDto);
    System.out.println("Message sent");
}

接收 message 的部分

這部分和 P2P 的接收一樣,只差在 containerFactory 需要指定前面 Config 建立的 Bean

@JmsListener(destination = "test9", containerFactory = "topicListenerFactory")
public void receiveMessageFromTopic1(SendMessageDto receivedDto,
                                     @Header(value = "target", required = false) String target) {
    System.out.println("成功接收並轉換 DTO: " + receivedDto);
    System.out.println("接收到的 'resource' 屬性為: " + target);
}

@JmsListener(destination = "test9", containerFactory = "topicListenerFactory")
public void receiveMessageFromTopic2(SendMessageDto receivedDto, //@JmsListener 監聽器收到一則訊息時,轉換器會先讀取訊息的屬性。它會尋找一個名叫 _type 的屬性。一旦找到,它就會讀取這個屬性的值(也就是那個類別路徑),然後轉換
                                     @Header(value = "target", required = false) String target) {
    System.out.println("2成功接收並轉換 DTO: " + receivedDto);
    System.out.println("2接收到的 'resource' 屬性為: " + target);
}

以上就是這次JMSTemplate 的教學啦,之後會再 push 一個Sample project 到 git 上,到時候就可以拿來參考啦。

留言

這個網誌中的熱門文章

基本 Spring security 快速入門

記帳專案說明