1.概念与上手实践

2.运行与发布

3. 配置文件

4.静态资源

5.使用 Redis

6.整合 mybatis

7.Thymeleaf

8.Freemarker

9.jsp

10.websocket

11.RabbitMQ

12.https 搭建

13.服务治理

14.Eureka 服务端

15.服务提供者与调用者

16.Eureka 高可用

17.服务续约

18.Ribbon 与 Feign

19.雪崩效应与 Hystrix

20.配置中心 Config

21.config-server-git

22.zuul 网关

1.概念与上手实践

微服务下每个服务都要引入Springsecurity 微服务是springcloud_spring

   

spring boot

Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。用我的话来理解,就是 Spring Boot 其实不是什么新的框架,它默认配置了很多框架的使用方式,就像 Maven 整合了所有的 Jar 包,Spring Boot 整合了所有的框架。

微服务

微服务架构的系统是一个分布式的系统,按业务进行划分为独立的服务单元,解决单体系统的不足,同时也满足越来越复杂的业务需求。每个微服务仅关注于完成一件任务并很好地完成该任务。在所有情况下,每个任务代表着一个小的业务能力。

微服务就是将一个单体架构的应用按业务划分为一个个的独立运行的程序即服务,它们之间通过 HTTP 协议进行通信(也可以采用消息队列来通信,如 RoocketMQ,Kafaka 等),可以采用不同的编程语言,使用不同的存储技术,自动化部署(如 Jenkins)减少人为控制,降低出错概率。服务数量越多,管理起来越复杂,因此采用集中化管理。例如 Eureka,Zookeeper 等都是比较常见的服务集中化管理框架。

微服务的开发思想是目前大项目的主流开发模式,spring boot 为微服务的开发思想提供相关技术手段,spring cloud 为微服务的统一管理提供技术手段。

微服务下每个服务都要引入Springsecurity 微服务是springcloud_spring_02

使用 IDEA 来创建一个项目工程。如果是第一次打开 IDEA,可以选择 Create New Project 创建一个新工程。如果已经打开了 IDEA,在 File 菜单中选择 New Project,也能打开 New Project 对话框,使用 IDEA 创建一个 Spring Initializr 创建一个新项目。
注意:Intializr Service URL 为 https://start.sprig.io/。IDEA 将会需要连接网络,以查询 Spring Boot 的当前可用版本和组件列表,最近该网址对国内不开放,可以使用阿里的 https://start.aliyun.com/ 地址,使用这种方式新建项目大体上也需要三个步骤。
1.选择类型
可以使用默认选项,注意 Type 为 Maven Project,Java Version 为 8,Packaging 为 Jar 或 war。单击 Next 进入下一步。
2.选择 Spring Boot 版本和组件
选择 Spring Boot 版本和 Spring Boot 组件,例如,在 Spring Boot Version 中选择 2.5.4 并勾选 Spring Web 项目组件,然后单击 Next 进入下一步。
3.输入项目名称
选择存放路径后输人项目名称,并选择项目存放路径,这里使用 helloworld 作为项目的名称。
单击 Finish,将创建一个初始化项目。这个项目不但有完整的目录结构,还有一个完整的 Maven 配置,并且生成了一个默认的主程序,几乎所有的准备工作都已经就绪,并且可以立即运行起来(虽然没有提供任何可用的服务)。这也是 Spring Boot 引以为傲的地方,即创建一个应用可以不用编写任何代码,只管运行即可。
maven 相关的依赖:
使用 Maven,通过导人 Spring Boot 的 starter 模块,可以将许多程序依赖包自动导人工程中。使用 Maven 的 parent POM,还可以更容易地管理依赖的版本和使用默的配置,工程中的模块也可以很方便地继承它。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.4</version>
    <relativePath/>
</parent>

这里只使用了一个依赖配置 spring-boot starter-web 和一个 parent 配置 spring-boot-sarter-parent,在工程的外部库(External Libraries)列表中,它将自动引人 springframework 依赖,以及 autoconfigure、logging、 slf4j、 jackson、tomcat 插件等,所有这些都是一个 Web 项目可能需要用到的东西(包括你已经考虑到的和没有考虑到的)。

Spring Boot 的官方文档中提供了一个最简单的 Web 实例程序,这个实例只使用了几行代码。虽然简单,但实际上这已经可以算作是一个完整的 Web 项目了。

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @RequestMapping("/")
    public String hello() {
        System.out.println("hello world!");
        return "hello world!!";
    }
}

这个简单实例,首先是一个 Spring Boot 应用的程序人口,或者叫作主程序,其中使用了一个注解 @SpringBootApplication 来标注它是一个 Spring Boot 应用,main 方法使它成为一个主程序,目的是将在应用启动,在控制台可以看到如下日志表明项目启动成功。

Tomcat started on port(s): 8080 (http) with context path ''
Started DemoApplication in 3.941 seconds (JVM running for 8.812)

从上面的输出中可以看出,Tomcat 默认开启了 8080 端口。要访问这个应用提供的服务,可以在浏览器的地址栏中输人 http://localhost:8080 这样就可以看到我们期望的输出字符:hello world !! 。

其次,HelloController上的注解 @RestController 标注这个程序还是一个控制器,如果在浏览器中访问应用的根目录,它将调用 hello 方法,并输出字符串:  hello world ! ,返回前端字符串 hello world !!

2.运行与发布

在 idea 创建的项目依赖中可以看到如下打包插件,使用该插件可以将 springboot 项目快速打包发布。

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

在配置了 spring-boot-maven-plugin 插件后,idea 的面板中多出相关的打包指令,双击 package 即可将项目打包。由于打包过程会测试相关的单元测试,可以点击微服务下每个服务都要引入Springsecurity 微服务是springcloud_微服务_03跳过测试。

微服务下每个服务都要引入Springsecurity 微服务是springcloud_eureka_04

当控制台输出:BUILD SUCCESS 时,表示项目已经成功打好对应的项目包。这时在 target 目录下便是对应的项目包。

最后通过 java -jar 命令即可运行当前应用

java -jar demo-0.0.1-SNAPSHOT.jar

当需要覆盖配置文件中的一些属性时通常使用--加内容,如:

java -jar demo-0.0.1-SNAPSHOT.jar --server.port=1234

3. 配置文件

关于 SpringBoot 配置,可以在工程的 resources 文件夹中创建一个 application.properties 或 aplication.yml 文件,这个文件会被发布在 classpath 中,并且被 Spring Boot 自动读取。这里推荐使用 application.yml 文件,因为它使用 yaml 的语法规则,提供了结构化及其嵌套的格式。例如,可以按如下所示配置上面的工程,将默认端口改为 80,并且将 Tomcat 的字符集定义为 UTF-8。

server:
  port: 80
  tomcat:
    uri-encoding: utf-8

如果要使用 application.properties 文件,上面的配置就要改成如下所示的样子,其结果完全相同。 

server.port=80
server.tomcat.uri-encoding=utf-8

使用这个配置文件可以直接使用 Spring Boot 预定义的一些配置参数,关于其他配置参数的详细说明和描述在后面详细介绍。 

yaml 语法:

  1. 缩进 yaml 使用一个固定的缩进风格表示数据层结构关系,Saltstack 需要每个缩进级别由两个空格组成。一定不能使用 tab 键
  2. 冒号 yaml: mykey: my_value 每个冒号后面一定要有一个空格(以冒号结尾不需要空格,表示文件路径的模版可以不需要空格)

map 对象书写示例

friends:
  lastName: zhangsan
  age: 20

行内写法

friends: {lastName: zhangsan,age: 18}

多环境配置文件激活

在实际开发过程中,配置文件一般分为开发模式(dev)和生产模式(pro),为了快速切换配置文件,springboot 提供了配置文件快速激活方案。即在application.properties 加入 spring.profiles.active=dev,即可以引用同目录下文件名为 application-dev.properties/yml 的配置文件。

当然如果只想使用一个配置文件解决两种模式配置的切换也是可以的。如下则为单文件多 profile 模式:

application.yml

spring:
  profiles:
    active: pro

application-dev.yml

server:
  port: 8080

application-pro.yml

server:
  port: 80

通过简单的一行配置即可改变配置文件的全部内容,将极大的方便项目部署到 linux 系统上时修改配置文件。

配置属性扩展

为了满足各种业务需要,springboot 对 spring 的配置属性进行了大量的扩展,方便开发中获取自定义属性、随机数、变量引用等。在配置文件 application.properties 中加入如下代码:

com.didispace.blog.name=程序猿
com.didispace.blog.title=Spring Boot教程
com.didispace.blog.desc=${com.didispace.blog.name}正在努力写《${com.didispace.blog.title}》

# 随机字符串
com.didispace.blog.value=${random.value}
# 随机int
com.didispace.blog.number=${random.int}
# 随机long
com.didispace.blog.bignumber=${random.long}
# 10以内的随机数
com.didispace.blog.test1=${random.int(10)}
# 10-20的随机数
com.didispace.blog.test2=${random.int[10,20]}

在启动程序的下级目录添加spring管理的一个bean

