Spring Cloud微服务环境搭建

PunkLu 2020年02月06日 345次浏览
Spring Cloud微服务环境搭建

可参考作者之前开发的Spring Cloud项目:

Spring Cloud开发优惠券系统项目

因为其中有大量业务代码,但是希望可以记录下单纯与Spring Cloud技术相关的内容,特抽离出此文。

父项目依赖

因为会有多个子模块,而这些子模块共同依赖一些开源Jar包,因此,先建立一个父模块,用来供子模块继承并使用其中的开源Jar包。

父模块Maven的pom文件内容:

<?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 https://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.4.RELEASE</version>
    </parent>
    <groupId>tech.punklu.springcloud</groupId>
    <artifactId>microservice</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>microservice</name>
    <description>Demo project for Spring Boot</description>

    <packaging>pom</packaging>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
    </properties>


    <dependencies>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.18</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <!-- 标识SpringCloud的版本 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

搭建Eureka服务发现环境

使用IDEA在父项目上右键 NEW -> MODULE搭建新的子项目。

pom文件:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>tech.punklu.springcloud</groupId>
        <artifactId>microservice</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>tech.punklu.springcloud</groupId>
    <artifactId>microservice-eureka</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>microservice-eureka</name>
    <description>Demo project for Spring Boot</description>

    <packaging>jar</packaging>

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

    <!-- Eureka Server:提供服务发现与服务注册 -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

在此项目的Spring Boot启动类上添加注解,以标注这是个Eureka Server。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * Eureka Server启动入口
 */
// 标记为一个Eureka Server应用
@EnableEurekaServer
@SpringBootApplication
public class MicroserviceEurekaApplication {

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

}

在src/main/resources目录下新增application.yml并配置相应的Eureka Server配置。

application.yml:

spring:
  application:
    # 此应用名称
    name: eureka
server:
  # Eureka Server的对外端口,供外部注册服务,发现服务
  port: 8000

eureka:
  instance:
    hostname: localhost
  client:
    # 标识是否从Eureka Server 获取注册信息,默认是true,如果是单节点的Eureka Server,不需要同步
    # 其他节点的数据,设置为false
    fetch-registry: false
    # 是否将自己注册到Eureka Server上,默认是true,单节点设置为false
    register-with-eureka: false
    # 设置Eureka Server的地址,查询服务和注册服务都需要依赖这个地址
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  server:
    renewal-percent-threshold: 0.45 # 设置eureka-server的 实际心跳/期望心跳比值大小,默认0.85,小于这个值,eureka就会进入自我保护机制
    # enable-self-preservation: false 关闭自我保护机制

搭建通用项目

涉及到多个微服务的时候,肯定会有共享资源的情况,比如,同一个VO对象、统一异常处理、统一请求响应对象,多个微服务都会使用到这些资源,这个时候可以封装一个单独的MODULE,来定义这些资源。并在各个微服务的MAVEN依赖中引入。

通用微服务的MAVEN POM文件:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>tech.punklu.springcloud</groupId>
        <artifactId>microservice</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>tech.punklu.springcloud</groupId>
    <artifactId>microservice-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>microservice-common</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>
    <packaging>jar</packaging>

    <dependencies>
        <!-- 引入SpringBoot web功能 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- JSON处理工具 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.31</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.0</version>
        </dependency>
        <!-- 集合工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.0</version>
        </dependency>


    </dependencies>



</project>

定义微服务公用Entity

