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:


<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.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.1.3.RELEASEversion>
        <relativePath/> 
    parent>

    <groupId>com.examplegroupId>
    <artifactId>commonartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <packaging>jarpackaging>
    <name>commonname>
    <description>Common util and config for backend projectsdescription>

    <properties>
        <java.version>1.8java.version>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.4version>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-aopartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>

        <dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-swagger2artifactId>
            <version>2.8.0version>
        dependency>

        <dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-swagger-uiartifactId>
            <version>2.2.2version>
        dependency>
    dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-dependenciesartifactId>
                <version>Finchley.SR2version>
                <type>pomtype>
                <scope>importscope>
            dependency>
        dependencies>
    dependencyManagement>

project>
  • 可以看到项目中引用了大量的springcloud等jar包。因为我们需要将common项目打成jar包,供其它后端微服务使用,因此,所有后端微服务所公共使用的jar包均可以放到本common项目中。

  • 需注意,要删除掉springboot项目中原来用于build jar的插件配置,该插件是用来构建正常的springboot项目的可运行jar包,而我们不需要构建可运行jar,因此需要删掉下面的配置:

  <build>
      <plugins>
          <plugin>
              <groupId>org.springframework.bootgroupId>
              <artifactId>spring-boot-maven-pluginartifactId>
          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内容,这在很多时候能够方便我们定位问题,效果如下图:

在这里插入图片描述

配置时,有个关键点在于切面位置的定义:@Pointcut(“execution(public * com.example.demo.controller..(…))”):此处com.example.demo.controller为之后后端微服务中controller的位置(在common项目中并不存在controller包,是后端微服务项目中才有controller包),即所有的后端微服务的XXController所在的包名都要是com.example.demo.controller,这样才能被该切面扫描到:

SpringCloud构建后端的common项目&使用Feign实现服务间调用_第1张图片

其它配置(如:GlobalExceptionHandler全局异常处理)、工具类(如:AssertUtil、DoubleFormatUtil浮点数的转换工具)和公共类(如:Response、ServiceInfo、PageVO)的配置在此不一一赘述。

4. 打包项目

完成上述步骤后,使用mvn install安装到本地仓库即可供本电脑上的项目使用。

【如果想在不同电脑上共享该jar包的使用又不想自己下源码,需要配置nexus私服,使用mvn deploy发布到私服中,此处省略具体步骤】

二、利用common构建后端微服务,使用Feign完成服务间调用

1. 新建SpirngBoot项目,取名为backend-one,修改其pom.xml:


<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.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.1.3.RELEASEversion>
        <relativePath/> 
    parent>
    <groupId>com.examplegroupId>
    <artifactId>backend-oneartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>backend-onename>
    <description>A simple backend projectdescription>

    <properties>
        <java.version>1.8java.version>
    properties>

    <dependencies>
        <dependency>
            <groupId>com.examplegroupId>
            <artifactId>commonartifactId>
            <version>0.0.1-SNAPSHOTversion>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            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项目&使用Feign实现服务间调用_第2张图片
SpringCloud构建后端的common项目&使用Feign实现服务间调用_第3张图片

刚启动完后可能需要等一会时间才能看到是"二号后端",因为注册至eureka需要时间,一开始断路器会fallback到备用服务上,因此会显示"二号的替代品"。过一会后再刷新url即可:
SpringCloud构建后端的common项目&使用Feign实现服务间调用_第4张图片
SpringCloud构建后端的common项目&使用Feign实现服务间调用_第5张图片

  1. 停掉backend-two后的运行效果:

SpringCloud构建后端的common项目&使用Feign实现服务间调用_第6张图片
SpringCloud构建后端的common项目&使用Feign实现服务间调用_第7张图片


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

有问题或者有什么好的想法欢迎大家和我交流噢!

你可能感兴趣的:(微服务)