spring Cloud--spring boot項目搭建步驟

爲什麼要說spring boot的搭建步驟呢,前面文章說了,spring Cloud是基於spring boot來簡化系統基礎設施的開發,所以這裏從spring boot的項目搭建步驟開始,具體如下:

在 Spring Tools 4 for Eclipse 中依次選擇 File->New->Maven Project,然後在出現的界面中按圖 1 所示增加相關信息。
 

創建maven項目
圖 1  創建 maven 項目


完了上述操作之後,在 pom.xml 中添加 Spring Boot 的依賴,代碼如下所示。


 
  • <parent>
  • <groupId>org.springframework.boot</groupId>
  • <artifactId>spring-boot-starter-parent</artifactId>
  • <version>2.0.6.RELEASE</version>
  • </parent>
  •  
  • <dependencies>
  • <dependency>
  • <groupId>org.springframework.boot</groupId>
  • <artifactId>spring-boot-starter-web</artifactId>
  • </dependency>
  • </dependencies>


編寫啓動類,代碼如下所示。


 

@SpringBootApplication

public class App {

 

public static void main(String[] args) {

SpringApplication.run(App.class, args);

}

}

啓動類使用了 @SpringBootApplication 註解,這個註解表示該類是一個 Spring Boot 應用。直接運行 App 類即可啓動,啓動成功後在控制檯輸出信息,默認端口是 8080,如圖 2 所示。
 

Spring Boot啓動成功
圖 2  Spring Boot啓動成功


可以看到,我們只在 pom.xml 中引入了一個 Web 的 Starter,然後創建一個普通的 Java 類,一個 Main 方法就可以啓動一個 Web 項目。

與之前的使用方式相比,這種方式簡單很多。以前需要配置各種 Spring 相關的包,還需要配置 web.xml 文件,還需要將項目放入 Tomcat 中去執行,搭建項目的過程還特別容易出錯,會出現各種 jar 包衝突。有了 Spring Boot 後這些問題都解決了。

我們之所以能夠通過一個 Main 方法啓動一個 Web 服務,是因爲 Sprig Boot 中內嵌了 Tomcat,然後通過內嵌的 Tomcat 來提供服務。當然,我們也可以使用別的容器來替換 Tomcat,比如 Undertow 或 Jetty。

Spring Tools 4 for Eclipse 還爲我們提供了更加便捷的項目創建方式,在 File->New 選項中有 Spring Starter Project,可以直接選擇 Spring Boot 的版本以及需要依賴的第三方包,直接生成 Spring Boot 項目,不用再去手動配置 Maven 依賴。

這個功能和 https://start.spring.io/ 提供的是同一個功能,方便快速搭建 Spring Boot 項目腳手架。

編寫第一個 REST 接口

本節將創建一個控制器,編寫第一個 REST 接口,訪問地址使用 /hello,代碼如下所示。


 

@RestController

public class HelloController {

 

@GetMapping("/hello")

public String hello() {

return "hello";

}

}

@RestController 是 @Controller 和 @ResponseBody 的組合註解,可以直接返回 Json 格式數據。

@GetMapping 其實就是 @RequestMapping(method=RequestMethod.GET),通過訪問 http://localhost:8080/hello 可以看到輸出的結果“hello”,如圖 3 所示。
 

運行結果
圖 3  運行結果

讀取配置文件

在以前的項目中我們主要在 XML 文件中進行框架配置,業務的相關配置會放在屬性文件中,然後通過一個屬性讀取的工具類來讀取配置信息。

在 Spring Boot 中我們不再需要使用這種方式去讀取數據了。Spring Boot 中的配置通常放在 application.properties 中,讀取配置信息非常方便,總共分爲 3 種方式。

1)Environment

可以通過 Environment 的 getProperty 方法來獲取想要的配置信息,代碼如下所示。


 

@RestController

public class HelloController {

 

// 注入對象

@Autowired

private Environment env;

 

@GetMapping("/hello")

public String hello() {

// 讀取配置

String port = env.getProperty("server.port");

return port;

}

}

2)@Value

可以注入具體的配置信息,代碼如下所示。


 

@RestController

public class HelloController {

 

// 注入配置

@Value("${server.port}")

private String port;

 

@GetMapping("/hello")

public String hello() {

return port;

}

}

3)自定義配置類

prefix 定義配置的前綴,代碼如下所示。


 