在下面将创建学生、班级两个微服务,这里在通用包里定义其Entity对象。

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Classname {


    private int id;

    private String classname;
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {


    private int id;

    private String name;
}

定义全局统一异常

/**
 * 通用异常定义
 */
public class CommonException extends Exception {

    public CommonException(String message){
        super(message);
    }
}

定义全局异常增强处理类

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import tech.punklu.springcloud.microservicecommon.exception.CommonException;
import tech.punklu.springcloud.microservicecommon.response.CommonResponse;

import javax.servlet.http.HttpServletRequest;

/**
 * 全局异常增强处理类
 */
@RestControllerAdvice // 对@Controller和@ResponseBody的增强
public class GlobalExceptionAdvice {

    /**
     * 全局异常统一处理方法
     * @param req
     * @param ex
     * @return
     */
    @ExceptionHandler(value = CommonException.class)  // 异常处理器,捕获CommonException异常并进行处理,声明后,Spring会向此方法注入Request请求及捕获的异常
    public CommonResponse<String> handlerCouponException(HttpServletRequest req, CommonException ex){
        CommonResponse<String> response = new CommonResponse<>(
                -1,
                "business error"
        );
        // 将错误信息写入CommonResponse的data
        response.setData(ex.getMessage());
        return response;
    }


}

配置Jackson的自定义配置

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.text.SimpleDateFormat;

/**
 * Jackson的自定义配置类
 */
@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper getObjectMapper(){
        ObjectMapper mapper = new ObjectMapper();
        // 设置日期格式
        mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        return mapper;
    }
}

定制Http消息转换器

import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

/**
 * 定制Http消息转换器
 */
