微服务概述

微服务是一种架构风格,把单个项目拆分成多个服务(比如页面展示、数据库操作),每个服务都能独立运行,各服务单元使用网络通信协议进行协作,最终构建起完整的系统。

为什么需要微服务?
传统软件工程中项目都是单体架构,这种方法在业务初期十分高效,开发简单,部署便捷,可随着业务的持续扩展,项目的可扩展性、可维护性和可迭代性都会变得越来越差,并且无法通过引入框架(如Spring)彻底解决,必须采取一种新的架构模式,对项目进行更细粒度的拆分,实现解耦业务模块,服务独立迭代,微服务架构就此应运而生了。

微服务是如何解决这些问题的?
微服务解决单体架构的核心逻辑在于解耦与自治重构系统,将原本单体架构下相互纠缠的各个功能模块,拆解为可独立运行的服务进程,避免原本单个程序的改动影响全局的情况,增强隔离性,便于维护;同时服务进程可以分别采用不同的存储形式,甚至技术栈,极大增强了开发模式的灵活性。

微服务的特征
微服务的主要特征包括:
单一职责:每个服务只完成指定业务,代码边界清晰;
自治:每个服务拥有独立存储模式,扩展和维护系统方便;
隔离性强:每个服务可以独立扩展、维护,而不影响其他服务或组件;
灵活性高:微服务可使用不同的语言、数据库和环境开发,可以为不同需求选择合适的工具。

微服务的应用场景
微服务降低程序耦合度,提升可扩展性的同时也带来了更高的开发、部署和维护成本,首先多个服务的通信成本就是项目的额外开销;并且管理多个交互的服务也远比管理单个程序更复杂,加上多个服务独立部署,使用微服务的运维成本也更高;同时要求每个服务能独立运行,这进一步要求每个服务的开发者都要具备全栈开发能力,抬高了团队合作的门槛
由此可见,微服务并非可以完全替代单体架构的新模式,而是针对特殊需求的解决方案,比如业务复杂,但模块边界清晰的场景,如电商系统(可分为用户、订单、支付等独立服务);需要弹性扩展,流量波动的场景,如直播(随观众增多扩展服务);以及技术栈多样和多团队协同开发的场景,而传统的简单需求仍然适合单体架构。

微服务与分布式
因为之前做过spark下的电影推荐系统,其中用到分布式的思想和微服务十分类似,分布式是指利用多个计算或存储资源处理数据,最后整合结果,达到突破单机限制,扩展硬件性能的效果,而微服务是将单体架构拆分成多个独立运行的服务,将服务组合成完整项目,其本质都是分治思想的运用。

微服务作为一种分治架构,一定要使用分布式落地,但分布式不一定是微服务,简单来说,微服务是程序设计层面的“分布式”,其底层一定依赖分布式,更详细的对比可参考分布式与微服务的关系

微服务的挑战

  1. 服务拆分粒度。拆分过粗会导致程序耦合性过高,拆分过细则会极大提高通信开销,通常要先粗后细经过几次拆分才能达到合理范围。
  2. 集群地址维护。微服务集群可能会频繁扩容缩容,传统静态地址完全失效,通常需引入注册中心对所有服务进行管理,实现动态配置同步。
  3. 健康状态感知。集群中单个服务可能因各种原因处于存活但不可用状态,其他服务无法感知,此时若不断向该服务发起请求,会导致故障扩散,需要建立熔断机制。
  4. 服务远程调用。跨网络调用会带来许多问题,通信成本,通信协议与序列化统一,调用链路追踪等。
  5. 分布式事务一致性。微服务中每个服务都有独立数据库,订单创建和扣库存无法通过事务保证一致性,高并发场景下可使用可靠消息队列同步管理事务,但不可避免的会带来延时。
  6. 日志追踪难。微服务中日志分布在多台机器上,一次请求出错需逐台机器排查,效率极低,同时难以感知整体流程耗时分布。

