Google Code Prettify

顯示具有 spring boot 標籤的文章。 顯示所有文章
顯示具有 spring boot 標籤的文章。 顯示所有文章

2018年7月20日 星期五

Spring Boot Angular Websocket

這篇要說明的是如何由 Angular 透過 websocket 與 spring boot 的 server 雙向溝通。我使用的版本為 Angular 5 及 spring boot 2.0.3。程式執行會如下圖,測試步驟如下:
  1. 按【Connect】連線到 server。
  2. 輸入名稱後按【Send】,將名稱送到 server。
  3. server 收到後,再將名稱回覆給 Angular。
  4. Angular 收到回覆後顯示「Greetings」表示成功。
  5. 按【Disconnect】終止與 server 的連線。

文章的來源是「Spring Boot Angular Websocket」,我直接執行裡面的程式會有問題,我修正後整理如下。
  • Angular 程式
    • install
npm install stompjs --save
npm install sockjs --save
使用這兩個函式庫可連與 server 端進行 websocket 連線。STOMP 的解釋如下 ...

What is STOMP

STOMP stands for Streaming Text Oriented Messaging Protocol. As per wiki, STOMP is a simple text-based protocol, designed for working with message-oriented middleware (MOM). It provides an interoperable wire format that allows STOMP clients to talk with any message broker supporting the protocol.
This means when we do not have STOMP as a client, the message sent lacks of information to make Spring route it to a specific message handler method. So, if we could create a mechanism that can make Spring to route to a specific message handler then probably we can make websocket connection without STOMP.
大意差不多是這樣的 ...
STOMP 即 Simple (or Streaming) Text Orientated Messaging Protocol,簡單(流)文本定向消息協議,它提供一個可互操作的連接格式,允許 STOMP 客戶端與任意 STOMP 訊息代理 (broker) 進行交互傳送訊息。STOMP 協議由於設計簡單,易於開發客戶端,因此在多種語言和多種平台上得到廣泛應用。
    • app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule, FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
下面的頁面會用到 Input,是屬於 FormsModule 裡的 component,這裡要先加入宣告。
    • app.component.html
<div id="main-content" class="container">
  <div class="row">
    <div class="col-md-6">
      <form class="form-inline">
        <div class="form-group">
          <label for="connect">WebSocket connection:</label>
          <button id="connect" class="btn btn-default" type="button" [disabled]="disabled" (click)="connect()">Connect</button>
          <button id="disconnect" class="btn btn-default" type="button" [disabled]="!disabled" (click)="disconnect()">Disconnect
          </button>
        </div>
      </form>
    </div>
    <div class="col-md-6">
      <form class="form-inline" name="test-form">
        <div class="form-group">
          <label for="name">What is your name?</label>
          <input type="text" id="name" name="name" class="form-control" placeholder="Your name here..." [(ngModel)]="name">
        </div>
        <button id="send" class="btn btn-default" type="button" (click)="sendName()">Send</button>
      </form>
    </div>
  </div>
  <div class="row">
    <div class="col-md-12" *ngIf="showConversation">
      <table id="conversation" class="table table-striped">
        <thead>
        <tr>
          <th>Greetings</th>
        </tr>
        </thead>
        <tbody *ngFor="let greeting of greetings" >
          <tr><td> </td></tr>
        </tbody>
      </table>
    </div>
  </div>
</div>
最前面那個圖就是這個網頁顯示出來的樣子,會搜尋到這篇網誌的人應該都懂 Angular 吧? 我把重點用紅色標出,它們會對應到下面 app.component.ts 裡的相關 method 或 property。
    • app.component.ts
