成熟的微服务架构,定位为开发人员提供工具,以快速构建分布式系统
核心组件 | Spring Cloud |
---|---|
服务注册中心 | Spring Cloud Netflix Eureka |
服务调用方式 | REST API、Feign、Ribbon |
服务网关 | Spring Cloud Netflix Zuul |
断路器 | Spring Cloud Netflix Hystrix |
IDEA新建Spring Initializr项目,填写好项目信息后,先选择一个Spring Boot版本,之后可以修改,等待Maven导入包完成
项目初始化成功后,可以删除不需要的src目录
之后在项目根目录新建模块,选择Maven项目填写你要创建的模块名,注意父级模块要选择确认
Netflix的核心子模块之一
完成服务注册与发现的作用
正常一个服务对另一个服务进行调用时,需要知道对方的IP地址和开放的端口号,但是当服务较多的时候,IP地址又经常发生变化时会变得难以维护,使用服务注册中心进行管理就可以方便的管理,并且使用服务注册中心管理还可以实现负载均衡,对资源进行合理分配。
Eureka Service和Eureka Client
首先需要有一个Eureka Server服务器启动,Service Provider启动后会找Eureka Service服务将自己的内容注册上去,还可以进行更新和取消注册,当Service Consumer需要调用Service Provider时,Service Consumer会先向Eureka Server请求注册表信息来找到最新的服务信息,根据得到的信息进行远程调用Service Provider服务。
当服务升级到集群项目时,会存在多个服务者和调用者,通过配置Eureka就可以实现负载均衡的效果。
服务提供者在启动后不仅要向Eureka服务中心进行注册,没过一段时间还要向Eureka进行续约,和服务下线时通知Eureka服务器,这样Eureka服务就可以保证提供的所有服务都是可用的。
服务消费者则需要向Eureka服务注册中心请求服务清单,根据清单找到需要请求的服务器信息进行调用。
服务注册中心Eureka则要对服务提供者的信息进行记录和失效清除。
新建一个Eureka模块,可以删除此模块pom文件中不需要的spring-boot-starter-web服务和mysql连接服务模块,添加一个eureka模块依赖。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
在整个项目的最外层(根项目)pom中进行Spring Cloud版本的管理,这样在所有子项目中Spring Cloud就可以对要使用的依赖进行统一的版本管理。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Greenwich.SR5version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
在上面父项目中修改之后,子项目中也会发生变化,例如在上面子项目中引入的dependecy左边有向上存在依赖的标记。
在eureka-service项目中的resources中新建application.properties配置,我这里给出的是yaml格式的,用可以用一些网址工具方便的转换
spring:
application:
name: eureka-server
server:
port: 8000
eureka:
instance:
hostname: localhost
client:
#fetch-registry: 获取注册表。此案例项目都是单节点对外提供服务,所以不需要同步其他节点数据
fetch-registry: false
#register-with-eureka 代表是否将自己注册到Eureka Server,默认为true。
register-with-eureka: false
#服务提供的地址
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
在eureka-service的java项目下新建包和启动类,在启动类上增加注解
// Eureka服务端
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
测试效果,访问localhost:8000(地址和上面配置文件中配置的端口)即可访问到Eureka提供的管理页面
在需要提供服务的Client项目pom文件中引入,注意artifactId要正确引入,有许多相似的;说明:这个依赖是一个比较高级的eureka依赖,在一些比较老的项目中引入的依赖,还需要在Client的SpringBoot启动类上增加@EnableEurekaClient注解才能将服务注册为Eureka的Client服务
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
在application配置文件中加入eureka服务器地址的配置
server:
port: 8081
spring:
datasource:
name: course_prepare_datasource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/course_prepare?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&userSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root1024
application:
name: course-list
logging:
pattern:
console: '%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:-}){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:}){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}'
# 支持数据库中下划线连接的字段和类中的驼峰命名属性进行自动匹配
mybatis:
configuration:
map-underscore-to-camel-case: true
eureka:
client:
service-url:
defaultZone: http://localhost:8000/eureka/
启动eureka-server和配置好的Client项目,在浏览器中访问eureka服务器即可看到eureka中已经注册好了刚刚配置的Client项目;当Clinet比eureka-service服务先启动后,它会不断尝试向eureka服务器注册
在pom文件中同样引入eureka,在application配置文件中同样配置eureka地址,和服务端端配置相同
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
在启动类上 增加注解
//实现服务间调用
@EnableFeignClients
新建一个client包,创建客户端接口,用来声明要调用服务端的接口,注意 @GetMapping({“/courses”})链接需要跟对应的controller下的方法相同
如下2,我这里测试用到的Course类是服务端提供的内容,需要进行引入,利用IDEA智能引入选择"Add dependency on module ‘服务端名’"
引入之后在pom文件中就会自动增加以下内容
<dependency>
<groupId>xx.xxgroupId>
<artifactId>服务端项目名artifactId>
<version>0.0.1-SNAPSHOTversion>
<scope>compilescope>
dependency>
在客户端client包下新建客户端接口,用来声明要调用的接口
//定义是针对哪个服务的内容
@FeignClient("course-list")
//@FeignClient(value = "course-list", fallback = CourseListClientHystrix.class) 这个是下面断路器指向的类
@Primary //告诉IDEA优先选择注入的对象,不写对程序也没有影响详细见下面说明
public interface CourseListClient {
/**
* 获取课程列表
* @return 课程列表
* @GetMapping({"/courses"})链接需要跟对应的controller下的方法相同
*/
@GetMapping({"/courses"})
List<Course> courseList();
}
引入建立好的客户端;在Controller类中创建接口,接口中去调用client的接口后返回
@Autowired
CourseListClient courseListClient;
@GetMapping({"/coursesInPrice"})
public List<Course> getCourseListInPrice() {
List<Course> courses = courseListClient.courseList();
return courses;
}
经过刚才修改后,controller中注入的客户端可能会提示有多个,不知道引入哪一个,这是因为对于Feign来说是在调用的时候才知道应该选择哪一个,而IDEA编辑器并不了解,本质上对程序运行没有影响,可以在client接上增加@Primary注解告诉编译器优先选择哪一个
调用时根据规则调用不同服务端
所有的请求都先到达Nginx服务器,由它来进行分发和转发
大型项目中两种请求可以同时存在
RandomRule 随机策略
RoundRobinRule 轮询策略
ResponseTimeWeightedRule 加权,根据每一个Server都平均响应时间动态加权
Ribbon.NFLoadBalancerRuleClassName
在客户端的application配置文件中进行配置
服务端项目名:
ribbon:
NFLoadBanlancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
当某个服务不可用时,返回一个默认的响应或者错误的响应,不会让用户一直等待
在客户端进行断路器的引入,因为服务端是否可用客户端是无法保证的
pom.xml
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
application配置 打开断路功能
feign:
hystrix:
enabled: true
在启动类上增加注解
//添加断路器配置
@EnableCircuitBreaker
client包下服务接口进行注解配置,引入发生错误时要调用的类
@FeignClient(value = "course-list", fallback = CourseListClientHystrix.class)
client包下创建断路器类,代表发生断路时要返回的内容
@Component
public class CourseListClientHystrix implements CourseListClient{
/**
* 获取课程列表
*
* @return 课程列表
* @GetMapping({"/courses"})链接需要跟对应的controller下的方法相同
*/
@Override
public List<Course> courseList() {
//返回一个默认的值
List<Course> defaultCourse = new ArrayList<>();
Course course = new Course();
course.setId(1);
course.setCourseId(1);
course.setCourseName("默认课程");
course.setValid(1);
defaultCourse.add(course);
return defaultCourse;
// return Collections.emptyList(); //也可以直接返回一个空列表
}
}
在客户端service接口中增加声明并在实现类中进行方法实现
接口中声明
/**
* 获取课程信息与价格
* @return 课程信息与价格列表
*/
public List<CourseAndPrice> getCourseAndPriceList();
entity包下增加整合的实体类
@Data
public class CourseAndPrice implements Serializable {
Integer id;
Integer courseId;
String name;
Integer price;
//get;set方法
}
@Autowired
CoursePriceMapper coursePriceMapper;
@Autowired
CourseListClient courseListClient;
//获取课程价格
@Override
public CoursePrice getCoursePrice(Integer courseId) {
return coursePriceMapper.findCoursePrice(courseId);
}
/**
* 获取课程信息与价格
* @return 课程信息与价格列表
*/
@Override
public List<CourseAndPrice> getCourseAndPriceList() {
List<Course> courseList = courseListClient.courseList();
List<CourseAndPrice> courseAndPriceList = new ArrayList<>();
for (Course course : courseList) {
CourseAndPrice courseAndPrice = new CourseAndPrice();
CoursePrice coursePrice = this.getCoursePrice(course.getCourseId());
courseAndPrice.setId(course.getId());
courseAndPrice.setCourseId(course.getCourseId());
courseAndPrice.setName(course.getCourseName());
courseAndPrice.setPrice(coursePrice.getPrice());
courseAndPriceList.add(courseAndPrice);
}
return courseAndPriceList;
}
controller暴露接口
@GetMapping({"/courseAndPrice"})
public List<CourseAndPrice> getCourseAndPrice() {
List<CourseAndPrice> courseAndPriceList = coursePriceService.getCourseAndPriceList();
if (CollectionUtils.isEmpty(courseAndPriceList)) {
return Collections.emptyList();
}
return courseAndPriceList;
}
以上就完成了本服务中课程价格的接口和利用Feign调用别的服务的接口
解决签名校验、登录校验冗余问题
Spring Cloud Zuul是Spring Cloud中的一个组件,它会和eureka进行整合,它本身也作为eureka的一个client被注册到eureka上面去,同时也可以通过eureka获取到其他各个模块的信息,网关可以把这些信息在网关自身上注册,把访问进行收口,比如在外部访问时,两个服务中的端口分别是8081和8082,而Zuul的端口号是8083,那么就可以直接访问8083端口,由网关为我们自动区分。另外还可以利用Zuul进行统一校验,凡是通过Zuul进行访问的,由Zuul进行校验,其他模块不必再进行校验,交给网关来做即可。
API网关允许您将API请求(内部或外部)路由到正确的位置
利用网关可以实现统一建权和正确路由两个功能。
新建一个maven项目的zuul模块
把自己注册到Eureka注册中心
eureka和网关依赖;同时由于它是一个springboot项目给它配置一个plugin,主项目的依赖中就会多一个zuul模块
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-zuulartifactId>
dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
创建一个网关启动类
//网关注解
@EnableZuulProxy
@SpringCloudApplication
public class ZuulGetawayApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulGetawayApplication.class, args);
}
}
application
spring:
application:
name: course-gateway
logging:
pattern:
console: '%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:-}){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:}){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}'
server:
port: 9000
eureka:
client:
service-url:
defaultZone: http://localhost:8000/eureka/
zuul:
prefix: /all
routes:
course-list:
path: /list/**
service-id: course-list
course-price:
path: /price/**
service-id: course-price
启动后访问eureka就可以看到启动的网关
访问 http://localhost:9000/要访问的模块名(或者上面自定义配置)/接口名 就可以访问了
http://localhost:9000/all/price/courseAndPrice
pre 过滤器在路由请求之前运行
route 过滤器可以处理请求的实际路由
post 路由请求后运行过滤器
error 如果在处理请求的过程中发生错误,则过滤器将运行
//请求处理前的过滤器
@Component
public class PreRequestFilter extends ZuulFilter {
@Override
public String filterType() {
//选择过滤器类型
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
//过滤器排序,在过滤器多的时候用到
return 0;
}
@Override
public boolean shouldFilter() {
//是否启用过滤器
//可实现复杂逻辑过滤某些链接
return true;
}
@Override
public Object run() throws ZuulException {
//过滤器逻辑
RequestContext currentContext = RequestContext.getCurrentContext();
currentContext.set("startTime", System.currentTimeMillis());
System.out.println("过滤器已经记录时间");
return null;
}
}
//请求处理后的过滤器
@Component
public class PostRequestFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
Long startTime = (Long) currentContext.get("startTime");
long duration = System.currentTimeMillis() - startTime;
String requestURI = currentContext.getRequest().getRequestURI();
System.out.println("uri:" + requestURI + "处理时长:" + duration);
return null;
}
}