@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    /**
     * 配置Http消息的转换器
     * @param converters
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        //  清空现有对象
        converters.clear();
        // 使用MappingJackson2HttpMessageConverter实现Java类与Json的转换
        converters.add(new MappingJackson2HttpMessageConverter());
    }
}

定义通用响应封装类

/**
 * 请求通用统一响应对象定义
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResponse<T> implements Serializable {

    private Integer code;

    private String message;

    private T data;

    public CommonResponse(Integer code,String message){
        this.code = code;
        this.message = message;
    }

}

搭建微服务

搭建班级微服务

在父项目下新建MODULE,POM依赖如下:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>tech.punklu.springcloud</groupId>
        <artifactId>microservice</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>tech.punklu.springcloud</groupId>
    <artifactId>microservice-classname</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>microservice-classname</name>
    <description>Demo project for Spring Boot</description>

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

    <dependencies>
        <!-- Eureka 客户端,客户端向Eureka Server注册的时候会提供一系列的元数据信息,例如,主机,端口,健康检查url等
         Eureka Server接受每个客户端发送的心跳信息,如果在某个配置的超时时间内未接收到心跳信息,实例会被从注册列表中移除-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <!-- 引入Feign,可以以声明的方式调用微服务 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!-- 引入 服务容错 Hystrix 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

        <!-- 引入 服务消费者 Ribbon 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>






        <!-- 通用模块 -->
        <dependency>
            <groupId>tech.punklu.springcloud</groupId>
            <artifactId>microservice-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- apache 工具类  -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.9</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

在Spring Boot启动类上标注注解,以将其作为一个微服务注册到Eureka Server上:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@EnableEurekaClient
@SpringBootApplication
public class MicroserviceClassnameApplication {

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

}

在src/main/resources下新建application.yml文件,进行相应微服务的配置:

server:
  port: 7002
  servlet:
    context-path: /microservice-classname  # controller 前缀

spring:
  application:
    # 此微服务名称
    name: eureka-client-microservice-classname


  # 开启 ribbon 重试机制, 即获取服务失败是否从另外一个节点重试,此处设置为true
  cloud:
    loadbalancer:
      retry:
        enabled: true

# 指定eureka server地址,完成此微服务向Eureka Server的注册
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8000/eureka/

# 日志配置
logging:
  level:
    com.imooc.coupon: debug

# feign 相关的配置
feign:
  hystrix:
    enabled: true   # Hystrix 默认是关闭的,这里启用
  client:
    config:
      default:  # 全局的
        connectTimeout: 2000  # 默认的连接超时时间是 10s 当前微服务访问其他微服务,默认的连接超时时间
        readTimeout: 5000 # 读取接口数据响应的超时时间
      eureka-client-coupon-template:  # 单独服务的配置,这里拿模板微服务进行配置,eureka-client-coupon-template即为spring:application:name的值
        connectTimeout: 3000  # 默认的连接超时时间是 10s
        readTimeout: 5000

定义相应的Controller及Service来实现,根据传入的id,返回相应的内容的功能:

ClassController:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import tech.punklu.springcloud.microserviceclassname.service.IClassnameService;
import tech.punklu.springcloud.microservicecommon.domain.Classname;
import tech.punklu.springcloud.microservicecommon.response.CommonResponse;

@Slf4j
@RestController
public class ClassController {

    @Autowired
    private IClassnameService iClassnameService;

    @PostMapping(value = "/classcontroller/selectClass")
    public CommonResponse<Classname> selectClass(@RequestBody Integer id){
        return new CommonResponse<>(1,"",iClassnameService.selectClassname(id));
    }

}

Service Interface:

import tech.punklu.springcloud.microservicecommon.domain.Classname;

public interface IClassnameService {

    Classname selectClassname(Integer id);
}

Service实现类:

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import tech.punklu.springcloud.microserviceclassname.service.IClassnameService;
import tech.punklu.springcloud.microservicecommon.domain.Classname;

@Slf4j
@Service
public class ClassnameServiceImpl implements IClassnameService {



    @Override
    public Classname selectClassname(Integer id) {
        Classname classname = new Classname(1,"本四");
        return classname;
    }
}

搭建学生微服务

在父项目上新建MODULE,创建学生微服务:

POM依赖:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>tech.punklu.springcloud</groupId>
        <artifactId>microservice</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>tech.punklu.springcloud</groupId>
    <artifactId>microservice-student</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>microservice-student</name>
    <description>Demo project for Spring Boot</description>

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

    <dependencies>
        <!-- Eureka 客户端,客户端向Eureka Server注册的时候会提供一系列的元数据信息,例如,主机,端口,健康检查url等
         Eureka Server接受每个客户端发送的心跳信息,如果在某个配置的超时时间内未接收到心跳信息,实例会被从注册列表中移除-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <!-- 引入Feign,可以以声明的方式调用微服务 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!-- 引入 服务容错 Hystrix 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

        <!-- 引入 服务消费者 Ribbon 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>






        <!-- 通用模块 -->
        <dependency>
            <groupId>tech.punklu.springcloud</groupId>
            <artifactId>microservice-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- apache 工具类  -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.9</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

因此此学生微服务需要调用班级微服务的接口,需要用到Feign完成调用、Ribbon完成负载均衡、以及需要启用保证高可用的Hystrix。因此,需要在Spring Boot启动类上添加相应注解。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableEurekaClient // 作为Eureka Client
@EnableFeignClients // 需要调用其他微服务,所以引入Feign
@EnableCircuitBreaker // 启用Hystrix 断路器
public class MicroserviceStudentApplication {

    // Ribbon 会发现微服务,并做负载均衡
    // 定义Ribbon的RestTemplate,通过RestTemplate作为调用其他微服务的入口
    @Bean
    @LoadBalanced
    // 实现Ribbon的负载均衡
    RestTemplate restTemplate(){
        return new RestTemplate();
    }

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

}

然后在src/main/resources目录下添加application.yml文件,并进行相应配置:

server:
  port: 7001
  servlet:
    context-path: /microservice-student  # controller 前缀

spring:
  application:
    # 此微服务名称
    name: eureka-client-microservice-student


  # 开启 ribbon 重试机制, 即获取服务失败是否从另外一个节点重试,此处设置为true
  cloud:
    loadbalancer:
      retry:
        enabled: true

# 指定eureka server地址,完成此微服务向Eureka Server的注册
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8000/eureka/

# 日志配置
logging:
  level:
    com.imooc.coupon: debug

# feign 相关的配置
feign:
  hystrix:
    enabled: true   # Hystrix 默认是关闭的,这里启用
  client:
    config:
      default:  # 全局的
        connectTimeout: 2000  # 默认的连接超时时间是 10s 当前微服务访问其他微服务,默认的连接超时时间
        readTimeout: 5000 # 读取接口数据响应的超时时间
      eureka-client-coupon-template:  # 单独服务的配置,这里拿模板微服务进行配置,eureka-client-coupon-template即为spring:application:name的值
        connectTimeout: 3000  # 默认的连接超时时间是 10s
        readTimeout: 5000

然后定义一个Controller以及对应的Service,供浏览器访问:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import tech.punklu.springcloud.microservicecommon.response.CommonResponse;
import tech.punklu.springcloud.microservicestudent.service.IStudentService;
import tech.punklu.springcloud.microservicestudent.vo.StudentVO;

@Slf4j
@RestController
public class StudentController {

    @Autowired
    private IStudentService iStudentService;

    @GetMapping(value = "/studentcontroller/selectStudent")
    @ResponseBody
    public CommonResponse<StudentVO> selectStudent(int id){
        CommonResponse<StudentVO> studentVOCommonResponse = new CommonResponse<>(1,"",iStudentService.selectStudent(id));
        return studentVOCommonResponse;
    }



}

其中StudentVO为封装对象:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class StudentVO {

    private int id;

    private String name;

    private String classname;

}
import tech.punklu.springcloud.microservicestudent.vo.StudentVO;

public interface IStudentService {

    /**
     * 根据编号查询学生
     * @param id
     * @return
     */
    StudentVO selectStudent(Integer id);
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tech.punklu.springcloud.microservicestudent.feign.ClassClient;
import tech.punklu.springcloud.microservicestudent.service.IStudentService;
import tech.punklu.springcloud.microservicestudent.vo.StudentVO;