@Value("${com.didispace.blog.name}")
private String name;
@Value("${com.didispace.blog.title}")
private String title;
@Value("${com.didispace.blog.desc}")
private String desc;
@Value("${com.didispace.blog.value}")
private String value;
@Value("${com.didispace.blog.number}")
private Integer number;
@Value("${com.didispace.blog.bignumber}")
private Long bignumber;
@Value("${com.didispace.blog.test1}")
private Integer test1;
@Value("${com.didispace.blog.test2}")
private Integer test2;

4.静态资源

springboot 项目微服务开发模式倡导使用前后端分离的开发模式,前端使用如 vue 脚手架等开发框架,开发完成后再打包前端项目到对应的 static 静态资源目录。

对于一般的开发人员来说,将普通 html,css,JavaScript 等资源放在 static 静态资源目录下开发也是一种不错的选择。使用 html 开发能加强对前端渲染的认识,为将来的前后端独立开发打下坚实基础。

一、默认静态资源映射

Spring Boot 对静态资源映射提供了默认配置,Spring Boot 默认将 /** 所有访问映射到以下目录:

classpath:/static
classpath:/public
classpath:/resources
classpath:/META-INF/resources

如:在 resources目录下新建 public、resources、static 三个目录,并分别放入 a.jpg b.jpg c.jpg 图片。浏览器分别访问: http://localhost:8080/a.jpg http://localhost:8080/b.jpg http://localhost:8080/c.jpg 均能正常访问相应的图片资源。那么说明,Spring Boot 默认会挨个从 public、resources 和 static 里面找是否存在相应的资源,如果有则直接返回。

二、自定义静态资源访问
静态资源路径是指系统可以直接访问的路径,且路径下的所有文件均可被用户直接读取。 在 Springboot 中默认的静态资源路径有:classpath:/META-INF/resources/、classpath:/resources/、classpath:/static/ 和 classpath:/public/ 从这里可以看出这里的静态资源路径都是在 classpath 中(也就是在项目路径下指定的这几个文件夹) 试想这样一种情况:一个网站有文件上传文件的功能,如果被上传的文件放在上述的那些文件夹中会有怎样的后果? 网站数据与程序代码不能有效分离; 当项目被打包成一个 .jar 文件部署时,再将上传的文件放到这个 .jar 文件中是有多么低的效率; 网站数据的备份将会很痛苦。 此时可能最佳的解决办法是将静态资源路径设置到磁盘的基本个目录。
1、配置类

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //将所有C:/aa/ 访问都映射到/aa/** 路径下
        registry.addResourceHandler("/images/**").addResourceLocations("file:C:/images/");
    }
}

第二种方式

配置 application.yml 的 spring.web.resources.static-locations。这种方式与第一种方式不同之处在于该目录 images 不需要在请求时指定对应的路径前缀。

spring:
  web:
    resources:
      static-locations: classpath:/static/,classpath:/public/,file:${web.upload-path}
web:
  upload-path: C:/images/

注意:

web.upload-path:这个属于自定义的属性,指定了一个路径,注意要以 / 结尾;

spring.web.resources.static-locations:在这里配置静态资源路径,前面说了这里的配置是覆盖默认配置,所以需要将默认的也加上否则 static、public 等这些路径将不能被当作静态资源路径,在这个最末尾的 file:${web.upload-path} 之所有要加 file:是因为指定的是一个具体的硬盘路径,其他的使用 classpath 指的是系统环境变量。

5.使用 Redis

关系型数据库在性能上总是存在一些这样那样的缺陷,所以大家有时候在使用传统关系型数据库时,会与具有高效存取功能的缓存系统结合使用,以提高系统的访问性能。在很多流行的缓存系统中,Redis 是一个不错的选择。Redis 是一种可以持久存储的缓存系统,是一个高性能的 key-value 数据库,它使用键-值对的方式来存储数据。 

1.添加依赖

要使用 Redis,需要在工程中加入 spring-boot-starter-data-redis

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.编写用户存取类

Redis 提供了 string、hash、list、set 和 zset 几种数据类型可供存取,在下面实例中,我们将使用 string 即字符串的类型来演示数据的存取操作。对于 Redis,Spring Boot 没有提供像 JPA 那样相应的资源库接口,所以只能仿照上节中 Repository 的定义编写一个实体 User 的服务类,代码如下所示。这个服务类可以存取对象 User 以及由 User 组成的列表 List,同时还提供了一个删除的方法。所有这些方法都是使用 RedisTemplate 来实现的。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Set;
import java.util.concurrent.TimeUnit;

public class UserRedis {
    @Autowired
    RedisTemplate redisTemplate;

    String projectName="springboot-redis";

    /**
     * 清空项目全部缓存
     */
    public  void deleteAll() {
        String keys = projectName + "_*";
        Set deleteKeys = redisTemplate.keys(keys);
        for (Object deleteKey : deleteKeys) {
            redisTemplate.delete(deleteKey);
        }
    }

    /**
    * 设置值
    * @param key 键
    * @param user 值
    */
    public  void put(String key, User user) {
        String keys = projectName + "_" + key;
        redisTemplate.opsForValue().set(keys, user);
    }

    /**
    * 获取值
    * @param key 键
    * @return 值
    */
    public  User get(String key) {
        String keys = projectName + "_" + key;
        Object o = redisTemplate.opsForValue().get(keys);
        return (User) o;
    }

    /**
     * 删除键
     * @param key 键
     */
    public  void delete(String key) {
        String keys = projectName + "_" + key;
        redisTemplate.delete(keys);
    }

    /**
     * 设置键的过期时间
     * @param key      键
     * @param time     时间
     * @param timeUnit 单位
     */
    public  void expire(String key, long time, TimeUnit timeUnit) {
        String keys = projectName + "_" + key;
        redisTemplate.expire(keys, time, timeUnit);
    }
}

3.配置 RedisTemplate

Redis 没有表结构的概念,所以要实现 MySQL 数据库中表的数据(即普通 Java 对象映射的实体数据)在 Redis 中存取,必须做一些转换, 使用 JSON 格式的文本作为 Redis 与 Java 普通对象互相交换数据的存储格式。这里使用 jackson 工具将类对象转换为 JSON 格式的文本进行存储,要取出数据时,再将 JSON 文本数据转化为 Java 对象。 

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 值采用jackson序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(jackson2JsonRedisSerializer);
        // 设置hash key 和value序列化模式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

}

因为 Redis 使用了 key-value 的方式存储数据,所以存入时要生成一个唯一的 key,而要查询或者删除数据时,就可以使用这个唯一的 key 进行相应的操作。 保存在 Redis 数据库中的数据默认是永久存储的,可以指定一个时限来确定数据的生命周期,超过指定时限的数据将被 Redis 自动清除。

另外,为了能正确调用 RedisTemplate,必须对其进行一些初始化工作,即主要对 它存取的字符串进行一个 JSON 格式的序列化初始配置,代码如下所示。 

注意:单独使用 jackson 时,需要导入 spring-boot-starter-web 依赖即可。

4.启动和配置 redis 服务

启动 redis 服务后,在工程的配置文件 aplication.yml 中配置连接 Redis 服务器连接等参数,代码如下所示。其 中 host 和 port 分别表示 Redis 数据库服务器的 IP 地址和开放端口,database 是对应的数据库索引。 

spring:
  redis:
    host: localhost
    port: 6379
    database: 1

5.单元测试
测试程序创建一个部门对象并将其命名为“开发部",创建一个角色对 象并把它命名为 admin,创建一个用户对象并把它命名为 user,同时设定这个用户属于"开发部",并把 admin 这个角色分配给这个用户。接着测试程序使用类名等参数生成一个 key,并使用这个 key 清空原来的数据,然后用这个 key 存储现在这个用户的数据, 最后使用这个 key 查询用户,并将查到的信息打印出来。

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

@SpringBootTest
class SpringbootRedisApplicationTests {
    @Autowired
    UserRedis userRedis;
    @Test
    void contextLoads() {
        userRedis.deleteAll();
        Department department = new Department();
        department.setName("开发部");
        Role role = new Role();
        role.setName(" admin");
        User user = new User();
        user.setName("user");
        user.setCreateTime(new Date());
        user.setDepartment(department);
        List<Role> roles=new ArrayList<>();
        roles.add(role);
        user.setRoles(roles);
        userRedis.put("1",user);
        userRedis.expire("1",1, TimeUnit.MINUTES);
        User userOut = userRedis.get("1");
        System.out.println(userOut);
    }
}

1.在 spring boot 项目中不推荐使用后端渲染,在 static 目录下创建 index.html 完成 user 的增删改查,后台数据全部存到 redis。

@RestController
@CrossOrigin
public class UserController {
    @Resource
    RedisTemplate<String, User> redisTemplate;

    @RequestMapping("list")
    public Map<String, Object> list() {
        Set<String> keys = redisTemplate.keys("user_*");
        List<User> users = new ArrayList<>();
        keys.forEach(k -> {
            users.add(redisTemplate.opsForValue().get(k));
        });
        users.sort(Comparator.comparingInt(User::getCode));
        Map<String, Object> map = new HashMap<>();
        map.put("data", users);
        map.put("message", "ok");
        map.put("success", true);
        return map;

    }

    @RequestMapping("add")
    public Map<String, Object> add(User user) {
        redisTemplate.opsForValue().set("user_" + user.getCode(), user);
        Map<String, Object> map = new HashMap<>();
        map.put("message", "ok");
        map.put("success", true);
        return map;

    }