@ConfigurationProperties(prefix = "net.biancheng")

@Component

public class MyConfig {

 

private String name;

 

public String getName() {

return name;

}

 

public void setName(String name) {

this.name = name;

}

 

}

讀取配置的方法代碼如下所示。


 

@RestController

public class HelloController {

 

@Autowired

private MyConfig myConfig;

 

@GetMapping("/hello")

public String hello() {

return myConfig.getName();

}

}

定義配置 application.properties 的方法如下:

net.biancheng.name=zhangsan

profiles 多環境配置

在平時的開發中,項目會被部署到測試環境、生產環境,但是每個環境的數據庫地址等配置信息都是不一樣的。通過 profile 來激活不同環境下的配置文件就能解決配置信息不一樣的問題。在 Spring Boot 中可以通過 spring.profiles.active=dev 來激活不同環境下的配置。

可以定義多個配置文件,每個配置文件對應一個環境,格式爲 application-環境.properties,如表 1 所示。
 

表 1 profile 多環境配置
application.properties 通用配置,不區分環境
application-dev.properties 開發環境
application-test.properties 測試環境
application-prod.properties 生產環境


在開發環境中,可以通過修改 application.properties 中的 spring.profiles.active 的值來激活對應環境的配置,在部署的時候可以通過 java–jar xxx.jar--spring.profiles.active=dev 來指定使用對應的配置。

熱部署

開發過程中經常會改動代碼,此時若想看下效果,就不得不停掉項目然後重啓。

對於 Spring Boot 項目來說,啓動時間是非常快的,在微服務的架構下,每個服務只關注自己的業務,代碼量也非常小,這個啓動時間是可以容忍的。

對於那些臃腫的單體老項目,啓動時間簡直是浪費生命。雖然 Spring Boot 啓動很快,但是我們還是要自己去重啓。能不能做到有改動,它就會悄無聲息地自己把改動的地方重新加載一遍?答案是肯定的,通過 spring-boot-devtools 就可以實現。

只需要添加 spring-boot-devtools 的依賴即可實現熱部署功能,代碼如下所示。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>

actuator 監控

Spring Boot 提供了一個用於監控和管理自身應用信息的模塊,它就是 spring-boot-starter-actuator。該模塊使用起來非常簡單,只需要加入依賴即可,代碼如下所示。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

啓動項目我們會發現在控制檯輸出的內容中增加了圖 4 所示的信息。

圖 4 所示的這些信息是 Actuator 模塊提供的端點信息,具體如表 2 所示,通過訪問這些端點我們可以得到很多監控信息。

比如,我們訪問 /actuator/health 可以得到下面的信息:

{
    "status": "UP"
}

Spring Boot啓動控制檯輸出
圖 4  Spring Boot啓動控制檯輸出

 

表 2 Actuator端點信息
Http方法 路徑 描述 Http默認暴露
GET /actuator/conflgprops 查看配置屬性,包含默認配置 false
GET /actuator/beans 查看bean及其關係列表 false
GET /actuator/heapdump 打印線程棧 false
GET /actuator/env 查看所有環境變量 false
GET /actuator/env/ {name} 查看具體變量值 true
GET /actuator/health 查看應用健康指標 true
GET /actuator/info 查看應用信息 false
GET /actuator/mappings 查看所有 URL 映射 false
GET /actuator/metrics 查看應用基本指標 false
GET /actuator/metrics/{name} 查看具體指標 false
POST /actuator/shutdown 關閉應用 false
GET /actuator/httptrace 查看基本追蹤信息 false
GET /actuator/loggers 顯示應用程序中 loggers 配置 false
GET /actuator/scheduledtasks 顯示定時任務 false

UP 表示當前應用處於健康狀態,如果是 DOWN 就表示當前應用不健康。增加下面的配置可以讓一些健康信息的詳情也顯示出來:

management.endpoint.health.show-details=ALWAYS

再次訪問 /actuator/health,就可以得到健康狀態的詳細信息:

{
    "status": "UP",
    "diskSpace": {
        "status": "UP",
        "total": 491270434816,
        "free": 383870214144,
        "threshold": 10485760
    }
}

大部分端點默認都不暴露出來,我們可以手動配置需要暴露的端點。如果需要暴露多個端點,可以用逗號分隔,如下所示:

management.endpoints.web.exposure.include=configprops,beans