SpringBoot

SpringBoot入门

SpringBoot是Spring框架的进一步封装,内部整合了Spring和SpringMVC,同时内嵌了Tomcat服务器,抛弃了原本复杂的XML配置文件,全部采用注解标识,该框架通过启动器starter引入和管理依赖,并可以将整个项目打包为jar格式,实现独立部署,与微服务的自治特征高度契合,所以是微服务开发的主流选择之一。

SpringBoot的核心设计理念是约定大于配置,即框架预先定义一套设计规则,开发者遵循该配置时无需额外指定配置,只有当配置方式与预先定义不同时才需手动指定。
比如将控制层Controller、逻辑层Service等类放在启动类的子包下;默认读取application.propertiesapplication.yml作为配置文件;引入启动器依赖自动加入容器,框架自动配置Servlet和Tomcat。

下面是快速创建SpringBoot的方法
1. 创建SpringBoot工程
因为网络原因,要将服务器URL替换为阿里云的start.aliyun.com
创建Springboot
2. 引入web启动器
pom.xml文件中添加如下配置:

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

也可以点击添加启动器:
添加启动器
选择添加web
添加web工程
3. 创建控制层文件
在启动器文件SpringootApplication同级文件夹下创建控制层包,新建控制层文件,项目结构如下:
创建控制层文件
控制层文件简单测试方法为,使用注解映射URL路径,在返回的消息体中提示测试方法,示例代码如下:

package org.test.springboot.controller;

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

@org.springframework.stereotype.Controller
public class Controller {

    @RequestMapping("/test")
    @ResponseBody
    public String test() {
        return "springboot 测试方法";
    }
}

4. 启动主程序并测试
启动类的代码是自动生成的,其中@SpringBootApplication注解是标识当前工程为springboot,随后调用类.run方法即启动web项目,同时启动服务器,无需手动配置。

// 声明当前工程为Springboot
@SpringBootApplication
public class SpringbootApplication {

    public static void main(String[] args) {

        SpringApplication.run(SpringbootApplication.class, args);
    }
}

出现下图说明项目启动成功
springbott启动成功
测试结果
5. 打包独立部署
首先注释掉pom.xml中带skip的一行,该标识告诉maven构建生命周期时跳过这个插件的运行,这会导致打包后的文件找不到主清单。
注释skip
随后使用maven的打包操作
maven打包
执行完毕后在项目的target目录下会生成jar包,进入目录在地址栏输入cmd,同时输入命令java -jar 包名即可启动该包独立运行。

jar包独立运行
此时在地址栏输入http://localhost:8080/test,仍然可以得到结果,证明springboot打包后的jar文件可以独立运行。
初体验小结
相比以往先配置beans.xml,配置springmvc-config.xml,再添加web项目和tomcat服务器的流程,使用springboot构建网络项目确实简单太多,另外基于springboot的项目打包后可以独立运行,部署为服务也很方便,为微服务的实现提供了技术支持。

整合MyBatis

1. 引入MyBatis启动器和数据库驱动

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

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

2. 配置文件中创建数据源对象

#配置数据源application.properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/db118?serverTimezone=GMT&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root

#配置mapper映射文件的位置,如果使用xml映射的话
mybatis.mapper-locations=classpath:org.test.springboot.dao*.xml

3. 新建接口,实例加入容器
前面的文章写过很多次User类了,这里省略,直接展示接口映射方法,SpringBoot虽然全面采用注解方案,但MyBatis仍然可用xml文件的方式,仍然可以在resources文件夹下新建与接口文件相同的目录并配置xml。

public interface UserDao {
    @Select("select * from userinfo")
    @Results({
            // 主键映射
            @Result(column = "id",property = "uid",id = true),
            @Result(column = "name",property = "name")
    })
    List<User> findAll();
}

控制层展示数据,扫描持久层接口并将其实例注入 Spring 容器,在控制层中通过自动注入获取接口实例,示例代码如下:

// 扫描持久层接口,将实例添加到容器
@MapperScan("org.test.springboot.dao")
// 控制层实例,类内所有方法加@ResponseBody
@RestController
public class Controller {
    @Autowired
    private UserDao userDao;
    @RequestMapping("/test")
    public String test() {
        List<User> users = userDao.findAll();
        System.out.println(users);
        return users.toString();
    }
}

页面输出如下:
springboot展示数据库内容

使用FreeMarker模板引擎

以前用过的jsp要先将jsp代码编译成servlet,再使用servlet输出内容,而SpringBoot自带的嵌入式servlet容器对jsp的解析有使用限制,我们通常使用其他模板引擎,这里介绍一种新的模板引擎Freemarker。

1. 添加依赖

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

模板视图应该放在resources/templates下,在配置文件中添加前后缀匹配

#视图的前缀
spring.freemarker.template-loader-path=classpath:/templates
#视图的后缀
spring.freemarker.suffix=.ftl

2. 控制层返回查询对象
控制层借助Model对象,向域中添加对象列表,修改注解为@Controller,将返回值跳转到指定find页面,修改示例代码如下:

@org.springframework.stereotype.Controller
public class Controller {
    @Autowired
    private UserDao userDao;
    @RequestMapping("/test")
    public String test(Model model) {
        List<User> users = userDao.findAll();
        model.addAttribute("users", users);
        return "find";
    }
}

3. find.ftl展示数据
FreeMarker功能不如jsp对Java支持的全面,集合对象是没法一次识别的,需要使用循环遍历读取每个元素再展示,示例如下:

<html>
<head>
    <meta charset="UTF-8">
    <title>Freemarker</title>
</head>
<body>
Hello!
<#list users as user>
    ${user}
</#list>
</body>
</html>

如果要引入其他文件如jquery,则需要在主目录下新建webapp文件夹,整体目录结构如图:
项目整体目录

使用Redis数据库

相比Mysql等有表结构的关系型数据库,Redis是大数据背景下常用的非关系数据库,其中数据大多通过Key-Value形式存储,其底层使用C实现,又因为redis其实是内存型的数据库,所以其访问速度要远远快于关系型数据库,所以项目中通常将其作为缓存使用,用于减轻数据库的访问压力,有关更底层更细节的性能分析可见Redis抗住百万高并发的秘密

因为最新版本的Redis都停止了对Windows的支持,所以本文不做示例,仅记录使用方法。

基本使用方法
Redis默认端口6379,服务端可存储操作多种不同类型的数据,String类型使用set key value进行赋值或修改,使用get key查询,append追加;hash表使用hset 名称 key value的形式存储或修改,使用hgetall获取所有元素,hget 名称获取指定元素,list使用lpush 名称 值将元素添加到表头,rpush元素添加到表尾,lrange 名称 start end返回指定范围内的元素。

SpringBoot整合redis
首先导入依赖

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

配置文件中写入配置,此时redis服务端应启动

# 指定IP和端口
spring.redis.host=localhost
spring.redis.port=6379

redis作为缓存
控制层中聚合RedisTemplate并自动注入,若要将对象存入缓存,需确保对象实现java.io.Serializable接口(开启对象序列化支持),获取绑定指定名称后的缓存,遇到查询先检查缓存内是否有数据,有则从中取,否则到数据库查询并加入缓存,修改后的控制层代码如下:

@org.springframework.stereotype.Controller
public class Controller {
    private RedisTemplate redisTemplate;
    @Autowired
    private UserDao userDao;
    @RequestMapping("/test")
    public String test(Model model) {
        List<User> users=null;
        // 绑定缓存名称,获取缓存
        BoundListOperations boundListOperations=redisTemplate.boundListOps("buffer");
        if(boundListOperations.size()>0){
            System.out.println("从缓存中取出数据");
            // 取出全部数据
            users=boundListOperations.range(0,-1);
        }else{
            System.out.println("从数据库中取出数据");
            users = userDao.findAll();
            // 将结果追加到缓存中
            boundListOperations.leftPush("buffer",users);
        }
        model.addAttribute("users", users);
        return "find";
    }
}