    @RequestMapping("findByCode")
    public Map<String, Object> findByCode(@RequestParam Integer code) {
        User user = redisTemplate.opsForValue().get("user_" + code);
        Map<String, Object> map = new HashMap<>();
        map.put("data", user);
        map.put("message", "ok");
        map.put("success", true);
        return map;

    }

    @RequestMapping("delete")
    public Map<String, Object> delete(@RequestParam Integer code) {
        redisTemplate.delete("user_" + code);
        Map<String, Object> map = new HashMap<>();
        map.put("message", "ok");
        map.put("success", true);
        return map;
    }
}

参考代码:方式2

@RestController
@CrossOrigin
public class UserController {
    @Resource
    RedisTemplate<String, User> redisTemplate;

    @RequestMapping("list")
    public Map<String, Object> list() {
        List<User> users = redisTemplate.opsForList().range("user", 0, -1);

        users.sort(Comparator.comparingInt(User::getCode));
        Map<String, Object> map = new HashMap<>();
        map.put("data", users);
        map.put("message", "ok");
        map.put("success", true);
        return map;

    }

    @RequestMapping("add")
    public Map<String, Object> add(User user) {
        redisTemplate.opsForList().leftPush("user", user);
        Map<String, Object> map = new HashMap<>();
        map.put("message", "ok");
        map.put("success", true);
        return map;

    }

    @RequestMapping("findByCode")
    public Map<String, Object> findByCode(@RequestParam Integer code) {
        Map<String, Object> map = new HashMap<>();
        List<User> users = redisTemplate.opsForList().range("user", 0, -1);
        for (int i = 0; i < users.size(); i++) {
            if (users.get(i).getCode().equals(code)) {
                map.put("data", users.get(i));
            }
        }
        map.put("message", "ok");
        map.put("success", true);
        return map;

    }

    @RequestMapping("delete")
    public Map<String, Object> delete(@RequestParam Integer code) {
        List<User> users = redisTemplate.opsForList().range("user", 0, -1);
        for (int i = 0; i < users.size(); i++) {
            if (users.get(i).getCode().equals(code)) {
                redisTemplate.opsForList().remove("user", 0, users.get(i));
            }
        }
        Map<String, Object> map = new HashMap<>();
        map.put("message", "ok");
        map.put("success", true);
        return map;
    }
}

6.整合 mybatis

虽然 springdata 足够智能和强大,但国内以腾讯阿里为首的 mybatis 大军仍然弃之不顾。为了满足相关人员的开发需求。springboot 对 mybatis 也有较好的支持。同时也简化了 mybatis 相关繁琐的配置。基本配置如下:

1.加入依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>

2.配置数据库连接信息和 mybatis 的 mapper.xml 位置