如果想全部端點都暴露的話直接配置成下面的方式:

management.endpoints.web.exposure.include=*

關於這些監控的信息不再贅述,大家可以自行了解。後面我們會介紹如何使用 Spring Boot Admin 在頁面上更加直觀地展示這些信息,目前都是 Json 格式的數據,不方便查看。

自定義 actuator 端點

在很多場景下,我們需要自定義一些規則來判斷應用的狀態是否健康,可以採用自定義端點的方式來滿足多樣性的需求。如果我們只是需要對應用的健康狀態增加一些其他維度的數據,可以通過繼承 AbstractHealthIndicator 來實現自己的業務邏輯。代碼如下所示。


 

@Component

public class UserHealthIndicator extends AbstractHealthIndicator {

@Override

protected void doHealthCheck(Builder builder) throws Exception {

builder.up().withDetail("status", true);

// builder.down().withDetail("status", false);

}

}

通過 up 方法指定應用的狀態爲健康,down 方法指定應用的狀態爲不健康。withDetail 方法用於添加一些詳細信息。訪問 /actuator/health,可以得到我們自定義的健康狀態的詳細信息:

{
    "status": "UP",
    "details": {
        "user": {
            "status": "UP",
            "details": {
                "status": true
            }
        },
        "diskSpace": {
            "status": "UP",
            "details": {
                "total":
                249795969024,
                "free": 7575375872,
                "threshold": 10485760
            }
        }
    }
}

上面我們是在框架自帶的 health 端點中進行擴展,還有一種需求是完全開發一個全新的端點,比如查看當前登錄的用戶信息的端點。自定義全新的端點很簡單,通過 @Endpoint 註解就可以實現。代碼如下所示。


 

@Component

@Endpoint(id = "user")

public class UserEndpoint {

 

@ReadOperation

public List<Map<String, Object>> health() {

List<Map<String, Object>> list = new ArrayList<>();

Map<String, Object> map = new HashMap<>();

map.put("userId", 1001);

map.put("userName", "zhangsan");

list.add(map);

return list;

}

}

訪問 /actuator/user 可以看到返回的用戶信息如下:

[
    {
        "userName": "zhangsan",
        "userId": 1001
    }
]

統一異常處理

對於接口的定義,我們通常會有一個固定的格式,比如:

{
    "status": true,
    "code": 200,
    "message": null,
    "data": [
        {
            "id": "101",
            "name": "jack"
        },
        {
            "id": "102",
            "name": "jason"
        }
    ]
}

但是,如果調用方在請求我們的 API 時把接口地址寫錯了,就會得到一個 404 錯誤:

{
    "timestamp": 1492063521109,
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/rest11/auth"
}

後端服務會告訴我們哪個地址沒找到,其實也挺友好。但是因爲我們上面自定義的數據格式跟下面的不一致,所以當用戶拿到這個返回的時候是無法識別的,其中最明顯的是 status 字段。

我們自定義的是 boolean 類型,用來表示請求是否成功,這裏返回的就是 Http 的狀態碼,所以我們需要在發生這種系統錯誤時也能返回我們自定義的那種格式,那就要定義一個異常處理類(代碼如下所示),通過這個類既可以返回統一的格式,也可以統一記錄異常日誌。


 

@ControllerAdvice

public class GlobalExceptionHandler {

private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

 

@ExceptionHandler(value = Exception.class)

@ResponseBody

public ResponseData defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {

logger.error("", e);

ResponseData r = new ResponseData();

r.setMessage(e.getMessage());

if (e instanceof org.springframework.web.servlet.NoHandlerFoundException) {

r.setCode(404);

} else {

r.setCode(500);

}

r.setData(null);

r.setStatus(false);

return r;

}

}

ResponseData 是我們返回格式的實體類,其發生錯誤時也會被捕獲到,然後封裝好返回格式並返回給調用方。最後關鍵的一步是,在 Spring Boot 的配置文件中加上如下代碼所示配置。

# 出現錯誤時, 直接拋出異常
spring.mvc.throw-exception-if-no-handler-found=true
# 不要爲我們工程中的資源文件建立映射
spring.resources.add-mappings=false

然後當我們調用一個不存在的接口時,返回的錯誤信息就是我們自定義的那種格式了:

{
    "status": false, "code": 404,
    "message": "No handler found for GET /rest11/auth", "data": null
}

