微服务知识以及项目实战
01-Spring Boot 快速入门Spring Boot 简介Spring 作为一个软件设计层面的框架,在 Java 企业级开发中应用非常广泛,但是 Spring 框架的配置非常繁琐,且大多是重复性的工作,Spring Boot 的诞生就解决了这一问题,通过 Spring Boot 可以快速搭建一个基于 Spring 的 Java 应用程序。Spring Boot 对常用的第三方库提供了配置方
01-Spring Boot 快速入门
Spring Boot 简介
Spring 作为一个软件设计层面的框架,在 Java 企业级开发中应用非常广泛,但是 Spring 框架的配置非常繁琐,且大多是重复性的工作,Spring Boot 的诞生就解决了这一问题,通过 Spring Boot 可以快速搭建一个基于 Spring 的 Java 应用程序。Spring Boot 对常用的第三方库提供了配置方案,可以很好地与 Spring 进行整合,如 MyBatis、Spring Data JPA 等,可以一键式搭建功能完备的 Java 企业级应用程序。
Spring Boot 的优势:
- 不需要任何 XML 配置文件。
- 内嵌 Web 服务器,可直接部署。
- 默认支持 JSON 数据,不需要额外配置。
- 支持 RESTful 风格。
- 最少一个配置文件可以配置所有的个性化信息。
简而言之,Spring Boot 就是一个用很少的配置就可以快速搭建 Spring 应用的框架,并且很好的集成了常用第三方库,让开发者能够快速进行企业级项目开发。
创建 Spring Boot 工程
有 3 种常用方式可以快速创建一个 Spring Boot 工程,接下来详细给大家介绍每种方式的具体操作。
1、在线创建工程
打开浏览器访问 https://start.spring.io,可在线创建一个 Spring Boot 工程
- 选择创建工程的方式为Maven、语言为Java,
- Spring Boot 的版本选择 ,
- 输入 Group Id 和 Artifact Id
- 选择需要依赖的模块。
- 点击下方的 Generate 按钮即可下载模版的压缩文件,解压后用 IDEA 打开即可。
2、使用 IDEA Spring Initializr 创建工程 - 打开 IDEA,选择 Create New Project;
- 选择 Spring Initializr,点击 Next,可以看到实际上 IDEA 还是通过 https://start.spring.io 来创建工程的;
- 输入 GroupId、ArtifactId 等基本信息,点击 Next;
- 选择需要依赖的模块,点击 Next;
- 选择项目路径,点击 Finish 即可完成创建。
3、手动创建 Spring Boot 工程 - 打开 IDEA,选择 Create New Project;
- 选择 Maven,点击 Next;
- 输入 GroupId 和 ArtifactId,点击 Next;
- 选择项目路径,点击 Finish 即可创建一个空的 Maven 工程。
- 手动添加 Spring Boot 相关依赖,在 parent 标签中配置 spring-boot-starter-parent 的依赖,相当于给整个工程配置了一个 Spring Boot 的父依赖,其他模块直接在继承父依赖的基础上添加特定依赖即可。
比如现在要集成 Web MVC 组件,直接在 dependencies 中添加一个 spring-boot-starter-web 依赖即可,默认使用 Tomcat 作为 Web 容器,pom.xml 如下所示。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
使用 Spring Boot
通过以上任意一种方式都可快速搭建一个 Spring Boot 工程,然后就可以根据需求添加各种子模块依赖了,比如上述的第 3 种方式,我们添加了 Web MVC 组件,当前工程就成为了一个 Spring MVC 框架项目,开发者可以按照 Spring MVC 的开发步骤直接写代码了,同时 Spring Boot 还帮助我们大大简化了配置文件。
你可以拿当前的工程和之前课程中我们创建的 Spring MVC 工程对比一下,会发现不需要在 web.xml 中配置 DispatcherServlet,同时也不需要创建 springmvc.xml 了。
传统 Spring MVC 工程的 springmvc.xml 中主要添加三个配置,一是启用注解驱动,二是自动扫包,三是视图解析器。Spring Boot 自动帮我们搞定了前两个配置,第三个视图解析器需要开发者手动配置,因为视图层资源的存储路径和文件类型框架是没有办法自动获取的,不同工程的具体方式也不一样,像这种个性化的配置,Spring Boot 框架是无法自动完成的,需要开发者在 Spring Boot 特定的配置文件中自己完成。
好了,接下来我们就一起来学习用 Spring Boot 启动 Web 应用的具体操作。
1、创建 HelloHandler,具体步骤与 Spring MVC 一样
@RestController
public class HelloHandler {
@GetMapping("/index")
public String index() {
return "Hello World";
}
}
2、创建 Spring Boot 启动类 Application
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
这个类是整个 Spring Boot 应用的入口,可以看到在类定义处添加了一个 @SpringBootApplication 注解,这个注解是 Spring Boot 的核心,它开启了 Spring Boot 的自动化配置,开发者可以使用自定义配置来覆盖自动化配置,同时它完成了自动扫包,默认的范围是该类所在包的所有子包,当然也包括所在包本身,因此我们在实际开发中应该将启动类放在跟目录下。
3、启动 Spring Boot 应用,直接运行启动类的 main 方法即可,会自动将项目部署到内置 Tomcat 中,并启动 Tomcat启动成功,并且默认端口为 8080,控制台运行curl http://localhost:8080/index
,打印Hello World
。应用启动后会自动进行扫描,将需要的类交给 Spring IoC 容器来管理,被扫描的类需要在 Application 同级或子级包下。
02-Spring Boot 启动原理
Spring Boot 自动配置原理
Spring Boot 的核心功能是自动配置,意思就是 Spring Boot 框架可以自动读取各种配置文件,并将项目所需要的组件全部加载到 IoC 容器中,包括开发者自定义组件(如 Controller、Service、Repository)以及框架自带组件。
那么 Spring Boot 是如何做到自动配置的呢?要探究底层原理,我们应该从哪里入手呢?入口就是@SpringBootApplication 注解。
@SpringBootApplication 注解实际是由 3 个注解组合而来的,它们分别是:
- @SpringBootConfiguration
- @EnableAutoConfiguration
- @ComponentScan
如下所示,两种配置方式结果是一样的。
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
要搞懂 Spring Boot 自动配置原理,只需要搞清楚这 3 个注解即可,我们分别来学习。
@SpringBootConfiguration
该注解其实就是 @Configuration,@Configuration 是 Spring Boot 中使用率非常高的一个注解,作用是标注一个配置类,用 Java 类的形式来替代 XML 的配置方式
@SpringBootConfiguration 注解在这里的作用是将启动类 Application 标注成为一个配置类
- 传统的 XML 配置 bean 的方式
@Data
@AllArgsConstructor
public class Account {
private String username;
private String password;
}
<beans>
<bean id="accout" class="com.southwind.Account">
<property name="username" value="root"></property>
<property name="password" value="root"></property>
</bean>
</beans>
- 使用 @Configuration 注解的方式
@Configuration
public class MyConfiguration {
@Bean(name = "accout")
public Account getAccount(){
return new Account("root","root");
}
}
上述两种方式的结果是一样的,都是在 IoC 容器中注入了一个 Account Bean。
@ComponentScan
该注解的作用是自动扫描并加载符合条件的组件,通过设置 basePackages 参数的值来指定需要扫描的根目录,该目录下的类及其子目录下的类都会被扫描,默认值为添加了该注解的类所在的包
启动类添加了该注解,那么默认的扫描根目录就是启动类所在的包。如,启动类 Application 所在的包是 com.southwind.springboottest,config、controller、repository 3 个包都是 com.southwind.springboottest 的子包,所以这些类的实例都会被注入到 IoC 容器中。
现在给 @ComponentScan 设置 basePackages 参数,修改代码如下所示。
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(basePackages = {"com.southwind.springboottest.controller","com.southwind.springboottest.repository"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
此时的配置意味着只扫描 com.southwind.springboottest.controller 和 com.southwind.springboottest.repository 这两个包,那么只有这两个包中类的实例会被注入到 IoC 容器中。
@ComponentScan常用参数如下所示:
- basePackages:指定需要扫描的根包目录
- basePackageClasses:指定需要扫描的根类目录
- lazyInit:是否开启惰性加载,默认关闭
- useDefaultFilters:是否启用自动扫描组件,默认 true
- excludeFilters:指定不需要扫描的组件类型
- includeFilters:指定需要扫描的组件类型
@EnableAutoConfiguration
该注解是自动配置的核心,非常重要,结合 @Import 注解,将依赖包中相关的 bean 进行注册
@EnableAutoConfiguration 是由两个注解组合而成,分别是
- @AutoConfigurationPackage
- @Import({AutoConfigurationImportSelector.class})
1、@AutoConfigurationPackage 注解的作用是自动配置框架自带组件,该注解其实也是使用了 @Import({Registrar.class})
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
}
@Import 注解的作用是根据其参数类所返回的信息,将对应的 bean 进行注册,比如这里的参数类是 Registrar.class,Registrar.class 返回信息如下所示:
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
Registrar() {
}
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, new String[]{(new AutoConfigurationPackages.PackageImport(metadata)).getPackageName()});
}
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new AutoConfigurationPackages.PackageImport(metadata));
}
}
类中方法的作用是将启动类 Application 所在的包下的所有组件注册到 IoC 容器中,即 SpringBoot 默认会扫描启动类所在的包下的所有组件。
2、@Import({AutoConfigurationImportSelector.class}) 完成注册开发者自定义组件,即AutoConfigurationImportSelector.class 返回信息就是框架默认加载的组件,打开AutoConfigurationImportSelector.class 源码如下所示:
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
}
核心代码的是调用 SpringFactoriesLoader.loadFactoryNames 方法从依赖的 jar 包中读取 META-INF/spring.factories 文件中的信息,如下所示。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration
该文件中存在一组 key = org.springframework.boot.autoconfigure.EnableAutoConfiguration 的配置信息,就是要自动配置的 bean,并且对应的都是 @Configuration 类,Spring Boot 通过加载这些配置类,将需要的组件加载到 IoC 容器中。
03-微服务概述
为什么要使用微服务?
传统的 Java Web 都是采用单体架构的方式来进行开发、部署、运维的,所谓单体架构就是将 Application 的所有业务模块全部打包在一个文件中进行部署。但是随着互联网的发展、用户数量的激增、业务的极速扩展,传统的单体应用方式的缺点就逐渐显现出来了,给开发、部署、运维都带来了极大的难度,工作量会越来越大,难度越来越高。
因为在单体架构中,所有的业务模块的耦合性太高,耦合性过高的同时项目体量又很大势必会给各个技术环节带来挑战。项目越进行到后期,这种难度越大,只要有改动,整个应用都需要重新测试,部署,极大的限制了开发的灵活性,降低了开发效率。同时也带来了更大的安全隐患,如果某个模块发生故障无法正常运行就有可能导致整个项目崩溃,单体应用的架构如下图所示。
单体应用存在的问题
- 随着业务的发展,开发变得越来越复杂。
- 修改、新增某个功能,需要对整个系统进行测试,重新部署。
- 一个模块出现问题,很可能导致整个系统崩溃。
- 多团队同时对数据进行管理,容易产生安全漏洞。
- 各模块使用同一种技术框架,很难根据具体业务需求选择更合适的框架,局限性太大。
- 模块内容太复杂,如果员工离职,可能需要很长时间才能完成任务交接。
为了解决上述问题,微服务架构应运而生,简单来说,微服务就是将一个单体应用拆分成若干个小型的服务,协同完成系统功能的一种架构模式,在系统架构层面进行解耦合,将一个复杂问题拆分成若干个简单问题。这样的好处是对于每一个简单问题,开发、维护、部署的难度就降低了很多,可以实现自治,可以自主选择最合适的技术框架,提高了项目开发的灵活性。
微服务架构不仅是单纯的拆分,拆分之后的各个微服务之间还要进行通信,否则就无法协同完成需求,也就失去了拆分的意义。不同的微服务之间可以通过某种协议进行通信,相互调用、协同完成功能,并且各服务之间只需要制定统一的协议即可,至于每个微服务是用什么技术框架来实现的,统统不需要关心。这种松耦合的方式使得开发、部署都变得更加灵活,同时系统更容易扩展,降低了开发、运维的难度,单体应用拆分之后的架构如下图所示。
微服务的优点
- 各个服务之间实现了松耦合,彼此之间不需要关注对方是用什么语言,什么技术开发的,只需要保证自己的接口可以正常访问,通过标准协议可以访问其他微服务的接口即可。
- 各个微服务之间是独立自治的,只需要专注于做好自己的业务,开发和维护不会影响到其他的微服务,这和单体架构中"牵一发而动全身"相比是有很大优势的。
- 微服务是一个去中心化的架构方式,相当于用零件去拼接一台机器,如果某个零件出现问题,可以随时进行替换从而保证机器的正常运转,微服务就相当于零件,整个项目就相当于零件组成的机器。
任何一种架构方式都有其优势,同时也一定会存在不足,我们需要做的是根据具体的业务场景,选择最合适的架构来进行开发。
微服务的不足
- 各个服务之间是通过远程调用的方式来完成协作任务的,如果因为某些原因导致远程调用出现问题,导致微服务不可用,就有可能产生级联反应,造成整个系统崩溃。
- 如果某个需求需要调用多个微服务,如何来保证数据的一致性是一个比较大的问题,这就给给分布式事务处理带来了挑战。
- 相比较于单体应用开发,微服务的学习难度会增加,对于加入团队的新员工来讲,如何快速掌握上手微服务架构,是他需要面对的问题。
即便是微服务存在这样那样的问题,也不能掩盖这种技术选型在当今互联网环境下的巨大优势,了解完微服务的基本概念、优缺点之后,我们来谈一谈微服务的设计原则。
我们说过微服务就是对应用进行拆分,在软件设计层面进行解耦合,说起来容易做起来难,如何来拆分服务也是一个有难度的挑战,如果拆分的服务粒度太小,导致微服务可完成的功能有限,这种情况反而会增加系统的复杂度。相反如果服务粒度太大,又没有实现足够程度的解耦合,那它本质上就还是单体应用的架构,同样是失去了拆分服务的意义。比较合理的拆分方式是由大到小,提炼出核心需求,搞清楚服务间的交互关系,先拆分成粒度相对较大的服务,然后再根据具体的业务需求逐步细化服务粒度,最终形成一套合理的微服务体系。
微服务设计原则
- 服务粒度不能过大也不能过小,提炼核心需求,根据服务间的交互关系找到最合理的服务粒度。
- 各个微服务的功能职责尽量单一,避免出现多个服务处理同一个需求。
- 各个微服务之间要相互独立、自治,自主开发、自主部署、自主维护。
- 保证数据独立性,各个微服务独立管理其业务模块下的数据,可开放出接口供其他微服务访问数据,但是其他微服务不能直接管理这些数据。
- 使用 REST 协议完成微服务之间的协作任务,数据交互采用 JSON 格式,方便调用与整合。
综上所述,微服务的架构设计不是一次成型的,需要反复进行实践和总结,不断进行优化,最终形成一套更为合理的解决方案,微服务架构图如下所示。
微服务架构的核心组件
- 服务治理(服务注册、服务发现)
拆分之后的微服务首先需要完成的工作就是实现服务治理,包括服务注册和服务发现。这里我们把微服务分为两类:提供服务的叫做服务提供者,调用服务的叫做服务消费者。一个服务消费者首先需要知道有哪些可供调用的微服务,以及如何来调用这些微服务。所以就需要将所有的服务提供者在注册中心完成注册,记录服务信息,如 IP 地址、端口等,然后服务消费者可以通过服务发现获取服务提供者的这些信息,从而实现调用。 - 服务负载均衡
微服务间的负载均衡是必须要考虑的,服务消费者在调用服务提供者的接口时,可根据配置选择某种负载均衡算法,从服务提供者列表中选择具体要调用的实例,从而实现服务消费者与服务提供者之间的负载均衡。 - 微服务容错
前面我们提到过,各个服务之间是通过远程调用的方式来完成协作任务的,如果因为某些原因使得远程调用出现问题,导致微服务不可用,就有可能产生级联反应,造成整个系统崩溃。这个问题我们可以使用微服务的熔断器来处理,熔断器可以防止整个系统因某个微服务调用失败而产生级联反应导致系统崩溃的情况。 - 分布式配置
每个微服务都有其对应的配置文件,在一个大型项目中管理这些配置文件也是工作量很大的一件事情,为了提高效率、便于管理,我们可以对各个微服务的配置文件进行集中统一管理,将配置文件集中保存到本地系统或者 Git 仓库,再由各个微服务读取自己的配置文件。 - 服务监控
一个分布式系统中往往会部署很多个微服务,这些服务彼此之间会相互调用,整个过程就会较为复杂,我们在进行问题排查或者优化的时候工作量就会比较大。如果我们能准确跟踪到每一个网络请求,了解它整个运行流程,经过了哪些微服务、是否有延迟、耗费时间等,这样的话我们分析系统性能,排查解决问题就会容易很多,这就是服务监控。
Spring Cloud
微服务是一种分布式软件架构设计方式,具体的落地框架有很多,如阿里的 Dubbo、Google 的 gRPC、新浪的 Motan、Apache 的 Thrift 等,都是基于微服务实现分布式架构的技术框架,本课程我们选择的框架的是 Spring Cloud,Spring Cloud 基于 Spring Boot 使得整体的开发、配置、部署都非常方便,可快速搭建基于微服务的分布式应用,Spring Cloud 相当于微服务各组件的集大成者,下图来自 Spring 官网。
Spring Boot 和 Spring Cloud 的关系可大致理解为,Spring Boot 快速搭建基础系统,Spring Cloud 在此基础上实现分布式系统中的公共组件,如服务注册、服务发现、配置管理、熔断器、控制总线等,服务调用方式是基于 REST API,整合了各种成熟的产品和架构。
对于 Java 开发者而言,Spring 框架已成为事实上的行业标准,Spring 全家桶系列产品的优势在于功能齐全、简单好用、性能优越、文档规范,实际开发中就各方面综合因素来看,Spring Cloud 是微服务架构中非常优秀的实现方案,本课程就将为各位读者详细解读如何快速上手 Spring Cloud,Spring Cloud 的核心组件如下图所示,我们会按照图中分布来逐一讲解各个组件的使用,最后会结合一个实战案例将各个组件进行串联,让读者可以将 Spring Cloud 运用在实际开发中。
04-注册中心
服务治理的核心组成有三部分:服务提供者,服务消费者,注册中心。
在分布式系统架构中,每个微服务(服务提供者、服务消费者)在启动时,将自己的信息存储在注册中心,我们把这个过程称之为服务注册。服务消费者要调用服务提供者的接口,就得找到服务提供者,从注册中心查询服务提供者的网络信息,并通过此信息来调用服务提供者的接口,这个过程就是服务发现。
既然叫服务治理就不仅仅是服务注册与服务发现,同时还包括了服务的管理,即注册中心需要对记录在案的微服务进行统一管理,如何来具体实现管理呢?各个微服务与注册中心通过心跳机制完成通信,每间隔一定时间微服务就会向注册中心汇报一次,如果注册中心长时间无法与某个微服务通信,就会自动销毁该微服务。当某个微服务的网络信息发生变化时,会重新注册。同时,微服务可以通过客户端缓存将需要调用的服务地址保存起来,并通过定时任务更新的方式来保证服务的时效性,这样可以降低注册中心的压力,如果注册中心出现问题,也不会影响微服务之间的调用。
服务提供者、服务消费者、注册中心的关联
- 首先启动注册中心。
- 服务提供者启动时,在注册中心注册可以提供的服务。
- 服务消费者启动时,在注册中心订阅需要调用的服务。
- 注册中心将服务提供者的信息推送给服务调用者。
- 服务调用者通过相关信息(IP、端口等)调用服务提供者的服务。
注册中心核心模块
- 服务注册表:用来存储各个微服务的信息,注册中心提供 API 来查询和管理各个微服务。
- 服务注册:微服务在启动时,将自己的信息在保存在注册中心。
- 服务发现:查询需要调用的微服务的网络信息,如 IP 地址、端口。
- 服务检查:通过心跳机制与完成注册的各个微服务完成通信。如果微服务长时间无法访问则销毁该服务,网络信息发生变化则重新注册。
Spring Cloud 的服务治理可以使用 Consul 和 Eureka 组件,这里我们选择 Eureka。
什么是 Eureka?
Eureka 是 Netflix 开源的基于 REST 的服务治理解决方案,Spring Cloud 集成了 Eureka,即 Spring Cloud Eureka,提供服务注册和服务发现功能,可以和基于 Spring Boot 搭建的微服务应用轻松完成整合,开箱即用,实现 Spring Cloud 的服务治理功能。Spring Cloud 对 Netflix 开源组件进行了二次封装,也就是 Spring Cloud Netflix,Spring Cloud Eureka 是 Spring Cloud Netflix 微服务套件中的一部分,基于 Netflix Eureka 实现了二次封装,实际开发中,我们就使用 Spring Cloud Eureka 来完成服务治理。
Spring Cloud Eureka 的组成
Spring Cloud Eureka 主要包含了服务端和客户端组件:Eureka Server 服务端、Eureka Client 客户端。
- Eureka Server,是提供服务注册与发现功能的服务端,也称作服务注册中心,Eureka 支持高可用的配置。
- Eureka Client 是客户端组件,它的功能是将微服务在 Eureka Server 完成注册和后期维护功能,包括续租、注销等。需要注册的微服务就是通过 Eureka Client 连接到 Eureka Server 完成注册的,通过心跳机制实现注册中心与微服务的通信,完成对各个服务的状态监控。
简单理解注册中心(Eureka Server)就相当于一个电商平台,服务提供者(Eureka Client)相当于卖家在平台上注册了一个店铺,提供出售商品的服务,服务消费者(另一个Eureka Client)相当于用户在平台上注册账号,然后就可以在平台的各个店铺中购买商品了,同时平台(Eureka Server)还提供管理买家与卖家信息的功能,比如卖家是否在线、可以提供哪些具体服务等等。
代码实现注册中心Eureka Server
1、首先创建一个 Maven 父工程myspringclouddemo
2、在 pom.xml 中添加相关依赖,Spring Cloud Finchley 使用的是 Spring Boot 2.0.x,不能使用 Spring Boot 1.5.x。
<!-- 引入 Spring Boot 的依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.7.RELEASE</version>
</parent>
<!-- 引入 Spring Cloud 的依赖,管理 Spring Cloud 各组件 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 解决 JDK9 以上版本没有 JAXB API jar 的问题,JDK9 以下版本不需要配置 -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
3、在父工程下创建一个名为 registrycenter 的 Module,实现 Eureka Server。
4、在 pom.xml 中添加 Eureka Server 依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
5、在 resources 路径下创建配置文件 application.yml,添加 Eureka Server 相关配置。
server:
port: 8761 # 当前 Eureka Server 服务端口
eureka:
client:
register-with-eureka: false # 是否将当前 Eureka Server 服务作为客户端进行注册
fetch-registry: false # 是否获取其他 Eureka Server 服务的数据
service-url:
defaultZone: http://localhost:8761/eureka/ # 注册中心的访问地址
6、在启动类 RegistryCenterApplication添加@EnableEurekaServer
注解。
声明该类是一个 Eureka Server 微服务,提供发现服务的功能,即注册中心。
7、启动运行RegistryCenterApplication,打开浏览器访问http://localhost:8761,注册中心启动成功。
8、「No instances avaliable」 表示当前没有发现微服务实例,即没有微服务完成注册。如果我们将 application.yml 中的 register-with-eureka
属性值改为 true,则表示 Eureka Server 将自己作为客户端进行注册。
9、重启 RegistryCenterApplication,打开浏览器访问http://localhost:8761,当前注册中心有一个客户端服务注册在案,即 Eureka Server 本身。
05-服务提供者
服务提供者和服务消费者是从业务角度来划分的,实际上服务提供者和服务消费者都是通过 Eureka Client 连接到 Eureka Server 完成注册,本节课我们就来一起实现一个服务提供者,并且在 Eureka Server 完成注册,大致思路是先通过 Spring Boot 搭建一个微服务应用,再通过 Eureka Client 将其注册到 Eureka Server,创建 Eureka Client 的过程与创建 Eureka Server 十分相似,如下所示。
代码实现服务提供者Eureka Client
1、在父工程下创建名为 provider 的 Module,实现 Eureka Client。
2、在 pom.xml 中添加 Eureka Client 依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
3、在 resources 路径下创建配置文件 application.yml,添加 Eureka Client 相关配置,此时的 Eureka Client 是服务提供者 provider
server:
port: 8010 # 当前 Eureka Client 服务端口
spring:
application:
name: provider # 当前服务注册在 Eureka Server 上的名称
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/ # 注册中心的访问地址
instance:
prefer-ip-address: true # 是否将当前服务的 IP 注册到 Eureka Server
4、依次启动注册中心、ProviderApplication,打开浏览器,访问http://localhost:8761 可以看到服务提供者 provider 已经在 Eureka Server 完成注册,接下来就可以访问 provider 提供的相关服务了
5、在 provider 服务中提供对 Student 的 CRUD 操作:
- 创建 Student 类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private long id;
private String name;
private char gender;
}
- 创建管理 Student 对象的接口 StudentRepositoy 及其实现类 StudentRepositoryImpl
public interface StudentRepository {
public Collection<Student> findAll();
public Student findById(long id);
public void saveOrUpdate(Student student);
public void deleteById(long id);
}
- 创建 StudentHandler
@RequestMapping("/student")
@RestController
public class StudentHandler {
@Autowired
private StudentRepository studentRepository;
@GetMapping("/findAll")
public Collection<Student> findAll() { }
@GetMapping("/findById/{id}")
public Student findById(@PathVariable("id") long id) { }
@PostMapping("/save")
public void save(@RequestBody Student student) { }
@PutMapping("/update")
public void update(@RequestBody Student student) { }
@DeleteMapping("/deleteById/{id}")
public void deleteById(@PathVariable("id") long id) { }
}
6、重启 ProviderApplication,通过 Postman 工具测试该服务的相关接口
curl -X GET http://localhost:8010/student/findAll
curl -X GET http://localhost:8010/student/findById/1
curl -X POST http://localhost:8010/student/save -d '{"id":4,"name":"tuyrk","gender":"男"}' -H "Content-Type: application/json"
curl -X PUT http://localhost:8010/student/update -d '{"id":4,"name":"tyk","gender":"男"}' -H "Content-Type: application/json"
curl -X DELETE http://localhost:8010/student/deleteById/1
06-跨服务接口调用神器 RestTemplate
在实现服务消费者之前,我们先来学习 RestTemplate 的使用,通过 RestTemplate 可以实现不同微服务之间的调用。
什么是 REST
REST 是当前比较流行的一种互联网软件架构模型,通过统一的规范完成不同终端的数据访问和交互,REST 是一个词组的缩写,全称为 Representational State Transfer,翻译成中文的意思是资源表现层状态转化。
特点
1、URL 传参更加简洁,如下所示
- 非 RESTful 的 URL:
http://…../queryUserById?id=1
- RESTful 的 URL:
http://…./queryUserById/1
2、完成不同终端之间的资源共享,RESTful 提供了一套规范,不同终端之间只需要遵守该规范,就可以实现数据交互。
RESTful 具体来讲就是四种表现形式,HTTP 协议中四种请求类型(GET、POST、PUT、DELETE)分别表示四种常规操作,即 CRUD:GET 获取资源、POST 创建资源、PUT 修改资源、DELETE 删除资源
什么是 RestTemplate
RestTemplate 是 Spring 框架提供的基于 REST 的服务组件,底层对 HTTP 请求及响应进行了封装,提供了很多访问远程 REST 服务的方法,可简化代码开发。
如何使用 RestTemplate
使用 RestTemplate 来访问搭建好的 REST 服务
1、实例化 RestTemplate 对象并通过 @Bean 注入 IoC
@Bean
public RestTemplate createRestTemplate(){
return new RestTemplate();
}
2、创建 RestHandler 类,通过 @Autowired 将 IoC 容器中的 RestTemplate 实例对象注入 RestHandler,在业务方法中就可以通过 RestTemplate 来访问 REST 服务了。
@RestController
@RequestMapping("/rest")
public class RestHandler {
@Autowired
private RestTemplate restTemplate;
}
3、调用 RestTemplate 的相关方法,分别通过 GET、POST、PUT、DELETE 请求,访问服务资源。
url 为请求的目标资源
request 为要保存/修改的目标对象
responseType 为响应数据的封装模版
uriVariables 是一个动态参数,可以根据实际请求传入参数
GET
- getForEntity() 方法的返回值类型为 ResponseEntity,通过调用其 getBody 方法可获取结果对象
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
RequestCallback requestCallback = this.acceptHeaderRequestCallback(responseType);
ResponseExtractor<ResponseEntity<T>> responseExtractor = this.responseEntityExtractor(responseType);
return (ResponseEntity)nonNull(this.execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables));
}
@GetMapping("/findAll")
public Collection<Student> findAll() {
return restTemplate.getForEntity("http://localhost:8010/student/findAll", Collection.class).getBody();
}
上述代码表示访问 http://localhost:8080/user/findAll,并将结果封装为一个 Collection 对象。
如果需要传参,直接将参数追加到 getForEntity 方法的参数列表中即可,如下所示。
@GetMapping("/findById/{id}")
public Student findById(@PathVariable("id") Long id) {
return restTemplate.getForEntity("http://localhost:8010/student/findById/{id}", Student.class, id).getBody();
}
- getForObject 方法的使用与 getForEntity 很类似,唯一的区别在于 getForObject 的返回值就是目标对象,无需通过调用 getBody 方法来获取
@Nullable
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
RequestCallback requestCallback = this.acceptHeaderRequestCallback(responseType);
HttpMessageConverterExtractor<T> responseExtractor = new HttpMessageConverterExtractor(responseType, this.getMessageConverters(), this.logger);
return this.execute(url, HttpMethod.GET, requestCallback, responseExtractor, (Object[])uriVariables);
}
@GetMapping("/findAll2")
public Collection<Student> findAll2() {
return restTemplate.getForObject("http://localhost:8010/student/findAll", Collection.class);
}
如果需要传参,直接将参数追加到 getForObject 方法的参数列表中即可,如下所示。
@GetMapping("/findById2/{id}")
public Student findById2(@PathVariable("id") Long id) {
return restTemplate.getForObject("http://localhost:8010/student/findById/{id}", Student.class, id);
}
POST
- postForEntity 方法的返回值类型也是 ResponseEntity,通过调用其 getBody 方法可获取结果对象
public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables) throws RestClientException {
RequestCallback requestCallback = this.httpEntityCallback(request, responseType);
ResponseExtractor<ResponseEntity<T>> responseExtractor = this.responseEntityExtractor(responseType);
return (ResponseEntity)nonNull(this.execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables));
}
@PostMapping("/save")
public Collection<Student> save(@RequestBody Student student) {
return restTemplate.postForEntity("http://localhost:8010/student/save", student, Collection.class).getBody();
}
- postForObject 方法的使用与 postForEntity 类似,唯一的区别在于 postForObject 的返回值就是目标对象,无需通过调用 getBody 方法来获取
@Nullable
public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables) throws RestClientException {
RequestCallback requestCallback = this.httpEntityCallback(request, responseType);
HttpMessageConverterExtractor<T> responseExtractor = new HttpMessageConverterExtractor(responseType, this.getMessageConverters(), this.logger);
return this.execute(url, HttpMethod.POST, requestCallback, responseExtractor, (Object[])uriVariables);
}
@PostMapping("/save2")
public Collection<Student> save2(@RequestBody Student student) {
return restTemplate.postForObject("http://localhost:8010/student/save", student, Collection.class);
}
PUT
public void put(String url, @Nullable Object request, Object... uriVariables) throws RestClientException {
RequestCallback requestCallback = this.httpEntityCallback(request);
this.execute(url, HttpMethod.PUT, requestCallback, (ResponseExtractor)null, (Object[])uriVariables);
}
@PutMapping("/update")
public void update(@RequestBody Student student) {
restTemplate.put("http://localhost:8010/student/update", student);
}
DELETE
public void delete(String url, Object... uriVariables) throws RestClientException {
this.execute(url, HttpMethod.DELETE, (RequestCallback)null, (ResponseExtractor)null, (Object[])uriVariables);
}
@DeleteMapping("/deleteById/{id}")
public void delete(@PathVariable("id") Long id) {
restTemplate.delete("http://localhost:8010/student/deleteById/{id}", id);
}
4、重启 ProviderApplication,通过 Postman 工具测试该服务的相关接口
curl -X GET http://localhost:8010/rest/findAll
curl -X GET http://localhost:8010/rest/findById/1
curl -X POST http://localhost:8010/rest/save -d '{"id":4,"name":"tuyrk","gender":"男"}' -H "Content-Type: application/json"
curl -X PUT http://localhost:8010/rest/update -d '{"id":4,"name":"tyk","gender":"男"}' -H "Content-Type: application/json"
curl -X DELETE http://localhost:8010/rest/deleteById/1
07-服务消费者
在前面的课程中,我们通过 Eureka Client 组件创建了一个服务提供者 provider,并且在注册中心完成注册,接下来其他微服务就可以访问 provider 相关服务了。
本节课我们就来实现一个服务消费者 consumer,调用 provider 相关接口,实现思路是先通过 Spring Boot 搭建一个微服务应用,再通过 Eureka Client 将其注册到 Eureka Server。此时的 provider 和 consumer 从代码的角度看并没有区别,都是 Eureka 客户端,我们人为地从业务角度对它们进行区分,provider 提供服务,consumer 调用服务,具体的实现需要结合 RestTemplate 来完成,即在服务消费者 consumer 中通过 RestTemplate 来调用服务提供者 provider 的相关接口。
代码实现服务消费者Eureka Client
1、在父工程下创建名为 consumer 的 Module,实现 Eureka Client。
2、在 pom.xml 中添加 Eureka Client 依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
3、在 resources 路径下创建配置文件 application.yml,添加 Eureka Client 相关配置,此时的 Eureka Client 为服务消费者 consumer
server:
port: 8020 # 当前 Eureka Client 服务端口
spring:
application:
name: consumer # 当前服务注册在 Eureka Server 上的名称
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/ # 注册中心的访问地址
instance:
prefer-ip-address: true # 是否将当前服务的 IP 注册到 Eureka Server
4、现在让 consumer 调用 provider 提供的服务,首先创建实体类 Student。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private long id;
private String name;
private char gender;
}
5、修改 ConsumerApplication 代码,创建 RestTemplate 实例并通过 @Bean 注解注入到 IoC 容器中
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
6、创建 StudentHandler,注入 RestTemplate 实例,业务方法中通过 RestTemplate 来调用 provider 的相关服务。
@RequestMapping("/consumer")
@RestController
public class StudentHandler {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/findAll")
public Collection<Student> findAll(){
return restTemplate.getForObject("http://localhost:8010/student/findAll",Collection.class);
}
@GetMapping("/findById/{id}")
public Student findById(@PathVariable("id") long id){
return restTemplate.getForObject("http://localhost:8010/student/findById/{id}",Student.class,id);
}
@PostMapping("/save")
public void save(@RequestBody Student student){
restTemplate.postForObject("http://localhost:8010/student/save",student,Student.class);
}
@PutMapping("/update")
public void update(@RequestBody Student student){
restTemplate.put("http://localhost:8010/student/update",student);
}
@DeleteMapping("/deleteById/{id}")
public void deleteById(@PathVariable("id") long id){
restTemplate.delete("http://localhost:8010/student/deleteById/{id}",id);
}
}
7、依次启动注册中心、服务提供者 provider,并运行 ConsumerApplication,打开浏览器访问http://localhost:8761可以看到服务提供者 provider 和服务消费者 consumer 已经在 Eureka Server 完成注册
8、访问 consumer 的相关服务了,通过 Postman 工具测试 consumer 接口
curl -X GET http://localhost:8020/consumer/findAll
curl -X GET http://localhost:8020/consumer/findById/1
curl -X POST http://localhost:8020/consumer/save -d '{"id":4,"name":"tuyrk","gender":"男"}' -H "Content-Type: application/json"
curl -X PUT http://localhost:8020/consumer/update -d '{"id":4,"name":"tyk","gender":"男"}' -H "Content-Type: application/json"
curl -X DELETE http://localhost:8020/consumer/deleteById/1
08-用服务网关统一 URL,开发更简洁
微服务网关使用场景
在分布式项目架构中,我们会将服务进行拆分,不同的微服务负责各自的业务功能,实现软件架构层面的解耦合。但是如果拆分之后的微服务数量太多,是不利于系统开发的,因为每个服务都有不同的网络地址,客户端多次请求不同的微服务需要调用不同的 URL,如果同时去维护多个不同的 URL 无疑会增加开发的成本。
如下图所示,一个外卖订餐系统,需要调用多个微服务接口才能完成一次订餐的业务流程,如果能有一种解决方案可以统一管理不同的微服务 URL,肯定会增强系统的维护性,提高开发效率。
这个解决方案就是 API 网关,API 网关对所有的 API 请求进行管理维护,相当于为系统开放出一个统一的接口,所有的外部请求只需要访问这个统一入口即可,系统内部再通过 API 网关去映射不同的微服务。对于开发者而言就不需要关注具体的微服务 URL 了,直接访问 API 网关接口即可,API 网关的结构如下图所示。
使用 Zuul 实现微服务网关
Spring Cloud 集成了 Zuul
1、在父工程下创建名为 gateway 的 Module
2、在 pom.xml 中添加 Zuul 和 Eureka Client 依赖,Zuul 也作为一个 Eureka Client 在注册中心完成注册。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
3、在 resources 路径下创建配置文件 application.yml,添加网关相关配置
server:
port: 8030 # 当前 gateway 服务端口
spring:
application:
name: gateway # 当前服务注册在 Eureka Server 上的名称
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/ # 注册中心的访问地址
zuul:
routes: # 自定义微服务的访问路径
provider: /p/** # provider 微服务就会被映射到 gateway 的 /p/** 路径
4、在启动类 GateWayApplication 添加@EnableZuulProxy
、@EnableAutoConfiguration
注解
- @EnableZuulProxy 包含 @EnableZuulServer 的功能,而且还加入了 @EnableCircuitBreaker 和 @EnableDiscoveryClient
- @EnableAutoConfiguration 将所有符合条件的 @Configuration 配置都加载到当前 Spring Boot 创建并使用的 IoC 容器。
5、依次启动注册中心、服务提供者 provider,运行 GateWayApplication。打开浏览器,访问 http://localhost:8761可以看到服务提供者 provider 和网关 gateway 已经在 Eureka Server 完成注册
6、通过 http://localhost:8030/p/student/findAll 访问 provider 提供的相关服务
curl -X GET http://localhost:8030/p/student/findAll
7、同时 Zuul 自带了负载均衡功能。在服务提供者 provider 添加返回当前服务端口的方法:
@Value("${server.port}")
private String port;
@GetMapping("/index")
public String index() {
return "当前端口:" + this.port;
}
8、复制idea启动实例,设置VM options参数-Dserver.port=8011
,再启动实例。最后重新启动 gateway,访问 http://localhost:8761
可以看到当前注册中心有两个 provider 服务
9、通过 gateway 来访问请求服务方法,端口为 8010 和 8011 的微服务交替被访问,实现了负载均衡
curl -X GET http://localhost:8030/p/student/index
09-Ribbon 负载均衡
在前面的课程中我们已经通过 RestTemplate 实现了服务消费者对服务提供者的调用,这只是实现了最基本的需求,如果在某个具体的业务场景下,对于某服务的调用需求激增,这时候我们就需要为该服务实现负载均衡以满足高并发访问,在一个大型的分布式应用系统中,负载均衡(Load Balancing)是必备的。
什么是 Ribbon?
Spring Cloud 提供了实现负载均衡的解决方案:Spring Cloud Ribbon,Ribbon 是 Netflix 发布的负载均衡器,而 Spring Cloud Ribbon 则是基于 Netflix Ribbon 实现的,是一个用于对 HTTP 请求进行控制的负载均衡客户端。
Spring Cloud Ribbon 官网地址:http://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-ribbon.html
Ribbon 的使用同样需要结合 Eureka Server,即需要将 Ribbon 在 Eureka Server 进行注册,注册完成之后,就可以通过 Ribbon 结合某种负载均衡算法,如随机、轮询、加权随机、加权轮询等帮助服务消费者去调用接口。除了 Ribbon 默认提供的这些负载均衡算法外,开发者也可以根据具体需求来设计自定义的 Ribbon 负载均衡算法。实际开发中,Spring Cloud Ribbon 需要结合 Spring Cloud Eureka 来使用,Eureka Server 提供所有可调用的服务提供者列表,Ribbon 基于特定的负载均衡算法从这些服务提供者中挑选出要调用的实例,如下图所示。
Ribbon 常用的负载均衡策略有以下几种:
- 随机:访问服务时,随机从注册中心的服务列表中选择一个。
- 轮询:当同时启动两个服务提供者时,客户端请求会由这两个服务提供者交替处理。
- 加权轮询:对服务列表中的所有微服务响应时间做加权处理,并以轮询的方式来访问这些服务。
- 最大可用:从服务列表中选择并发访问量最小的微服务。
使用 Ribbon 实现负载均衡
1、在父工程下创建名为 ribbon 的 Module
2、在 pom.xml 中添加 Eureka Client 依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency>
3、在配置文件 application.yml 添加 Ribbon 相关配置
server:
port: 8040 was # 当前 Ribbon 服务端口
spring:
application:
name: ribbon # 当前服务注册在 Eureka Server 上的名称
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/ # 注册中心的访问地址
instance:
prefer-ip-address: true # 是否将当前服务的 IP 注册到 Eureka Server
4、通过 @Bean 注解注入 RestTemplate 实例,@LoadBalanced 注解提供负载均衡。
@Bean
@LoadBalanced // 声明一个基于 Ribbon 的负载均衡
public RestTemplate restTemplate(){
return new RestTemplate();
}
5、使用 Ribbon 调用 Provider 服务
- 创建接口对应的实体类 Student
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private long id;
private String name;
private char gender;
}
- 创建 RibbonHandler,通过 RestTemplate 调用 Provider 相关接口
RestTemplate 访问的 URL 不需要指定 IP 和 端口,直接访问 Provider 在 Eureka Server 注册的 application name 即可。
比如:http://localhost:8010/ 可替换为 http://provider/
@RestController
@RequestMapping("/ribbon")
public class RibbonHandler {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/findAll")
public Collection<Student> findAll() {
return restTemplate.getForObject("http://provider/student/findAll", Collection.class);
}
}
6、依次启动注册中心、Provider、Ribbon。打开浏览器,访问 http://localhost:8761
可以看到 Provider 和 Ribbon 已经在注册中心完成注册,接下来用 Postman 工具测试 Ribbon 相关接口
curl -X GET http://localhost:8040/ribbon/index
curl -X GET http://localhost:8040/ribbon/findAll
curl -X GET http://localhost:8040/ribbon/findById/1
curl -X POST http://localhost:8040/ribbon/save -d '{"id":4,"name":"tuyrk","gender":"男"}' -H "Content-Type: application/json"
curl -X PUT http://localhost:8040/ribbon/update -d '{"id":4,"name":"tyk","gender":"男"}' -H "Content-Type: application/json"
curl -X DELETE http://localhost:8040/ribbon/deleteById/1
7、测试 Ribbon 的负载均衡,在 RibbonHandler 中添加如下代码。
@GetMapping("/index")
public String index(){
return restTemplate.getForObject("http://provider/student/index",String.class);
}
8、分别启动注册中心、端口为 8010 的 Provider、端口为 8011 的 Provider、Ribbon。打开浏览器,访问 http://localhost:8761
可以看到两个 Provider 和 Ribbon 已经在注册中心完成注册
9、访问请求服务方法,端口为 8010 和 8011 的微服务交替被访问,实现了负载均衡
curl -X GET http://localhost:8040/ribbon/index
10-Spring Cloud Feign 声明式接口调用
什么是 Feign
与 Ribbon 一样,Feign 也是由 Netflix 提供的,Feign 是一个提供模版的声明式 Web Service 客户端, 使用 Feign 可以简化 Web Service 客户端的编写,开发者可以通过简单的接口和注解来调用 HTTP API,使得开发更加快捷、方便。Spring Cloud 也提供了 Feign 的集成组件:Spring Cloud Feign,它整合了 Ribbon 和 Hystrix,具有可插拔、基于注解、负载均衡、服务熔断等一系列便捷功能。
在 Spring Cloud 中使用 Feign 非常简单,我们说过 Feign 是一个声明式的 Web Service 客户端,所以只需要创建一个接口,同时在接口上添加相关注解即可完成服务提供方的接口绑定,相比较于 Ribbon + RestTemplate 的方式,Feign 大大简化了代码的开发,Feign 支持多种注解,包括 Feign 注解、JAX-RS 注解、Spring MVC 注解等。Spring Cloud 对 Feign 进行了优化,整合了 Ribbon 和 Eureka,从而让 Feign 的使用更加方便。
我们说过 Feign 是一种比 Ribbon 更加方便好用的 Web 服务客户端,那么二者有什么具体区别呢?Feign 好用在哪里?
Ribbon 与 Feign 的区别
关于 Ribbon 和 Feign 的区别可以简单地理解为 Ribbon 是个通用的 HTTP 客户端工具,而 Feign 则是基于 Ribbon 来实现的,同时它更加灵活,使用起来也更加简单。上节课中我们通过 Ribbon + RestTemplate 实现了服务调用的负载均衡,相比较于这种方式,使用 Feign 可以直接通过声明式接口的形式来调用服务,非常方便,比 Ribbon 使用起来要更加简便,只需要创建接口并添加相关注解配置,即可实现服务消费的负载均衡。
Feign 的特点
- Feign 是一个声明式 Web Service 客户端。
- 支持 Feign 注解、JAX-RS 注解、Spring MVC 注解。
- Feign 基于 Ribbon 实现,使用起来更加简单。
- Feign 集成了 Hystrix,具备服务熔断功能。
使用 Feign 实现服务调用
1、在父工程下创建名为 feign 的 Module
2、在 pom.xml 中添加 Eureka Client 和 Feign 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
3、在配置文件 application.yml 添加 Feign 相关配置
server:
port: 8050 # 当前 Feign 服务端口
spring:
application:
name: feign # 当前服务注册在 Eureka Server 上的名称
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/ # 注册中心的访问地址
instance:
prefer-ip-address: true # 是否将当前服务的 IP 注册到 Eureka Server
4、在启动类 FeignApplication 添加@EnableFeignClients
注解,用于声明启用 Feign
5、通过接口的方式调用 Provider 服务
- 创建对应的实体类 Student
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private long id;
private String name;
private char gender;
}
- 创建 FeignProviderClient 接口
@FeignClient(value = "provider") // 指定 Feign 要调用的微服务,直接指定服务提供者在注册中心的 application name 即可
public interface FeignProviderClient {
@GetMapping("/student/index")
public String index();
@GetMapping("/student/findAll")
public Collection<Student> findAll();
@GetMapping("/student/findById/{id}")
public Student findById(@PathVariable("id") long id);
@PostMapping("/student/save")
public void save(@RequestBody Student student);
@PutMapping("/student/update")
public void update(@RequestBody Student student);
@DeleteMapping("/student/deleteById/{id}")
public void deleteById(@PathVariable("id") long id);
}
- 创建 FeignHandler,通过 @Autowired 注入 FeignProviderClient 实例,完成相关业务
@RequestMapping("/feign")
@RestController
public class FeignHandler {
@Autowired
private FeignProviderClient feignProviderClient;
@GetMapping("/index")
public String index(){
return feignProviderClient.index();
}
@GetMapping("/findAll")
public Collection<Student> findAll(){
return feignProviderClient.findAll();
}
}
6、依次启动注册中心、端口为 8010 的 Provider、端口为 8011 的 Provider、Feign。打开浏览器访问 http://localhost:8761可以看到两个 Provider 和 Feign 已经在注册中心完成注册
注:需添加web依赖,否则出现异常
7、使用 Postman 工具测试 Feign 相关接口
curl -X GET http://localhost:8050/feign/index
curl -X GET http://localhost:8050/feign/findAll
curl -X GET http://localhost:8050/feign/findById/1
curl -X POST http://localhost:8050/feign/save -d '{"id":4,"name":"tuyrk","gender":"男"}' -H "Content-Type: application/json"
curl -X PUT http://localhost:8050/feign/update -d '{"id":4,"name":"tyk","gender":"男"}' -H "Content-Type: application/json"
curl -X DELETE http://localhost:8050/feign/deleteById/1
8、Feign 也提供了负载均衡功能。访问请求服务方法,端口为 8010 和 8011 的微服务交替被访问
curl -X GET http://localhost:8050/feign/index
9、Feign 同时提供了容错功能。如果服务提供者 Provider 出现故障无法访问,访问 Feign 会直接报错:Internal Server Error
显然这种直接返回错误状态码的交互方式很不友好,可以通过容错机制给用户相应的提示信息,而非错误状态码,使得交互方式更加友好。使用容错机制非常简单:
- 在 application.yml 中添加配置
feign:
hystrix:
enabled: true # 是否开启熔断器
- 创建 FeignProviderClient 接口的实现类 FeignError ,定义容错处理逻辑,通过 @Component 将 FeignError 实例注入 IoC 容器
@Component
public class FeignError implements FeignProviderClient {
@Override
public String index() { return "服务器维护中...."; }
@Override
public Collection<Student> findAll() { return null; }
}
- 在 FeignProviderClient 定义处通过 @FeignClient 的 fallback 属性设置映射
@FeignClient(value = "provider", fallback = FeignError.class)
public interface FeignProviderClient { }
- 启动注册中心和 Feign,此时没有服务提供者 Provider 被注册,直接访问 Feign 接口提示:服务器维护中…
curl -X GET http://localhost:8050/feign/index
11-Hystrix 容错监控机制
什么是微服务容错机制?
在一个分布式系统中,各个微服务之间相互调用、彼此依赖,实际运行环境中可能会因为各种个样的原因导致某个微服务不可用,则依赖于该服务的其他微服务就会出现响应时间过长或者请求失败的情况,如果这种情况出现的次数较多,从一定程度上就可以能导致整个系统崩溃,如何来解决这一问题呢?
在不改变各个微服务调用关系的前提下,可以针对这些错误情况提前设置处理方案,遇到问题时,整个系统可以自主进行调整,这就是微服务容错机制的原理,发现故障并及时处理。
什么是 Hystrix?
Hystrix 是 Netflix 的一个开源项目,是一个熔断器模型的具体实现,熔断器就类似于电路中的保险丝,某个单元发生故障就通过烧断保险丝的方式来阻止故障蔓延,微服务架构的容错机制也是这个原理。Hystrix 的主要作用是当服务提供者发生故障无法访问时,Hystrix向服务消费者返回 fallback 降级处理,从而避免响应时间过长或者直接抛出异常的情况
Hystrix 设计原则
- 服务隔离机制,防止某个服务提供者出问题而影响到整个系统的运行
- 服务降级机制,服务出现故障时向服务消费者返回 fallback 处理机制
- 熔断机制,当服务消费者请求的失败率达到某个特定数值时,会迅速启动熔断机制并进行修复
- 提供实时的监控和报警功能,迅速发现服务中存在的故障
- 提供实时的配置修改功能,保证故障问题可以及时得到处理和修复
上节课中我们演示了 Hystrix 的熔断机制,本节课重点介绍 Hystrix 的另外一个重要功能——数据监控
Hystrix 除了可以为服务提供容错机制外,同时还提供了对请求的监控,这个功能需要结合 Spring Boot Actuator 来使用,Actuator 提供了对服务的健康监控、数据统计功能,可以通过 hystrix-stream 节点获取监控到的请求数据,Dashboard 组件则提供了数据的可视化监控功能
实现 Hystrix 的数据监控
1、在父工程下创建名为 hystrix 的 Module
2、在 pom.xml 中添加 Eureka Client、Feign、Hystrix 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
3、在配置文件 application.yml 添加 Hystrix 相关配置
server:
port: 8060 # 当前 Hystrix 服务端口
spring:
application:
name: hystrix # 当前服务注册在 Eureka Server 上的名称
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/ # 注册中心的访问地址
instance:
prefer-ip-address: true # 是否将当前服务的 IP 注册到 Eureka Server
feign:
hystrix:
enabled: true # 是否开启熔断器
management:
endpoints:
web:
exposure:
include: 'hystrix.stream' # 用来暴露 endpoints 的相关信息
4、在启动类 HystrixApplication 添加注解
@EnableFeignClients // 声明启用 Feign
@EnableHystrixDashboard // 声明启用可视化数据监控
@EnableCircuitBreaker // 声明启用数据监控
5、通过接口的方式调用 Provider 服务
- 创建对应的实体类 Student
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private long id;
private String name;
private char gender;
}
- 创建 FeignProviderClient 接口
@FeignClient 指定 Feign 要调用的微服务,直接指定服务提供者 Provider 在注册中心的 application name 即可
@FeignClient(value = "provider")
public interface FeignProviderClient {
@GetMapping("/student/index")
public String index();
@GetMapping("/student/findAll")
public Collection<Student> findAll();
@GetMapping("/student/findById/{id}")
public Student findById(@PathVariable("id") long id);
@PostMapping("/student/save")
public void save(@RequestBody Student student);
@PutMapping("/student/update")
public void update(@RequestBody Student student);
@DeleteMapping("/student/deleteById/{id}")
public void deleteById(@PathVariable("id") long id);
}
- 创建 HystrixHandler,通过 @Autowired 注入 FeignProviderClient 实例,完成相关业务
@RequestMapping("/hystrix")
@RestController
public class HystrixHandler {
@Autowired
private FeignProviderClient feignProviderClient;
@GetMapping("/index")
public String index(){
return feignProviderClient.index();
}
@GetMapping("/findAll")
public Collection<Student> findAll(){
return feignProviderClient.findAll();
}
}
6、依次启动注册中心、Provider、Hystrix。
打开浏览器访问 http://localhost:8060/actuator/hystrix.stream
查看监控数据,此时没有客户端请求,所以没有数据。
访问 http://localhost:8060/hystrix/index
后,再次访问 http://localhost:8060/actuator/hystrix.stream
,可以看到显示了监控数据情况,但是这种方式并不直观,通过可视化界面的方式能够更加直观地进行监控数据
7、通过 Dashboard 实现可视化监控
- 在 pom.xml 添加相关依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
- 在启动类 HystrixApplication 添加注解
@EnableHystrixDashboard // 声明启用可视化数据监控
- 访问
http://localhost:8060/hystrix
,在地址框输入http://localhost:8060/actuator/hystrix.stream
,点击 Monitor Stream 按钮来到可视化监控数据界面
12-Spring Cloud Config 本地配置
在基于微服务的分布式系统中,每个业务模块都可以拆分为一个独立自治的服务,多个请求来协同完成某个需求,在一个具体的业务场景中某个请求可能需要同时调用多个服务来完成。这就存在一个问题,多个微服务所对应的配置项也会非常多,一旦某个微服务进行了修改,则其他服务也需要作出调整,直接在每个微服务中修改对应的配置项是非常麻烦的,改完之后还需要重新部署项目。
微服务是分布式的,但是我们希望可以对所有微服务的配置文件进行集中统一管理,便于部署和维护,Spring Cloud 提供了对应的解决方案,即 Spring Cloud Config,通过服务端可以为多个客户端提供配置服务。
Spring Cloud Config 可以将配置文件存放在本地,也可以存放在远程 Git 仓库中。拿远程 Git 仓库来说,具体的操作思路是将所有的外部配置文件集中放置在 Git 仓库中,然后创建 Config Server,通过它来管理所有的配置文件,需要更改某个微服务的配置信息时,只需要在本地进行修改,然后推送到远程 Git 仓库即可,所有的微服务实例都可以通过 Config Server 来读取对应的配置信息。
搭建本地 Config Server
我们可以将微服务的相关配置文件存储在本地文件中,然后让微服务来读取本地配置文件,具体操作如下
创建本地 Config Server
1、在父工程下创建名为 nativeconfigserver 的 Module
2、在 pom.xml 中添加 Spring Cloud Config 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
3、在配置文件 application.yml 添加 Config Server 相关配置
server:
port: 8762 # 当前 Config Server 服务端口
spring:
application:
name: nativeconfigserver # 当前服务注册在 Eureka Server 上的名称
profiles:
active: native # 配置文件获取方式
cloud:
config:
server:
native:
search-locations: classpath:/shared # 本地配置文件的存放路径
4、在 resources 路径下新建 shared 文件夹,并在此目录下创建本地配置文件 configclient-dev.yml,定义 port 和 foo 信息
server:
port: 8070
foo: foo version 1
5、在启动类 NativeConfigServerApplication 添加注解
@EnableConfigServer // 声明配置中心
本地配置中心已经创建完成,接下来创建客户端来读取本地配置中心的配置文件
创建Config Client客户端
1、在父工程下创建名为 nativeconfigclient 的 Module
2、在 pom.xml 中添加 Spring Cloud Config 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
3、在 resources 路径下创建 bootstrap.yml,配置读取本地配置中心的相关信息
spring:
application:
name: nativeconfigclient # 当前服务注册在 Eureka Server 上的名称
profiles:
active: dev # 配置文件名,这里需要与当前微服务在 Eureka Server 注册的名称结合起来使用,两个值用 - 连接,比如当前微服务的名称是 configclient,profiles.active 的值是 dev,那么就会在本地 Config Server 中查找名为 configclient-dev 的配置文件
cloud:
config:
uri: http://localhost:8762 # 本地 Config Server 的访问路径
fail-fast: true # 设置客户端优先判断 config server 获取是否正常,并快速响应失败内容
4、创建 NativeConfigHandler,定义相关业务方法
@RestController
@RequestMapping("/native")
public class NativeConfigHandler {
@Value("${server.port}")
private String port;
@Value("${foo}")
private String foo;
@GetMapping("/index")
public String index() {
return this.port + "-" + this.foo;
}
}
5、依次启动注册中心、 NativeConfigServer、ConfigClient。访问 index() 方法并打印:8070-foo version 1
,说明读取本地配置成功
curl -X GET http://localhost:8070/native/index
13-搭建消息中间件 RabbitMQ 环境
什么是 RabbitMQ
RabbitMQ 是消息队列中间件,它适用于分布式系统,功能是完成消息的存储转发,RabbitMQ 底层是用 Erlang 语言来实现的。消息队列(Message Queue )为不同的 Application 之间完成通信提供了可能,需要传输的消息通过队列来交互,发消息是向队列中写入数据,获取消息是从队列中读取数据。RabbitMQ 是目前主流的中间件产品,适用于多个行业,具有高可用、易于扩展、安全可靠等优点。
Mac 下安装 RabbitMQ:安装 Homebrew
Homebrew 简介(摘自 Homebrew 官网)
Homebrew 是一个包管理器,用于安装 Apple 没有预装但是你需要的工具。
Homebrew 会将软件包安装到独立目录 /usr/local/Cellar,并将其文件软链接至 /usr/local。
Homebrew 不会将文件安装到它本身目录之外,所以你可将 Homebrew 安装到任意位置。
安装 Homebrew
打开终端,执行如下命令即可,官网提供的安装包已经包含了 Erlang,所以无需单独安装 Erlang。
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
检验是否安装成功,在终端执行 brew 命令
卸载 Homebrew
打开终端,执行如下命令即可。
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/uninstall)"
通过 Homebrew 来安装 RabbitMQ
1、打开终端,执行如下命令即可。
//更新 brew 资源
brew update
//执行安装
brew install rabbitmq
2、安装完成之后,需要配置环境变量。在终端执行 vim .bash_profile
,将下面两行配置添加到 .bash_profile 中,注意 RABBIT_HOME 替换成你自己的安装路径和版本,我安装的版本是 3.7.10。
export RABBIT_HOME=/usr/local/Cellar/rabbitmq/3.7.10
export PATH=$PATH:$RABBIT_HOME/sbin
编辑完成之后输入 :wq
保存退出,并执行如下命令使环境变量生效。
source ~/.bash_profile
3、环境变量配置完成之后就可以启动 RabbitMQ 了,执行如下命令。
//进入安装路径下的 sbin 目录
cd /usr/local/Cellar/rabbitmq/3.7.10/sbin
//启动服务
sudo rabbitmq-server
4、打开浏览器在地址栏输入 http://localhost:15672/
,进入登录页面。输入用户名密码,均为 guest,即可进入主页面。在终端输入 control+c 即可关闭 RabbitMQ。
Windows 下安装 RabbitMQ
1、 安装 Erlang,RabbitMQ 服务端代码是用 Erlang 编写的,所以安装 RabbitMQ 必须先安装 Erlang。
进入官网,下载 exe 安装包,双击运行完成安装。
2、配置环境变量,与 Java 环境配置方式一致。
高级系统设置 → 环境变量 → 新建系统环境变量,变量名 ERLANG_HOME,变量值为 Erlang 的安装路径 D:\Program Files\erl9.2,注意这里替换成你自己的安装路径。
将 ;%ERLANG_HOME%\bin
加入到 path 中。
3、 安装 RabbitMQ
进入官网,下载 exe 安装包,双击运行完成安装。
配置环境变量,与 Java 环境配置方式一致,高级系统设置 → 环境变量 → 新建系统环境变量,变量名 RABBITMQ_SERVER,变量值为 RabbitMQ 的安装路径 D:\Program Files\RabbitMQ Server\rabbitmq_server-3.7.10,注意这里替换成你自己的安装路径。
将 ;%RABBITMQ_SERVER%\sbin
加入到 path 中。
安装完成后,打开计算机服务列表,可以看到 RabbitMQ 的服务
4、安装 RabbitMQ 管理插件
进入安装路径下的 sbin 目录,如下所示。
cd D:\Program Files\RabbitMQ Server\rabbitmq_server-3.7.10\sbin>
执行如下命令,安装管理插件。
rabbitmq-plugins enable rabbitmq_management
打开浏览器在地址栏输入 http://localhost:15672/
,进入登录页面。
输入用户名密码,均为 guest,即可进入主页面。
14-Spring Cloud Config 远程配置
前面的课程我们学习了本地 Config Server 的搭建方式,本节课我们一起学习远程 Config Server 的环境搭建,即将各个微服务的配置文件放置在远程 Git 仓库中,通过 Config Server 进行统一管理。本课程中我们使用基于 Git 的第三方代码托管远程仓库 GitHub 作为远程仓库,实际开发中也可以使用 Gitee、SVN 或者自己搭建的私服作为远程仓库,Config Server 结构如下图所示。
接下来我们就来一起搭建远程 Config Server。
GitHub 远程配置文件
首先将配置文件上传到 GitHub 仓库
1、在父工程下创建文件夹 config,config 中创建 configclient.yml。
2、configclient.yml 中配置客户端相关信息。
server:
port: 8070
spring:
application:
name: configclient
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
management:
security:
enabled: false
3、将 config 上传至 GitHub,作为远程配置文件。
创建 Config Server
1、在父工程下创建名为 configserver 的 Module
2、在 pom.xml 中添加 Spring Cloud Config 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-configserver</artifactId>
</dependency>
3、在 resources 路径下创建配置文件 application.yml,添加 Config Server 相关配置。
server:
port: 8888 # 当前 Config Server 服务端口
spring:
application:
name: configserver # 当前服务注册在 Eureka Server 上的名称
cloud:
config:
server:
git: # Git 仓库配置文件信息
uri: https://github.com/tuyrk/myspringclouddemo.git # Git Repository 地址
searchPaths: config # 配置文件路径
username: root # 访问 Git Repository 的用户名
password: root # 访问 Git Repository 的密码
label: master # Git Repository 的分支
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/ # 注册中心的访问地址
4、在启动类 ConfigServerApplication 添加注解
@EnableConfigServer // 声明配置中心
远程 Config Server 环境搭建完成,接下来创建 Config Client,读取远程配置中心的配置信息。
创建 Config Client
1、在父工程下创建名为 configclient 的 Module
2、在 pom.xml 中添加 Eureka Client、Spring Cloud Config 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
3、在 resources 路径下新建 bootstrap.yml,配置读取远程配置中心的相关信息
spring:
cloud:
config:
name: configclient # 当前服务注册在 Eureka Server 上的名称,与远程 Git 仓库的配置文件名对应
label: master # Git Repository 的分支
discovery:
enabled: true # 是否开启 Config 服务发现支持
serviceId: configserver # 配置中心的名称
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/ # 注册中心的访问地址
4、创建 HelloHandler,定义相关业务方法。
@RequestMapping("/config")
@RestController
public class HelloHandler {
@Value("${server.port}")
private int port;
@RequestMapping(value = "/index")
public String index() {
return "当前端口:" + this.port;
}
}
5、依次启动注册中心、configserver、configclient。
通过控制台输出信息可以看到,configclient 已经读取到了 Git 仓库中的配置信息。
6、访问请求服务方法
curl -X GET http://localhost:8070/config/index
动态更新
如果此时对远程配置中心的配置文件进行修改,微服务需要重启以读取最新的配置信息,实际运行环境中这种频繁重启服务的方式是需要避免的,我们可以通过动态更新的方式,实现在不重启服务的前提下自动更新配置信息的功能。
动态更新的实现需要借助于 Spring Cloud Bus 来完成,Spring Cloud Bus 是一个轻量级的分布式通信组件,它的原理是将各个微服务组件与消息代理进行连接,当配置文件发生改变时,会自动通知相关微服务组件,从而实现动态更新,具体实现如下。
1、修改 Config Server 的 application.yml,添加 RabbitMQ
spring:
cloud:
bus:
trace:
enable: true
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
management:
endpoints:
web:
exposure:
include: bus-refresh
2、修改 Config Client 的 pom.xml,添加 actuator、bus-amqp 依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
3、修改 Config Client 的 bootstrap.yml,添加 rabbitmq、bus-refresh
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
management:
endpoints:
web:
exposure:
include: bus-refresh
4、修改 HelloHandler,添加 @RefreshScope 注解
5、修改 config 中的配置文件,将端口改为 8080,并更新到 GitHub
6、在不重启服务的前提下,实现配置文件的动态更新,启动 RabbitMQ
7、发送 POST 请求,访问 http://localhost:8070/actuator/bus-refresh
curl -X POST http://localhost:8070/actuator/bus-refresh
8、这样就实现动态更新了,再来访问 http://localhost:8070/config/index
,可以看到端口已经更新为 8080
curl -X GET http://localhost:8070/config/index
9、设置 GitHub 自动推送更新,添加 Webhooks,如下图所示
Settings -> Webhooks -> Add webhook
10、在 Payload URL 输入你的服务地址,如 http://localhost:8070/actuator/bus-refresh
,注意将 localhost 替换成服务器的外网 IP。
15-Zipkin 服务跟踪
我们知道一个分布式系统中往往会部署很多个微服务,这些服务彼此之间会相互调用,整个过程就会较为复杂,我们在进行问题排查或者优化的时候工作量就会比较大。如果能准确跟踪每一个网络请求的整个运行流程,获取它在每个微服务上的访问情况、是否有延迟、耗费时间等,这样的话我们分析系统性能,排查解决问题就会容易很多,我们使用 Zipkin 组件来实现服务跟踪。
什么是 Zipkin
Zipkin 是一个可以采集并且跟踪分布式系统中请求数据的组件,可以为开发者采集某个请求在多个微服务之间的追踪数据,并以可视化的形式呈现出来,让开发者可以更加直观地了解到请求在各个微服务中所耗费的时间等信息。
ZipKin 组件包括两部分:Zipkin Server 和 Zipkin Client,服务端用来采集微服务之间的追踪数据,再通过客户端完成数据的生成和展示,Spring Cloud 为服务跟踪提供了解决方案,Spring Cloud Sleuth 集成了 Zipkin 组件。
接下来我们通过实际代码来完成服务跟踪的实现,首先来实现 Zipkin Server。
实现 Zipkin Server
1、在父工程下创建名为 zipkin 的 Module
2、在 pom.xml 中添加 Zipkin Server 依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-server</artifactId>
<version>2.12.9</version>
</dependency>
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-ui</artifactId>
<version>2.12.9</version>
</dependency>
3、在 application.yml 添加 Zipkin 相关配置
server:
port: 9090 # 当前 Zipkin Server 服务端口
4、在启动类 ZipkinApplication 添加注解
@EnableZipkinServer // 声明启动 Zipkin Server
Zipkin Server 搭建成功,接下来创建 Zipkin Client
实现 Zipkin Client
1、在父工程下创建名为 zipkinclient 的 Module
2、在 pom.xml 中添加 Zipkin 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
3、在 application.yml 添加 Zipkin 相关配置
server:
port: 8090 # 当前 Zipkin Client 服务端口
spring:
application:
name: zipkinclient # 当前服务注册在 Eureka Server 上的名称
sleuth:
web:
client:
enabled: true # 设置是否开启 Sleuth
sampler:
probability: 1.0 # 设置采样比例,默认是 0.1
zipkin:
base-url: http://localhost:9090/ # Zipkin Server 地址
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/ # 注册中心的访问地址
4、创建 ZipkinHandler,定义相关业务方法
@RestController@RequestMapping("/zipkin")
public class ZipkinHandler {
@Value("${server.port}")
private String port;
@GetMapping("/index")
public String index(){
return "当前端口:"+this.port;
}
}
5、依次启动注册中心、Zipkin、ZipkinClient。打开浏览器访问http://localhost:9090/zipkin/
,可看到 Zipkin 首页。
6、点击 Find Traces 按钮可看到监控数据情况,当前没有监控到任何数据。
7、访问请求后再次刷新 http://localhost:9090/zipkin/,可看到监控数据,点击可查看详情。
curl -X GET http://localhost:8090/zipkin/index
16-微服务项目实战:环境搭建
项目需求
本项目分为客户端和后台管理系统两个界面:
- 客户端针对普通用户,功能包括用户登录、用户退出、菜品订购、我的订单
-
- 后台管理系统针对管理员,功能包括管理员登录、管理员退出、添加菜品、查询菜品、修改菜品、删除菜品、订单处理、添加用户、查询用户、删除用户
了解完需求之后,接下来设计系统架构,首先分配出 4 个服务提供者:account、menu、order、user。
- account 提供账户服务:用户和管理员登录。
- menu 提供菜品服务:添加菜品、查询菜品、修改菜品、删除菜品。
- order 提供订单服务:添加订单、查询订单、删除订单、处理订单。
- user 提供用户服务:添加用户、查询用户、删除用户。
接下来分配出 1 个服务消费者,包括客户端的前端页面和后台接口、后台管理系统的前端页面和后台接口,用户/管理员直接访问的资源都保存在服务消费者中,然后服务消费者调用 4 个服务提供者对应的接口完成业务逻辑,并通过 Feign 实现负载均衡。
4 个服务提供者和 1 个服务消费者都需要在注册中心进行注册,同时要注册配置中心,提供远程配置信息读取,服务提供者和服务消费者的配置信息保存在 Git 远程仓库,由配置中心负责拉取。
本系统共由 8 个模块组成,包括注册中心、配置中心、Git 仓库配置信息、服务消费者、4 个服务提供者,关系如下图所示。
系统架构搞清楚之后,接下来开始写代码。
代码实现
1、新建名为 orderingsystem的 Maven 工程
2、在 pom.xml 中引入 Spring Boot 和 Spring Cloud 相关依赖,其中 JAXB API 的依赖只针对 JDK 9 以上版本,如果你是 JDK 9 以下版本,不需要配置。
<!-- 引入 Spring Boot 的依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.7.RELEASE</version>
</parent>
<!-- 引入 Spring Cloud 的依赖,管理 Spring Cloud 生态各个组件 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 解决 JDK 9 以上版本没有 JAXB API 的问题 -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
系统环境搭建完成,从下节课开始我们来实现各种服务提供者。
17-注册中心和配置中心
注册中心
注册中心是管理调度微服务的核心组件,每个服务提供者或者服务消费者在启动时,会将自己的信息存储在注册中心,服务消费者可以从注册中心查询服务提供者的网络信息,并通过此信息来调用服务提供者的接口。微服务实例与注册中心通过心跳机制完成交互,如果注册中心长时间无法连接某个微服务实例,就会自动销毁该微服务,当某个微服务的网络信息发生变化时,会重新注册。所有的微服务(无论是服务提供者还是服务消费者,包括配置中心)都需要在注册中心进行注册,才能实现调用。
1、在父工程下创建名为 registrycenter 的 Module
2、在 pom.xml 中引入 Eureka Server 相关依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
3、在 application.yml 添加 Eureka Server 相关配置
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka/
4、在启动类 RegistryCenterApplication 添加注解@EnableEurekaServer
配置中心
配置中心可以对所有微服务的配置文件进行统一管理,便于部署和维护。
接下来我们为系统创建配置中心 Config Server,将所有微服务的配置文件统一通过 Git 仓库进行管理。
1、在父工程下创建名为 configserver 的 Module
2、在 pom.xml 添加 Spring Cloud Config 相关依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
3、在 application.yml 添加 Config Server 相关配置
server:
port: 8888
spring:
application:
name: configserver
cloud:
bus:
trace:
enable: true
config:
server:
git:
uri: https://github.com/southwind9801/orderingsystem.git # Git仓库地址
searchPaths: config # 仓库路径
username: southwind9801 # Git仓库用户名
password: southwind9801 # Git仓库密码
label: master # 仓库的分支
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
4、在启动类 ConfigServerApplication 添加注解@EnableConfigServer
创建数据库
1、数据库共 5 张表,分别是:
- t_admin:保存管理员数据
- t_user:保存用户数据
- t_type:保存菜品分类数据
- t_menu:保存菜品数据
- t_order:保存订单数据
2、SQL 脚本如下
DROP DATABASE IF EXISTS `orderingsystem`;
CREATE DATABASE `orderingsystem`;
USE `orderingsystem`;
DROP TABLE IF EXISTS `t_admin`;
SET character_set_client = utf8mb4 ;
CREATE TABLE `t_admin` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(11) DEFAULT NULL,
`password` varchar(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `t_user`;
SET character_set_client = utf8mb4 ;
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(11) DEFAULT NULL,
`password` varchar(11) DEFAULT NULL,
`nickname` varchar(11) DEFAULT NULL,
`gender` varchar(2) DEFAULT NULL,
`telephone` varchar(20) DEFAULT NULL,
`registerdate` date DEFAULT NULL,
`address` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `t_type`;
SET character_set_client = utf8mb4 ;
CREATE TABLE `t_type` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `t_menu`;
SET character_set_client = utf8mb4 ;
CREATE TABLE `t_menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(11) DEFAULT NULL,
`price` double DEFAULT NULL,
`flavor` varchar(11) DEFAULT NULL,
`tid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `tid` (`tid`),
CONSTRAINT `t_menu_ibfk_1` FOREIGN KEY (`tid`) REFERENCES `t_type` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `t_order`;
SET character_set_client = utf8mb4 ;
CREATE TABLE `t_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`mid` int(11) DEFAULT NULL,
`aid` int(11) DEFAULT NULL,
`date` date DEFAULT NULL,
`state` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `uid` (`uid`),
KEY `mid` (`mid`),
KEY `aid` (`aid`),
CONSTRAINT `t_order_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `t_user` (`id`),
CONSTRAINT `t_order_ibfk_2` FOREIGN KEY (`mid`) REFERENCES `t_menu` (`id`),
CONSTRAINT `t_order_ibfk_3` FOREIGN KEY (`aid`) REFERENCES `t_admin` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8;
18-服务提供者 account
实现服务提供者 account,account 为系统提供所有的账户相关业务,包括用户和管理员登录、退出,具体实现如下所示。
1、在父工程下创建建名为 account 的 Module,pom.xml 添加相关依赖
account 配置文件从 Git 仓库拉取,所以需要添加 Spring Cloud Config 相关依赖;同时需要访问数据库,因此还要添加 MyBatis 相关依赖。
<!-- eurekaclient -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 配置中心 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
2、在 resources 目录下创建 bootstrap.yml,在该文件中配置拉取 Git 仓库相关配置文件的信息
spring:
cloud:
config:
name: account # 对应的配置文件名称
label: master # Git仓库分支名
discovery:
enabled: true
serviceId: configserver # 连接的配置中心名称
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
在 Git 仓库配置文件 account.yml 中添加配置信息,服务提供者 account 集成 MyBatis 环境
server:
port: 8010
spring:
application:
name: account
datasource:
name: orderingsystem
url: jdbc:mysql://localhost:3306/orderingsystem?useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
mybatis:
mapper-locations: classpath:mapping/*.xml
type-aliases-package: com.southwind.entity
3、创建entity包,新建 Account 类
@Data
public class Account {
private long id;
private String username;
private String password;
private String nickname;
private String gender;
private String telephone;
private Date registerdate;
private String address;
}
新建 User 类继承 Account 类,对应数据表 t_user
@Data
public class Admin extends Account {
}
4、创建 repository 包,新建 UserRepository 接口
public interface UserRepository {
public User login(String username, String password);
}
新建 AdminRepository 接口
public interface AdminRepository {
public Admin login(String username, String password);
}
5、在 resources 目录下创建 mapping 文件夹,存放 Mapper.xml
新建 UserRepository.xml,编写 UserRepository 接口方法对应的 SQL
<mapper namespace="com.southwind.repository.UserRepository">
<select id="login" resultType="User">
select * from t_user where username = #{param1} and password = #{param2}
</select>
</mapper>
新建 AdminRepository.xml,编写 AdminRepository 接口方法对应的 SQL
<mapper namespace="com.southwind.repository.AdminRepository">
<select id="login" resultType="Admin">
select * from t_admin where username = #{param1} and password = #{param2}
</select>
</mapper>
将 Mapper 注入,在启动类添加注解 @MapperScan("com.southwind.repository")
6、新建 AccountHandler,将 UserRepository 和 AdminRepository 通过 @Autowired 注解进行注入,完成相关业务逻辑
@RestController
@RequestMapping("/account")
public class AccountHandler {
@Autowired private UserRepository userRepository;
@Autowired private AdminRepository adminRepository;
@GetMapping("/login/{username}/{password}/{type}")
public Account login(@PathVariable("username") String username, @PathVariable("password") String password, @PathVariable("type") String type) {
Account account = null;
switch (type){
case "user":
account = userRepository.login(username, password);
break;
case "admin":
account = adminRepository.login(username, password);
break;
}
return account;
}
}
7、依次启动注册中心、configserver、AccountApplication。调用接口
curl -X GET http://localhost:8010/account/login/zhangsan/123123/user
18-服务提供者 menu
本节课我们来实现服务提供者 menu。menu 为系统提供菜品相关服务,包括添加菜品、查询菜品、修改菜品、删除菜品,具体实现如下所示。
1、在父工程下创建建名为 menu 的 Module,pom.xml 添加相关依赖
menu 配置文件从 Git 仓库拉取,所以需要添加 Spring Cloud Config 相关依赖;同时需要访问数据库,因此还要添加 MyBatis 相关依赖。
<!-- eurekaclient -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 配置中心 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
2、在 resources 目录下创建 bootstrap.yml,在该文件中配置拉取 Git 仓库相关配置文件的信息
spring:
cloud:
config:
name: menu # 对应的配置文件名称
label: master # Git仓库分支名
discovery:
enabled: true
serviceId: configserver # 连接的配置中心名称
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
在 Git 仓库配置文件 menu.yml 中添加配置信息,服务提供者 menu 集成 MyBatis 环境
server:
port: 8020
spring:
application:
name: menu
datasource:
name: orderingsystem
url: jdbc:mysql://localhost:3306/orderingsystem?useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
mybatis:
mapper-locations: classpath:mapping/*.xml
type-aliases-package: com.southwind.entity
3、创建entity包,新建 Menu 类,对应数据表 t_menu
@Data
public class Menu {
private long id;
private String name;
private double price;
private String flavor;
private Type type;
}
新建 MenuVO 类为 layui 框架提供封装类
@Data
public class MenuVO {
private int code;
private String msg;
private int count;
private List<Menu> data;
}
新建 Type 类,对应数据表 t_type
@Data
public class Type {
private long id;
private String name;
}
4、创建 repository 包,新建 MenuRepository 接口
public interface MenuRepository {
List<Menu> findAll(int index,int limit);
int count();
void save(Menu menu);
Menu findById(long id);
void update(Menu menu);
void deleteById(long id);
}
新建 TypeRepository 接口
public interface TypeRepository {
List<Type> findAll();
}
5、在 resources 目录下创建 mapping 文件夹,存放 Mapper.xml
新建 MenuRepository.xml,编写 MenuRepository 接口方法对应的 SQL
<mapper namespace="com.southwind.repository.MenuRepository">
<resultMap id="menuMap" type="Menu">
<id property="id" column="mid"/>
<result property="name" column="mname"/>
<result property="author" column="author"/>
<result property="price" column="price"/>
<result property="flavor" column="flavor"/>
<!-- 映射 type -->
<association property="type" javaType="Type">
<id property="id" column="tid"/>
<result property="name" column="tname"/>
</association>
</resultMap>
<select id="findAll" resultMap="menuMap">
select m.id mid,m.name mname,m.price,m.flavor,t.id tid,t.name tname from t_menu m,t_type t where m.tid = t.id order by mid limit #{param1},#{param2}
</select>
<select id="count" resultType="int">
select count(*) from t_menu
</select>
<insert id="save" parameterType="Menu">
insert into t_menu(name,price,flavor,tid) values(#{name},#{price},#{flavor},#{type.id})
</insert>
<select id="findById" resultMap="menuMap">
select id mid,name mname,price,flavor,tid from t_menu where id = #{id}
</select>
<update id="update" parameterType="Menu">
update t_menu set name = #{name},price = #{price},flavor = #{flavor},tid = #{type.id} where id = #{id}
</update>
<delete id="deleteById" parameterType="long">
delete from t_menu where id = #{id}
</delete>
</mapper>
新建 TypeRepository.xml,编写 TypeRepository 接口方法对应的 SQL
<mapper namespace="com.southwind.repository.TypeRepository">
<select id="findAll" resultType="Type">
select * from t_type
</select>
</mapper>
将 Mapper 注入,在启动类添加注解 @MapperScan("com.southwind.repository")
6、新建 MenuHandler,将 MenuRepository 通过 @Autowired 注解进行注入,完成相关业务逻辑
@RestController
@RequestMapping("/menu")
public class MenuHandler {
@Autowired private MenuRepository menuRepository;
@Autowired private TypeRepository typeRepository;
@GetMapping("/findAll/{page}/{limit}")
public MenuVO findAll(@PathVariable("page") int page, @PathVariable("limit") int limit){
MenuVO menuVO = new MenuVO();
menuVO.setCode(0);
menuVO.setMsg("");
menuVO.setCount(menuRepository.count());
menuVO.setData(menuRepository.findAll((page-1)*limit,limit));
return menuVO;
}
@GetMapping("/findAll")
public List<Type> findAll(){
return typeRepository.findAll();
}
@PostMapping("/save")
public void save(@RequestBody Menu menu){
menuRepository.save(menu);
}
@GetMapping("/findById/{id}")
public Menu findById(@PathVariable("id") long id){
return menuRepository.findById(id);
}
@PutMapping("/update")
public void update(@RequestBody Menu menu){
menuRepository.update(menu);
}
@DeleteMapping("/deleteById/{id}")
public void deleteById(@PathVariable("id") long id){
menuRepository.deleteById(id);
}
}
7、依次启动注册中心、configserver、MenuApplication。调用接口
curl -X GET http://localhost:8020/menu/findAll/2/5
curl -X GET http://localhost:8020/menu/findAll
curl -X POST http://localhost:8020/menu/save -d '{"name":"酸汤肥牛","price":"4.5","flavor":"五香","type":{"id":4}}' -H "Content-Type: application/json"
curl -X GET http://localhost:8020/menu/findById/28
curl -X PUT http://localhost:8020/menu/update -d '{"id":28,"name":"酸汤肥牛","price":"4.5","flavor":"五香","type":{"id":1}}' -H "Content-Type: application/json"
curl -X DELETE http://localhost:8020/menu/deleteById/28
19-服务提供者 menu
本节课我们来实现服务提供者 menu。menu 为系统提供菜品相关服务,包括添加菜品、查询菜品、修改菜品、删除菜品,具体实现如下所示。
1、在父工程下创建建名为 menu 的 Module,pom.xml 添加相关依赖
menu 配置文件从 Git 仓库拉取,所以需要添加 Spring Cloud Config 相关依赖;同时需要访问数据库,因此还要添加 MyBatis 相关依赖
<!-- eurekaclient -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 配置中心 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
2、在 resources 目录下创建 bootstrap.yml,在该文件中配置拉取 Git 仓库相关配置文件的信息
spring:
cloud:
config:
name: menu # 对应的配置文件名称
label: master # Git仓库分支名
discovery:
enabled: true
serviceId: configserver # 连接的配置中心名称
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
在 Git 仓库配置文件 menu.yml 中添加配置信息,服务提供者 menu 集成 MyBatis 环境
server:
port: 8020
spring:
application:
name: menu
datasource:
name: orderingsystem
url: jdbc:mysql://localhost:3306/orderingsystem?useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
mybatis:
mapper-locations: classpath:mapping/*.xml
type-aliases-package: com.southwind.entity
3、创建entity包,新建 Menu 类,对应数据表 t_menu
@Data
public class Menu {
private long id;
private String name;
private double price;
private String flavor;
private Type type;
}
新建 MenuVO 类为 layui 框架提供封装类
@Data
public class MenuVO {
private int code;
private String msg;
private int count;
private List<Menu> data;
}
新建 Type 类,对应数据表 t_type
@Data
public class Type {
private long id;
private String name;
}
4、创建 repository 包,新建 MenuRepository 接口
public interface MenuRepository {
List<Menu> findAll(int index,int limit);
int count();
void save(Menu menu);
Menu findById(long id);
void update(Menu menu);
void deleteById(long id);
}
新建 TypeRepository 接口
public interface TypeRepository {
List<Type> findAll();
}
5、在 resources 目录下创建 mapping 文件夹,存放 Mapper.xml
新建 MenuRepository.xml,编写 MenuRepository 接口方法对应的 SQL
<mapper namespace="com.southwind.repository.MenuRepository">
<resultMap id="menuMap" type="Menu">
<id property="id" column="mid"/>
<result property="name" column="mname"/>
<result property="author" column="author"/>
<result property="price" column="price"/>
<result property="flavor" column="flavor"/>
<!-- 映射 type -->
<association property="type" javaType="Type">
<id property="id" column="tid"/>
<result property="name" column="tname"/>
</association>
</resultMap>
<select id="findAll" resultMap="menuMap">
select m.id mid,m.name mname,m.price,m.flavor,t.id tid,t.name tname from t_menu m,t_type t where m.tid = t.id order by mid limit #{param1},#{param2}
</select>
<select id="count" resultType="int">
select count(*) from t_menu
</select>
<insert id="save" parameterType="Menu">
insert into t_menu(name,price,flavor,tid) values(#{name},#{price},#{flavor},#{type.id})
</insert>
<select id="findById" resultMap="menuMap">
select id mid,name mname,price,flavor,tid from t_menu where id = #{id}
</select>
<update id="update" parameterType="Menu">
update t_menu set name = #{name},price = #{price},flavor = #{flavor},tid = #{type.id} where id = #{id}
</update>
<delete id="deleteById" parameterType="long">
delete from t_menu where id = #{id}
</delete>
</mapper>
新建 TypeRepository.xml,编写 TypeRepository 接口方法对应的 SQL
<mapper namespace="com.southwind.repository.TypeRepository">
<select id="findAll" resultType="Type">
select * from t_type
</select>
</mapper>
将 Mapper 注入,在启动类添加注解 @MapperScan("com.southwind.repository")
6、新建 MenuHandler,将 MenuRepository 通过 @Autowired 注解进行注入,完成相关业务逻辑
@RestController
@RequestMapping("/menu")
public class MenuHandler {
@Autowired private MenuRepository menuRepository;
@Autowired private TypeRepository typeRepository;
@GetMapping("/findAll/{page}/{limit}")
public MenuVO findAll(@PathVariable("page") int page, @PathVariable("limit") int limit){
MenuVO menuVO = new MenuVO();
menuVO.setCode(0);
menuVO.setMsg("");
menuVO.setCount(menuRepository.count());
menuVO.setData(menuRepository.findAll((page-1)*limit,limit));
return menuVO;
}
@GetMapping("/findAll")
public List<Type> findAll(){
return typeRepository.findAll();
}
@PostMapping("/save")
public void save(@RequestBody Menu menu){
menuRepository.save(menu);
}
@GetMapping("/findById/{id}")
public Menu findById(@PathVariable("id") long id){
return menuRepository.findById(id);
}
@PutMapping("/update")
public void update(@RequestBody Menu menu){
menuRepository.update(menu);
}
@DeleteMapping("/deleteById/{id}")
public void deleteById(@PathVariable("id") long id){
menuRepository.deleteById(id);
}
}
7、依次启动注册中心、configserver、MenuApplication。调用接口
curl -X GET http://localhost:8020/menu/findAll/2/5
curl -X GET http://localhost:8020/menu/findAll
curl -X POST http://localhost:8020/menu/save -d '{"name":"酸汤肥牛","price":"4.5","flavor":"五香","type":{"id":4}}' -H "Content-Type: application/json"
curl -X GET http://localhost:8020/menu/findById/28
curl -X PUT http://localhost:8020/menu/update -d '{"id":28,"name":"酸汤肥牛","price":"4.5","flavor":"五香","type":{"id":1}}' -H "Content-Type: application/json"
curl -X DELETE http://localhost:8020/menu/deleteById/28
20-服务提供者 order
本节课我们来实现服务提供者 orde,order 为系统提供订单相关服务,包括添加订单、查询订单、删除订单、处理订单,具体实现如下所示。
1、在父工程下创建建名为 order 的 Module,pom.xml 添加相关依赖
order 配置文件从 Git 仓库拉取,所以需要添加 Spring Cloud Config 相关依赖;同时需要访问数据库,因此还要添加 MyBatis 相关依赖。
<!-- eurekaclient -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 配置中心 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
2、在 resources 目录下创建 bootstrap.yml,在该文件中配置拉取 Git 仓库相关配置文件的信息
spring:
cloud:
config:
name: order # 对应的配置文件名称
label: master # Git仓库分支名
discovery:
enabled: true
serviceId: configserver # 连接的配置中心名称
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
在 Git 仓库配置文件 order.yml 中添加配置信息,服务提供者 order 集成 MyBatis 环境
server:
port: 8040
spring:
application:
name: order
datasource:
name: orderingsystem
url: jdbc:mysql://localhost:3306/orderingsystem?useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
mybatis:
mapper-locations: classpath:mapping/*.xml
type-aliases-package: com.southwind.entity
3、创建entity包,新建 Order 类,对应数据表 t_order
@Data
public class Order {
private long id;
private User user;
private Menu menu;
private Admin admin;
private Date date;
private int state;
}
新建 OrderVO 类为 layui 框架提供封装类
@Data
public class OrderVO {
private int code;
private String msg;
private int count;
private List<Order> data;
}
4、创建 repository 包,新建 OrderRepository 接口
public interface OrderRepository {
void save(Order order);
List<Order> findAllByUid(long uid,int index,int limit);
int countByUid(long uid);
void deleteByMid(long mid);
void deleteByUid(long uid);
List<Order> findAllByState(int state,int index,int limit);
int countByState(int state);
void updateState(long id,long aid,int state);
}
5、在 resources 目录下创建 mapping 文件夹,存放 Mapper.xml
新建 OrderRepository.xml,编写 OrderRepository 接口方法对应的 SQL
<mapper namespace="com.southwind.repository.OrderRepository">
<resultMap id="orderMap" type="Order">
<id property="id" column="oid"/>
<result property="date" column="date"/>
<result property="state" column="state"/>
<!-- 映射 menu -->
<association property="menu" javaType="Menu">
<id property="id" column="mid"/>
<result property="name" column="name"/>
<result property="price" column="price"/>
<result property="flavor" column="flavor"/>
</association>
</resultMap>
<resultMap id="orderMap2" type="Order">
<id property="id" column="oid"/>
<result property="date" column="date"/>
<!-- 映射 menu -->
<association property="menu" javaType="Menu">
<id property="id" column="mid"/>
<result property="name" column="name"/>
<result property="price" column="price"/>
<result property="flavor" column="flavor"/>
</association>
<!-- 映射 user -->
<association property="user" javaType="User">
<id property="id" column="uid"/>
<result property="nickname" column="nickname"/>
<result property="telephone" column="telephone"/>
<result property="address" column="address"/>
</association>
</resultMap>
<insert id="save" parameterType="Order">
insert into t_order(uid,mid,aid,date,state) values(#{user.id},#{menu.id},#{admin.id},#{date},0)
</insert>
<select id="findAllByUid" resultMap="orderMap">
select m.id mid,m.name,m.price,m.flavor,o.id oid,o.date,o.state from t_order o,t_menu m where o.mid = m.id and o.uid = #{param1} order by oid limit #{param2},#{param3}
</select>
<select id="countByUid" parameterType="long" resultType="int">
select count(*) from t_order where uid = #{uid}
</select>
<delete id="deleteByMid" parameterType="long">
delete from t_order where mid = #{mid}
</delete>
<delete id="deleteByUid" parameterType="long">
delete from t_order where uid = #{uid}
</delete>
<select id="findAllByState" resultMap="orderMap2">
select m.id mid,m.name,m.price,m.flavor,o.id oid,o.date,u.id uid,u.nickname,u.telephone,u.address from t_order o,t_menu m,t_user u where o.mid = m.id and o.uid = u.id and o.state = #{param1} order by oid limit #{param2},#{param3}
</select>
<select id="countByState" parameterType="int" resultType="int">
select count(*) from t_order where state = #{state}
</select>
<update id="updateState">
update t_order set aid = #{param2},state = #{param3} where id = #{param1}
</update>
</mapper>
将 Mapper 注入,在启动类添加注解 @MapperScan("com.southwind.repository")
6、新建 OrderHandler,将 OrderRepository 通过 @Autowired 注解进行注入,完成相关业务逻辑
@RestController
@RequestMapping("/order")
public class OrderHandler {
@Autowired private OrderRepository orderRepository;
@PostMapping("/save")
public void save(@RequestBody Order order){
orderRepository.save(order);
}
@GetMapping("/findAllByUid/{uid}/{page}/{limit}")
public OrderVO findAllByUid(@PathVariable("uid") long uid, @PathVariable("page") int page, @PathVariable("limit") int limit){
OrderVO orderVO = new OrderVO();
orderVO.setCode(0);
orderVO.setMsg("");
orderVO.setCount(orderRepository.countByUid(uid));
orderVO.setData(orderRepository.findAllByUid(uid,(page-1)*limit,limit));
return orderVO;
}
@DeleteMapping("/deleteByMid/{mid}")
public void deleteByMid(@PathVariable("mid") long mid){
orderRepository.deleteByMid(mid);
}
@DeleteMapping("/deleteByUid/{uid}")
public void deleteByUid(@PathVariable("uid") long uid){
orderRepository.deleteByUid(uid);
}
@GetMapping("/findAllByState/{state}/{page}/{limit}")
public OrderVO findAllByState(@PathVariable("state") int state, @PathVariable("page") int page, @PathVariable("limit") int limit){
OrderVO orderVO = new OrderVO();
orderVO.setCode(0);
orderVO.setMsg("");
orderVO.setCount(orderRepository.countByState(0));
orderVO.setData(orderRepository.findAllByState(0,(page-1)*limit,limit));
return orderVO;
}
@PutMapping("/updateState/{id}/{state}/{aid}")
public void updateState(@PathVariable("id") long id, @PathVariable("state") int state, @PathVariable("aid") long aid){
orderRepository.updateState(id,aid,state);
}
}
7、依次启动注册中心、configserver、OrderApplication。调用接口
curl -X POST http://localhost:8040/order/save -d '{"user":{"id":"1"},"menu":{"id":"11"},"admin":{"id":"1"},"date":"2020-07-17"}' -H "Content-Type: application/json"
curl -X GET http://localhost:8040/order/findAllByUid/1/1/100
curl -X DELETE http://localhost:8040/order/deleteByMid/mid
curl -X DELETE http://localhost:8040/order/deleteByUid/uid
curl -X GET http://localhost:8040/order/findAllByState/0/1/100
curl -X PUT http://localhost:8040/order/updateState/28/1/1
21-服务提供者 user
本节课我们来实现服务提供者 user,user 为系统提供用户相关服务,包括添加用户、查询用户、删除用户,具体实现如下所示。
1、在父工程下创建建名为 user 的 Module,pom.xml 添加相关依赖
user 配置文件从 Git 仓库拉取,所以需要添加 Spring Cloud Config 相关依赖;同时需要访问数据库,因此还要添加 MyBatis 相关依赖
<!-- eurekaclient -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 配置中心 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
2、在 resources 目录下创建 bootstrap.yml,在该文件中配置拉取 Git 仓库相关配置文件的信息
spring:
cloud:
config:
name: user # 对应的配置文件名称
label: master # Git仓库分支名
discovery:
enabled: true
serviceId: configserver # 连接的配置中心名称
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
在 Git 仓库配置文件 order.yml 中添加配置信息,服务提供者 user 集成 MyBatis 环境
server:
port: 8050
spring:
application:
name: user
datasource:
name: orderingsystem
url: jdbc:mysql://localhost:3306/orderingsystem?useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
mybatis:
mapper-locations: classpath:mapping/*.xml
type-aliases-package: com.southwind.entity
3、创建entity包,新建 User 类,对应数据表 t_user
@Data
public class User {
private long id;
private String username;
private String password;
private String nickname;
private String gender;
private String telephone;
private Date registerdate;
private String address;
}
新建 UserVO 类为 layui 框架提供封装类
@Data
public class UserVO {
private int code;
private String msg;
private int count;
private List<User> data;
}
4、创建 repository 包,新建 UserRepository 接口
public interface UserRepository {
List<User> findAll(int index, int limit);
int count();
void save(User user);
void deleteById(long id);
}
5、在 resources 目录下创建 mapping 文件夹,存放 Mapper.xml
新建 UserRepository.xml,编写 UserRepository 接口方法对应的 SQL
<mapper namespace="com.southwind.repository.UserRepository">
<select id="findAll" resultType="User">
select * from t_user order by id limit #{param1},#{param2}
</select>
<select id="count" resultType="int">
select count(*) from t_user
</select>
<insert id="save" parameterType="User">
insert into t_user(username,password,nickname,gender,telephone,registerdate,address) values(#{username},#{password},#{nickname},#{gender},#{telephone},#{registerdate},#{address})
</insert>
<delete id="deleteById" parameterType="long">
delete from t_user where id = #{id}
</delete>
</mapper>
将 Mapper 注入,在启动类添加注解 @MapperScan("com.southwind.repository")
6、新建 UserHandler,将 UserRepository 通过 @Autowired 注解进行注入,完成相关业务逻辑
@RestController
@RequestMapping("/user")
public class UserHandler {
@Autowired private UserRepository userRepository;
@GetMapping("/findAll/{page}/{limit}")
public UserVO findAll(@PathVariable("page") int page, @PathVariable("limit") int limit){
UserVO userVO = new UserVO();
userVO.setCode(0);
userVO.setMsg("");
userVO.setCount(userRepository.count());
userVO.setData(userRepository.findAll((page-1)*limit,limit));
return userVO;
}
@PostMapping("/save")
public void save(@RequestBody User user){
user.setRegisterdate(new Date());
userRepository.save(user);
}
@DeleteMapping("/deleteById/{id}")
public void deleteById(@PathVariable("id") long id){
userRepository.deleteById(id);
}
}
7、依次启动注册中心、configserver、UserApplication。调用接口
curl -X GET http://localhost:8050/user/findAll/1/100
curl -X POST http://localhost:8050/user/save -d '{"username":"tuyrk","password":"123456","nickname":"神秘的小岛岛","gender":"男","telephone":"18382471393","address":"三色路"}' -H "Content-Type: application/json"
curl -X DELETE http://localhost:8050/user/deleteById/6
22-服务消费者 clientfeign
前面的课程我们已经实现了注册中心、配置中心以及各种服务提供者,本节课我们来实现服务消费者 clientfeign,完成客户端的相关业务,分别调用服务提供者 account、menu、order、user 的相关服务,并通过 Feign 实现负载均衡。
1、在父工程下创建名为 clientfeign 的 Module ,pom.xml 添加相关依赖
配置文件从 Git 仓库拉取,添加配置中心 Spring Cloud Config 相关依赖;集成 Feign 和 Thymeleaf 模版相关依赖
<!-- eurekaclient -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 配置中心 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!-- 集成feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- thymeleaf模版 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2、在 resources 目录下创建 bootstrap.yml,在该文件中配置拉取 Git 仓库相关配置文件的信息
spring:
cloud:
config:
name: clientfeign # 对应的配置文件名称
label: master # Git仓库分支名
discovery:
enabled: true
serviceId: configserver # 连接的配置中心名称
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
在 Git 仓库配置文件 clientfeign.yml 中添加相关信息
server:
port: 8030
spring:
application:
name: clientfeign
thymeleaf:
prefix: classpath:/static/
suffix: .html
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
3、在启动类 ClientFeignApplication 添加注解
@EnableFeignClients
@ServletComponentScan
4、创建 entity 包,拷贝 account、menu、order、user 中的实体类
创建 feign 包,新建 AccountFeign 接口、MenuFeign 接口、OrderFeign 接口、UserFeign 接口
通过 @FeignClient 注解直接调用服务提供者 account、menu、order、user 的相关服务
- AccountFeign 接口
@FeignClient(value = "account")
public interface AccountFeign {
@GetMapping("/account/login/{username}/{password}/{type}")
public Account login(@PathVariable("username") String username, @PathVariable("password") String password, @PathVariable("type") String type);
}
- MenuFeign 接口
@FeignClient(value = "menu")
public interface MenuFeign {
@GetMapping("/menu/findAll/{page}/{limit}")
public MenuVO findAll(@PathVariable("page") int page, @PathVariable("limit") int limit);
@GetMapping("/menu/findAll")
public List<Type> findAll();
@PostMapping("/menu/save")
public void save(@RequestBody Menu menu);
@GetMapping("/menu/findById/{id}")
public Menu findById(@PathVariable("id") long id);
@PutMapping("/menu/update")
public void update(@RequestBody Menu menu);
@DeleteMapping("/menu/deleteById/{id}")
public void deleteById(@PathVariable("id") long id);
}
- OrderFeign 接口
@FeignClient(value = "order")
public interface OrderFeign {
@PostMapping("/order/save")
public void save(@RequestBody Order order);
@GetMapping("/order/findAllByUid/{uid}/{page}/{limit}")
public OrderVO findAllByUid(@PathVariable("uid") long uid, @PathVariable("page") int page, @PathVariable("limit") int limit);
@DeleteMapping("/order/deleteByMid/{mid}")
public void deleteByMid(@PathVariable("mid") long mid);
@DeleteMapping("/order/deleteByUid/{uid}")
public void deleteByUid(@PathVariable("uid") long uid);
@GetMapping("/order/findAllByState/{state}/{page}/{limit}")
public OrderVO findAllByState(@PathVariable("state") int state, @PathVariable("page") int page, @PathVariable("limit") int limit);
@PutMapping("/order/updateState/{id}/{state}/{aid}")
public void updateState(@PathVariable("id") long id, @PathVariable("state") long state,@PathVariable("aid") long aid);
}
- UserFeign 接口
@FeignClient(value = "user")
public interface UserFeign {
@GetMapping("/user/findAll/{page}/{limit}")
public UserVO findAll(@PathVariable("page") int page, @PathVariable("limit") int limit);
@PostMapping("/user/save")
public void save(@RequestBody User user);
@DeleteMapping("/user/deleteById/{id}")
public void deleteById(@PathVariable("id") long id);
}
5、创建 controller 包,新建 AccountHandler、MenuHandler、OrderHandler、UserHandler 类
- AccountHandler
@Controller
@RequestMapping("/account")
public class AccountHandler {
@Autowired
private AccountFeign accountFeign;
@PostMapping("/login")
public String login(@RequestParam("username") String username, @RequestParam("password") String password, @RequestParam("type") String type, HttpSession session){
Account account = accountFeign.login(username,password,type);
String target = null;
if(account == null){
target = "login";
}else{
switch (type){
case "user":
User user = convertUser(account);
session.setAttribute("user",user);
target = "redirect:/account/redirect/index";
break;
case "admin":
Admin admin = convertAdmin(account);
session.setAttribute("admin",admin);
target = "redirect:/account/redirect/main";
break;
}
}
return target;
}
@GetMapping("/logout")
public String logout(HttpSession session){
session.invalidate();
return "login";
}
@RequestMapping("/redirect/{target}")
public String redirect(@PathVariable("target") String target){
return target;
}
private User convertUser(Account account){
User user = new User();
user.setUsername(ReflectUtils.getFieldValue(account,"username")+"");
user.setPassword(ReflectUtils.getFieldValue(account,"password")+"");
user.setGender(ReflectUtils.getFieldValue(account,"gender")+"");
user.setId((long)(ReflectUtils.getFieldValue(account,"id")));
user.setNickname(ReflectUtils.getFieldValue(account,"nickname")+"");
user.setRegisterdate((Date)(ReflectUtils.getFieldValue(account,"registerdate")));
user.setTelephone(ReflectUtils.getFieldValue(account,"telephone")+"");
return user;
}
private Admin convertAdmin(Account account){
Admin admin = new Admin();
admin.setUsername(ReflectUtils.getFieldValue(account,"username")+"");
admin.setPassword(ReflectUtils.getFieldValue(account,"password")+"");
admin.setId((long)(ReflectUtils.getFieldValue(account,"id")));
return admin;
}
}
这里不能直接完成类型转换,因为虚拟机的默认类加载机制是通过双亲委派实现的,Spring Boot 为了实现程序动态性,破坏了双亲委派模型。用户自定义类会被 Spring Boot 自定义的加载器 RestartClassLoader 所截获,一旦发现类路径下有文件的修改,Spring Boot 中的 spring-boot-devtools 会重新生成新的类加载器来加载新的类文件,从而实现动态功能,但是也会带来因类加载器不同导致的转换异常问题,这里我们编写一个工具类 ReflectUtils,通过反射机制手动完成类型转换,ReflectUtils 结合 convertUser 和 convertAdmin 即可完成 Account 类型到 User 及 Admin 类型的转换,ReflectUtils 代码如下:
public class ReflectUtils {
public static Object getFieldValue(Object obj, String fieldName){
if(obj == null) { return null; }
Field targetField = getTargetField(obj.getClass(), fieldName);
try {
return FieldUtils.readField(targetField, obj, true ) ;
} catch (IllegalAccessException e) { e.printStackTrace(); }
return null ;
}
public static Field getTargetField(Class<?> targetClass, String fieldName) {
if (targetClass == null) { return field; }
Field field = null;
try {
if (Object.class.equals(targetClass)) {
return field;
}
field = FieldUtils.getDeclaredField(targetClass, fieldName, true);
if (field == null) {
field = getTargetField(targetClass.getSuperclass(), fieldName);
}
} catch (Exception e) { }
return field;
}
}
- MenuHandler
@Controller
@RequestMapping("/menu")
public class MenuHandler {
@Autowired private MenuFeign menuFeign;
@Autowired private OrderFeign orderFeign;
@GetMapping("/findAll")
@ResponseBody
public MenuVO findAll(@RequestParam("page") int page, @RequestParam("limit") int limit){
return menuFeign.findAll(page, limit);
}
@GetMapping("/prepareSave")
public String prepareSave(Model model){
model.addAttribute("list",menuFeign.findAll());
return "menu_add";
}
@PostMapping("/save")
public String save(Menu menu){
menuFeign.save(menu);
return "redirect:/account/redirect/menu_manage";
}
@GetMapping("/findById/{id}")
public String findById(@PathVariable("id") long id,Model model){
model.addAttribute("list",menuFeign.findAll());
model.addAttribute("menu",menuFeign.findById(id));
return "menu_update";
}
@PostMapping("/update")
public String update(Menu menu){
menuFeign.update(menu);
return "redirect:/account/redirect/menu_manage";
}
@GetMapping("/deleteById/{id}")
public String deleteById(@PathVariable("id") long id){
orderFeign.deleteByMid(id);
menuFeign.deleteById(id);
return "redirect:/account/redirect/menu_manage";
}
}
- OrderHandler
@Controller
@RequestMapping("/order")
public class OrderHandler {
@Autowired private OrderFeign orderFeign;
@GetMapping("/save/{mid}")
public String save(@PathVariable("mid") long mid, HttpSession session){
User user = (User) session.getAttribute("user");
Order order = new Order();
Menu menu = new Menu();
menu.setId(mid);
order.setUser(user);
order.setMenu(menu);
order.setDate(new Date());
orderFeign.save(order);
return "redirect:/account/redirect/order";
}
@GetMapping("/findAllByUid")
@ResponseBody
public OrderVO findAllByUid(@RequestParam("page") int page, @RequestParam("limit") int limit,HttpSession session){
User user = (User) session.getAttribute("user");
return orderFeign.findAllByUid(user.getId(), page, limit);
}
@GetMapping("/findAllByState")
@ResponseBody
public OrderVO findAllByState(@RequestParam("page") int page, @RequestParam("limit") int limit){
return orderFeign.findAllByState(0, page, limit);
}
@GetMapping("/updateState/{id}/{state}")
public String updateState(@PathVariable("id") long id,@PathVariable("state") int state,HttpSession session){
Admin admin = (Admin) session.getAttribute("admin");
orderFeign.updateState(id,state,admin.getId());
return "redirect:/account/redirect/order_handler";
}
}
- UserHandler
@Controller
@RequestMapping("/user")
public class UserHandler {
@Autowired private UserFeign userFeign;
@Autowired private OrderFeign orderFeign;
@GetMapping("/findAll")
@ResponseBody
public UserVO findAll(@RequestParam("page") int page, @RequestParam("limit") int limit){
return userFeign.findAll(page, limit);
}
@PostMapping("/save")
public String save(User user){
userFeign.save(user);
return "redirect:/account/redirect/user_manage";
}
@GetMapping("/deleteById/{id}")
public String deleteById(@PathVariable("id") long id){
orderFeign.deleteByUid(id);
userFeign.deleteById(id);
return "redirect:/account/redirect/user_manage";
}
}
6、添加过滤器,防止用户在未登录的状态下访问资源页面
创建 filter 包,新建 AdminFilter 和 UserFilter,实现登录过滤的业务逻辑
- AdminFilter
@Component
@WebFilter(urlPatterns = {"/main.html", "/account/redirect/main"}, filterName = "adminFilter")
public class AdminFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException { }
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpSession session = request.getSession();
Admin admin = (Admin) session.getAttribute("admin");
if(admin == null) {
response.sendRedirect("login.html");
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() { }
}
- UserFilter
@Component
@WebFilter(urlPatterns = {"/index.html","/account/redirect/index","/order.html","/account/redirect/order"},filterName = "userFilter")
public class UserFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException { }
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpSession session = request.getSession();
User user = (User) session.getAttribute("user");
if(user == null) {
response.sendRedirect("login.html");
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() { }
}
启动类 ClientFeignApplication 添加 @ServletComponentScan
注解,使过滤器生效
7、在 resources 目录下创建 static 文件夹
前端的静态资源全部放入 static 目录下,客户端即可通过浏览器直接访问这些静态资源而不必通过后台映射。
但是需要注意的是,Spring Boot 中使用 Thymeleaf 模版来处理 HTML 的话,Session 会失效,必须通过后台 Handler 的映射来到 HTML,才可以访问到 Session。那么为什么直接访问 JSP 就可以拿到 Session 呢?因为 Session 是 JSP 内置对象,也就是一个 Java 对象,能被访问的前提是必须实例化,JSP 资源在被访问时,JSP 引擎会将 JSP 转化成 Servlet,然后在这个 Servlet 中会实例化 Session 对象,即在 JSP 资源中,Session 已经完成了实例化,所以是可以访问。
但是在静态 HTML 页面中,没有实例化 Session 的动作,所以无法访问 Session,必须先通过 Handler 来实例化 Session 对象,并且以转发的形式来到 HTML,Session 才可以被访问。这里必须是转发,如果后台通过重定向来到 HTML 页面,同样无法访问 Session,核心问题在于 Session 被访问之前是否已经被实例化。
8、依次启动注册中心、配置中心、account、menu、order、user、clientfeign
打开浏览器访问 http://localhost:8761
,可以看到所有微服务的注册信息
4 个服务提供者和 1 个服务消费者已经全部完成注册,接下来就可以在 http://localhost:8030/
访问服务消费者 clientfeign 的相关业务功能了
总结
讲解了项目实战客户端模块的搭建,在整个系统中作为服务消费者,调用各个服务提供者包括 account、menu、order、user 的相关接口完成具体业务需求,客户端模块相当于一个大集成者,统一处理系统的业务请求,同时整合了视图层技术 Thymeleaf,完成用户交互。
更多推荐
所有评论(0)