很久沒寫SpringCloud相關的部落格啦QaQ【總是借口拖延部落格TUT我要反思】,不過期間我也進一步學習了SpringCloud。
言歸正傳,此次系列部落格準備介紹如下方面(如果之後我還有動力繼續寫下去的話TUT):
- 如何搭建SpringCloud下一些實用工具(Zipkin、SpringConfigServer等)的搭建(之前一直拖着沒寫…)
- 關于我新學習的SpringCloud下的一些技術和技巧;
此次部落格将基于較新的SpringCloud和SpringBoot版本(是以會重新開個GitHub連結完成後續更新):【版本的一緻性對于SpringCloud很重要!不一緻可能會帶來很多未知問題!】
- spring-boot-starter-parent:2.1.3.RELEASE
- spring-cloud-dependencies:Finchley.SR2
本篇部落格将介紹如下兩個方面:
- 建構後端的common項目
- 使用Feign實作服務間調用(之前介紹過RestTemplate等方式實作後端服務間調用)
準備工作:建立EurekaServer項目,此處省略,可以直接參考GitHub倉庫代碼和快速搭建EurekaServer。
一、建構後端的common項目
在做這件事前,先要闡明動機,為什麼要建構後端的common服務(也可以說是公共jar)呢?
因為在微服務體系中,後端微服務經常會存在多個項目,但這些項目中往往會有很多公共的配置和工具等,這時候我們就需要一個common項目同時為多個後端項目服務,這樣可以減少許多重複代碼和重複配置後端項目的時間。
1. 建立SpringBoot項目,取名為common,修改其pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>common</name>
<description>Common util and config for backend projects</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.2.2</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
- 可以看到項目中引用了大量的springcloud等jar包。因為我們需要将common項目打成jar包,供其它後端微服務使用,是以,所有後端微服務所公共使用的jar包均可以放到本common項目中。
- 需注意,要删除掉springboot項目中原來用于build jar的插件配置,該插件是用來建構正常的springboot項目的可運作jar包,而我們不需要建構可運作jar,是以需要删掉下面的配置:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
2. 删除CommonApplication.java、test目錄和resources下的application.properties
因為我們不需要其運作,我們隻需要其靜态代碼,是以這些檔案都可以且需要被删除。
3. 在項目中增加公共配置和公共工具類
以下僅拿幾個關鍵類舉例介紹,完整代碼和功能參加GitHub倉庫:
- Swagger配置類:
package com.example.demo.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class Swagger2 {
@Value("${spring.application.name}")
private String name;
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.ant("/api/**"))
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(name + "微服務")
.description("提供" + name + "相關功能")
.version("1.0")
.build();
}
}
- 可以看到,我們使用了spring.application.name這個屬性,這個屬性是需要在後面的後端微服務中配置,而不是在這裡配置
- 配置swagger的識别路徑時,PathSelectors.ant("/api/")是為了不讓swagger去識别actuator(這個是spirng自帶的一些統計接口,後續會簡單介紹)的接口,是以我們之後配置後端微服務的路徑時,前面都需要有/api,否則就無法被swagger識别讀取
- AOP實作Web日志列印類:
package com.example.demo.aspect;
import lombok.extern.apachecommons.CommonsLog;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
/**
* 列印web請求和傳回值
*
* @author deng
* @date 2018/10/18
*/
@Aspect
@Component
@CommonsLog
public class WebLogAspect {
@Pointcut("execution(public * com.example.demo.controller.*.*(..))")
public void webLog() {
}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 接收到請求,記錄請求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 記錄下請求内容
log.info("URL : " + request.getRequestURL().toString());
log.info("HTTP_METHOD : " + request.getMethod());
log.info("IP : " + request.getRemoteAddr());
log.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
log.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) throws Throwable {
// 處理完請求,傳回内容
log.info("RESPONSE : " + ret);
}
}
pom.xml中需要引用aop的jar包,是為了配置這個,配置完成後,控制台/日志系統便能夠列印出每次Request和Response内容,這在很多時候能夠友善我們定位問題,效果如下圖:
配置時,有個關鍵點在于切面位置的定義:@Pointcut(“execution(public * com.example.demo.controller..(…))”):此處com.example.demo.controller為之後後端微服務中controller的位置(在common項目中并不存在controller包,是後端微服務項目中才有controller包),即所有的後端微服務的XXController所在的包名都要是com.example.demo.controller,這樣才能被該切面掃描到:
其它配置(如:GlobalExceptionHandler全局異常處理)、工具類(如:AssertUtil、DoubleFormatUtil浮點數的轉換工具)和公共類(如:Response、ServiceInfo、PageVO)的配置在此不一一贅述。
4. 打包項目
完成上述步驟後,使用mvn install安裝到本地倉庫即可供本電腦上的項目使用。
【如果想在不同電腦上共享該jar包的使用又不想自己下源碼,需要配置nexus私服,使用mvn deploy釋出到私服中,此處省略具體步驟】
二、利用common建構後端微服務,使用Feign完成服務間調用
1. 建立SpirngBoot項目,取名為backend-one,修改其pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>backend-one</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>backend-one</name>
<description>A simple backend project</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
可以看到,隻需要依賴我們之前建構的common(其中的groupId、artifactId和version都取決于之前common項目中pom.xml的配置)包和spring-boot-starter-test包。
2. 修改BackendOneApplication:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class BackendOneApplication {
public static void main(String[] args) {
SpringApplication.run(BackendOneApplication.class, args);
}
}
- 增加@EnableDiscoveryClient允許服務注冊上EurekaServer
- 增加@EnableFeignClients允許使用Feign
3. 配置application.yml:
server:
port: 8880
spring:
application:
name: backend-one
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
feign:
hystrix:
enabled: true
logging:
level:
web: TRACE
org:
springframework:
web: TRACE
- 配置端口号和spring.application.name
- 配置eureka注冊中心位址
- 開啟feign的hystrix功能(斷路器,很重要的功能)
- 修改日志等級,進而可以在日志中看到所有注冊的RequestMapping路徑(1.X版本中不需要配置,INFO級别就可以看到)
4. 仿照backend-one的1-3步驟建構backend-two
注意3中的配置檔案略有不同(name和端口)
5. 實作one和two中的Controller,在one中使用feign通路two項目的接口
- backend-one中通路backend-two的controller:
package com.example.demo.service;
import com.example.demo.bean.Response;
import com.example.demo.constant.ServiceInfo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @author deng
* @date 2019/03/13
*/
@FeignClient(value = ServiceInfo.BACKEND_TWO, fallback = TwoApiFallback.class)
public interface TwoApi {
@GetMapping("/api/v1/two/getName")
Response<String> getName();
}
- 配置TwoApi的Fallback(服務降級:在two不可用/調用失敗的時候,one利用服務降級保證原服務的暢通,而不會因為two的fail導緻服務雪崩式不可用)
package com.example.demo.service;
import com.example.demo.bean.Response;
import com.example.demo.util.ResponseFactory;
import org.springframework.stereotype.Component;
/**
* @author deng
* @date 2019/03/13
*/
@Component
public class TwoApiFallback implements TwoApi {
@Override
public Response<String> getName() {
return ResponseFactory.okResponse( "二号的替代品");
}
}
- one的controller調用two的接口:
package com.example.demo.controller;
import com.example.demo.bean.Response;
import com.example.demo.service.TwoApi;
import com.example.demo.util.ResponseFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author deng
* @date 2019/03/13
*/
@RestController
@RequestMapping("/api/v1/one")
public class OneController {
@Autowired
private TwoApi twoApi;
@GetMapping("/test")
public Response<String> get() {
return ResponseFactory.okResponse("你好");
}
@GetMapping("/sayHi")
public String sayHi() {
return "Hello," + twoApi.getName().getRes();
}
}
可以看到,對比RestTemplate,Feign的使用非常簡單,就像調用自己服務内的Service一樣,這就是Feign的僞RPC的特性。
将eureka、backend-one、backend-two均啟動後:
- 正常運作效果:
剛啟動完後可能需要等一會時間才能看到是"二号後端",因為注冊至eureka需要時間,一開始斷路器會fallback到備用服務上,是以會顯示"二号的替代品"。過一會後再重新整理url即可:
- 停掉backend-two後的運作效果:
GitHub位址:https://github.com/LeiDengDengDeng/spring-cloud-common-demo
有問題或者有什麼好的想法歡迎大家和我交流噢!