import { Component } from '@angular/core';
import * as Stomp from 'stompjs';
import * as SockJS from 'sockjs-client';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app';

  greetings: string[] = [];
  showConversation: boolean = false;
  ws: any;
  name: string;
  disabled: boolean;

  constructor(){}

  connect() {
    let socket = new WebSocket("ws://localhost:8080/greeting");
    this.ws = Stomp.over(socket);
    let that = this;
    this.ws.connect({}, function(frame) {
      that.ws.subscribe("/errors", function(message) {
        alert("Error " + message.body);
      });
      that.ws.subscribe("/topic/reply", function(message) {
        console.log(message)
        that.showGreeting(message.body);
      });
      that.disabled = true;
    }, function(error) {
      alert("STOMP error " + error);
    });
  }

  disconnect() {
    if (this.ws != null) {
      this.ws.ws.close();
    }
    this.setConnected(false);
    console.log("Disconnected");
  }

  sendName() {
    let data = JSON.stringify({
      'name' : this.name
    })
    this.ws.send("/app/message", {}, data);
  }

  showGreeting(message) {
    this.showConversation = true;
    this.greetings.push(message)
  }

  setConnected(connected) {
    this.disabled = connected;
    this.showConversation = connected;
    this.greetings = [];
  }
}
  1. server 提供的 broker endpoint 為 /greeting,所以一開始要先從這個介面與 server 端建立連線。
  2. 傳送資料是傳送到 /app/message,回覆資料是以 call back 的方式回覆,所以要訂閱。
  3. 上面可以看到我們訂閱了兩個訊息 "/topic/reply"、"/errors",分別是正確時回覆的訊息及當發生錯誤時回覆的錯誤訊息。
  • spring boot Server 端程式
    • build.gradle