@Service
public class StudentServiceImpl implements IStudentService {


    @Autowired
    private ClassClient classClient;

    @Override
    public StudentVO selectStudent(Integer id) {
        StudentVO studentVO = new StudentVO();
        studentVO.setId(id);
        studentVO.setName("朋克卢");
        studentVO.setClassname(classClient.selectClass(id).getData().getClassname());
        return studentVO;
    }
}

Service实现类中的ClassClient即为为使用Feign调用班级微服务创建的接口:

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import tech.punklu.springcloud.microservicecommon.domain.Classname;
import tech.punklu.springcloud.microservicecommon.response.CommonResponse;
import tech.punklu.springcloud.microservicestudent.feign.hystrix.ClassClientHystrix;

// value与班级微服务中的spring application name的值一致,fallback指定服务熔断兜底策略
@FeignClient(value = "eureka-client-microservice-classname",fallback = ClassClientHystrix.class)
public interface ClassClient {

    @RequestMapping(value = "/microservice-classname/classcontroller/selectClass",method = RequestMethod.POST)
    CommonResponse<Classname> selectClass(Integer id);
}

为了保证微服务的高可用,不至于因为一个微服务挂掉导致整个系统挂掉,还要创建兜底策略相关的处理类ClassClientHystrix:

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import tech.punklu.springcloud.microservicecommon.domain.Classname;
import tech.punklu.springcloud.microservicecommon.response.CommonResponse;
import tech.punklu.springcloud.microservicestudent.feign.ClassClient;

@Slf4j
@Component
public class ClassClientHystrix implements ClassClient {

    @Override
    public CommonResponse<Classname> selectClass(Integer id) {
        log.error("[eureka-client-classname] request error");
        return new CommonResponse<>(-1,"[eureka-client-coupon-settlement] request error",new Classname());
    }
}

搭建微服务网关

对于微服务应用而言,有必要使用网关来统一管理对各微服务的访问,同时,还可以在网关处设置过滤器及限流器来保证功能及微服务的高可用性。

使用IDEA在父项目上右键 NEW -> MODULE新建网关项目。

pom文件:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>tech.punklu.springcloud</groupId>
        <artifactId>microservice</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>tech.punklu.springcloud</groupId>
    <artifactId>microservice-gateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>microservice-gateway</name>
    <description>Demo project for Spring Boot</description>

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

    <dependencies>
        <!-- Eureka 客户端,客户端向Eureka Server注册的时候会提供一系列的元数据信息,例如,主机,端口,健康检查url等
         Eureka Server接受每个客户端发送的心跳信息,如果在某个配置的超时时间内未接收到心跳信息,实例会被从注册列表中移除-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <!-- 服务网关gateway(使用Zuul组件) -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>

        <!-- Apache工具类 -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>1.3.2</version>
        </dependency>

        <!--- guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

在Spring Boot启动类上添加注解,以实现网关效果:

import org.springframework.boot.SpringApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

