spring cloud简单搭建 2020-06-18 程序之旅,记录 暂无评论 732 次阅读 [TOC] ## spring cloud简单搭建 spring cloud的搭建是基于spring boot,使用spring cloud能够搭建一个微服务分布式架构,分布式的定义为:只在支持应用程序和服务的开发,可以利用物理架构由多个自治的处理元素,不共享内存,但通过网络发送消息合作。分布式和集群之间是有区别的,在厨房两个员工,一个切菜一个洗碗,那么这个是分布式,如果两个都是在洗碗,那么这就称为集群。 ### 注册服务与客户端 使用spring boot 2.1.3版本,ide使用intellij IDEA spring cloud Eureka基于Netflix Eureka做了二次封装,由Eureka server注册中心,Eureka Client两个组件组成。 #### 搭建注册服务端 新建一个maven项目,命名为`lightning-protection`。使用maven导入依赖包 ```xml org.springframework.cloud spring-cloud-dependencies Greenwich.RELEASE pom import ``` 在该项目下添加一个注册服务端模板 右键项目点击新建`module`,新建maven,命名为`register`。在register模板pom.xml中添加依赖 ```xml com.vulnova common 1.0-SNAPSHOT org.springframework.cloud spring-cloud-starter-netflix-eureka-server org.springframework.cloud spring-cloud-starter-netflix-hystrix org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` 依赖中注入eureka,而eureka server默认基本熔断器(hystrix),所以必须得添加hystrix依赖,否者会报错。eureka-client的添加是让服务注册中心也能够以服务提供者的形式存在。 >common 报错,这里可以先不添加,之后会有介绍 在注册服务模板`register`添加配置文件,文件内容如下; ```yaml # application.yml server: port: 8761 # 注册端口 spring: application: name: register # 服务模板名称 profiles: active: dev # 开发配置引用 cloud: inetutils: preferred-networks: 127.0.0.1 client: ip-address: 127.0.0.1 eureka: server: peer-node-read-timeout-ms: 3000 # 微服务节点连接超时时间 enable-self-preservation: false # 是否开启自我保护,默认为 true,表示客户端不会因为丢失而被注册中心删除 instance: prefer-ip-address: true # 是否以 IP 注册到注册中心 instance-id: ${spring.cloud.client.ip-address}:${server.port} # 注册限制的实例 ID client: registerWithEureka: true fetchRegistry: false healthcheck: enabled: true serviceUrl: defaultZone: http://localhost:8761/eureka/ # 注册中心的默认地址 ``` 创建启动类Application.java ```java import org.springframework.boot.SpringApplication; import org.springframework.cloud.client.SpringCloudApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; /** *服务注册类 */ @SpringCloudApplication @EnableEurekaServer public class Application { public static void main(String[] args) { SpringApplication.run(Application.class,args); } } ``` 启动打开页面 http://127.0.0.1:8761,出现以下页面表示成功。 ![1553651993636](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/typecho/20200618090353.png) 在服务注册中心,也把自己当作服务提供者注册进去。 ![1553652043606](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/typecho/20200618090430.png) ##### eureka的高可用 搭建多个注册中心,如果其中一个注册中心发现异常不能正常使用,会有另一个注册中心进行顶替。 ![Eureka高可用](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/typecho/20200618090437.png) eureka 1注册中心的配置 ```yaml eureka: client: service-url: defaultZone: http://localhost:8762/eureka/ # 注册到eureka 2上 ``` eureka 2 注册中心的配置 ```yaml eureka: client: service-url: defaultZone: http://localhost:8761/eureka/, http://localhost:8762/eureka/ # 把自己也注册到自己的上 ``` 这样就能实现当eureka 1注册中心发生问题的时候,eureka 2就能替换eureka 1的注册功能。 #### 搭建客户端 搭建完成服务注册中心后,那么就可以搭建服务提供者--客户端。同样的,在主项目`lightning-protection`中添加module,名为`client`,在`client`下创建客户端所需要开发的模板,先添加一个index模板。在client的pom.xml依赖注入 ```xml 4.0.0 客户端 client pom index com.vulnova common ``` 在index模板中pom.xml添加依赖: ```xml index 首页模板 org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-netflix-hystrix org.springframework.boot spring-boot-starter-web ``` 添加index模板启动类Application.java: ```java import org.springframework.boot.SpringApplication; import org.springframework.cloud.client.SpringCloudApplication; @SpringCloudApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` 给index服务者模板添加配置文件 ```yaml eureka: client: service-url: defaultZone: http://localhost:8761/eureka # 服务注册中心地址 server: port: 8762 # 服务端口 spring: application: name: index # 服务者名称 ``` 打开服务注册中心地址http://localhost:8761,查看注册实例 ![1553653728967](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/typecho/20200618090444.png) 看到这里这情况表示注册成功。 添加控制层测试效果,在`index`模板下添加controller文件夹,在该文件夹下添加IndexController.java文件 ```java import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class IndexController { @Value("${server.port}") private int port; @RequestMapping("index") public String index(){ return "Hello World! port = " + port; } } ``` 在浏览器中输入http://localhost:9872/index,显示如下表示成功 :) ![1553654629013](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/typecho/20200618090449.png) ### 服务网关 为微服务提供消息转发功能,即中转站。在实际开发中,每一个模板所占用的端口和ip都可能不一样,以这种信息提供给外部调用,效果体验并不是非常好,从使用安全的角度来进行考虑,对于一些非法的请求,需要一一校验,这样对开发的代价也是非常大。 为了解决这一问题,我们就需要一个统一的入口地址,搭建一个服务专用来处理服务请求分配的工作,这就是我们所说的服务网关。 ![网关](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/typecho/20200618090452.png) spring cloud给我们提供了一个解决方案zuul,负责处理路由转发、异常处理和过滤拦截功能实现。 #### 搭建服务网关 在lightning-protection中创建服务`gateway`,添加pom.xml依赖 ```xml org.springframework.cloud spring-cloud-starter-gateway org.springframework.boot spring-boot-starter-webflux org.springframework.cloud spring-cloud-starter-netflix-hystrix org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` > spring cloud gateway依赖webFlux,同时添加熔断器hystrix **创建Appliction.java启动类** ```java import org.springframework.boot.SpringApplication; import org.springframework.cloud.client.SpringCloudApplication; @SpringCloudApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` **添加配置文件** ```yaml server: port: 8080 spring: application: name: gateway # 服务名称 cloud: gateway: discovery: locator: enabled: true # 表示是否与服务发现组件(register)进行结合,默认为false # lower-case-service-id: true # serviceId 可以改为小写 logging: level: # 日志配置策略 org.springframework.cloud.gateway: trace org.springframework.http.server.reactive: debug org.springframework.web.reactive: debug reactor.ipc.netty: debug eureka: client: service-url: defaultZone: http://localhost:8761/eureka/ ``` 路由访问的方式:http://gateway_host:gateway_port/SERVICEID/** > 服务名称必须大写。 例如结合之前创建的index服务。开启服务路由。访问http://localhost:8080/INDEX/index。出现以下情况表示成功。 ![1553655990044](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/typecho/20200618090457.png) #### 服务拦截 实现消息请求的安全校验功能,通过网关统一对消息拦截,其中spring cloud gateway最常用的两个拦截器,GatewayFileter、GlobalFileter。GatewayFileer对单一服务的拦截处理,GlobaFilter对所有的服务进行拦截处理。 **搭建拦截器** 在gateway中创建文件filter,ApiGlobalFilter.java ```java import org.apache.commons.lang.StringUtils; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; /** * 全局拦截器 */ @Component public class ApiGlobalFilter implements GlobalFilter { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 获取请求中变量 String token = exchange.getRequest().getQueryParams().getFirst("token"); // 判断是否为空 if(StringUtils.isBlank(token)) { // 创建响应 ServerHttpResponse response = exchange.getResponse(); Map message = new HashMap<>(); message.put("status", -1); message.put("data", "鉴权失败"); byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8); DataBuffer buffer = response.bufferFactory().wrap(bits); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().add("Content-Type", "text/json;charset=UTF-8"); return response.writeWith(Mono.just(buffer)); } return chain.filter(exchange); } } ``` 该类通过重写filter进行请求验证,由于gateway依赖webflux,因此返回mono对象。访问http://localshot:8080/INDEX/index?token=*出现以下现象表示成功。 ![1553664842550](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/typecho/20200618090503.png) ![1553664861433](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/typecho/20200618090506.png) 简单说说`webflux`是什么。大家对springmvc应该比较了解,springmvc的通过servlet接受消息并处理,每次都会创建一个新的线程来对service()进行处理,而不是在accept进行处理。大概模型如下: ![springmvc](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/typecho/20200618090510.png) 与springmvc不同的是,webflux是直接在loop中进行对象的返回(也就相当于springmvc中的accept),controller直接绑定在loop中,loop处理完消息直接一mono或flux包裹返回。大概模型如下: ![WebFlux](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/typecho/20200618090514.png) 言归正传,接下来是gateway的错误拦截。 #### 错误拦截 如果微服务架构中存在一些服务不能访问,架构服务器就会返回一个500的错误,对用户非常不友好。为了解决服务发生错误并返回不友好页面,需要gateway捕获错误消息并进行处理。 **错误拦截的创建** 创建一个处理异常的自定义类,继承`DefaultErrorWebExceptionHandler` ```java import org.springframework.boot.autoconfigure.web.ErrorProperties; import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler; import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.cloud.gateway.support.NotFoundException; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; import org.springframework.web.reactive.function.server.*; import java.util.HashMap; import java.util.Map; /** * 自定义服务器异常处理 */ public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler { public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) { super(errorAttributes, resourceProperties, errorProperties, applicationContext); } /** * 获取异常属性 * @param request * @param includeStackTrace * @return */ @Override protected Map getErrorAttributes(ServerRequest request, boolean includeStackTrace) { int code = 500; Throwable error = super.getError(request); if (error instanceof NotFoundException) { code = 404; } return response(code, this.buildMessage(request, error)); } /** * 指定响应处理方法为json处理方法 * @param errorAttributes * @return */ @Override protected RouterFunction getRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse); } /** * 根据code获取对应的HttpStatus * @param errorAttributes * @return */ @Override protected HttpStatus getHttpStatus(Map errorAttributes) { int statusCode = (int) errorAttributes.get("code"); return HttpStatus.valueOf(statusCode); } /** * 构建异常消息 * @param request * @param ex * @return */ private String buildMessage(ServerRequest request, Throwable ex) { StringBuilder message = new StringBuilder("Failed to handle" + "request ["); message.append(request.methodName()); message.append(" "); message.append(request.uri()); message.append("]"); if (ex != null) { message.append(": "); message.append(ex.getMessage()); } return message.toString(); } /** * 构建返回的json数据格式 * @param status 状态码 * @param errorMessage 异常消息 * @return */ public static Map response(int status, String errorMessage) { Map map = new HashMap<>(); map.put("code", status); map.put("message", errorMessage); map.put("data", null); return map; } } ``` 将类加载到spring容器中 ```java import com.vulnova.gateway.excption.JsonExceptionHandler; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.result.view.ViewResolver; import java.util.List; @SpringBootConfiguration @EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class}) public class ErrorHandlerConfiguration { private final ServerProperties serverProperties; private final ApplicationContext applicationContext; private final ResourceProperties resourceProperties; private final List viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; public ErrorHandlerConfiguration(ServerProperties serverProperties, ApplicationContext applicationContext, ResourceProperties resourceProperties, List viewResolvers, ServerCodecConfigurer serverCodecConfigurer) { this.serverProperties = serverProperties; this.applicationContext = applicationContext; this.resourceProperties = resourceProperties; this.viewResolvers = viewResolvers; this.serverCodecConfigurer = serverCodecConfigurer; } /** * gateway 启动时执行此方法,键JsonExeptionHandler注入到spring容器当中 * @param errorAttributes * @return */ @Bean @Order(Ordered.HIGHEST_PRECEDENCE) // 注入的优先级,优先级最高 public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) { JsonExceptionHandler exceptionHandler = new JsonExceptionHandler( errorAttributes, this.resourceProperties, this.serverProperties.getError(), this.applicationContext ); exceptionHandler.setViewResolvers(this.viewResolvers); exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters()); exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders()); return exceptionHandler; } } ``` 重新启动`gateway`服务,关闭`index`模块,在浏览器中输入http://localshot:8080/INDEX/index?token=*,出现以下情况表示配置成功。 ![1553669381562](https://mufeng-blog.oss-cn-beijing.aliyuncs.com/typecho/20200618090523.png) ### 服务的调用 搭建完网关解决了外部访问的接口问题,现在是内部服务发送的问题,服务与服务之间能够通过 http 来进行通信,spring cloud 有两种服务调用方式,一种是ribbon + restTemplate;另一种是 feign 。feign 直接使用注解的方式来进行服务调用,并且内部已经集成了 ribbon 负载均衡,这里使用的是 feign 进行服务调用的演示。 Feign 的使用非常简单,只需要在接口处加上注解就能很轻松的对外提供 http 接口服务。 > Feign 默认集成了 Ribbon,默认实现负载均衡 #### 搭建 Feign 服务 在主服务上添加模板`feign`,然后添加依赖 pom.xml ``` 消费服务 org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-netflix-hystrix org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-openfeign-core 2.1.0.RELEASE compile org.springframework.boot spring-boot-starter-test test ``` > 由于spring boot 2.x版本的原因,feign 的依赖包的使用 openfeign-core,才能使用 @EnableFeignClients 注解。如果直接使用 spring cloud alibaba openfeign 的依赖,会直接把 netflix 和 openfeign 的 jar 包给依赖进来。 创建启动类Application.java ```java import org.springframework.boot.SpringApplication; import org.springframework.cloud.client.SpringCloudApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.netflix.feign.EnableFeignClients; @SpringCloudApplication @EnableEurekaClient @EnableFeignClients // 启动 feign 功能 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` > 使用Feign声明成为HTTP客户端,就必须在启动类中加入@EnableFeignClients Feign配置文件 ```xml eureka: client: service-url: defaultZone: http://localhost:8761/eureka/ server: port: 8081 spring: application: name: feign ``` 创建一个服务,配置要调用的地址进行服务消费测试 ```java import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; /** * 服务消费调用接口 */ @FeignClient(value = "index") // 需要调用的服务名称 public interface ApiService { @RequestMapping(value = "/index", method = RequestMethod.GET) String index(); } ``` 进行单元测试 服务拆分的方法论: - 单一职责,松耦合、高内聚 - 关注点分离 - 按职责 - 按通用性 - 按粒度级别 服务和数据的关系 - 先考虑业务功能,再考虑数据 - 无状态服务:无需共享数据才能完成的服务 当时在实际开发中会遇到以下的三个问题: 1. 数据库对象不能直接暴露给外部使用,不能把数据表的实体类映射出去。 2. 持久层或实体类在不同的模块中会出现重复定义的现象 3. 定义 feign 的时候会使用到其他板块的接口,这样模块间会产生耦合性。 解决方法: 多模块 - server:所有的业务逻辑 - client:对外暴露的接口,在启动类中记得添加 client 包的位置,否则就调用不了业务接口,开发者在完成接口服务的时候打成 jar 包,服务调用者能够导入 jar 包,并直接使用 client 包内的接口。 - common:放置公用的对象,里边的对象既能供内部服务的使用,也能供外部服务的调用。 --- ### 2022年3月14日 OpenFeign 默认使用 Java 自带的 URLConnection 对象创建 HTTP 请求,作为 OpenFeign 目前默认支持 Apache HttpClient 与 OKHttp 两款产品。 以使用 OKHttpClient 对象。在启动类中,添加 okhttpclient 的配置。 ```java // spring IOC 容器初始化时构建 okHttpClient 对象 @Bean public okhttp3.OkHttpClient okHttpClient() { return new okhttp3.OkHttpClient.Builder() .readTimeout(10, TimeUnit.SECONDS) .connectTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) // 连接池 .connectionPool(new ConnectionPool()) .build(); } ``` 在 application.yml 中启用 OkHttp ```yaml feign: okhttp: enabled: true ``` 打赏: 微信, 支付宝 标签: java, spring cloud, 分布式, openfeign 本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。