java版工程管理系統原始碼Spring Cloud之SpringCloud-Spring Boot專案詳細搭建步驟

JIAN2發表於2022-04-19

工程管理系統原始碼:

@獲取原始碼+wx: haiwabbc2

在 Spring Tools 4 for Eclipse 中依次選擇 File->New->Maven Project,然後在出現的介面中按圖所示增加相關資訊。
在這裡插入圖片描述

 

<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>

 編寫啟動類,程式碼如下所示。

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

啟動類使用了 @SpringBootApplication 註解,這個註解表示該類是一個 Spring Boot 應用。直接執行 App 類即可啟動,啟動成功後在控制檯輸出資訊,預設埠是 8080,如圖所示。

 

可以看到,我們只在 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 依賴。

這個功能和 提供的是同一個功能,方便快速搭建 Spring Boot 專案腳手架。
編寫第一個 REST 介面
本節將建立一個控制器,編寫第一個 REST 介面,訪問地址使用 /hello,程式碼如下所示。

@RestControllerpublic class HelloController {    @GetMapping("/hello")
    public String hello() {        return "hello";
    }
}

 是   和 @ResponseBody 的組合註解,可以直接返回 Json 格式資料。

@GetMapping 其實就是 @RequestMapping(method=RequestMethod.GET),透過訪問 可以看到輸出的結果“hello”。

讀取配置檔案
在以前的專案中我們主要在 XML 檔案中進行框架配置,業務的相關配置會放在屬性檔案中,然後透過一個屬性讀取的工具類來讀取配置資訊。

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

1)Environment
可以透過 Environment 的 getProperty 方法來獲取想要的配置資訊,程式碼如下所示。
 

@RestControllerpublic class HelloController {    // 注入物件
    @Autowired
    private Environment env;    @GetMapping("/hello")
    public String hello() {        // 讀取配置
        String port = env.getProperty("server.port");        return port;
    }
}

2)@Value
可以注入具體的配置資訊,程式碼如下所示。

@RestControllerpublic class HelloController {    // 注入配置
    @Value("${server.port}")
    private String port;    @GetMapping("/hello")
    public String hello() {        return port;
    }
}

3)自定義配置類
prefix 定義配置的字首,程式碼如下所示。

@ConfigurationProperties(prefix = "net.biancheng")@Componentpublic class MyConfig {    private String name;    public String getName() {        return name;
    }    public void setName(String name) {        this.name = name;
    }
}

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

@RestControllerpublic 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 所示的資訊。

下圖所示的這些資訊是 Actuator 模組提供的端點資訊,具體如表 2 所示,透過訪問這些端點我們可以得到很多監控資訊。

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

{    "status": "UP"}

表 2 Actuator端點資訊

 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 來實現自己的業務邏輯。程式碼如下所示。
 

@Componentpublic 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 的狀態碼,所以我們需要在發生這種系統錯誤時也能返回我們自定義的那種格式,那就要定義一個異常處理類(程式碼如下所示),透過這個類既可以返回統一的格式,也可以統一記錄異常日誌。

@ControllerAdvicepublic 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 註解即可,程式碼如下所示。

@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 ...}


我們可以直接在 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 ...}


然後我們重新定義執行緒池的配置,程式碼如下所示。

@Configurationpublic 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}


關於獲取可用埠的程式碼如下所示。

server.port=${server.port}



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

最後在啟動類中呼叫埠即可使用,程式碼如下所示。

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;
    }
}

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

在專案的 pom.xml 中增加打包的 Maven 外掛,程式碼如下所示。

<build>    <plugins>        <!-- 打包外掛 -->        <plugin>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-maven-plugin</artifactId>            <configuration>                <executable>true</executable>                <mainClass>net.biancheng.spring_boot_example.App</mainClass>            </configuration>        </plugin>        <!-- 編譯外掛, 指定JDK版本 -->        <plugin>            <groupId>org.apache.maven.plugins</groupId>            <artifactId>maven-compiler-plugin</artifactId>            <configuration>                <source>1.8</source>                <target>1.8</target>            </configuration>        </plugin>    </plugins></build>


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


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70006413/viewspace-2887733/,如需轉載,請註明出處,否則將追究法律責任。

相關文章