buildscript {
  ext {
    springBootVersion = '2.0.3.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'idv.steven.demowebsocket'
version = '1.0'
sourceCompatibility = 1.8

repositories {
  mavenCentral()
}

dependencies {
  compile('org.springframework.boot:spring-boot-starter-security')
  compile('org.springframework.boot:spring-boot-starter-web')
  compile('org.springframework.boot:spring-boot-starter-websocket')
  compileOnly('org.projectlombok:lombok')
  testCompile('org.springframework.boot:spring-boot-starter-test')
  testCompile('org.springframework.security:spring-security-test')
 
  compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
}
特別注意紅色那三個函式庫,我們正在寫 websocket 程式,當然要引入第二個紅色的函式庫,第一個則是因為 spring security 需要開啟一些連線的設定,Angular 端才能連的進來,第三個紅色的函式庫則是因為 client、server 間是以 JSON 格式傳遞,所以需用到。
    • SecurityConfig.java
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/**").permitAll();
  }
}
為了測試方便,簡單的設定為允許所有 request 不需認證。
    • WebSocketConfig.java
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

  @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
    config.enableSimpleBroker("/topic/", "/queue/");
    config.setApplicationDestinationPrefixes("/app");
  }

  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/greeting").setAllowedOrigins("*");
  }
}
要使用 STOMP,需在有 @Configuration 的類別上加上 @EnableWebSocketMessageBroker (紅色),下面橘色是開發 broker 的 endpoint 為 "/greeting",並且允許所有人都可以連線進來。
    • WebSocketController.java

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Controller;

import com.google.gson.Gson;

@Controller
public class WebSocketController {

  @Autowired
  private SimpMessageSendingOperations messagingTemplate;

  @MessageMapping("/message")
  @SendTo("/topic/reply")
  public String processMessageFromClient(@Payload String message) throws Exception {
    String name = new Gson().fromJson(message, Map.class).get("name").toString();
    return name;
  }
 
  @MessageExceptionHandler
  public String handleException(Throwable exception) {
    messagingTemplate.convertAndSend("/errors", exception.getMessage());
    return exception.getMessage();
  }
}
  1. @MessagMapping("/message") 是 client 傳過來時的路徑,要注意,在前面我們有設定 prefixes 為 "app",所以實際的路徑為"/app/message"。
  2. @SendTo("...") 是回覆給 client 的路徑,client 想收到回覆訊息,要先訂閱。
  3. @MessageExceptionHandler 是發生錯誤時會執行的 method,client 端想收到錯誤訊息,要訂閱 "/errors"。
    • DemowebsocketApplication.java
@SpringBootApplication
public class DemowebsocketApplication {

  public static void main(String[] args) {
    SpringApplication.run(DemowebsocketApplication.class, args);
  }
}

2018年6月3日 星期日

application.properties@spring boot

spring boot 預設會讀取 classpath 下的 application.properties,但是,大部份的維運團隊通常會同時面對一個系統要部署到開發、測試、正式等多個環境的問題,為解決這個問題,spring boot 有了以下的預設。
  • application.properties
  • spring.profiles.active=test
    
    util.count=0
    util.total=4
    
    紅色部份可以用來設定環境,在這裡,我假定會有 dev、test、prod 三個環境,application.properties 放的是所有環境都會用到的設定,dev、test、prod 這三個各別的環境如果有什麼特殊的設定,則可以在 application-dev.properties、application-test.properties、application-prod.properties 這三個設定檔分別設定。萬一,這三個設定檔裡的設定是 application.properties 已經有的,則會覆蓋預設的設定。假設,另三個設定檔的內容如下。
  • application-dev.properties
  • util.count=1
  • application-test.properties
  • util.count=2
    
  • application-prod.properties
  • util.count=3
在上述的設定下,因為 spring.profiles.active 設定為 test,spring boot 載入的設定會如下:
util.count=2
util.total=4
寫個程式測試看看 …
  • ForTestUtil (單純的載入兩個設定參數並提供印出的 method)
  • import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    @Component
    public class ForTestUtil {
        @Value("${util.count}")
        private Long count;
     
        @Value("${util.total}")
        private Long total;
     
        public void print() {
            System.out.println("count = " + count);
            System.out.println("total = " + total);
        }
    }
  • ForTestUtilTest (上述類別的單元測試程式)
  • import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    
    import idv.steven.demo2.Demo2Application;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = Demo2Application.class)
    public class ForTestUtilTest {
        @Autowired
        private ForTestUtil util;
    
        @Test
        public void test() {
            util.print();
        }
    }
執行結果,會如下:
count = 2
total = 4

2018年3月23日 星期五

spring boot RESTful 程式的單元測試

用 spring boot 寫 RESTful service,如果要單元測試很簡單,如下:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class LibraryControllerTest {
 
    @LocalServerPort
    private int port;

    @Test
    public void test() {
        RestTemplate rest = new RestTemplate();
  
        List model = rest.getForObject("http://localhost:" + port + "/library/book", List.class);
        assertNotNull(model);  
    }
}
只要加上紅色的部份,執行單元測試不需要先啟動 ap server,直接執行,spring boot 會自動執行 embedded tomcat 並且動態的找到一個可以用的 port 進行測試。但是 … 如果只是這樣寫,很可能測試時會出現 403 Forbidden 的錯誤,這是因為 spring boot 自動加上的 web security,所以,要改變一下設定,如下,我新增了一個 SecurityConfig 的類別 …
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter { 
    @Override 
    protected void configure(HttpSecurity http) throws Exception {   
        http.authorizeRequests().anyRequest().permitAll();
    }
}
這個類別要繼承 WebSecurityConfigurerAdapter,並 override configure(HttpSecurity http) method,在裡面改變 request 的權限設定,我這裡很偷懶的全部的 request 都允許存取,實務上當然會加上一些限制。



2018年3月18日 星期日

在 embedded tomcat 中使用 Tomcat 資料庫連線

在 spring boot 中,如果在 build.gradle 中包含了 web 套件,就會自動包裝了 embedded tomcat,那麼,如果程式要連線資料庫,要怎麼利用這個 embedded tomcat 中的 connection pool ? 很簡單,如下。
  • build.gradle
buildscript {
    ext {
        springBootVersion = '2.0.0.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse-wtp'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'war'

group = 'idv.steven.jndi'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

configurations {
    providedRuntime
}

dependencies {
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.apache.tomcat:tomcat-jdbc')
    compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
 
    compile group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '2.2.1'
}
如上所示,我引入了 spring boot 2.x 版,因為我的資料庫是 maria db,所以也要把相關的 JDBC 驅動程式引入,同時,因為這裡要使用 tomcat connection pool,要引入 tomcat jdbc。spring boot 在後續程式中,會根據資料庫的 url 判斷是什麼資料庫,而載入相關驅動程式。
  • application.properties
spring.datasource.type=org.apache.tomcat.jdbc.pool.DataSource
#spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://192.168.0.103:3306/demo
spring.datasource.username=steven
spring.datasource.password=P@ssw0rd

spring.datasource.tomcat.max-active=23
spring.datasource.tomcat.max-idle=7
spring.datasource.tomcat.min-idle=2
spring.datasource.tomcat.max-wait=9997
spring.datasource.type 指定要使用的 connection pool,spring.datasource.tomcat.*,是設定 connection pool 的一些參數,我故意設定一些比較奇怪的值,是為了觀察是否真的有使用到 tomcat connection pool。spring.datasource.driver-class-name 不需設定,spring boot 從 url 中可以自行判斷。在 application.properties 設定完,不需要寫任何程式,spring boot 就幫我們建立好 tomcat connection pool。
  • 觀察
我在 eclipse 使用 debug mode 執行,觀察 EntityManager 的值,於「em > h > target Factory > h > entityManagerFactoryBean > persistenceUnitInfo > nonJtaDataSource > poolProperties」裡看到如下內容,證明確實有用到 tomcat connection pool。

2018年2月28日 星期三

Spring Boot Admin

Spring Boot 廣泛用於微服務,在這種分散式系統,監控是一件很重要的事,Spring Boot 提供了一個名為 Spring Boot Admin 的監控工具,只需要進行一些設定就可以監控,非常方便,說明如下:
  • 建立一個 spring boot admin 的 server 端程式
下面是這個程式的 build.gradle,和一般的 spring boot 沒什麼不同,主要就是加入紅色那幾行,讓 gradle 協助我們載入相關的 jar 檔。spring boot 預設使用的 log 是 logback,但是我習慣用 log4j2,所以加入綠色的那幾行,將 logback 移除並加入 log4j2。
buildscript {
 ext {
  springBootVersion = '1.5.10.RELEASE'
 }
 repositories {
  mavenCentral()
 }
 dependencies {
  classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
 }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

group = 'idv.steven'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
 mavenCentral()
}

configurations.all {
      exclude group: 'log4j', module: 'log4j'
      exclude group: 'org.slf4j', module: 'slf4j-log4j12'
      exclude group: 'org.slf4j', module: 'log4j-over-slf4j'
      exclude group: 'ch.qos.logback', module: 'logback-core'
      exclude group: 'ch.qos.logback', module: 'logback-classic'
}

ext {
      springBootAdminVersion = '1.5.7'
      log4j2Version = '2.10.0'
}

dependencies {
      compile('de.codecentric:spring-boot-admin-starter-server')
      compile('de.codecentric:spring-boot-admin-server-ui')
 
      compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: "${log4j2Version}"
      compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: "${log4j2Version}"
      compile group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: "${log4j2Version}"
 
      testCompile('org.springframework.boot:spring-boot-starter-test')
}

dependencyManagement {
      imports {
            mavenBom "de.codecentric:spring-boot-admin-dependencies:${springBootAdminVersion}"
      }
}
  • 設定 port
server.port=8081
embedded tomcat 預設的 port 是 8080,因為我打算用來測試的 client 程式「第一個 spring boot web 程式 (使用 embedded tomcat)」也有用到 embedded tomcat 已經佔去了 8080 port,所以在 application.properties 中加入如上內容設定使用 8081 port。
  • 執行 server
執行上述 server 程式後,打開瀏覽器,打入 http://localhost:8081,可以看到如下畫面,這樣就確定已經正常啟動了!
接下來要來看一下 client 程式怎麼修改,可以讓這個 server 進行監控。
  • 修改 client 程式的 build.gradle
buildscript {
 ext {
  springBootVersion = '1.5.10.RELEASE'
 }
 repositories {
  mavenCentral()
 }
 dependencies {
  classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
 }
}

apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

group = 'idv.steven'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
      mavenCentral()
}

configurations.all {
      exclude group: 'log4j', module: 'log4j'
      exclude group: 'org.slf4j', module: 'slf4j-log4j12'
      exclude group: 'org.slf4j', module: 'log4j-over-slf4j'
      exclude group: 'ch.qos.logback', module: 'logback-core'
      exclude group: 'ch.qos.logback', module: 'logback-classic'
}

ext {
      springBootAdminVersion = '1.5.7'
      log4j2Version = '2.10.0'
}

dependencies {
      compile('org.springframework.boot:spring-boot-starter-data-jpa') 
      compile('org.springframework.boot:spring-boot-starter-web')
      compile('org.springframework.boot:spring-boot-starter-thymeleaf')
      compile('de.codecentric:spring-boot-admin-starter-client')
 
      compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: "${log4j2Version}"
      compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: "${log4j2Version}"
      compile group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: "${log4j2Version}"
      
      compile group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '2.2.1'
      compile group: 'javax.inject', name: 'javax.inject', version: '1'
      compileOnly('org.projectlombok:lombok')
      testCompile('org.springframework.boot:spring-boot-starter-test')
}

dependencyManagement {
      imports {
            mavenBom "de.codecentric:spring-boot-admin-dependencies:${springBootAdminVersion}"
      }
}
紅色部份是為了導入 Spring Boot Admin client 而加入的。
  • 修改 application.properties (client)
spring.boot.admin.url=http://localhost:8081
spring.boot.admin.client.name=demo

endpoints.sensitive=false
endpoints.logfile.external-file=d:/tmp/demo.log
在 application.properties 中加入如上設定,前兩行指出 Spring Boot Admin Server 的網址及要顯示在監控畫面的程式名稱; 後兩行是為了將 client 的 log4j2 產生的 log file 送到監控畫面而設定的,這樣萬一監控時發現錯誤,可以立即看 log。
  • 執行 client
執行後,client 會自動連到 server 註冊,然後就會顯示在監控畫面,如上。如果想看更詳細的資料,可以按【Details】,將顯示如下畫面,其中第二項「Log」就是 log4j2 的 log file 內容。



2018年2月26日 星期一

Gradle: 編譯 spring boot application

這篇說明如何使用 Gradle 編譯 spring boot application,直接來看個例子。
buildscript {
    ext {
        springBootVersion = '1.5.10.RELEASE'
    }
    repositories {
        maven { url "http://maven.springframework.org/milestone" }
        maven { url "http://repo.maven.apache.org/maven2" }
        maven { url "http://repo1.maven.org/maven2/" }
        maven { url "http://amateras.sourceforge.jp/mvn/" }
        mavenCentral()
        jcenter()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'org.springframework.boot'

group = 'idv.steven.crawler'
sourceCompatibility = 1.8
targetCompatibility = 1.8

bootRepackage {
    mainClass = 'idv.steven.crawler.CrawlerStarter'
}

jar {
    baseName = 'my-crawler'
    version =  '1.0.0'  
}

buildDir = 'build'

repositories {
      mavenCentral()
      maven { url "http://maven.springframework.org/milestone" }
      maven { url "http://repo.maven.apache.org/maven2" }
      maven { url "http://repo1.maven.org/maven2/" }
      maven { url "http://amateras.sourceforge.jp/mvn/" }
}

configurations.all {
      exclude group: 'log4j', module: 'log4j'
      exclude group: 'org.slf4j', module: 'slf4j-log4j12'
      exclude group: 'org.slf4j', module: 'log4j-over-slf4j'
      exclude group: 'ch.qos.logback', module: 'logback-core'
      exclude group: 'ch.qos.logback', module: 'logback-classic'
}

dependencies {
      def log4j2Version = '2.10.0'
      def fasterxmlVersion = '2.9.3'
      def hibernateVersion = '5.2.13.Final'
 
      compile fileTree(dir: 'lib', include: ['*.jar'])

      compile("org.springframework.boot:spring-boot-starter-web") {
          exclude module: "spring-boot-starter-tomcat"
      }
      compile('org.springframework.boot:spring-boot-starter-aop')
      compile('org.springframework.boot:spring-boot-starter-batch')
      compile('org.springframework.boot:spring-boot-starter-data-jpa')
      compile('org.springframework.boot:spring-boot-starter-mail')
      compile("org.thymeleaf:thymeleaf-spring4")
      compile("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect")
 
      compile group: 'org.hibernate', name: 'hibernate-core', version: "${hibernateVersion}"
      compile group: 'org.hibernate', name: 'hibernate-entitymanager', version: "${hibernateVersion}"
      compile group: 'org.hibernate', name: 'hibernate-validator', version: '6.0.7.Final'
 
      compile group: 'javax.inject', name: 'javax.inject', version: '1'
      compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: "${log4j2Version}"
      compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: "${log4j2Version}"
      compile group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: "${log4j2Version}"
      compile group: 'joda-time', name: 'joda-time', version: '2.9.9'
      compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: "${fasterxmlVersion}"
      compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "${fasterxmlVersion}"
      compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: "${fasterxmlVersion}"
      compile group: 'org.json', name: 'json', version: '20180130'
      compile group: 'net.minidev', name: 'json-smart', version: '2.3'
      compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.0'
      compile group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final'
      compile group: 'com.lmax', name: 'disruptor', version: '3.3.7'
 
      compileOnly "org.projectlombok:lombok:1.16.20"
 
      testCompile('org.springframework.boot:spring-boot-starter-test')
      testCompile('org.springframework.batch:spring-batch-test')
}

  • 一開始 buildscript 中紅色部份,設定 spring boot 版本,後面 dependencies (依賴) 的部份引入 spring boot 時,即會引入這個版本。
  • buildscript 的 repositories 指定了好幾個版本庫,一般都只指定 mavenCentral(),但是,有時這個版本庫裡可能沒有我們要的 jar 檔,也可能剛好掛了或被公司防火牆擋了,設定多個比較可以確保可以抓到需要的 jar 檔。
  • apply plugin 我們導入 java 和 org.springframe.boot,這樣我們就可以使用這兩個物件的 method。
  • bootRepackage (黃底) 設定這個 application 的 main 檔在那一個類別裡。
  • jar (綠底) 設定了這個 application 編譯出來的檔名及版號。
  • buildDir (紅底) 指出編譯出來的檔案要放在那個目錄下,不指定的話,預設就會放在 build 目錄下。
  • configurations.all 在這裡可以定義一些 global 的設定,以上例來說,定義了在依賴關係中,如果有依賴這些 jar 檔的,就不要引入,因為 spring boot 預設是的 log 機制是用 logback,有些其它的 framework 預設是用 log4j,但是我希望這個程式用 log4j2,所以在 這些設定排除 logback 及 log4j 的 jar 檔。
  • compile fileTree 是當有一些 jar 檔就在自己的本機,因為我有使用一些公司內部自行開發的共用函式庫的 jar 檔,這些 jar 檔放在 lib 目錄下,如此設定,就會把這些 jar 檔也包進來一起編譯。
  • exclude module (紫底) 是在設定要引入的 jar 檔時,Gradle 會自動將相依的 jar 檔包裝進來,但是,有時有些 jar 檔我們並不需要,就可以用這個方法將它排除! 在 spring boot 中,引入 web 時,預設會包含 embedded tomcat,但是這個程式不是 web 程式,只是會用到 http client,所以將 embedded tomcat 排程。這裡還有個要特別注意的,spring boot 在一開頭已經有設定版號,這些不需寫版號。
  • 有些 jar 檔只有在 compile 階段會用到,這時候就可以用 compileOnly (橘底),lombok 只是在編譯時幫程式員產生些像是 getter、setter method,或是 log 的宣告等,以簡化程式的撰寫,但是產生好讓 Gradle 編譯後,就沒它的事了,runtime 並不會用到,也不會被包入最後產生的 jar 檔裡。
  • testCompile 是用來引入單元測試用的 jar 檔,這些 jar 檔只有開發階段進行單元測試時會用到,也不會被包入最後產生的 jar 檔裡。

2017年12月31日 星期日

第一個 spring boot web 程式 (使用 embedded tomcat)

經過了一年八個月,這個世界的技術已經又有改變 ... (其實是我當時還沒學到比較新的技術)
現在要用 spring boot 來改寫 spring MVC - getting started 這一篇的程式,同樣的,架構採用 spring MVC,spring framework 最讓人詬病的就是設定非常複雜,spring boot 的出現主要就在解決這個問題。現在開始來看一下程式怎麼寫 …


上圖是程式碼放罝的位置,這是 Gradle 的習慣。
  1. embedded tomcat 不支援 jsp,所以這個程式的顯示都使用 thymeleaf 板型的 html,這是 spring boot 提供的,支援 html 5。
  2. 網頁相關的檔案放在 /src/main/resources 目錄下,html 放在 templates 子目錄,css、javascript 放在 static 子目錄。
  3. spring boot 的設定預設放在 application.properties,這個檔通常放在 classpath 的根目錄 【註】。
buildscript {
    ext {
        springBootVersion = '1.5.9.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

group = 'idv.steven'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile('org.springframework.boot:spring-boot-starter-data-jpa') 
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    compile group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '2.2.1'
    compile group: 'javax.inject', name: 'javax.inject', version: '1'
    compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}
這是 gradle 的設定檔,前一篇採用 maven,這裡使用 gradle,兩個都很流行,看個人習慣,沒什麼特別的好壞之分。紅色那一行,設定了我們要採用的 spring boot 版本,藍色那行指定了要使用 spring boot 插件,上面那行深綠色,指出要依賴 spring boot 插件。因為現在要寫的程式是 web 程式,且會用 JPA 到資料庫中存取使用者資料,並且網頁會套用 thymeleaf 版型,所以寫了橘色那三行設定,gradle 會自行去找出相關依賴的 jar 檔。紫色那行是引入 MariaDB 的 jar 檔,因為這裡的資料庫是使用 MariaDB
package idv.steven.demo.database.entity;

import java.sql.Timestamp;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.Data;

@Entity
@Table(name="users")
@Data
public class UsersEntity {
    @Id
    private String name;
    private String password;
    private Timestamp createtime;
}
上面是對應到資料庫 table Users 的 entity。
package idv.steven.demo.database.dao;

import org.springframework.data.repository.CrudRepository;
import idv.steven.demo.database.entity.UsersEntity;

public interface UsersDao extends CrudRepository {

}
存取 table Users 就採用 spring data 提供的 CrudRepository,不知道怎麼用可以參考「spring data: CrudRepository 自動生成」。
server.port=8080

spring.datasource.url=jdbc:mariadb://192.168.0.103:3306/demo
spring.datasource.username=root
spring.datasource.password=p@ssw0rd##
  • 資料庫連線相關設定,只要在 application.properties 中如上設定即可,通常只要設定 url、username、password,不需要設定 driver-class-name,spring boot 從 url 就能判斷現在要使用的是那一種資料庫。
  • 第一行 server.port 是設定 tomcat 要啟動後監聽那一個 port,預設就是 8080,如果本來就是要使用 8080,這一行可以刪除。與 tomcat 相關的設定還有:
    • server.session-timeout (以秒為單位)
    • server.context-path (預設為 / )
    • server.tomcat.uri-encoding (預設為 UTF-8)
    • server.tomcat.compression (預設為 off)
package idv.steven.demo.controller;

import javax.inject.Inject;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import idv.steven.demo.database.dao.UsersDao;
import idv.steven.demo.database.entity.UsersEntity;
import idv.steven.demo.model.IndexForm;

@Controller
public class IndexController {
    @Inject
    private UsersDao daoUsers;
 
    @RequestMapping("/index")
    public String login(IndexForm form) {
        if (form.getUserName() != null) {
            UsersEntity user = daoUsers.findOne(form.getUserName());
   
            if (user != null && user.getPassword().equals(form.getPassword())) {
                return "success";
            }
        }
        else {
            return "login";
        }
  
        return "fail";
    } 
}
Controller 如上所示,很簡單的針對傳入的登入帳號、密碼,檢查與資料庫中的是否相同? 相同就導向 success.html 網頁,不相同就導向 fail.html 網頁。
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <link rel="sytlesheet" th:href="@{bootstrap/css/bootstrap.min.css}"></link>
 <link rel="sytlesheet" th:href="@{bootstrap/css/bootstrap-theme.min.css}"></link>
</head>
<body>

 <form action="index.do" method="post">
        帳號: <input name="userName" type="text" />
        密碼: <input name="password" type="password" />
        <input type="submit" value="登入" />
    </form>
</body>
</html>
登入頁,如上所示,簡單的輸入帳號、密碼後按【登入】。
package idv.steven.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApp {
 
    public static void main(String[] args) {
        SpringApplication.run(DemoApp.class, args);
    }
}
最後很重要的,spring boot 要有一個起始的類別,這個類別上面要寫上 @SpringBootApplication 註記,這樣 spring boot 會自動的掃描所有類別與 jar 檔。


如果是在 eclipse 中開發程式,可以在專案名稱上按右鍵,選用「Run As > Spring Boot App」,spring boot 就會啟動 embedded tomcat,然後可以打開瀏覽器測試。
如上,輸入帳號、密碼後按【登入】,即會導向 Controller,再由 Controller 判斷要導向 success.html 或 fail.html。

【註】
application.properties 可以放在以下四個位置:
  1. 外置,在相對於應用程式運行目錄的 /config 子目錄裡。
  2. 外置,在應用程式運行的目錄裡。
  3. 內置,在 config package 內。
  4. 內置,在 classpath 根目錄。
spring boot 會依上列順序搜尋。
〈參考資料〉https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-application-property-files

【補充】
上面的程式是 compile 成 jar 檔,執行在 spring boot 包裝進 jar 檔裡的 embedded tomcat 上,如果想要 compile 成 war 檔並執行在一般的 tomcat,要如下修改:
  • DemoApp.java (啟動類別)
package idv.steven.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;

@SpringBootApplication
public class DemoApp extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(DemoApp.class);
    }
 
    public static void main(String[] args) {
        SpringApplication.run(DemoApp.class, args);
    }
}
如紅色部份所示,繼承 SpringBootServletInitializer,並覆寫 configure method。
  • build.gradle
apply plugin: 'war'
在 build.gradle 中加上如上內容,導入 war 插件,並確定環境變數有設定 JAVA_HOME 及安裝了 gradle,然後在 build.gradle 所在路徑上下如下指令:
gradle war

2015年11月8日 星期日

web development

項目日期
*
  Environment
1 Tomcat 7 安裝 SSL 憑證
2014/08/30
2 install GlassFish Server
2015/06/22
3 在 WebSphere 8.5.5.x 上遇到的 jar 檔版本和衝突問題
2017/03/30
*
  Programming (front-end)
1 struts2 jQuery tag (1) - TabbedPanel & DatePicker
2014/10/18
2 struts2 jQuery tag (2) - Grid
2014/10/19
3 jQuery: 處理 json 資料
2016/05/11
4 看見 Angular 的車尾燈
2017/03/26
5 第一支 Angular 程式
2017/04/06
6 在 WebSphere 上 Angular 中文存入資料庫變亂碼!
2017/06/20
7 Updating Angular CLI
2017/07/06
8 Spring Boot Angular Websocket
2018/07/20
*
  Programming (back end)
1 spring 4.x + struts 2.x + mybatis 3.x: getting started
2014/10/11
2 unit test on spring + struts2
2015/07/02
3 spring db connection
2015/12/22
4 spring MVC: getting started
2016/04/30
5 spring MVC + jQuery ajax
2016/05/02
6 spring MVC: 在 view 和 controller 間傳遞多列資料
2016/06/19
7 Spring Application Event
2017/07/23
8 變更 property 檔裡的設定值
2017/07/27
9 第一個 spring boot web 程式 (使用 embedded tomcat)
2017/12/31
10 application.properties@spring boot
2018/06/03
11 web server (https)
2023/06/11
*
  Programming (web services)
1 CXF - getting started
2016/07/04
2 spring boot RESTful 程式的單元測試
2018/03/23