springCloud 核心组件
Springcloud
一 springcloud 过滤网关
1.1 网关基础知识
从架构图中可以看出,客户端请求微服务时,先经过Zuul之后再请求,这样就可以将一些类似于校验的业务逻辑放到zuul中完成。而微服务自身只需要关注自己的业务逻辑即可。
实现步奏
1 引入zuul的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
2 指定网关的启动类
@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
3设置yml
server:
port: 6677 #服务端口
spring:
application:
name: itcasst-microservice-api-gateway #指定服务名
zuul:
routes:
item-service: #item-service这个名字是任意写的
path: /item-service/** #配置请求URL的请求规则
url: http://127.0.0.1:8081 #真正的微服务地址
4 测试结果
如图所示我们访问的6677端口已经访问了/item-service映射路径之后,已经映射到了到了真实的服务地址8081了,已经经过zuul穿透到具体的服务地址了。
由于我们的商品服务如上图所示,写的是真实的主机地址,这不服务我们使用注册中心的思想,所以我们得更改yml的配置
server:
port: 6677 #服务端口
spring:
application:
name: itcasst-microservice-api-gateway #指定服务名
zuul:
routes:
item-service: #item-service这个名字是任意写的
path: /item-service/** #配置请求URL的请求规则
#url: http://127.0.0.1:8081 #真正的微服务地址
serviceId: itcast-microservice-item #指定Eureka注册中心中的服务id
eureka:
client:
registerWithEureka: true #是否将自己注册到Eureka服务中,默认为true
fetchRegistry: true #是否从Eureka中获取注册信息,默认为true
serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址
defaultZone: http://itcast:aaa@qq.com:6868/eureka/
instance:
prefer-ip-address: true #将自己的ip地址注册到Eureka服务中
如上所示是修改以后的yml配置信息。如上测试可以得到相同的测试结果。
1.2 单点登录认证系统
如上所示是单点登录的设计流程, 单点登录服务参考代码
@RestController
@EnableEurekaClient
@SpringBootApplicationpublic class SsoServerApplication {
public static void main(String[] args) {
SpringApplication.run(SsoServerApplication.class, args);
}
@Autowired
private StringRedisTemplate template;
/**
* 判断key是否存在
*/
@RequestMapping("/redis/hasKey/{key}")
public Boolean hasKey(@PathVariable("key") String key) {
try {
return template.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 校验用户名密码,成功则返回通行令牌(这里写死huanzi/123456)
*/
@RequestMapping("/sso/checkUsernameAndPassword")
private String checkUsernameAndPassword(String username, String password) {
//通行令牌
String flag = null;
if ("huanzi".equals(username) && "123456".equals(password)) {
//用户名+时间戳(这里只是demo,正常项目的令牌应该要更为复杂)
flag = username + System.currentTimeMillis();
//令牌作为key,存用户id作为value(或者直接存储可暴露的部分用户信息也行)设置过期时间(我这里设置3分钟)
template.opsForValue().set(flag, "1", (long) (3 * 60), TimeUnit.SECONDS);
}
return flag;
}
/**
* 跳转登录页面
*/
@RequestMapping("/sso/loginPage")
private ModelAndView loginPage(String url) {
ModelAndView modelAndView = new ModelAndView("login");
modelAndView.addObject("url", url);
return modelAndView;
}
/**
* 页面登录
*/
@RequestMapping("/sso/login")
private String login(HttpServletResponse response, String username, String password, String url) {
String check = checkUsernameAndPassword(username, password);
if (!StringUtils.isEmpty(check)) {
try {
Cookie cookie = new Cookie("accessToken", check);
cookie.setMaxAge(60 * 3);
//设置域// cookie.setDomain("huanzi.cn");
//设置访问路径
cookie.setPath("/");
response.addCookie(cookie);
//重定向到原先访问的页面 response.sendRedirect(url);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
return "登录失败";
}
}
1 核心实现首先如果是登录页面,直接跳转登录页面
2 其次如 调用用户服务验证用户名与密码
3 如果登录成功 生成token放入缓存 放入cookies返回到界面 重定向到原先访问的url
4 如果登录失败
**
* Zuul过滤器,实现了路由检查
*/public class AccessFilter extends ZuulFilter {
@Autowired
private SsoFeign ssoFeign;
/**
* 通过int值来定义过滤器的执行顺序
*/
@Override`
public int filterOrder() {
// PreDecoration之前运行
return PRE_DECORATION_FILTER_ORDER - 1;
}
/**
* 过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型:
* public static final String ERROR_TYPE = "error";
* public static final String POST_TYPE = "post";
* public static final String PRE_TYPE = "pre";
* public static final String ROUTE_TYPE = "route";
*/
@Override
public String filterType() {
return PRE_TYPE;
}
/**
* 过滤器的具体逻辑
*/
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response = ctx.getResponse();
//访问路径
StringBuilder url = new StringBuilder(request.getRequestURL().toString());
//从cookie里面取值(Zuul丢失Cookie的解决方案:https://blog.csdn.net/lindan1984/article/details/79308396)
String accessToken = request.getParameter("accessToken");
Cookie[] cookies = request.getCookies();
if (null != cookies) {
for (Cookie cookie : cookies) {
if ("accessToken".equals(cookie.getName())) {
accessToken = cookie.getValue();
}
}
}
//过滤规则:
//访问的是登录页面、登录请求则放行
if (url.toString().contains("sso-server/sso/loginPage") ||
url.toString().contains("sso-server/sso/login") ||
//cookie有令牌且存在于Redis
(!StringUtils.isEmpty(accessToken) && ssoFeign.hasKey(accessToken))
) {
ctx.setSendZuulResponse(true);
ctx.setResponseStatusCode(200);
return null;
} else {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
//如果是get请求处理参数,其他请求统统跳转到首页 这里会出现get参数丢失 所以要处理get参数的重新拼接
String method = request.getMethod();
if("GET".equals(method)){
url.append("?");
Map<String, String[]> parameterMap = request.getParameterMap();
Object[] keys = parameterMap.keySet().toArray();
for (int i = 0; i < keys.length; i++) {
String key = (String) keys[i];
String value = parameterMap.get(key)[0];
url.append(key).append("=").append(value).append("&");
}
//处理末尾的&符合
url.delete(url.length() -1,url.length());
}else{
//首页链接,或者其他固定页面
url = new StringBuilder("XXX");
}
//重定向到登录页面
try {response.sendRedirect("http://localhost:10010/sso-server/sso/loginPage?url=" + url);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
/**
* 返回一个boolean类型来判断该过滤器是否要执行
*/
@Override
public boolean shouldFilter() {
return true;
}
}
1 需要执行认证 如果登录成功 放行
2 如果登录失败 带上原有的请求的url 到登录认证服务
3 网关配置
zuul.routes.sso-server.path=/sso-server/**
zuul.routes.sso-server.service-id=sso-server
zuul.host.socket-timeout-millis=60000
zuul.host.connect-timeout-millis=10000#Zuul丢失Cookie的解决方案:https://blog.csdn.net/lindan1984/article/details/79308396
zuul.sensitive-headers=
4 feig nClient调用端
@FeignClient(name = "sso-server", path = "/")public interface SsoFeign {
/**
* 判断key是否存在
*/
@RequestMapping("redis/hasKey/{key}")
public Boolean hasKey(@PathVariable("key") String key);
解决 zuul中cookies丢失的问题
springcloud 配置智能路由zuul 后 转发请求指定的方法后会导致 cookie 无法获取的问题,主要解决方法是
再application 配置文件中加入 sensitive-headers:
zuul:
routes:
yyxt:
path: /**
serviceId: com.modou.dpt
sensitive-headers:
custom-sensitive-headers: true
我的是这样的,具体原理是zuul 中 sensitiveHeaders的默认值初始值是"Cookie", "Set-Cookie", "Authorization"这三项,可以看到Cookie被列为了敏感信息,所以不会放到新header中
二 springcloud 配置中心
2.1 配置中心原理
之前的配置存在什么问题,在我们开发项目的时候,需要很多的配置项需要写在配置文件中,比如数据连接的配置,其他的一些公共配置。如果系统在运行过程中,数据库连接地址突然切换了,那我们要怎么处理等等的问题。
怎么解决这个问题呢,spring cloud config 分配式配置中心,统一管理配置文件,以及实时更新,不需要重新启动应用程序。
Spring Cloud Config 为分布式系统外部化配置提供了服务端和客户端的支持,它包括了Config Server 和Config Client两部分。由于Config Server和Config Client 都实现了对Spring Enviroment和PropertySource抽象的映射,因此,Spring Cloud Config 非常适合Spirng应用程序,当然也可以与任何其他编写的应用程序配合使用。
Config Server是一个可横向扩展、集中式的配置服务器,它用于集中管理应用程序各个环境下的配置,默认使用Git存储配置文件内容,也可以使用SVN存储,或者是本地文件存储。
Config Client是Config Server的客户端,用于操作存储在Config Server中的配置内容。微服务在启动时会请求Config Server获取配置文件的内容,请求到后再启动容器。其实也可以说ConfigClient就是我们的业务服务类。
2.2 搭建配置中心
2.2.1准备配置文件
准备3个文件:
microservice-dev.properties
microservice-production.properties
microservice-test.properties
该文件的命名规则是:{application}-{profile}.properties
其内容是(另外2个文件内容稍有不同即可):
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/taotao?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
jdbc.username=root
jdbc.password=root
推送文件到git服务器,这里使用的是我们内网的git服务器(Gogs),当然也可以使用github或者使用svn。
2.2.2构建config server
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
</dependencies>
@EnableConfigServer //开启配置服务
@SpringBootApplication
public class ConfigApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigApplication.class, args);
}
}
Yml配置
server:
port: 6688 #服务端口
spring:
application:
name: itcasst-microservice-config-server #指定服务名
cloud:
config:
server:
git: #配置git仓库地址
uri: http://xxxxxx:10080/zhangzhijun/itcast-config-server.git
#username: zhangzhijun
#password: 123456
测试结果
如上图客户端依赖的配置中心。但是如上图配置改变了之后,你会发现实际上客户端并没有更新,那么怎么解决呢,两种办法,一种是手动刷新配置文件,一种是自动刷新。我们先看一下配置的处理方式。
1 手动刷新,先加入依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2 在要更新的实体商添加RefreshScope的注解
3 开启actuator
server:
port: 8181 #服务端口
spring:
application:
name: itcast-microservice-item #指定服务名
logging:
level:
org.springframework: INFO
eureka:
client:
registerWithEureka: true #是否将自己注册到Eureka服务中,默认为true
fetchRegistry: true #是否从Eureka中获取注册信息,默认为true
serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址
defaultZone: http://itcast:aaa@qq.com:6868/eureka/
instance:
prefer-ip-address: true #将自己的ip地址注册到Eureka服务中
ipAddress: 127.0.0.1
management:
security:
enabled: false #是否开启actuator安全认证
2.2.3手动更新测试
刷新Config Client的地址:
为了测试,需要再次修改配置文件的内容,将原来的端口4444改为5555。
可以看到Config Client中依然是4444:
然后,post请求/refresh来更新配置内容:
响应:
可以看到有配置文件内容更新了。
可以看到应该更新了最新的配置文件内容,我们也就实现了在未重启项目的情况下实现了动态修改配置文件内容。
但是,这并不实用,原因是项目已经发布上线了,不可能人为的守在服务器前,发现有更新了就手动请求/refresh.实现config-client 的自动刷新,但是这个并不适用,关键在于怎么,项目已经发布上线了,人不可能每天都守着服务器。
2.2.4自动更新测试
gogs、github等git服务器提供了web hook功能,意思是,在仓库中的资源发生更新时会通知给谁,这里的谁是一个url地址。
查看本机ip地址:
点击“添加Web钩子”。
总结下流程:
如上图所示,首先Config Client 通过git/svn的web hook通知,触发refresh函数自动拉去配置中心的配置文件,就实现了自动更新的目的。但是又有一个问题产生了,如上所示,一般我们的项目都会有一大堆的服务,不可能通过手动刷新去一个一个的刷新的,那么怎么解决这种问题呢。
2.2.5批量更新 Spring Cloud Bus
2.2.6 spring cloud bus 系统架构
在configclient中添加
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
三 springcloud 注册中心
由上图可以看出:
- 服务提供者将服务注册到注册中心
- 服务消费者通过注册中心查找服务
- 查找到服务后进行调用(这里就是无需硬编码url的解决方案)
- 服务的消费者与服务注册中心保持心跳连接,一旦服务提供者的地址发生变更时,注册中心会通知服务消费者
注册中心注册中心Eureka
Eureka是Netflix开源的服务发现组件,本身是一个基于REST的服务。它包含Server和client两部分。Spring Cloud将它集成在子项目Spring Cloud Netflix中,从而实现微服务的注册与发现。
Eureka包含两个组件:Eureka Server和Eureka Client。
Eureka Server提供服务注册服务,各个节点启动后,会在Eureka Server中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。
Eureka Client是一个java客户端,用于简化与Eureka Server的交互,客户端同时也就别一个内置的、使用轮询(round-robin)负载算法的负载均衡器。
在应用启动后,将会向Eureka Server发送心跳,默认周期为30秒,如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)。
Eureka Server之间通过复制的方式完成数据的同步,Eureka还提供了客户端缓存机制,即使所有的Eureka Server都挂掉,客户端依然可以利用缓存中的信息消费其他服务的API。综上,Eureka通过心跳检查、客户端缓存等机制,确保了系统的高可用性、灵活性和可伸缩性。
构建Eureka服务注册中心
1 引入Eureka依赖
<!-- 导入Eureka服务的依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
2 配置Eureka启动类
@EnableEurekaServer //申明这是一个Eureka服务
@SpringBootApplication
public class EurekaServer {
public static void main(String[] args) {
SpringApplication.run(EurekaServer.class, args);
}
}
3 编写application.yml文件
server:
port: 6868 #服务端口
eureka:
client:
registerWithEureka: false #是否将自己注册到Eureka服务中,本身就是所有无需注册
fetchRegistry: false #是否从Eureka中获取注册信息
serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址
defaultZone: http://127.0.0.1:${server.port}/eureka/
4 启动测试
四 SpringCloud 负载均衡
4.1 Feign的简介
Feign 是Netflix开发的声明式,模板化的HTTP客户端,其灵感来自于Retrofit,JAXRS-2.0 以及WebSocket。Feign可帮助我们更新便捷。优雅地调用HTTP API.
在Spring Cloud中,使用Feign非常简单---创建一个接口,并在接口上添加一些注解,代码就完成了。Feign支持多种注解,例如Feign自带的注解或者JAX-RS注解。
Spring Cloud 对Feign进行了增强,使Feign支持SpringMVC注解,并整合了Ribbon和Eureka,从而让Feign的使用更加方便。
1 引入jar包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
2 创建Feign的接口
@FeignClient(value = "itcast-microservice-item") // 申明这是一个Feign客户端,并且指明服务id
public interface ItemFeignClient {
// 这里定义了类似于SpringMVC用法的方法,就可以进行RESTful的调用了
@RequestMapping(value = "/item/{id}", method = RequestMethod.GET)
public Item queryItemById(@PathVariable("id") Long id);
}
3 改造RestController
4设置启动类
5测试结果
五 SpringCloud 短路器(Hystrix)
若一个单元出现故障,就很容易因为依赖关系而引发故障的蔓延,最终导致整个系统的瘫痪,这样的架构相对较传统的架构更加不稳定。为了解决这样的问题,产生了短路器等一系列的服务保护机制。
针对上述问题,Spring Cloud Hytrix 实现了短路器,线程隔离等一系列服务保护功能。他也是基于Netflix的开源框架Hystrix实现的,该框架的目标在于通过控制那些访问远程系统,服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备服务降级,服务熔断,线程和信号隔离,请求缓存,请求整合以及服务监控等强大功能。
雪崩效应
在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。服务雪崩效应是一种因“服务提供者”的不可用导致“服务消费者”的不可用,并将不可用逐渐放大的过程。
如果下图所示:A作为服务提供者,B为A的服务消费者,C和D是B的服务消费者。A不可用引起了B的不可用,并将不可用像滚雪球一样放大到C和D时,雪崩效应就形成了。
1创建hystrix 引入jar包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
2 创建容错回调函数
@HystrixCommand(fallbackMethod = "queryItemByIdFallbackMethod") // 进行容错处理
public Item queryItemById(Long id) {
String serviceId = "itcast-microservice-item";
return this.restTemplate.getForObject("http://" + serviceId + "/item/" + id, Item.class
);
}
public Item queryItemByIdFallbackMethod(Long id){ // 请求失败执行的方法
return new Item(id, "查询商品信息出错!", null, null, null);
}
指定hystrix方法。
3 创建启动类
4 测试结果