/**
 * 网关应用启动入口
 * 1、@EnableZuulProxy标识当前的应用是Zuul Server
 * 2、@SpringCloudApplication是组合注解,包含启动项目的@SpringBootApplication注解、发现Eureka Server的@EnableDiscoveryCient注解,
 * 以及实现网关层面Hystrix熔断的启动注解@EnableCircuitBreaker
 */
@EnableZuulProxy
@SpringCloudApplication
public class MicroserviceGatewayApplication {

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

}

在src/main/resources目录下新建application.yml配置文件,并配置网关相应配置:

server:
  port: 9000
spring:
  application:
    name: microservice-gateway

# 将此网关应用注册到Eureka服务器上
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8000/eureka/


# 配置好Zuul网关后,本来应该访问127.0.0.1:7001/microservice-student/template/build的请求可以用网关地址
# 127.0.0.1:9000/punklu/microservice-student/template/build 通过Zuul转发进行访问

# 配置网关
zuul:
  prefix: /punklu # 网关的前缀
  routes:
    # 学生微服务路由配置定义
    student: # 名字自定义
      path: /microservice-student/**  # 此处的microservice-student即为学生微服务配置文件中的servlet:context-path值
      serviceId: eureka-client-microservice-student                  # 即为学生微服务配置文件中的spring:application:name的值
      strip-prefix: false # 是否跳过前缀(microservice-student)
    # 班级微服务 路由配置定义
    class:
      path: /microservice-classname/**  # 此处的microservice-classname即为班级微服务配置文件中的servlet:context-path值
      serviceId: eureka-client-microservice-classname                 # 即为班级微服务配置文件中的spring:application:name的值
      strip-prefix: false # 是否跳过前缀(microservice-b)

  host:
    connect-timeout-millis: 15000 # 连接微服务超时时间 15s
    socket-timeout-millis: 60000  # TCP连接的超时时间
# Zuul 负载均衡配置
ribbon:
  ConnectTimeOut: 30000 # Zuul通过ribbon实现负载均衡转发时的超时时间
  ReadTimeOut: 12000 # 获取相应的超时时间

其中的zuul网关配置就是将多个微服务在这里一一声明出来。

使用过滤器:

首先创建抽象Zuul网关过滤器:

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;

/**
 * 通用的抽象Zuul网关过滤器(需要继承ZuulFilter类)
 * ZuulFilter方法:
 * 1、filterType():过滤器类型,可为pre,route,post和error,route负责分发到具体微服务,pre和post分别是在此之前和之后,error是错误过滤器
 * 2、filterOrder():指定过滤器执行顺序,越小越先执行,需要注意的是只对同一类型的过滤器进行排序
 * 3、shouldFilter():返回boolean类型,true时表示执行该过滤器的run方法,false表示不执行,可用于根据条件判断是否执行
 * 4、run():过滤器的实际执行方法
 */
public abstract class AbstractZuulFilter extends ZuulFilter {

    // 用于在过滤器之间传递信息,数据保存在每个请求的ThreadLocal中
    // 扩展了Map
    RequestContext context;

    // 标识请求是否已经结束,用于判断是否继续向下执行
    private final static String NEXT = "next";

    @Override
    public boolean shouldFilter() {
        // 获取当前线程的RequestContext
        RequestContext ctx = RequestContext.getCurrentContext();
        // 获取是否包含名为next的key,默认值为true,即为继续向下执行
        return (boolean)ctx.getOrDefault(NEXT,true);
    }

    @Override
    public Object run() throws ZuulException {
        // 请求上下文初始化
        context = RequestContext.getCurrentContext();
        return cRun();
    }

    // 抽象的Zuul过滤器类,不要有真正的实现类,所以定义为抽象方法
    protected abstract Object cRun();

    // 过滤不通过的情况下的返回信息
    Object fail(int code,String msg){
        // 执行失败,把next设为false,过滤器不再继续向下执行
        context.set(NEXT,false);
        // 设置Zuul对请求返回false
        context.setSendZuulResponse(false);
        // 设置响应的内容类型
        context.getResponse().setContentType("text/html;charset=UTF-8");
        // 设置返回错误的响应码
        context.setResponseStatusCode(code);
        // 设置响应内容
        context.setResponseBody(String.format("{\"result\": \"%s!\"}",msg));
        return null;
    }