Dubbo框架

SpringBoot是开发具体服务的框架,Dubbo则是阿里整合服务的框架(现在是apache的顶级框架,真还得是大厂),负责解决多个服务间的通信问题,服务提供者将自己的地址注册到注册中心如ZooKeeper,消费者从服务中心获取服务者地址,封装了HTTP等底层协议,实现跨服务调用,同时提供了复杂均衡等治理手段。

安装注册中心ZooKeeper

ZooKeeper是开源的分布式协调服务中心,可用于管理分布式系统中的数据和服务,服务提供方注册地址和服务到该中心,供消费者查询服务地址,详细介绍可见ZooKeeper看这一篇
安装Java环境和tomcat
本次使用Linux作为注册中心,首先执行更新一下源,避免源错误产生问题,然后直接使用命令安装jdk8,命令如下:

sudo apt-get update
sudo apt install openjdk-8-jdk

使用java -version发现有输出就说明安装成功,随后配置环境:

# 打开配置文件
nano ~/.bashrc
# 将如下内容复制到文件末尾
# 配置 JAVA_HOME(OpenJDK 8 安装路径)
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
# 配置 JRE_HOME(可选,一般无需单独配置)
export JRE_HOME=${JAVA_HOME}/jre
# 配置 CLASSPATH(Java 类路径,. 表示当前目录)
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
# 将 Java 的 bin 目录加入系统 PATH
export PATH=${JAVA_HOME}/bin:$PATH

# 最后ctrl+x保存退出

tomcat9下载服务器,复制到Linux服务器中解压,可以修改conf/server.xmlConnector标签的端口号,避免冲突。
修改端口号
最后到/bin目录下执行./startup.sh,启动服务器,最后访问127.0.0.1:8088出现如下界面说明安装成功。
tomcat启动成功

注意版本对应!!!
折腾好几遍,最后因为下载的是tomcat10,支持jdk11以上的版本,还是尽量用老版本,jdk8+tomcat9,仍然更新且稳定。

安装配置ZooKeeper
到apache的zookeeper官网下载zookeeper安装包,本次示例使用的是稳定的3.8.5包。
ZooKeeper安装包
下载并解压后将其中的配置文件示例/conf/zoo_sample.cfg赋值为zoo.cfg文件作为真实配置文件,其中dataDir可设置为自己的输出文件路径。
修改dataDir路径
修改完成后进入bin目录,使用./zkServer.sh start启动注册中心服务

安装可视化工具dubbo-admin
注册中心启动后没有任何提示信息,很不方便,所以需要一种可视化管理服务的方法。

新版本其实不支持这种方式查看,但是因为相关资料比较少,只有这个能实现,并且支持本文的ZooKeeper版本。

首先到war包下载地址下载要使用的war包,把该war包放到tomcat/webapp下,重新启动服务,使用127.0.0.1:8088/dubbo-admin可进入如下页面,登录用户名和密码默认都是root
dubbo-admin管理系统

服务提供者注册服务

传统的调用方法是基于接口的,即提供者实现接口,重写其中方法,消费者聚合接口类,容器自动注入,通过接口类调用提供者重写的接口方法。

首先创建接口类,并通过maven的install部署到本地仓库。

public interface TestInterface {
    public void getMessage();
}

随后复制本项目坐标,引入服务提供者。

<groupId>org.test</groupId>
<artifactId>Interface</artifactId>
<version>1.0-SNAPSHOT</version>

<!--此外还需要引入日志依赖-->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.16</version>
</dependency>
<!--dubbo的启动器-->
<dependency>
    <groupId>io.dubbo.springboot</groupId>
    <artifactId>spring-boot-starter-dubbo</artifactId>
    <version>1.0.0</version>
