天天看點

SpringCloud建構後端的common項目&使用Feign實作服務間調用

很久沒寫SpringCloud相關的部落格啦QaQ【總是借口拖延部落格TUT我要反思】,不過期間我也進一步學習了SpringCloud。

言歸正傳,此次系列部落格準備介紹如下方面(如果之後我還有動力繼續寫下去的話TUT):

  1. 如何搭建SpringCloud下一些實用工具(Zipkin、SpringConfigServer等)的搭建(之前一直拖着沒寫…)
  2. 關于我新學習的SpringCloud下的一些技術和技巧;

此次部落格将基于較新的SpringCloud和SpringBoot版本(是以會重新開個GitHub連結完成後續更新):【版本的一緻性對于SpringCloud很重要!不一緻可能會帶來很多未知問題!】

  • spring-boot-starter-parent:2.1.3.RELEASE
  • spring-cloud-dependencies:Finchley.SR2

本篇部落格将介紹如下兩個方面:

  1. 建構後端的common項目
  2. 使用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倉庫:

  1. 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識别讀取
  1. 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内容,這在很多時候能夠友善我們定位問題,效果如下圖:

SpringCloud建構後端的common項目&amp;使用Feign實作服務間調用

配置時,有個關鍵點在于切面位置的定義:@Pointcut(“execution(public * com.example.demo.controller..(…))”):此處com.example.demo.controller為之後後端微服務中controller的位置(在common項目中并不存在controller包,是後端微服務項目中才有controller包),即所有的後端微服務的XXController所在的包名都要是com.example.demo.controller,這樣才能被該切面掃描到:

SpringCloud建構後端的common項目&amp;使用Feign實作服務間調用

其它配置(如: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均啟動後:

  1. 正常運作效果:
SpringCloud建構後端的common項目&amp;使用Feign實作服務間調用
SpringCloud建構後端的common項目&amp;使用Feign實作服務間調用

剛啟動完後可能需要等一會時間才能看到是"二号後端",因為注冊至eureka需要時間,一開始斷路器會fallback到備用服務上,是以會顯示"二号的替代品"。過一會後再重新整理url即可:

SpringCloud建構後端的common項目&amp;使用Feign實作服務間調用
SpringCloud建構後端的common項目&amp;使用Feign實作服務間調用
  1. 停掉backend-two後的運作效果:
SpringCloud建構後端的common項目&amp;使用Feign實作服務間調用
SpringCloud建構後端的common項目&amp;使用Feign實作服務間調用

GitHub位址:https://github.com/LeiDengDengDeng/spring-cloud-common-demo

有問題或者有什麼好的想法歡迎大家和我交流噢!

繼續閱讀