    // 设置过滤成功情况下的返回信息
    Object success(){
        context.set(NEXT,true);
        return null;
    }
}

创建相应的具体过滤器类:

Pre类型的抽象Zuul过滤器:

import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;

/**
 * Pre类型的抽象Zuul过滤器
 */
public abstract class AbstractPreZuulFilter extends AbstractZuulFilter {

    /**
     * 指定此过滤器类型为Pre类型
     * @return
     */
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }
}

Post类型的抽象Zuul过滤器类:

import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;

/**
 * Post类型的抽象Zuul过滤器类
 */
public abstract class AbstractPostZuulFilter extends AbstractZuulFilter{

    /**
     * 指定此过滤器类型为Post类型
     * @return
     */
    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }
}

日志过滤器:

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@Component
public class AccessLogFilter extends AbstractPostZuulFilter {

    @Override
    protected Object cRun() {
        HttpServletRequest request = context.getRequest();
        // 从请求上下文中取出在PreRequestFilter中设置的请求时间戳
        Long startTime = (Long) context.get("startTime");
        String uri = request.getRequestURI();
        long duration = System.currentTimeMillis()-startTime;
        // 打印从网关通过的请求的uri以及执行花费的时间
        log.info("uri: {} , duration: {} ",uri,duration);
        return success();
    }

    @Override
    public int filterOrder() {
        // 设置执行时间为返回所有的过滤器之前
        return FilterConstants.SEND_RESPONSE_FILTER_ORDER-1;
    }
}

Pre类型的过滤器,在过滤器中存储客户端发起请求的时间戳:

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * 在过滤器中存储客户端发起请求的时间戳
 */
@Slf4j
@Component
public class PreRequestFilter extends AbstractPreZuulFilter{

    @Override
    protected Object cRun() {
        // 在请求上下文中存储当前时间戳
        context.set("startTime",System.currentTimeMillis());
        return success();
    }

    @Override
    public int filterOrder() {
        return 0;
    }
}

限流过滤器:

import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * 限流过滤器
 */
@Slf4j
@Component
public class RateLimiterFilter extends AbstractPreZuulFilter {


    // 限流器(每秒可以获取到两个令牌)
    RateLimiter rateLimiter = RateLimiter.create(2.0);

    /**
     * 限流方法
     * @return
     */
    @Override
    protected Object cRun() {
        // 获取当前请求对象
        HttpServletRequest request = context.getRequest();
        // 尝试获取令牌
        if (rateLimiter.tryAcquire()){
            log.info("get rate token success");
            return success();
        }else {
            log.error("rate limit: {}",request.getRequestURI());
            return fail(402,"error : rate limit");
        }
    }

    @Override
    public int filterOrder() {
        return 2;
    }
}

Token校验过滤器:

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * 校验请求中传递的token是否存在的Zuul过滤器
 */
@Slf4j
@Component
public class TokenFilter extends AbstractPreZuulFilter {

    /**
     * 请求的token校验
     * @return
     */
    @Override
    protected Object cRun() {
        HttpServletRequest request = context.getRequest();
        // 打印请求的方法,URL
        log.info(String.format("%s request to %s",request.getMethod(),request.getRequestURL().toString()));
        Object token = request.getParameter("token");
        if (null == token){
            log.error("error: token is empty");
            // 401:没有权限访问
            return fail(401,"error: token is empty");
        }
        return success();
    }

    @Override
    public int filterOrder() {
        return 1;
    }
}

测试微服务

至此,微服务整体就已经搭建起来了,在浏览器中使用以下地址进行访问:

http://localhost:9000/punklu/microservice-student/studentcontroller/selectStudent?id=1&token=1

A4BF3468-BD12-4cf7-82F4-077DFB731813

可以看到已经正确返回了结果,这里需要注意的是,因为在Zuul处配置了token过滤器,所以必须在最后加上token字段及值才可访问,否则会报在过滤器处定义好的 "result": "error: token is empty!"错误。