</dependency>

在包路径com.Service下新建Service类,继承中间类TestInterface并重写getMessage方法,最后用导入dubbo包的注解Service修饰,测试的简单代码如下:

package com.Service;
import org.test.TestService;

// 修饰服务的注解
@com.alibaba.dubbo.config.annotation.Service
public class Service implements TestService {

    @Override
    public String getMessage() {
        return "dubbo service is running";
    }
}

resource目录下新建application.properties配置文件,用于配置与注册中心ZooKeeper的交互,规定如下:

# 修改本项目地址避免冲突
server.port=8082
# dubbo提供者配置,注册中心名称
spring.dubbo.application.name=dubbo-server
# 到注册中心登记自己的信息,注册中心IP加默认端口2181
spring.dubbo.registry.address=zookeeper://192.168.145.129:2181
# 声明dubbo通信协议和本机服务端口,提供者将持续监控20880端口
spring.dubbo.protocol.name=dubbo
spring.dubbo.protocol.port=20880
#暴露服务所在包的位置
spring.dubbo.scan=com.Service

主函数中运行,可在部署的ZooKeeper见到服务数和应用数的变化。
ZooKeeper变化

消费者订阅服务

消费者首先引入依赖,包括中间接口、dubbo启动器,日志以及本次示例要用到的web启动器支持。

<dependency>
   <groupId>org.test</groupId>
   <artifactId>Interface</artifactId>
   <version>1.0-SNAPSHOT</version>
</dependency>
<!--        此外还需要引入日志依赖-->
<dependency>
   <groupId>log4j</groupId>
   <artifactId>log4j</artifactId>
   <version>1.2.16</version>
</dependency>
<!--dubbo的启动器-->
<dependency>
   <groupId>io.dubbo.springboot</groupId>
   <artifactId>spring-boot-starter-dubbo</artifactId>
   <version>1.0.0</version>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

然后配置文件,包括注册中心的名称地址,以及订阅服务类的包所在位置

server.port=8081
#dubbo消费者配置
spring.dubbo.application.name=dubbo-client
spring.dubbo.registry.address=zookeeper://192.168.145.129:2181
#订阅服务所在包的位置
spring.dubbo.scan=com.Controller

订阅类使用集成接口类的方法实现远程调用,订阅类使用Reference注解修饰,实现类似自动填充Autowired的效果,但底层原理完全不同。
Reference会先借助Dubbo将方法调用转换为RPC远程请求,通过注册中心找到服务者提供的地址,再通过网络发送请求并接收结果。

消费者类的示例代码为:

package com.Controller;

import com.alibaba.dubbo.config.annotation.Reference;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.test.TestService;

@org.springframework.stereotype.Controller
public class Controller {
    @Reference
    private TestService testService;

    @RequestMapping("/service")
    @ResponseBody
    public String getMessage() {
        return this.testService.getMessage();
    }
}

服务端和消费端都启动时,可通过消费端地址访问服务端的方法。
访问成功

Spring Cloud

Dubbo是微服务框架的落地,还有很多不足,比如需要额外配置注册中心,Spring Cloud则是分布式微服务架构的一站式解决方案,它将市面上成熟的微服务框架整合起来,使用Spring Boot的思想再次封装,提供的功能有:

  1. 分布式/版本化配置
  2. 服务注册与发现
  3. 服务间调用
  4. 负载均衡

详细文档可见官网:https://springframework.org.cn/projects/spring-cloud/,以及SpringCloud从入门到精通

不同版本的Spring Cloud使用伦敦地铁站命名,与Spring Boot的版本对应如图:
版本对应图

继承父工程

微服务项目通常要有多个模块,为了简化配置通常使用父工程指定依赖的版本,不同服务继承父工程获取依赖版本,简化配置的同时也保证版本对应,该工程只用于约束子模块依赖版本,故其中存放src的代码目录可直接删掉。