mybatis:
  mapper-locations: classpath:mappers/*.xml
spring:
  datasource:
    url: jdbc:mysql:///unifiedquery?characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver

3.在启动类上添加 mybatis 接口扫描路径

@SpringBootApplication
@MapperScan("cn.hx.dao")
public class QueryApplication {
    public static void main(String[] args) {
        SpringApplication.run(QueryApplication.class, args);
    }
}

若需要显示 sql,只需如下简单配置即可。

logging:
  level:
    cn.hx.dao: debug

cn.hx.dao 是 mybatis 接口的位置。

7.Thymeleaf

Thymeleaf [taim li:f] 是目前最流行的后端引擎之一。虽然现在很多开发,都采用了前后端完全分离的模式,即后端只提供数据接口,前端通过 AJAX 请求获取数据,完全不需要用的后端模板引擎。这种方式的优点在于前后端完全分离,并且随着近几年前端工程化工具和 MVC 框架的完善,使得这种模式的维护成本相对来说也更加低一点。但是这种模式不利于 SEO(搜索引擎优化 Search Engine Optimization),并且在性能上也会稍微差一点。

1.添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

2.配置 thymeleaf

spring:
  thymeleaf:
    prefix: classpath:/templates/
    cache: false
    suffix: .html
    mode: HTML

prefix:指定模板所在的目录

cache:是否缓存,开发模式下设置为 false,避免改了模板还要重启服务器,线上设置为 true,可以提高性能。

3.编写 thymeleaf 模板文件

<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta content="text/html;charset=UTF-8"/>
</head>
<body>
<table border="1">
    <thead>
    <tr>
        <th>序号</th>
        <th>标题</th>
        <th>摘要</th>
        <th>创建时间</th>
    </tr>
    </thead>
    <tbody>
    <tr th:each="article : ${list}">
        <td th:text="${article.id}"></td>
        <td th:text="${article.title}"></td>
        <td th:text="${article.summary}"></td>
        <td th:text="${article.createTime}"></td>
    </tr>
    </tbody>
</table>
</body>
</html>

可以看到,thymeleaf 还是比较简单的,并且最大的特点就是的标签是作为 HTML 元素的属性存在的,也就是说,该页面是可以直接通过浏览器来预览的。

8.Freemarker

与thymeleaf 作用相同,在 web 开发中可能还会用到Freemarker 后端模板引擎,他的原理和thymeleaf 相同,只是语法上有稍微的区别。

官方文档:地址

1.添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

2.配置 freemarker

spring:
  freemarker:
    template-loader-path: classpath:/templates/
    suffix: .ftl
    content-type: text/html
    charset: UTF-8
    settings:
      number_format: '0.##'

除了 settings 外,其他的配置选项和 thymeleaf 类似。settings 会对 freemarker 的某些行为产生影响,如日期格式化,数字格式化等,感兴趣的可以参考官网提供的说明。

3.编写 freemarker 模板文件

<html>
<title>文章列表</title>
<body>
<h6>Freemarker 模板引擎</h6>
<table border="1">
    <thead>
    <tr>
        <th>序号</th>
        <th>标题</th>
        <th>摘要</th>
        <th>创建时间</th>
    </tr>
    </thead>
    <#list list as article>
        <tr>
            <td>${article.id}</td>
            <td>${article.title}</td>
            <td>${article.summary}</td>
            <td>${article.createTime?string('yyyy-MM-dd hh:mm:ss')}</td>
        </tr>
    </#list>
</table>

</body>
</html>

9.jsp

提起 java Web 开发绕不开的一个技术就是 JSP,因为目前市面上仍有很多的公司在使用 JSP,所以本节就来介绍一下 Spring Boot 怎么集成 JSP 开发。springboot 整合 jsp 有如下几步:创建 war 项目结构。

1.添加相关依赖

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
</dependency>

2.创建war包项目,并在 application.properties 文件中增加如下配置

spring.mvc.view.suffix=.jsp
spring.mvc.view.prefix=/WEB-INF/jsp/
server.servlet.jsp.init-parameters.development=true

3.创建webapp目录和/WEB-INF/jsp/index.jsp

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>demo</title>
</head>
<body>
<table border="1">
    <thead>
    <tr>
        <th>序号</th>
        <th>标题</th>
        <th>摘要</th>
        <th>创建时间</th>
    </tr>
    </thead>
    <c:forEach var="article" items="${list}">
        <tr>
            <td>${article.id}</td>
            <td>${article.title}</td>
            <td>${article.summary}</td>
            <td>${article.createTime}</td>
        </tr>
    </c:forEach>
</table>
</body>
</html>

10.websocket

WebSocket 协议是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信,即允许服务器主动发送信息给客户端。

WebSocket 协议支持客户端与远程服务器之间进行全双工通信。用于此的安全模型是 Web 浏览器常用的基于原始的安全模式。 协议包括一个开放的握手以及随后的 TCP 层上的消息帧。

WebSocket 协议之前,双工通信是通过不停发送 HTTP 请求,从服务器拉取更新来实现,这导致了效率低下,同时增加不必要的服务器压力,WebSocket 解决了这个问题。springboot 整合 websocket 有以下几个步骤:

1.加入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2.配置类

注意:如果打 war 包部署到 tomcat 内运行时,则不能配置 ServerEndpointExporter bean,打成 jar 包独立运行时必须有该 bean

@Bean
public ServerEndpointExporter serverEndpointExporter() {
    return new ServerEndpointExporter();
}

服务类

import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
@ServerEndpoint(value = "/ws/{userId}")
public class ChatServer {

    private static Map routeTab = new HashMap<>();

    @OnOpen
    public void onOpen(@PathParam("userId") String userId, Session session) throws IOException {
        routeTab.put(userId, session);
    }

    @OnClose
    public void onClose(@PathParam("userId") String userId) throws IOException {
        routeTab.remove(userId);
    }

    @OnMessage
    public void onMessage(String message) throws IOException {
        for (String s : routeTab.keySet()) {
            Session session = routeTab.get(s);
            session.getBasicRemote().sendText(message);
        }
    }

    @OnError
    public void onError(Throwable error) {
        error.printStackTrace();
    }
}

前端按照 websocket 规范编写即可

<meta charset="UTF-8">
    <title>聊天</title>


<button onclick="send_msg()">
    发个消息试一下
</button>

<script>
    let ws = new WebSocket("ws://localhost:8080/ws/" + Math.random());
    ws.onopen = function (evt) {
        alert("建立连接");
    };

    ws.onmessage = function (evt) {
        alert(evt.data);

    };

    ws.onerror = function (evt) {
        alert(evt.data);
    };

    ws.onclose = function (evt) {
        alert(evt.data);
    };

    function send_msg(){
        ws.send('发个消息试一下!!');
    }
</script>

11.RabbitMQ

我们可以把消息队列比作是一个存放消息的容器,当我们需要使用消息的时候可以取出消息供自己使用。消息队列是分布式系统中重要的组件,使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。目前使用较多的消息队列有 ActiveMQ,RabbitMQ,Kafka,RocketMQ。

RabbitMQ官网提供了七种队列模型,分别是:简单队列、工作队列、发布订阅、路由模式、主题模式、RPC模式、发布者确认模式。

本案例只简单演示简单队列。

尝试在 windows 上安装 rabbitmq,如无法安装建议使用 docker 安装。

添加依赖

<dependency>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-amqp</artifactid>
</dependency>

配置文件

spring.application.name=rabbitmq-hello
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

配置序列

import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitConfig {
    @Bean
    public Queue helloQueue() {
        return new Queue("hello");
    }
}

发送信息

@Component
public class Sender {
    @Autowired
    private AmqpTemplate rabbitTemplate;

    public void send() {
        String context = "hello " + new Date();
        System.out.println("Sender : " + context);
        rabbitTemplate.convertAndSend("hello", context);
    }
}

接受者

@Component
@RabbitListener(queues = "hello")
public class Receiver {
    @RabbitHandler
    public void process(String hello) {
        System.out.println("Receiver : " + hello);
    }
}

测试使用

@SpringBootTest
public class HelloApplicationTests {
    @Autowired
    private Sender sender;

    @Test
    public void hello() throws Exception {
        sender.send();
    }
}

从本质上来说是因为互联网的快速发展,业务不断扩张,促使技术架构需要不断的演进。

从以前的单体架构到现在的微服务架构,成百上千的服务之间相互调用和依赖。从互联网初期一个服务器上有 100 个在线用户已经很了不得,到现在坐拥10 亿日活的微信。此时,我们需要有一个「工具」来解耦服务之间的关系、控制资源合理合时的使用以及缓冲流量洪峰等等。因此,消息队列就应运而生了。

它常用来实现:异步处理、服务解耦、流量控制(削峰)。

12.https 搭建 

HTTPS(Secure Hypertext Transfer Protocol)安全超文本传输协议它是一个安全通信通道,它基于 HTTP 开发,用于在客户计算机和服务器之间交换信息。通俗来说,https 比 http 安全性更高,但 http 更加便捷。

采用 https 的 server 必须从 CA 申请一个用于证明服务器用途类型的证书,改证书只有用于对应的 server 的时候,客户端才信任该主机。因此使用 https 工作起来会非常麻烦。

以下是 http 与 https 的不同点:

  1. https 协议需要到 ca 申请证书,一般免费证书较少,因而需要一定费用。
  2. http 是超文本传输协议,信息是明文传输,https 则是具有安全性的 ssl 加密传输协议
  3. http 和 https 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443
  4. http 的连接很简单,是无状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 http 协议安全

下面我们将使用 JAVA 自带的 keytool 生成证书,搭建我们的 https 服务器。使用 JAVA 自带的 keytool 生成的证书不是有效证书,不被浏览器信任,如果是被信任的站点,浏览器左侧会有个绿色的图标。

1.打开命令行输入

keytool -genkey -v -alias testKey -keyalg RSA -validity 3650 -keystore D:\keys\test.keystore

alias:别名 这里起名testKey

keyalg:证书算法,RSA

validity:证书有效时间,10年

keystore:证书生成的目标路径和文件名,其文件夹必须存在

回车,然后会让你输入一些信息,其中秘钥库口令和密钥口令最好输入同一个,并且记下这个口令,(配置 tomcat 会用到该口令)其他的可随意填。

<connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol" maxthreads="150" sslenabled="true"
 scheme="https" secure="true" clientauth="false"
 sslprotocol="TLS" keystorefile="你的keystore路径" keystorepass="生成证书时的口令"></connector>

cmd 输出结果如下:

C:\Users\Administrator>keytool -genkey -v -alias testKey -keyalg RSA -validity 3
650 -keystore D:\keys\test.keystore
输入密钥库口令:
再次输入新口令:
您的名字与姓氏是什么?
  [Unknown]:  张三
您的组织单位名称是什么?
  [Unknown]:  华信智原
您的组织名称是什么?
  [Unknown]:  组织
您所在的城市或区域名称是什么?
  [Unknown]:  中国
您所在的省/市/自治区名称是什么?
  [Unknown]:  贵州省
该单位的双字母国家/地区代码是什么?
  [Unknown]:  ZH
CN=张三, OU=华信智原, O=组织, L=中国, ST=贵州省, C=ZH是否正确?
  [否]:  y

正在为以下对象生成 2,048 位RSA密钥对和自签名证书 (SHA256withRSA) (有效期为 3,650
 天):
         CN=张三, OU=华信智原, O=组织, L=中国, ST=贵州省, C=ZH
输入 <testkey> 的密钥口令
        (如果和密钥库口令相同, 按回车):
[正在存储D:\keys\test.keystore]</testkey>

将生成的 test.keystore 拷贝到 springboot resource 下,并配置 springboot 的配置文件

#端口号
server.port=443
#你生成的证书名字
server.ssl.key-store=classpath:test.keystore
#密钥库密码
server.ssl.key-store-password=123456
server.ssl.keyStoreType=JKS
server.ssl.keyAlias=testKey

写好一个简单的接口即可以测试。

13.服务治理

Spring Cloud 是一个基于 Spring Boot 实现的云应用开发工具,其中有一个重要概念叫服务治理,服务治理可以说是微服务架构中最为核心和基础的模块,它主要用来实现各个微服务实例的自动化注册与发现。

在最初开始构建微服务系统的时候可能服务并不多,我们可以通过做一些静态配置来完成服务的调用。比如,有两个服务 A 和 B,其中服务 A 需要调用服务 B 来完成一个业务操作时,为了实现服务 B 的高可用,不论采用服务端负载均衡还是客户端负载均衡,都需要手工维护服务 B 的具体实例清单。但是随着业务的发展,系统功能越来越复杂,相应的微服务应用也不断增加,我们的静态配置就会变得越来越难以维护。并且面对不断发展的业务,我们的集群规模、服务的位置、服务的命名等都有可能发生变化,如果还是通过手工维护的方式,那么极易发生错误或是命名冲突等问题。同时,对于这类静态内容的维护也必将消耗大量的人力。为了解决微服务架构中的服务实例维护问题,产生了大量的服务治理框架和产品。这些框架和产品的实现都围绕着服务注册与服务发现机制来完成对微服务应用实例的自动化管理。

服务注册:在服务治理框架中,通常都会构建一个注册中心,每个服务单元向注册中心登记自己提供的服务,将主机与端口号、版本号、通信协议等一些附加信息告知注册中心,注册中心按服务名分类组织服务清单。

服务发现:由于在服务治理框架下运作,服务间的调用不再通过指定具体的实例地址来实现,而是通过向服务名发起请求调用实现。所以,服务调用方在调用服务提供方接口的时候,并不知道具体的服务实例位置。因此,调用方需要向服务注册中心咨询服务,并获取所有服务的实例清单,以实现对具体服务实例的访问。

Spring Cloud Eureka 是 Spring Cloud Netlix 微服务套件中的一部分,它基于 Netlix Eureka 做了二次封装,主要负责完成微服务架构中的服务治理功能。Spring Cloud 通过为 Eureka 增加了SpringBoot 风格的自动化配置,我们只需通过简单引入依赖和注解配置就能让 Spring Boot 构建的微服务应用轻松地与 Eureka 服务治理体系进行整合。

Eureka 服务端,我们也称为服务注册中心。它同其他服务注册中心一样,支持高可用配置。它依托于强一致性提供良好的服务实例可用性,可以应对多种不同的故障场景。

Eureka 客户端,主要处理服务的注册与发现。客户端服务通过注解和参数配置的方式,嵌入在客户端应用程序的代码中,在应用程序运行时,Eureka 客户端向注册中心注册自身提供的服务并周期性地发送心跳来更新它的服务续约。同时,它也能从服务端查询当前注册的服务信息并把它们缓存到本地并周期性地刷新服务状态。

Eureka 提供基于 REST 的服务,在集群中主要用于服务管理。Eureka 提供了基于 Java 语言的客户端组件,客户端组件实现了负载均衡的功能,为业务组件的集群部署创造了条件。 使用该框架,可以将业务组件注册到 Eureka 容器中,进行集群部署,Eureka 提供的服务调用功能,可以发布容器中的服务并进行调用。

一个简单的 Eureka 服务治理体系,需要有一个 Eureka 服务器、若干个服务提供者和服务调用者。我们可以将服务提供组件注册到 Eureka 服务器中,其他服务调用组件可以向服务器获取服务并且进行远程调用。体系中最重要的三个角色如下:

1.服务器端

对于注册到服务器端的服务组件,Eureka 服务器并没有提供后台的存储,这些注册的服务实例被保存在内存的注册中心,它们通过心跳来保持其最新状态,这些操作都可以在内存中完成。客户端存在着相同的机制,同样在内存中保存了注册表信息,这样的机制提升了 Eureka 组件的性能,每次服务的请求都不必经过服务器端的注册中心。

2.服务提供者

作为 Eureka 客户端存在的服务提供者,主要进行以下工作:第一、向服务器注册服务;第二、发送心跳给服务器;第三、向服务器端获取注册列表。当客户端注册到服务器时,它将会提供一些关于它自己的信息给服务器端,例如自己的主机、端口、健康检测连接等。

3.服务调用者

对于发布到 Eureka 服务器的服务,使用调用者可对其进行服务查找与调用,服务调用者也是作为客户端存在,但其职责主要是发现与调用服务。在实际情况中,有可能出现本身既是服务提供者,也是服务调用者的情况,例如传统的企业应用三层架构中,服务层会调用数据访问层的接口进行数据操作,它本身也会提供服务给控制层使用。

微服务下每个服务都要引入Springsecurity 微服务是springcloud_spring_05

Eureka 服务为本例的“eureka-server”,服务发布者为“eureka-service-provider”,而调用者为“eureka-service-invoker”,用户通过浏览器访问调用者的 9000 端口的 router 服务,router 服务中查找服务提供者的服务并进行调用。

14.Eureka 服务端

在完成了服务注册中心的搭建之后,接下来我们尝试将一个既有的 Spring Boot 应用加入 Eureka 的服务治理体系中去。

使用 Idea 创建名为 Eureka 的 maven 项目。

用 springboot 创建 eureka-server 模块,勾选 Spring Cloud Discovery->Eureka Server

spring-cloud 与 springboot 版本对应如下:

Finchley.M2    "Spring Boot >=2.0.0.M3 and <2.0.0.M5"
Finchley.M3    "Spring Boot >=2.0.0.M5 and <=2.0.0.M5"
Finchley.M4    "Spring Boot >=2.0.0.M6 and <=2.0.0.M6"
Finchley.M5    "Spring Boot >=2.0.0.M7 and <=2.0.0.M7"
Finchley.M6    "Spring Boot >=2.0.0.RC1 and <=2.0.0.RC1"
Finchley.M7    "Spring Boot >=2.0.0.RC2 and <=2.0.0.RC2"
Finchley.M9    "Spring Boot >=2.0.0.RELEASE and <=2.0.0.RELEASE"
Finchley.RC1    "Spring Boot >=2.0.1.RELEASE and <2.0.2.RELEASE"
Finchley.RC2    "Spring Boot >=2.0.2.RELEASE and <2.0.3.RELEASE"
Finchley.SR4    "Spring Boot >=2.0.3.RELEASE and <2.0.999.BUILD-SNAPSHOT"
Finchley.BUILD-SNAPSHOT    "Spring Boot >=2.0.999.BUILD-SNAPSHOT and <2.1.0.M3"
Greenwich.M1    "Spring Boot >=2.1.0.M3 and <2.1.0.RELEASE"
Greenwich.SR2    "Spring Boot >=2.1.0.RELEASE and <2.1.9.BUILD-SNAPSHOT"
Greenwich.BUILD-SNAPSHOT    "Spring Boot >=2.1.9.BUILD-SNAPSHOT and <2.2.0.M4"
Hoxton.M2    "Spring Boot >=2.2.0.M4 and <=2.2.0.M5"
Hoxton.BUILD-SNAPSHOT    "Spring Boot >=2.2.0.BUILD-SNAPSHOT"

本课程选择的依赖版本分别为:

springboot -> 2.1.12.RELEASE

springcloud -> Greenwich.SR2

微服务下每个服务都要引入Springsecurity 微服务是springcloud_微服务_06

编写一个最简单的启动类,启动我们的 Eureka 服务器。

@SpringBootApplication
@EnableEurekaServer
public class FirstServer {
    public static void main(String[] args) {
        new SpringApplicationBuilder(FirstServer.class).run(args);
    }
}

配置服务器端口,因此默认端口为 8080,我们将端口配置为 8761,在 src/main/resources 目录下创建 application.yml 配置文件

server:
    port: 8761

运行应用的启动类的主函数方法后程序正常启动,在启动过程中会出现部分异常信息,暂时不需要进行处理。启动成功启动后,打开浏览器地址:http://localhost:8761 可以看到 Eureka 服务器控制台页面。

注意:关于控制台的报错,这是由于在服务器启动时,服务器会把自己当作一个客户端,注册去 Eureka 服务器,并且会到 Eureka 服务器抓取注册信息,它自己本身只是一个服务器,而不是服务的提供者(客户端),因此可以修改 application.yml 文件,增加以下两个配置:

eureka:
    client:
        registerWithEureka: false
        fetchRegistry: false

以上配置中的 eureka.client.registerWithEureka 属性,声明是否将自己的信息注册到 Eureka 服务器,默认值为 true。属性 eureka.client.fetchRegistry 则表示,是否到 Eureka 服务器中抓取注册信息。将这两个属性设置为 false,则启动时不会出现异常信息。

15.服务提供者与调用者

在 Eureka 项目中用 springboot 创建 eureka-service-provider 模块,勾选 Spring Cloud Discovery->Eureka Server 模块,修改相应的版本和上面一致。并在项目启动类的主函数中加入 @EnableEurekaClient 注解,声明该应用是一个 Eureka 客户端。

@SpringBootApplication
@EnableEurekaClient
public class EurekaServiceProviderApplication {

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

创建一个控制器服务,用于给服务调用者调用。

@RestController
public class FirstController {
    @RequestMapping(value = "/person/{personId}")
    public Map<String, Object> findPerson(@PathVariable Integer personId) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("id", personId);
        map.put("name", "张三");
        map.put("age", 18);
        return map;
    }
}

配置当前项目注册到 Eureka 服务器。

spring:
  application:
    name: eureka-service-provider
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

以上配置中,将应用名称配置为 eureka-service-provider,该服务将会被注册到端口为 8761 的 Ereka 服务器,也就是本小节前面所构建的服务器。刷新 Eureka 控制台页面。可以看到当前注册的服务列表,只有我们编写的 eureka-service-provider。服务注册成功后,接下来编写服务调用者。

编写服务调用者

在 Eureka 项目中用 springboot 创建 eureka-service-invoker 模块,并勾选 Spring Cloud Discovery->Eureka Server 和 Ribbon ,修改相应的版本和上面一致。并在在启动类中,使用了@EnableDiscoveryClient 注解来修改启动类,该注解使得服务调用者有能力去 Eureka 中发现服务,需要注意的是@EnableEurekaClient 注解已经包含了 @EnableDiscoveryClient 的功能,也就是说,一个 Eureka 客户端,本身就具有发现服务的能力。

@SpringBootApplication
@EnableDiscoveryClient
public class EurekaServiceInvokerApplication {

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

创建调用者

@RestController
@Configuration
public class InvokerController {

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }

    @RequestMapping(value = "/router/{personId}")
    public String router(@PathVariable Integer personId) {
        RestTemplate restTpl = getRestTemplate();
        // 根据应用名称调用服务
        String json = restTpl.getForObject(
                "http://eureka-service-provider/person/" + personId, String.class);
        return json;
    }
}

在控制器中,配置了 RestTemplate 的 bean,RestTemplate 本来是 spring-web 模块下面的类,主要用来调用 REST 服务,本身并不具备调用分布式服务的能力,但是 RestTemplate 的 bean 被 @LoadBalanced 注解修饰后,这个 RestTemplate 实例就具有访问分布式服务的能力,关于该类的一些机制,我们将放到负载均衡章节中讲解。

配置该项目

server:
  port: 9000
spring:
  application:
    name: eureka-service-invoker
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

启动完成后,可以下面的路径,调用服务提供者的服务

http://localhost:9000/router/12

注意:

依次执行以下操作:

  1. 启动服务器(eureka-server)
  2. 启动服务提供者(eureka-service-provider)
  3. 启动服务调用者(eureka-service-invoker)
  4. 通过服务调用者调用服务提供者

16.Eureka 高可用 

在微服务架构这样的分布式环境中,我们需要充分考虑发生故障的情况,所以在生产环境中必须对各个组件进行高可用部署,对于微服务如此,对于服务注册中心也一样。但是到本节为止,我们一直都在使用单节点的服务注册中心,这在生产环境中显然并不合适,我们需要构建高可用的服务注册中心以增强系统的可用性。 Eureka Server 的设计一开始就考虑了高可用问题,在 Eureka 的服务治理设计中,所有节点即是服务提供方,也是服务消费方,服务注册中心也不例外。

Eureka Server 的高可用实际上就是将自己作为服务向其他服务注册中心注册自己,这样就可以形成一组互相注册的服务注册中心,以实现服务清单的互相同步,达到高可用的效果。下面我们就来尝试搭建高可用服务注册中心的集群。可以在本章第1节中实现的服务注册中心的基础之上进行扩展,构建一个双节点的服务注册中心集群。

微服务下每个服务都要引入Springsecurity 微服务是springcloud_静态资源_07

本例将会运行两个服务器实例、两个服务提供者实例,然后服务调用者请求服务,第一个 Eureka 应用,使用的是浏览器访问 Eureka 的服务调用者,而改造后,为了能看到负载均衡的效果,会编写一个 HttpClient 的 REST 客户端访问服务调用者发布的服务。

新建模块 eureka-cloud-server,并添加依赖

在 Eureka 项目中用 springboot 创建 eureka-cloud-server 模块,勾选 Spring Cloud Discovery->Eureka Server ,修改相应的版本和上面一致。

@SpringBootApplication
@EnableEurekaServer
public class EurekaCloudServerApplication {
    public static void main(String[] args) {       
        new SpringApplicationBuilder(EurekaCloudServerApplication.class).run(args);
    }
}

配置文件如下

server:
  port: 8761
spring:
  application:
    name: eureka-cloud-server
  profiles: slave1
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8762/eureka/
---
server:
  port: 8762
spring:
  application:
    name: eureka-cloud-server
  profiles: slave2
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

配置了两个 profiles,名称分别为 slave1 和 slave2。

在 slave1 中,配置了应用端口为 8761,主机名为 slave1,当使用 salve1 这个 profiles 来启动服务器时,将会向http://slave2:8762/eureka/注册自己。

使用salve2来启动服务器,会向 http://slave1:8761/eureka/ 注册自己。

简单点说,就是两个服务器启动后,它们会互相注册

微服务下每个服务都要引入Springsecurity 微服务是springcloud_spring cloud_08

注意:第一个启动的服务器会抛出异常,异常原因我们前已经讲述,抛出的异常可不必理会。

当 Eureka 配置高可用后,服务提供者也需要进行相应的调整。eureka-service-provider 复制出来,并改名为 eureka-cloud-provider。修改配置文件,将服务提供者注册到两个服务器中

spring:
  application:
    name: eureka-cloud-provider
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

修改控制器,可以看到是请求的 url

@RestController
public class FirstController {
    @RequestMapping(value = "/person/{personId}")
    public Map<String, Object> findPerson(@PathVariable Integer personId, HttpServletRequest request) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("id", personId);
        map.put("name", "张三");
        map.put("age", 18);
        map.put("url", request.getRequestURL().toString());
        return map;
    }
}

分别在 8886 和 8888 端口启动两个服务提供者

微服务下每个服务都要引入Springsecurity 微服务是springcloud_静态资源_09

调整服务调用者,将 eureka-service-invoker 复制并改名为 eureka-cloud-invoker。本例中的服务调用者只需启动一个实例,因此修改下配置文件即可使用

server:
  port: 9000
spring:
  application:
    name: eureka-cloud-invoker
eureka:
  client:
    serviceUrl:
      defaultZone: http://slave1:8761/eureka/,http://slave2:8761/eureka/

调用类修改一下服务名

@RestController
@Configuration
public class InvokerController {

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }

    @RequestMapping(value = "/router/{personId}")
    public String router(@PathVariable Integer personId) {
        RestTemplate restTpl = getRestTemplate();
        // 根据应用名称调用服务
        String json = restTpl.getForObject(
                "http://eureka-cloud-provider/person/" + personId, String.class);
        return json;
    }
}

启动调用者服务后即可完成浏览器测试,但是建议使用 HttpClient 进行测试

<dependencies>
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.2</version>
    </dependency>
</dependencies>

main方法如下

public static void main(String[] args) throws Exception {
	// 创建默认的HttpClient
	CloseableHttpClient httpclient = HttpClients.createDefault();
	// 调用6次服务并输出结果
	for(int i = 0; i < 6; i++) {
		// 调用 GET 方法请求服务
		HttpGet httpget = new HttpGet("http://localhost:9000/router/"+i);
		// 获取响应
		HttpResponse response = httpclient.execute(httpget);
		// 根据 响应解析出字符串
		System.out.println(EntityUtils.toString(response.getEntity()));
	}
}

完成编写后,按以下顺序启动各个组件:

  1. 启动两个服务器端,控制台中分别输入“slave1”和“slave2”。
  2. 启动两个服务提供者,控制台分别输入 8888 与 8886。
  3. 启动服务调用者。

启动了整个集群后,运行TestHttpClient,可以看到输出如下

{"name":"张三","id":3,"age":18,"url":"http://localhost:8886/person/0"}
{"name":"张三","id":3,"age":18,"url":"http://localhost:8888/person/1"}
{"name":"张三","id":3,"age":18,"url":"http://localhost:8886/person/2"}
{"name":"张三","id":3,"age":18,"url":"http://localhost:8888/person/3"}
{"name":"张三","id":3,"age":18,"url":"http://localhost:8886/person/4"}
{"name":"张三","id":3,"age":18,"url":"http://localhost:8888/person/5"}

根据输出结果可知,8888 与 8886端口分别被请求了 3 次,可见已经达到负载均衡的目的,关于负载均衡更详细的内容,将在后面章节中讲解。

17.服务续约

在注册完服务之后,服务提供者会维护一个心跳用来持续告诉 Eureka Server:“我还活着”,以防止 Eureka Server 的“剔除任务”将该服务实例从服务列表中排除出去,我们称该操作为服务续约(Renew)。使用以下配置修改服务提供者默认发送心跳包的时间间隔(30秒)。

微服务下每个服务都要引入Springsecurity 微服务是springcloud_微服务_10

同时需要修改 spring boot 某些包下的类的日志等级,可以看的提供者发送心跳包的相关信息。

logging:
  level: 
    com.netflix: DEBUG

可以明显看到控制台每隔 5 秒连接一次服务器。

如果 Eureka 服务器发现如果在 90 秒内未收到某个服务器的心跳续约请求,则判定该服务器挂掉,60 秒后清除其在服务列表中的位置。一般这种服务器检查机制可以由提供者设定,但是 Eureka 服务器必须关闭相关的保护模式。

eureka:
  client: 
    register-with-eureka: false
    fetch-registry: false
  server:
    enable-self-preservation: false
    eviction-interval-timer-in-ms: 5000

enable-self-preservation 关闭服务器自身 90 秒检查的设置,而由服务提供者提供该参数

eviction-interval-timer-in-ms 更新服务列表间隔,默认 60 秒(60000ms)

服务提供者

eureka:
  instance:
    leaseRenewalIntervalInSeconds: 5
    leaseExpirationDurationInSeconds: 15

leaseRenewalIntervalInSeconds 每隔 5 秒发一次心跳续约请求。leaseExpirationDurationInSeconds 告诉 Eureka 服务器 10 秒不收到续约请求就认为该服务提供者挂掉。结合上面 15 秒后就清除服务提供者在服务列表中的位置。

18.Ribbon 与 Feign

负载均衡是分布式架构的重点,负载均衡机制将决定着整个服务集群的性能与稳定。根据前面章节可知,Eureka 服务实例可以进行集群部署,每个实例都均衡处理服务请求,那么这些请求是如何被分摊到各个服务实例中的?本章将讲解 Netflix 的负载均衡项目 Ribbon。

客户端负载均衡

负载均衡在系统架构中是一个非常重要,并且是不得不去实施的内容。因为负载均衡是对系统的高可用、网络压力的缓解和处理能力扩容的重要手段之一。我们通常所说的负载均衡都指的是服务端负载均衡,其中分为硬件负载均衡和软件负载均衡。硬件负载均衡主要通过在服务器节点之间安装专门用于负载均衡的设备,比如 F5 等;而软件负载均衡则是通过在服务器上安装一些具有均衡负载功能或模块的软件来完成请求分发工作,比如 Nginx 等。

spring cloud 中使用 Ribbon

创建 maven 项目 ribbon,将 Eureka 章节的eureka-server、eureka-service-invoker、eureka-service-provider模块拷贝到ribbon下

启动eureka-server后需要启动两个 eureka-service-provider,端口分别为8080和8888

微服务下每个服务都要引入Springsecurity 微服务是springcloud_静态资源_11

Ribbon 主要在调用者上实现负载均衡,下面是重点部分

@RestController
@Configuration
public class InvokerController {

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }

    @RequestMapping(value = "/router/{personId}")
    public String router(@PathVariable Integer personId) {
        RestTemplate restTpl = getRestTemplate();
        // 根据应用名称调用服务
        String json = restTpl.getForObject(
                "http://eureka-service-provider/person/" + personId, String.class);
        return json;
    }
}

RestTemplate 本是 spring-web 项目中的一个 REST 客户端访问类,它遵循 REST 的设计原则,提供简单的 API 让调用去访问 HTTP 服务器。RestTemplate 本身不具有负载均衡的功能,该类也与 Spring Cloud 没有关系,但为何加入@LoadBalanced 注解后,一个 RestTemplate 实例就具有负载均衡的功能呢?实际上这要得益于 RestTemplate 的拦截器功能。

在 Spring Cloud 中,使用@LoadBalanced 修饰的 RestTemplate,在 Spring 容器启动 时,会为这些被修饰过的 RestTemplate 添加拦截器,拦截器中使用了 LoadBalancerClient 来处理请求,LoadBalancerClient 本来就是 Spring 封装的负载均衡客户端,通过这样间接处理,使得 RestTemplate 就拥有了负载均衡的功能。

Feign

Feign 是一个 Github 上一个开源项目,目的是为了简化 Web Service 客户端的开发。在使用 Feign 时,可以使用注解来修饰接口,被注解修饰的接口具有访问 Web Service 的 能力,这些注解中既包括了 Feign 自带的注解,也支持使用第三方的注解。除此之外,Feign 还支持插件式的编码器和解码器,使用者可以通过该特性,对请求和响应进行不同的封装与解析。

Spring Cloud 将 Feign 集成到 netflix 项目中,当与 Eureka、Ribbon 集成时,Feign 就具有负载均衡的功能。Feign 本身在使用上的简便性,加上与Spring Cloud 的高度整合,使 用该框架在 Spring Cloud 中调用集群服务,将会大大降低开发的工作量。

创建maven项目feign,将Eureka章节的eureka-server、eureka-service-invoker、eureka-service-provider模块拷贝到 feign 下

在服务提供者的控制器中加入一个方法

@RequestMapping(value = "/hello")
public String hello() {
    return "hello feign!";
}

修改ribbon的依赖为feign的依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

在服务调用者的启动类中,打开 Feign 开关 @EnableFeignClients 接下来,编写客户端接口

@FeignClient("eureka-service-provider") //声明调用的服务名称
public interface PersonClient {
    @RequestMapping("/hello")
    String hello();

    @RequestMapping(value = "/person/{personId}")
    Map<String, Object> findPerson(@PathVariable Integer personId);
}

@FeignClient 注解声明了需要调用的服务名称。

另外,除了方法的@RequestMapping 注解外 , 默认还支持 @RequestParam 、 @RequestHeader、@PathVariable 这 3 个参数注解,也就是说,在定义方法时,可以使用 方式定义参数:

@RequestMapping(method = RequestMethod.GET, value = "/hello/{name}")
String hello(@PathVariable("name") String name);

在 InvokerController 加入

@Autowired
private PersonClient personClient;

@RequestMapping(value = "/invokeHello")
public String invokeHello() {
    return personClient.hello();
}

依次启动服务后,在浏览器中输入:http://localhost:9000/invokeHello,可以看到服务提供者的 “/hello”服务被调用。

19.雪崩效应与 Hystrix

在微服务架构中,我们将系统拆分成了很多服务单元,各单元的应用间通过服务注册与发现的方式互相依赖。由于每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会因等待出现故障的依赖方响应形成任务积压,最终导致自身服务的瘫痪。

举个例子,在一个电商网站中,我们可能会将系统拆分成用户、订单、库存、积分、评论等一系列服务单元。用户创建一个订单的时候,客户端将调用订单服务的创建订单接口,此时创建订单接口又会向库存服务来请求出货(判断是否有足够库存来出货)。此时若库存服务因自身处理逻辑等原因造成响应缓慢,会直接导致创建订单服务的线程被挂起,以等待库存申请服务的响应,在漫长的等待之后用户会因为请求库存失败而得到创建订单失败的结果。如果在高并发情况之下,因这些挂起的线程在等待库存服务的响应而未能释放,使得后续到来的创建订单请求被阻塞,最终导致订单服务也不可用。

在微服务架构中,存在着那么多的服务单元,若一个单元出现故障,就很容易因依赖关系而引发故障的蔓延,最终导致整个系统的瘫瘓,这样的架构相较传统架构更加不稳定。为了解决这样的问题,产生了断路器等一系列的服务保护机制。

在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。

针对上述问题,Spring Cloud Hystrix 实现了断路器、线程隔离等一系列服务保护功能。它也是基于Netlix的开源框架Hystrix 实现的,该框架的目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix 具备服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等强大功能。

接下来,我们就从一个简单示例开始对 Spring Cloud Hystrix 的学习与使用。

spring cloud 中使用 hystrix

创建 maven 项目 hystrix ,将 Eureka 章节的 eureka-server、eureka-service-invoker、eureka-service-provider 模块拷贝到 hystrix 下,按照如下顺序启动。

  1. 启动 eureka-server
  2. 启动 eureka-service-provider 在 8888 端口和 8886 端口
  3. 启动 eureka-service-invoker

关闭 8888 端口的服务后刷新 http://localhost:9000/router/123 地址,发现地址有时报错如下:

微服务下每个服务都要引入Springsecurity 微服务是springcloud_spring_12

在 eureka-service-invoker(消费者)中加入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

在启动类上添加注解 @EnableCircuitBreaker 开启断路器功能。
注意:这里还可以使用 Spring Cloud 应用中的 @SpringCloudApplication 注解来修饰应用主类,该注解的具体定义如下所示。可以看到,该注解中包含了上述我们所引用的三个注解,这也意味着一个 Spring Cloud 标准应用应包含服务发现以及断路器。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}

改造服务消费者,新增 errorFallback 方法,该方法参数和原方法参数列表和返回类型必须一致,在 router 函数上增加 @HystrixCommand 注解来指定错误时回调方法:

@RestController
@Configuration
public class InvokerController {

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }

    @RequestMapping(value = "/router/{personId}")
    @HystrixCommand(fallbackMethod = "errorFallback")
    public String router(@PathVariable Integer personId) {
        RestTemplate restTpl = getRestTemplate();
        // 根据应用名称调用服务
        String json = restTpl.getForObject(
                "http://eureka-service-provider/person/" + personId, String.class);
        return json;
    }
    public String errorFallback(Integer integer){
        System.out.println(integer);
        return "Hystrix Error!";
    }
}

当报错时,返回指定字符串。

微服务下每个服务都要引入Springsecurity 微服务是springcloud_微服务_13

上面简单的演示了断路器调用保存时处理办法,但丝毫不能体现断路器的熔断作用。原因是演示断路器的熔断和关闭需要在高并发情况下。下面我们从理论上来看断路器的工作流程。Hystrix 整个工作流如下:

  1. 构造一个 HystrixCommand 或 HystrixObservableCommand 对象,用于封装请求,并在构造方法配置请求被执行需要的参数;
  2. 执行命令,Hystrix 提供了 4 种执行命令的方法,主要用于方法执行顺序的线程控制;
  3. 判断是否使用缓存响应请求,若启用了缓存,且缓存可用,直接使用缓存响应请求。Hystrix 支持请求缓存,但需要用户自定义启动;
  4. 判断熔断器是否打开,如果打开,跳到第 8 步;
  5. 判断线程池/队列/信号量是否已满,已满则跳到第 8 步;
  6. 执行 HystrixObservableCommand.construct() 或 HystrixCommand.run(),如果执行失败或者超时,跳到第 8 步;否则,跳到第 9 步;
  7. 统计熔断器监控指标;
  8. 走 Fallback 备用逻辑
  9. 返回请求响应

从流程图上可知道,第 5 步线程池/队列/信号量已满时,还会执行第 7 步逻辑,更新熔断器统计信息,而第 6 步无论成功与否,都会更新熔断器统计信息。

线程池隔离的思想是:把 tomcat 中先一个线程池分成两个线程池。比如 tomcat 线程池中初始有 200 个线程,分成两个线程池 A , B 后,A 线程池有 50 个线程可以用,B 线程池有 150 个线程可以用。将访问量较大的接口单独配置给一个线程池,其他接口使用另一个线程池,使其访问量激增时不要影响其他接口的调用。 然后,将访问量暴增的接口访问交给 A 线程池,其他接口的访问交给B线程池。A , B两个线程池是相互隔离的, 互不影响。 这时候, 如果商品查询接口访问量激增 , 被挤爆的线程池也只是A线程池,A,B线程池互不影响,所以其他接口如:个人中心接口, 付款接口, 订单查询接口依然可用。线程池隔离主要针对C端用户对服务的访问. 线程池隔离起到分流的作用。

信号量隔离: 可以把信号量理解成一个计数器 , 对这个计数器规定一个计数上限,代表一个接口被访问的最大量。假定设置付款接口的信号量最大值为10,(这个接口最多占用线程池中10个线程) 初始值为0。每调用一次接口信号量加一 , 接口处理完后信号量减一. 当信号量值达到最大时 , (10时) , 对后续的调用请求拒接处理. 信号量隔离主要是针对各个服务内部的调用处理, 起到限流的作用。

20.配置中心 Config

Spring Cloud Config 是 Spring Cloud 团队创建的一个全新项目,用来为分布式系统中的基础设施和微服务应用提供集中化的外部配置支持,它分为服务端与客户端两个部分。其中服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置仓库并为客户端提供获取配置信息、加密/解密信息等访问接口;而客户端则是微服务架构中的各个微服务应用或基础设施,它们通过指定的配置中心来管理应用资源与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。Spring Cloud Config 实现了对服务端和客户端中环境变量和属性配置的抽象映射,所以它除了适用于 Spring 构建的应用程序之外,也可以在任何其他语言运行的应用程序中使用。由于 Spring Cloud Config 实现的配置中心默认采用 Git 来存储配置信息,所以使用 Spring Cloud Config 构建的配置服务器,天然就支持对微服务应用配置信息的版本管理,并且可以通过 Git 客户端工具来方便地管理和访问配置内容。当然它也提供了对其他存储方式的支持,比如 SVN 仓库、本地化文件系统。接下来,我们从一个简单的入门示例开始学习 Spring Cloud Config 服务端以及客户端的详细构建与使用方法。

在本节中,我们将演示如何构建一个基于本地化文件系统的分布式配置中心,同时对配置的详细规则进行讲解,并在客户端中演示如何通过配置指定微服务应用的所属配置中心,并让其能够从配置中心获取配置信息并绑定到代码中的整个过程。

Spring Cloud Config 搭建

创建 maven 项目 config,将 Eureka 章节的 eureka-server、eureka-service-invoker、eureka-service-provider 模块拷贝到 config 下

创建 config-server 模块,并勾选 Spring Cloud Config->Config Server 和 Spring Cloud Discovery->Eureka Server

启动类加 @EnableConfigServer @EnableEurekaClient 注解启用配置。

配置文件

server:
  port: 1201
spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        native:
          search-locations: classpath:/properties/
  profiles:
    active: native
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

在 resources 下创建 properties 文件夹用于存储其他应用的配置文件。

eureka-service-invoker-pro.yml
server:
  port: 9000

eureka-service-provider-pro.yml
user: hhh

修改 eureka-service-provider 配置文件为 bootstrap.properties

spring.application.name=eureka-service-provider
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
spring.cloud.config.discovery.serviceId=config-server
spring.cloud.config.discovery.enabled=true
spring.cloud.config.profile=pro
spring.cloud.config.label=master

修改 eureka-service-invoker 配置文件为 bootstrap.properties

spring.application.name=eureka-service-invoker
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
spring.cloud.config.discovery.serviceId=config-server
spring.cloud.config.discovery.enabled=true
spring.cloud.config.profile=pro
spring.cloud.config.label=master

这里需要格外注意,”上面这些属性必须配置在 bootstrap.properties 中,这样 config-server 中的配置信息才能被正确加载。bootstrap.yml(bootstrap.properties)用来在程序引导时执行,应用于更加早期配置信息读取,如可以使用来配置 application.yml 中使用到参数等 application.yml(application.properties)应用程序特有配置信息,可以用来配置后续各个模块中需使用的公共参数等。bootstrap.yml 先于 application.yml 加载。

在 provider 的 controller 中获取 user 属性

@RefreshScope
@RestController
public class FirstController {
    @Value("${user}")
    private String user;

    @RequestMapping(value = "/person/{personId}")
    public Map<String, Object> findPerson(@PathVariable Integer personId, HttpServletRequest request) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("id", personId);
        map.put("name", user);
        map.put("age", 18);
        map.put("url", request.getRequestURL());
        return map;
    }
}

依次启动 eureka-server、config-sever、eureka-service-provider:8888、eureka-service-provider:8886、eureka-service-invoker

访问路径后发现可以获取到配置文件 user 的值,而且 eureka-service-invoker 也在 9000 端口正常启动。

应用启动时,根据 bootstrap.properties 中配置的应用名 {application}、环境名 {profile}、分支名 {label},向 Config Server 请求获取配置信息。

需要在服务提供者和服务调用者加入依赖。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>

21.config-server-git

本文使用开源中国的码云来创建我们的 Git 仓库,当然你也可以选择其他的 Github 或者阿里云 Git 等创建自己的 Git 仓库。 点击导航栏中的“+”按钮==>新建仓库,填入仓库信息,完成创建。 点击 克隆/下载 ==>复制,将 Git 仓库地址复制下来备用。基本配置如下:

spring:
  application:
    name: config-server
  cloud:
    config:
      label: master
      server:
        git:
          uri: https://gitee.com/wpfhhh/config-server
          search-paths: springcloud
          username:
          password:
server:
  port: 1201
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

动态刷新配置

有时候,我们需要对配置内容做一些实时更新,这时就需要Spring Boot Actuator。它可以帮助你监控和管理 Spring Boot 应用,比如健康检查、审计、统计和 HTTP 追踪等。Actuator 同时还可以与外部应用监控系统整合,比如 Prometheus, Graphite, DataDog, Influx, Wavefront, New Relic 等。这些系统提供了非常好的仪表盘、图标、分析和告警等功能,使得你可以通过统一的接口轻松的监控和管理你的应用。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

SpringCloud2.0 以后,没有 /refresh 手动调用的刷新配置地址

2.0 之前只要加入依赖:spring-boot-starter-actuator 并且在类上,变量类上打上@RefreshScope 的注解,在启动的时候,都会看到RequestMappingHandlerMapping : Mapped "{/refresh,methods=[post]}"

也就是 SpringCloud 暴露了一个接口 /refresh 来给我们去刷新配置,但是 SpringCloud 2.0.0 以后,需要在 bootstrap.yml 里面加上需要暴露出来的地址

management.endpoints.web.exposure.include=refresh,health

现在的地址也不是 /refresh 了,而是 /actuator/refresh

22.zuul 网关

通过前几章的介绍,我们对于 Spring Cloud Nettlix下的核心组件已经了解了一大半。这些组件基本涵盖了微服务架构中最为基础的几个核心设施,利用这些组件我们已经可以构建起一个简单的微服务架构系统,比如,通过使用 Spring Cloud Eureka 实现高可用的服务注册中心以及实现微服务的注册与发现;通过Spring Cloud Ribbon 或 Feign 实现服务间负载均衡的接口调用;同时,为了使分布式系统更为健壮,对于依赖的服务调用使用 Spring Cloud Hystrix 来进行包装,实现线程隔离并加入熔断机制,以避免在微服务架构中因个别服务出现异常而引起级联故障蔓延。

在传统模式中,我们的服务集群包含内部服务 Service A 和 Service B,它们都会向 Eureka Server 集群进行注册与订阅服务,而 Open Service 是一个对外的RESTful API 服务,它通过 F5、Nginx 等网络设备或工具软件实现对各个微服务的路由与负载均衡,并公开给外部的客户端调用。

该模式有以下的几个缺点:

1.当消费者数量增加,或更换 ip 时,我们不得不修改 F5 或 nginx 的配置文件,使其均衡到该消费者上。

2.权限验证模块不得不因为不同的消费者使用相同的逻辑,增加代码的冗余。

为了解决上面这些常见的架构问题,API 网关的概念应运而生。API 网关是一个更为智能的应用服务器,它的定义类似于面向对象设计模式中的 Facade (外观)模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤。它除了要实现请求路由、负载均衡、校验过滤等功能之外,还需要更多能力,比如与服务治理框架的结合、请求转发时的熔断机制、服务的聚合等一系列高级功能。

首先,对于路由规则与服务实例的维护问题。Spring Cloud Zuul 通过与 Spring Cloud Eureka 进行整合,将自身注册为 Eureka 服务治理下的应用,同时从Eureka 中获得了所有其他微服务的实例信息。这样的设计非常巧妙地将服务治理体系中维护的实例信息利用起来,使得将维护服务实例的工作交给了服务治理框架自动完成,不再需要人工介入。而对于路由规则的维护,Zuul 默认会将通过以服务名作为 ContextPath 的方式来创建路由映射,大部分情况下,这样的默认设置已经可以实现我们大部分的路由需求,除了一些特殊情况(比如兼容一些老的 URL )还需要做一些特别的配置。但是相比于之前架构下的运维工作量,通过引入 Spring Cloud Zuul 实现 API 网关后,工作量已经能够大大减少了。

其次,对于类似签名校验、登录校验在微服务架构中的冗余问题。理论上来说,这些校验逻辑在本质上与微服务应用自身的业务并没有多大的关系,所以它们完全可以独立成一个单独的服务存在,只是它们被剥离和独立出来之后,并不是给各个微服务调用,而是在 API 网关服务,上进行统一调用来对微服务接口做前置过滤,以实现对微服务接口的拦截和校验。Spring Cloud Zuul 提供了一套过滤器机制,它可以很好地支持这样的任务。开发者可以通过使用 Zuul 来创建各种校验过滤器,然后指定哪些规则的请求需要执行校验逻辑,只有通过校验的才会被路由到具体的微服务接口,不然就返回错误提示。通过这样的改造,各个业务层的微服务应用就不再需要非业务性质的校验逻辑了,这使得我们的微服务应用可以更专注于业务逻辑的开发,同时微服务的自动化测试也变得更容易实现。

使用 idea 创建名为 zuul 的maven项目,将Eureka章节的eureka-server、eureka-service-invoker、eureka-service-provider 模块拷贝到 zuul 下

用springboot 创建 zuul-server模块,勾选Spring Cloud Routing->Zuul [Maintenance],Spring Cloud Discovery->Eureka Server

在启动类上使用@EnableZuulProxy注解开启路由功能,使用@EnableEurekaClient 注解开启 eureka 注册功能

配置文件加入基本信息

spring:
  application:
    name: zuul-server
server:
  port: 5555
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
zuul:
  routes:
    api-1:
      path: /api-1/**
      serviceId: eureka-service-invoker
    api-2:
      path: /api-2/**
      serviceId: eureka-service-provider

我们这样就完成了配置

打开浏览器访问

http://localhost:9000/router/123和http://localhost:5555/api-1/router/123

http://localhost:8888/person/123和http://localhost:5555/api-2/person/123

发现获取到的内容一样

通过上面的面向服务的配置方式,我们不需要再为各个路由维护微服务应用的具体实例的位置,而是通过简单的path与serviceld的映射组合,使得维护工作变得非常简单。这完全归功于Spring Cloud Eureka的服务发现机制,它使得API网关服务可以自动化完成服务实例清单的维护,完美地解决了对路由映射实例的维护问题。

注意:如果在项目的配置文件没有加入zuul.routes... 的配置,zuul 会自动为每个应用根据名字创建对应的路径。类似于前面开发中的项目名。但是前提条件是所有项目的 contentPath 都只是斜杠。