最後貼上 ResponseData 的定義,代碼如下所示。


 

public class ResponseData {

private Boolean status = true;

private int code = 200;

private String message;

private Object data;

// get set ...

}

異步執行

異步調用就是不用等待結果的返回就執行後面的邏輯;同步調用則需要等待結果再執行後面的邏輯。

通常我們使用異步操作時都會創建一個線程執行一段邏輯,然後把這個線程丟到線程池中去執行,代碼如下所示。


 

ExecutorService executorService = Executors.newFixedThreadPool(10);

executorService.execute(() -> {

try {

// 業務邏輯

} catch (Exception e) {

e.printStackTrace();

} finally {

}

});

這種方式儘管使用了 Java 的 Lambda,但看起來沒那麼優雅。在 Spring 中有一種更簡單的方式來執行異步操作,只需要一個 @Async 註解即可,代碼如下所示。


 

@Async

public void saveLog() {

System.err.println(Thread.currentThread().getName());

}

我們可以直接在 Controller 中調用這個業務方法,它就是異步執行的,會在默認的線程池中去執行。需要注意的是,一定要在外部的類中去調用這個方法,如果在本類調用則不起作用,比如 this.saveLog()。最後在啓動類上開啓異步任務的執行,添加 @EnableAsync 即可。

另外,關於執行異步任務的線程池我們也可以自定義,首先我們定義一個線程池的配置類,用來配置一些參數,具體代碼如下所示。


 

@Configuration

@ConfigurationProperties(prefix = "spring.task.pool")

public class TaskThreadPoolConfig {

// 核心線程數

private int corePoolSize = 5;

 

// 最大線程數

private int maxPoolSize = 50;

 

// 線程池維護線程所允許的空閒時間

private int keepAliveSeconds = 60;

 

// 隊列長度

private int queueCapacity = 10000;

 

// 線程名稱前綴

private String threadNamePrefix = "FSH-AsyncTask-";

// get set ...

}

然後我們重新定義線程池的配置,代碼如下所示。


 

@Configuration

public class AsyncTaskExecutePool implements AsyncConfigurer {

private Logger logger = LoggerFactory.getLogger(AsyncTaskExecutePool.class);

@Autowired

private TaskThreadPoolConfig config;

 

@Override

public Executor getAsyncExecutor() {

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

executor.setCorePoolSize(config.getCorePoolSize());

executor.setMaxPoolSize(config.getMaxPoolSize());

executor.setQueueCapacity(config.getQueueCapacity());

executor.setKeepAliveSeconds(config.getKeepAliveSeconds());

executor.setThreadNamePrefix(config.getThreadNamePrefix());

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

executor.initia lize();

return executor;

}

 

@Override

public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {

// 異步任務中異常處理

return new AsyncUncaughtExceptionHandler() {

@Override

public void handleUncaughtException(Throwable arg0, Method arg1, Object... arg2) {

 

logger.error("==========================" + arg0.getMessage() + "=======================", arg0);

logger.error("exception method:" + arg1.getName());

}

};

}

}

配置完之後我們的異步任務執行的線程池就是我們自定義的了,我們可以在屬性文件裏面配置線程池的大小等信息,也可以使用默認的配置:

spring.task.pool.maxPoolSize=100

最後講一下線程池配置的拒絕策略。當我們的線程數量高於線程池的處理速度時,任務會被緩存到本地的隊列中。隊列也是有大小的,如果超過了這個大小,就需要有拒絕的策略,不然就會出現內存溢出。目前支持兩種拒絕策略:

  • AbortPolicy:直接拋出 java.util.concurrent.RejectedExecutionException 異常。
  • CallerRunsPolicy:主線程直接執行該任務,執行完之後嘗試添加下一個任務到線程池中,這樣可以有效降低向線程池內添加任務的速度。


建議大家用 CallerRunsPolicy 策略,因爲當隊列中的任務滿了之後,如果直接拋異常,那麼這個任務就會被丟棄。如果是 CallerRunsPolicy 策略,則會用主線程去執行,也就是同步執行,這樣操作最起碼任務不會被丟棄。

隨機端口

在實際的開發過程中,每個項目的端口都是定好的,通過 server.port 可以指定端口。

當一個服務想要啓動多個實例時,就需要改變端口,特別是在我們後面進行 Spring Cloud 學習的時候,服務都會註冊到註冊中心裏去,爲了能夠讓服務隨時都可以擴容,在服務啓動的時候能隨機生成一個可以使用的端口是最好不過的。