新建普通maven工程的pom.xml内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.test</groupId>
    <artifactId>cloud-parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!-- 声明工程为 依赖管理容器,不编译生成 jar/war 包 -->
    <packaging>pom</packaging>

    <repositories>
        <repository>
            <id>aliyun maven</id> <!-- 与镜像的mirrorOf="aliyun maven"匹配 -->
            <url>https://maven.aliyun.com/repository/central</url>
        </repository>
    </repositories>
		<!-- 继承父工程,其中规定了常用依赖的适配版本 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.9.RELEASE</version>
        <relativePath/>
    </parent>
	<!-- 指定版本 -->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR10</spring-cloud.version>
        <mysql.version>8.0.22</mysql.version>
        <mybatis.version>2.1.1</mybatis.version>
    </properties>

    <!--子模块继承时,使用下面的依赖会自动拉取父工程规定的版本-->
    <dependencyManagement>
        <dependencies>
            <!-- springCloud -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- mysql驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            <!--mybatis-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

上述配置首先继承自父工程spring-boot-starter-parent,该工程作为Spring Boot的父POM,其核心功能就是预定义了常用的依赖版本,这些依赖与当前parent版本对应。

后续通过<properties>指定依赖版本,使用<dependencyManagement>通过el表达式获取版本信息,继承的子工程需要显式调用,在导入相同依赖时会自动使用父工程的版本,若父工程使用<dependencies>声明依赖,子工程会自动继承。

微服务的节点就是Spring Boot模块,所有的微服务都在一个工程下,所以我们在当前工程下新建Spring Boot的cloud-server模块,作为服务提供者,在pom.xml中引入父工程坐标即可获得所有依赖,添加成功后可在左侧发现maven的向上提示,点击即可跳入父工程pom.xml文件。
引入父工程提示
子模块引入MyBatisweb等启动器依赖,点击左侧版本跳入,有的版本由父工程cloud-parent指定,有的由父工程继承的spring-boot-starter-parent指定,可见继承控制版本由父工程和指定配置spring-boot-starter-parent共同决定。

父工程依赖测试

本文以两个简单表作持久层查询为例学习Spring Cloud的运行流程,实现消费者向提供者拉取查询服务的场景,两个表的结构如下:
表1
表2
首先封装一个maven类型的中间模块store,用于存储查询得到的信息,其pom.xml会自动继承父工程。
使用父工程的lombok依赖类的相关方法,其中@Data注解用于生成getsettoString方法,@AllArgsConstructor用于生成全参构造,@NoArgsConstructor用于生成无参构造,两个类的实例代码如下:

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

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private int id;
    private String name;
    private String sex;
    private String phone;
    public String hobby;
    public String description;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Address {
    private int id;
    private String address;
    private int uid;

// 使用下面代码测试可见构造函数和set方法及toString均成功生成 
    public static void main(String[] args) {
        Address address = new Address();
        address.setId(1);
        address.setAddress("address");
        address.setUid(1);
        System.out.println(address);
    }
}

将中间模块通过pom.xml依赖引入后,使用简单示例测试父工程依赖是否可用,首先生成MyBatis接口如下:

package com.mapper;

import com.pojo.User;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.ArrayList;

@Repository
public interface UserMapper {

    @Select("select * from userinfo")
    ArrayList<User> AllUsers();
}

随后在控制层建立映射:

package com.controller;

import com.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class UserController {
    @Autowired
    private UserMapper userMapper;

    @GetMapping("/test")
    @ResponseBody
    public String test() {
        return "connect success";
    }
    @GetMapping("/getall")
    @ResponseBody
    public String AllUsers() {
        return userMapper.AllUsers().toString();
    }

}

在启动器中指定要扫描的包位置

package org.test.serveruser;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@MapperScan("com.mapper")
@SpringBootApplication
@ComponentScan("com.controller")
public class ServerUserApplication {

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

}

