我们用maven管理项目
第一步:选择maven
第二步:项目命名,项目路径
第三步:进入项目,把src文件夹删掉(不删也没事,主要是用不到这个文件夹)
在父项目pom文件加入下面依赖。
注:springboot项目版本与cloud版本有一定的对应关系,在官网可以看到,下面是对应的版本关系,选择合适的即可。
本项目所用依赖版本:
4.0.0
org.example
project_springcloud
1.0-SNAPSHOT
8
8
2.3.8.RELEASE
Hoxton.SR10
2.2.1.RELEASE
org.springframework.boot
spring-boot-dependencies
${spring.boot.version}
pom
import
org.springframework.cloud
spring-cloud-dependencies
${spring.cloud.version}
pom
import
注意:
模拟生产环境,采用Linux部署
第一步:官网下载nacos(Nacos | Nacos),下载不了的可以关注公众号【JavaCoding】回复nacos获取
第二步:进入bin文件,修改startup.sh配置文件,下载的默认启动配置需要的内存太大,改小一点
第三步:命令启动sh startup.sh -m standalone,注:这里standalone参数代表着单机模式运行,非集群模式
控制台输出这样的内容代表启动成功,我们打开start.out日志查看内容:
第一步:新建一个服务service1
要在父级目录下创建service1项目,具体步骤与上述创建父项目类似,过程省略,创建后的项目结构如下:
pom依赖:
project_springcloud
org.example
1.0-SNAPSHOT
4.0.0
service1
8
8
org.springframework.boot
spring-boot-starter-web
${spring.boot.version}
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
${spring.cloud.alibaba.version}
nacos-client
com.alibaba.nacos
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
${spring.cloud.alibaba.version}
nacos-client
com.alibaba.nacos
com.alibaba.nacos
nacos-client
1.4.2
第二步:登录我们启动的nacos地址,新建一个命名空间
第三步:点击配置列表右侧加号,新建一个配置
第四步:yml中配置(注意这里项目中配置文件名一定要是bootstrap)
spring:
application:
name: service1
cloud:
nacos:
discovery:
# 你的服务器ip
server-addr: 114.115.xxx.xx:8848
namespace: 900c8c5e-b442-4b61-abf4-90b640e643ea
config:
server-addr: 114.115.xxx.xx:8848
namespace: 900c8c5e-b442-4b61-abf4-90b640e643ea
prefix: service1
group: DEFAULT_GROUP
file-extension: yaml
最后启动项目:
我们可以看到,项目端口是用的我们nacos里配置里写好的端口
点开nacos服务列表:
我们可以看到我们启动的服务已经注册进来了,这里的服务名就是我们项目里spring.application.name配置的名
最后接口测试:
注意:
我们这里的启动类并没有加@EnableDiscoveryClient注解,是因为从Spring Cloud Edgware开始,这个注解可以省略。但是maven依赖中必须要有spring-cloud-starter-alibaba-nacos-discovery服务注册的依赖。
但是值得注意的是,如果使用了Eureka作为注册中心,那么需要使用@EnableEurekaClient注解来启动Eureka客户端。因此,是否需要省略@EnableDiscoveryClient注解取决于你的具体需求和使用的服务注册中心类型。
第一步:新建Gateway项目,过程省略,项目结构如下
第二步:引入pom依赖
project_springcloud
org.example
1.0-SNAPSHOT
4.0.0
gateway
8
8
org.springframework.cloud
spring-cloud-starter-gateway
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
${spring.cloud.alibaba.version}
nacos-client
com.alibaba.nacos
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
${spring.cloud.alibaba.version}
nacos-client
com.alibaba.nacos
com.alibaba.nacos
nacos-client
1.4.2
第三步:nacos新增配置文件,与service1一样,端口我们设置为8002
第四步:配置文件
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: 114.115.xxx.xx:8848
namespace: 900c8c5e-b442-4b61-abf4-90b640e643ea
config:
server-addr: 114.115.xxx.xx:8848
namespace: 900c8c5e-b442-4b61-abf4-90b640e643ea
prefix: gateway
group: DEFAULT_GROUP
file-extension: yaml
第五步:启动项目
在nacos里可以看到我们的Gateway服务已经注册进来了
上面步骤我们只是把Gateway服务启动,并没有体现出Gateway的作用,我们想要看到的是通过访问不通的接口,Gateway可以帮我自动找到到相应的服务。
简单介绍Gateway配置项:
路由(Route):由ID、目标URI、断言集合和过滤器集合组成。如果聚合断言结果为真,则转发到该路由。
(1)id:路由标识,要求唯一,名称任意(默认值 uuid,一般不用,需要自定义)
(2)uri:请求最终被转发到的目标地址
(3)order: 路由优先级,数字越小,优先级越高
(4)predicates:断言数组,即判断条件,如果返回值是boolean,则转发请求到 uri 属性指定的服务中
(5)filters:过滤器数组,在请求传递过程中,对请求做一些修改
断言(Predicate):参照 Java8 的新特性Predicate,允许开发人员匹配 HTTP 请求中的任何内容,比如请求头或请求参数,最后根据匹配结果返回一个布尔值。
Predicate 来自于 Java8 的接口。Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
Predicate 可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。Spring Cloud Gateway 内置了许多 Predict,这些 Predict 的源码在 org.springframework.cloud.gateway.handler.predicate 包中,有兴趣可以阅读一下。
过滤器(Filter):可以在返回请求之前或之后修改请求和响应的内容。
Gateway 过滤器的生命周期:
PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
Gateway 过滤器从作用范围可分为两种:
GatewayFilter:应用到单个路由或者一个分组的路由上(需要在配置文件中配置)
GlobalFilter:应用到所有的路由上(无需配置,全局生效)
有两种方式可以实现Gateway路由配置:
准备工作:我们先按照service1服务创建一个service2服务,端口用8003,创建后的项目架构:
service1和service2接口方法如下:
第一种:yml配置
在Gateway项目的yml中加入以下配置:
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: 114.115.xxx.xx:8848
namespace: 900c8c5e-b442-4b61-abf4-90b640e643ea
config:
server-addr: 114.115.xxx.xx:8848
namespace: 900c8c5e-b442-4b61-abf4-90b640e643ea
prefix: gateway
group: DEFAULT_GROUP
file-extension: yaml
# gateway 配置
gateway:
# 路由数组:指当请求满足什么样的断言时,转发到哪个服务上
routes:
# 路由唯一标识
- id: gateway-service1
# 要转发到哪个服务,我们这里不直接写ip:端口的形式,因为一旦服务的域名或IP地址发生修改,路由配置中的 uri 就必须修改
# 使用了lb形式,从注册中心负载均衡的获取uri
uri: lb://service1
# 设置断言
predicates:
# 满足/service1的请求路径会路由到localhost:8001服务
- Path=/service1/**
# 过滤器
filters:
# 去除原始请求路径中的前1级路径,也就是/service1
- StripPrefix=1
# 路由唯一标识
- id: gateway-service2
# 要转发到哪个服务
uri: lb://service2
# 设置断言
predicates:
# 满足/service1的请求路径会路由到localhost:8001服务
- Path=/service2/**
# 过滤器
filters:
# 去除原始请求路径中的前1级路径,也就是/service1
- StripPrefix=1
启动项目:
通过网关访问service1服务的接口:
通过网关访问service2服务的接口:
结论:虽然两个服务接口地址一样,但通过网关配置,我们可以根据不同的请求路径前缀来访问不通的服务
第二种:代码配置
通过代码我们可以更加灵活的对Gateway进行配置,面对的场景也更加复杂。
第一步:先把yml中Gateway配置注释
第二步:添加自定义配置类
package com.javacoding.config;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.*;
@Component
public class MyRouteDefinitionRepository implements RouteDefinitionRepository {
@Override
public Flux getRouteDefinitions() {
List routeDefinitions = new ArrayList<>();
// service1的route配置
RouteDefinition service1 = setService1();
// service2的route配置
RouteDefinition service2 = setService2();
routeDefinitions.add(service1);
routeDefinitions.add(service2);
return Flux.fromIterable(routeDefinitions);
}
private RouteDefinition setService2() {
RouteDefinition definition = new RouteDefinition();
// id
definition.setId("service2");
URI uri = UriComponentsBuilder.fromUriString("lb://service2").build().toUri();
// uri
definition.setUri(uri);
PredicateDefinition predicate = new PredicateDefinition();
predicate.setName("Path");
Map predicateParams = new HashMap<>(8);
predicateParams.put("pattern", "/service2/**");
predicate.setArgs(predicateParams);
//定义Filter
FilterDefinition filter = new FilterDefinition();
filter.setName("StripPrefix");
Map filterParams = new HashMap<>(8);
//该_genkey_前缀是固定的,见org.springframework.cloud.gateway.support.NameUtils类
filterParams.put("_genkey_0", "1");
filter.setArgs(filterParams);
definition.setFilters(Collections.singletonList(filter));
definition.setPredicates(Collections.singletonList(predicate));
return definition;
}
private RouteDefinition setService1() {
RouteDefinition definition = new RouteDefinition();
// id
definition.setId("service1");
URI uri = UriComponentsBuilder.fromUriString("lb://service1").build().toUri();
// uri
definition.setUri(uri);
PredicateDefinition predicate = new PredicateDefinition();
predicate.setName("Path");
Map predicateParams = new HashMap<>(8);
predicateParams.put("pattern", "/service1/**");
predicate.setArgs(predicateParams);
//定义Filter
FilterDefinition filter = new FilterDefinition();
filter.setName("StripPrefix");
Map filterParams = new HashMap<>(8);
//该_genkey_前缀是固定的,见org.springframework.cloud.gateway.support.NameUtils类
filterParams.put("_genkey_0", "1");
filter.setArgs(filterParams);
definition.setFilters(Collections.singletonList(filter));
definition.setPredicates(Collections.singletonList(predicate));
return definition;
}
@Override
public Mono save(Mono route) {
return null;
}
@Override
public Mono delete(Mono routeId) {
return null;
}
}
第三步:启动项目测试
总结:这些只是基础的配置,实际生产中还会有自定义全局过滤器等。
当我们在service1调用service2中的接口时,传统的项目我们直接用maven引入service2项目就可以直接调用。但在微服务中,各个项目都是独立存在的,我们如果用http去调用每次都非常繁琐,而且不方便拓展。因此Feign就是解决这个问题。
第一步:创建Feign项目,过程省略,项目结构如下
第二步:pom依赖
project_springcloud
org.example
1.0-SNAPSHOT
4.0.0
feign
8
8
org.springframework.cloud
spring-cloud-starter-openfeign
第三步:写feign客户端接口
package com.javacoding.service;
import com.javacoding.service.impl.Service2ClientBack;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "service2", fallback = Service2ClientBack.class)
public interface Service2Client {
@RequestMapping("/test/msg")
public String test(@RequestParam("msg") String msg);
}
说明:
1. @FeignClient注解标识当前接口是feign调用接口,value代表哪个服务调用接口,这里我们想调用service2服务,所以写service2(为什么写service2,因为service2服务里的配置文件spring.application.name,与他保持一致);fallback是如果服务调用失败,我该怎么处理,这个类就是处理这个问题的。
2. 接口请求路径,请求名和方法与service2要调用的接口保持一致。
package com.javacoding.service.impl;
import com.javacoding.service.Service2Client;
import org.springframework.stereotype.Component;
/**
* 调用失败返回
*/
@Component
public class Service2ClientBack implements Service2Client {
@Override
public String test(String msg) {
return "service2服务调用失败!!!";
}
}
第四步:在service1服务中通过feign实现service2接口请求响应
1、在service1的pom中加入feign依赖
org.example
feign
1.0-SNAPSHOT
org.springframework.cloud
spring-cloud-starter-openfeign
2、启动类添加@EnableFeignClients注解
package com.javacoding;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
public class Service1Application {
public static void main(String[] args) {
SpringApplication.run(Service1Application.class, args);
}
}
3、controller添加调用的方法
package com.javacoding.controller;
import com.javacoding.service.Service2Client;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/test")
public class TestController {
@Resource
private Service2Client service2Client;
@GetMapping("/msg")
public String test(@RequestParam("msg") String msg) {
return "service1:" + msg;
}
/**
* 通过feign调用service2的接口
*
* @param msg
* @return
*/
@GetMapping("/feign/msg")
public String test2(@RequestParam("msg") String msg) {
String result = service2Client.test(msg);
return "通过feign调用请求返回:" + result;
}
}
4、测试
总结:我们并没有在service1中引入service2的服务,也实现了调用service2服务中的接口。之后我们想要在微服务中引入其他服务只需写一个某某服务的接口(xxxClient)利用@FeignClient注解指明哪个服务,通过这个接口就可以去调用我们的目标服务的接口。
前面我在@FeignClient注解里加了fallback参数。Fallback是通过Hystrix实现的, 所以需要开启Hystrix,spring boot application.properties文件配置feign.hystrix.enabled=true,这样就开启了Fallback。
service1的nacos中的yml配置:
server:
port: 8001
feign:
hystrix:
enabled: true
service2中服务抛出个异常,模拟服务不可用:
package com.javacoding.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/msg")
public String test(@RequestParam("msg") String msg) {
throw new RuntimeException();
// return "service2:" + msg;
}
}
测试:
如果我们service2服务是非常重要的,我们通过会部署好几个防止其中一个崩溃导致服务不可用,那么service1在通过feign调用的时候到底调用了哪一个service2呢?如何把请求进行分配呢?
第一步:先把service2的nacos配置的端口注释掉,改为写在项目bootstrap.yml里面。经过测试发现,如果不写在项目bootstrap.yml里面,后续我们启动两个service2服务的时候会一直用nacos配置里的端口。
spring:
application:
name: service2
cloud:
nacos:
discovery:
server-addr: 114.115.xxx.xx:8848
namespace: 900c8c5e-b442-4b61-abf4-90b640e643ea
config:
server-addr: 114.115.xxx.xx:8848
namespace: 900c8c5e-b442-4b61-abf4-90b640e643ea
prefix: service2
group: DEFAULT_GROUP
file-extension: yaml
server:
port: 8003
第二步:改造一下service2中的方法,返回信息中加入端口信息,方便区分请求的是哪个服务
package com.javacoding.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/msg")
public String test(@RequestParam("msg") String msg, HttpServletRequest request) {
// throw new RuntimeException();
int serverPort = request.getServerPort();
return "service2:" + msg + ",端口:" + serverPort;
}
}
第三步:启动网关、service1、service2服务,然后复制service2更改端口启动,这样service2服务就启动了两个实例。
所有服务启动后nacos里的实例信息如下:
第四步:测试接口
总结:可以看到我们的请求在8003和8004端口来回切换,这就是默认的轮训策略。
拓展:除了默认的轮训还有哪些策略?
通常我们都是自定义的方式来配置我们的负载均衡策略,这样我们会有更多的操作空间。
在service1中我们自定义配置:
package com.javacoding.config;
import com.netflix.loadbalancer.RandomRule;
import com.netflix.loadbalancer.RoundRobinRule;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class FeignBalanceConfig {
@Bean
@LoadBalanced
public RestTemplate getResTemplate() {
return new RestTemplate();
}
// @Bean
// public RoundRobinRule getRule() {
// // 轮训
// return new RoundRobinRule();
// }
@Bean
public RandomRule getRule() {
//随机策略
return new RandomRule();
}
}
测试随机策略:
还有其他一些策略,不在一一演示了,配置如下:
@Bean
public AvailabilityFilteringRule getRule() {
//首先会过滤掉故障机或者并发链接数超过阈值的服务器.剩余的机器轮询配置
return new AvailabilityFilteringRule();
}
@Bean
public WeightedResponseTimeRule getRule() {
//服务器影响时间越快,则权重越高
return new WeightedResponseTimeRule();
}
@Bean
public BestAvailableRule getRule() {
//最大可用策略,即先过滤出故障服务器后,选择一个当前并发请求数最小的
return new BestAvailableRule();
}
ok,以上内容如果都学会并了解那代表你已经可以用springcloud微服务应对90%开发需求了。当然还有Hystrix、Ribbon组件,虽然没讲如何用,但这两个组件Feign都已经很好了集成了,如果你面对的是非常复杂的开发场景,那也可以单独去配置这两个组件(原理都是一样的)。
希望这篇微服务项目可以带你很好的入门~~~