在 Spring Boot 中,可以通過 ${random} 來生成隨機數字,我們可以這樣使用:

server.port=${random.int[2000,8000]}

通過 random.int 方法,指定隨機數的訪問,生成一個在 2000 到 8000 之間的數字,這樣每次啓動的端口就都不一樣了。

其實上面的方法雖然能夠達到預期的效果,但是也會存在一些問題:如果這個端口已經在使用了,那麼啓動必然會報錯。所以我們可以通過代碼的方式來隨機生成一個端口,然後檢測是否被使用,這樣就能生成一個沒有被使用的端口。

編寫一個啓動參數設置類,代碼如下所示。


 

public class StartCommand {

private Logger logger = LoggerFactory.getLogger(StartCommand.class);

 

public StartCommand(String[] args) {

Boolean isServerPort = false;

String serverPort = "";

if (args != null) {

for (String arg : args) {

if (StringUtils.hasText(arg) && arg.startsWith("--server.port")) {

isServerPort = true;

serverPort = arg;

break;

}

}

}

// 沒有指定端口, 則隨機生成一個可用的端口

if (!isServerPort) {

int port = ServerPortUtils.getAvailablePort();

logger.info("current server.port=" + port);

System.setProperty("server.port", String.valueOf(port));

} else {

logger.info("current server.port=" + serverPort.split("=")[1]);

System.setProperty("server.port", serverPort.split("=")[1]);

}

}

}

通過對啓動參數進行遍歷判斷,如果有指定啓動端口,後續就不自動生成了;如果沒有指定,就通過 ServerPortUtils 獲取一個可以使用的端口,然後設置到環境變量中。在 application.properties 中通過下面的方式獲取端口:

server.port=${server.port}

關於獲取可用端口的代碼如下所示。


 

public static int getAvailablePort() {

int max = 65535;

int min = 2000;

Random random = new Random();

int port = random.nextInt(max)%(max-min+1) + min;

boolean using = NetUtils.isLoclePortUsing(port);

if (using) {

return getAvailablePort();

} else {

return port;

}

}

獲取可用端口的主要邏輯是指定一個範圍,然後生成隨機數字,最後通過 NetUtils 來檢查端口是否可用。如果獲取到可用的端口則直接返回,沒有獲取到可用的端口則執行回調邏輯,重新獲取。檢測端口是否可用主要是用 Socket 來判斷這個端口是否可以被鏈接。

最後在啓動類中調用端口即可使用,代碼如下所示。


 

public class FshHouseServiceApplication {

public static void main(String[] args) {

// 啓動參數設置, 比如自動生成端口

new StartCommand(args);

SpringApplication.run(FshHouseServiceApplication.class, args);

}

}

編譯打包

傳統的 Web 項目在部署的時候,是編譯出一個 war 包放到 Tomcat 的 webapps 目錄下。而在 Spring Boot 構建的 Web 項目中則打破了這一傳統部署的方式,它採用更加簡單的內置容器方式來部署應用程序,只需要將應用編譯打包成一個 jar 包,直接可以通過 java–jar 命令啓動應用。

在項目的 pom.xml 中增加打包的 Maven 插件,代碼如下所示。


 
  1. <build>
  2. <plugins>
  3. <!-- 打包插件 -->
  4. <plugin>
  5. <groupId>org.springframework.boot</groupId>
  6. <artifactId>spring-boot-maven-plugin</artifactId>
  7. <configuration>
  8. <executable>true</executable>
  9. <mainClass>net.biancheng.spring_boot_example.App</mainClass>
  10. </configuration>
  11. </plugin>
  12.  
  13. <!-- 編譯插件, 指定JDK版本 -->
  14. <plugin>
  15. <groupId>org.apache.maven.plugins</groupId>
  16. <artifactId>maven-compiler-plugin</artifactId>
  17. <configuration>
  18. <source>1.8</source>
  19. <target>1.8</target>
  20. </configuration>
  21. </plugin>
  22. </plugins>
  23. </build>

mainClass 配置的是我們的啓動入口類,配置完成後可以通過 Maven 的 mvn clean package 命令進行編譯打包操作。編譯完成後在 target 目錄下會生成對應的 jar 包,部署的時候直接調用 java–jar xx.jar 即可啓動應用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章