最后通过yml配置文件绑定设置,如端口号和数据库连接信息:

server:
  port: 8081
spring:
  application:
    name: user-server
  datasource:
    url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT&characterEncoding=utf-8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
  type-aliases-package:
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.eagles: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS

相比xmlyml可读性更强,是现代Spring Boot和Spring Cloud的主流配置文件方案。

此时访问指定URL可见数据库信息如下图,说明依赖成功继承:
URL功能测试

服务拉取

服务开放
服务端新增根据id查找方法,Mapper接口中,id参数使用el表达式,即#{变量名}填充,示例为:

@Select("select * from userinfo where id=#{id}")
User getUserById(int id);

控制层映射路径使用单括号{变量名}表示路径变量,参数可使用注解@PathVariable("路径变量")修饰参数,将路径变量赋值给参数,示例如下:

@GetMapping("/user/{uid}")
@ResponseBody
public User FindUserById(@PathVariable("uid")int id) {
    return userMapper.getUserById(id);
}

此时访问http://localhost:8081/user/1即可返回查找id为1的数据。

远程调用——RestTemplate
基本的拉取服务需要借助RestTemplate类完成,该类可基于核心配置类的方法创建,即在核心配置类中使用@Bean修饰返回实例的方法添加到容器中,启动器注解@SpringBootApplication也用于声明核心配置类。

故启动器部分代码为:

package org.test.customer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.client.RestTemplate;

@ComponentScan("com.controller")
@SpringBootApplication
public class CustomerApplication {

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

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

控制层聚合RestTemplate类并自动注入,调用类的getForObject方法,并传入URL和返回类的字节码,使用实例如下:

@Controller
public class customer {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/customer/{id}")
    @ResponseBody
    // 远程调用方法
    public User findUserById(@PathVariable("id")int id) {
        return restTemplate.getForObject("http://localhost:8081/user/"+id, User.class);
    }
}

此时访问调用者URLhttp://localhost:8082/customer/1即可查询到用户信息。

RestTemplate是Spring MVC提供的方法,使用这种方法进行远程调用无需语言一致,只需要IP、端口和请求参数,但这种方法已经即将过时,在高并发和复杂场景下有很大不足,并且服务提供者的URL还需要开发者维护拼接,代码冗余且容易出错。

Spring Cloud微服务生态中,多使用注册中心EUREKA和远程调用Feign的搭配,实现远程调用。

配置注册中心EUREKA

注册中心EUREKA实现了服务治理功能,用于实现服务的自动注册与发现,同时为了保证服务可用性,注册中心使用30s的心跳检测机制,服务每隔30s向服务中心发送状态信息,超时过长会将认为该服务已经下线。

与Dubbo搭配的ZooKeeper相比,Spring Cloud最大的优点是具备 “一站式、开箱即用的微服务生态”,与 Spring 技术栈无缝兼容的同时,还提供了服务管理、网关和熔断等功能,详细介绍可见:Eureka原理和使用

新建模块Eurekapom.xml导入依赖

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

配置文件yml中使用如下字段声明地址:

server:
  port: 10001
spring:
  application:
    name: eureka-server
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10001/eureka/

最后给启动器添加注解@EnableEurekaServer,启动后可访问http://localhost:10001,进入如下页面:
Eureka注册中心

经过注册中心拉取服务

服务提供者引入客户端依赖


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

配置文件中增加注册配置

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10001/eureka/

启动启动器后可在注册中心中看到注册的服务:
注册服务
服务名称就是配置中spring下的application:name

可修改启动器配置实现多个服务:
修改运行配置
在虚拟机选项中增加修改端口的参数-Dserver.port=8083避免端口冲突:
修改端口
运行后可在注册中心发现两个不同端口的同名服务:
同名服务
客户端配置一致,拉取时最大的不同是使用服务名替换原本的IP+端口号,同时需在RestTemplate添加负载均衡注解,使该类能识别服务器,实例代码如下:

	@GetMapping("/customer/{id}")
    @ResponseBody
    // 远程调用方法
    public User findUserById(@PathVariable("id")int id) {
        // return restTemplate.getForObject("http://localhost:8081/user/"+id, User.class);
        String service="http://USER-SERVER/user/";
        return restTemplate.getForObject(service+id, User.class);
    }

// 启动器代码
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }

可服务名称仅仅代替IP+端口号,其实际意义会不会小了一些?

传统RestTemplate远程调用方法最大的缺陷是需要将URL写死,但在微服务中:

  • 服务可能部署多实例(集群),地址可能是 8081、8082、8083 等;
  • 服务可能动态扩缩容(新增 / 下线实例),地址会频繁变化;
  • 服务可能迁移(IP 变更),URL 也会失效。

通过服务名称,该类会自动向服务中心查询所有可用地址,而无需开发者指定IP和端口,同时结合Spring Cloud LoadBalancer(或 Ribbon)完成负载均衡调用。

远程调用组件Feign

经过服务中心的传递,原有服务提供方的IP+端口已经被换为服务名称,但是调用者仍然需要使用URL的方式调用服务,参数复杂时往往难以维护,远程调用组件Feign提供了优雅发送http请求的方法,通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,封装了http调用流程。

首先新增模块cloud-feign并引入依赖:

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

新增接口类UserClient用于确认远程调用信息,通过注解@FeignClient("服务名")声明当前类为客户端并绑定服务,方法内可使用@GetMapping("url")的方式指定拉取服务的具体参数,完整示例代码如下:

import com.eagles.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient("USER-SERVER")
public interface UserClient {

    @GetMapping("/user/{uid}")
    User findUserById(@PathVariable("uid") int uid);
}

用户模块引入Feign组件依赖后只需继承示例并自动注入,即可调用指定服务,而无需关注具体使用方法与url,示例代码如下:

@Service
public class cotroller{
    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private UserClient userClient;
		
	@GetMapping ("/order/{oid}")
	@ResponseBody
    public User findOrderById(int id) {
        //远程调用 根据用户id查询用户信息
        //User user = restTemplate.getForObject("http://USER-SERVER/user/" + order.getUserId(), User.class);

        User user = userClient.findUserById(order.getUserId());
        return user;
    }
}

这么看来这个远程调用组件并没有解决URL与服务的维护问题,只是将这些问题剥离出来成为一个单独组件,真正调用时无需关注具体细节,Feign与服务名+RestTemplate相比,只是封装程度不同

另外Spring Cloud还有负载均衡和网关功能,目前还不涉及企业级并发需求,所以不作详细展开,可见:
Spring Cloud负载均衡原理
Spring Cloud网关Gateway

总结

本文围绕微服务技术落地展开,从基础支撑到框架选型层层递进:首先以 SpringBoot 为基石,其通过将依赖封装为 “启动器”(Starter),打包生成的可执行 JAR 包内置内嵌服务器,无需额外部署容器,为微服务拆分与独立部署提供关键支撑。

随后介绍分布式远程调用框架 Dubbo:它以 RPC 为核心,借助 ZooKeeper 实现服务注册与发现 —— 提供者自动注册服务,消费者动态获取地址,ZooKeeper 还能监控服务健康;但 Dubbo 聚焦服务通信,需额外整合其他组件才能满足完整微服务需求。

最后聚焦主流的 Spring Cloud 生态:针对微服务配置复杂、版本冲突问题,它通过父工程dependencyManagement统一声明依赖版本,子模块继承后无需重复指定版本;在此基础上,基于 Eureka 实现服务治理:提供者自动注册,消费者按服务名拉取服务并实现负载均衡,充分体现其 “一站式微服务生态” 优势,无需额外整合即可覆盖核心需求。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