# 微服务技术栈 ## 远程调用 ### java请求http资源 启动项 ```java @MapperScan("cn.itcast.order.mapper") @SpringBootApplication public class OrderApplication{ public static void main(String[] args){ SpringApplication.run(OrderApplication.class,args); } @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } } ``` service 中的 queryOrderById ```java @Service public class OrderService{ @Autowired private RestTemplate restTemplate; public Order queryOrderById(Long orderId){ Order order=orderMapper.findById(orderId); String url ="http://localhost:8081/user/"+order.getUserId(); User user=restTemplate.getForObject(url,User.classs); order.setUser(user); return order; } } ``` Feign ![](src/2024_03_20_21_39_30.png) ![](src/2024_03_20_21_40_20.png) @LoadBalanced 是 Spring Cloud 中的一个注解,用于在使用 RestTemplate 进行 HTTP 请求时实现负载均衡。 RestTemplate 服务远程调用 发送http请求 ```java @Bean public RestTemplate xxrestTemplate(){     return new RestTemplate(); } @Autowired private RestTemplate kkrestTemplate; String xxurl = "http://localhost:8081/user/"+order.getUserId(); User gguser = kkrestTemplate.getForObject(xxurl, User.class);//第一个参数是路径,第二个参数是你想要拿到什么类型的数据 order.setUser(gguser);//把向用户项目拿到的数据封装到这个订单项目 ``` ## Feign Feign声明式客户端 RestTemplate 发起远程调用的代码 ```java String url="http://userservice/user/"+order.getuserId(); User user=restTemplate.getForObject(url,User.class); ``` 声明式 编写Feign客户端 类似操作数据库的mapper 例子 ```java @FeignClient("userservice") public interface UserClient{ GetMapping("/user/{id}") User findById(@PathVariablke("id") Long id); } ``` 引入使用 1. 引入依赖 ```yml org.springframework.cloud spring-cloud-start-openfeign ``` 2. 添加注解 在启动类上标注`@EnableFeignClient` 配置 yml配置 ```yml feign: client: config: default: loggerLevel: FULL ``` java代码配置 新建一个类 ```java public class FeignClientConfiguration{ @Bean public Logger.Level feignLogLevel(){ return Logger.Level.BASIC; } } ``` 如果是全局配置,放到`@EnableFeignClients`注解中 ```java @EnableFeignClients(defaultConfigutation=FeignClietnConfiguration.class) ``` 如果是局部配置 注解添加到`public interface UserClient{}`上 ```java @FeignClient(value="userservice",configuration=FeignClientConfiguration.class) ``` Feign的性能优化 Feign的底层实现 - URLConnection: **默认** ,不支持连接池 - Apache HttpClient: 支持连接池 - OKHttp: 支持连接池 1. 使用连接池替代默认的 2. 日志级别最好用basic或none Feign 添加Http Client的支持 - 引入依赖 ```yml io.github.openfeign feign-httpclient ``` - 配置连接池 ```yml feign: client: config: default: loggerLevel: BASIC httpclient: enable: true max-connections: 200 # 最大连接数 max-connections-per-route: 50 # 每个路径的最大连接数 ``` 企业中Feign的两种最佳实践 1. 继承 给**消费者FeignClient**和**提供者的controller**定义统一的父接口作为标准 - 但是会产生耦合 - 对springmvc不起效(父接口参数列表中的映射不会被继承) ```java public interface UserAPI{ @GetMapping("/user/{id}") User findById(@PathVariable("id")) } ``` 客户端 ```java @FeignClient(value="userservice") public interface UserClient extends UerAPI{ } ``` 服务端 ```java @RestController public class UserController implements UserAPI{ } ``` 2. 抽取FeignClient为独立模块,并且把接口有关的POJO,默认的Feign配置都i放在这个文件夹 order-service,pay-service引用上述的独立模块 远程调用user-service 实践 1. 删除service中的Client,Config,User类 2. 新建项目,引入feign的统一api,引入自己写的包 ```xml cn.itcast.demo feign-api 1.0 ``` 3. 修改原来代码中的类的引入位置 4. 发现无法自动注入成功 两种方式 - 指定FeignClient所在的包 @EnableFeignClients(basePackages="cn.itcast.feign.clents") 适合批量 - 指定FeignClient的字节码 @EnableFeignClients(clients={UserClient.class}) 适合单独 ## 注册中心 Eureka Nacos ### Nasco 服务分级存储 1. 一级是服务 2. 二级是集群 例如杭州 上海 3. 三级是实例 服务搭建 windows平台下载后用命令行直接运行 服务注册到nacos 在clouud-demo父工程中添加spring-cloud-alibaba的管理;依赖 ```xml com.alibaba.cloud spring-cloud-alibab-dependencies 2.2.5.RELEASE pom import ``` 添加nacos的客户端依赖 ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discover ``` 修改user-service&order-service中的application.yml文件 ```yml spring: cloud: nacos: server-addr: localhost:8848 ``` 配置集群属性 ```yml spring: cloud: nacos: discovery: cluster-name: HZ #设置集群名称,例如杭州 ``` 优先修改相同集群的,要修改负载均衡规则 ```yml userservice: ribbon: NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule ``` 相同集群中随机挑选 权重 Nacos控制台中编辑,即可修改权重 ``` 0.1 1 频率越高 0 不被访问 ``` 环境隔离namespace 不同namespace下的服务不可见 树状图 ``` namespace group service/data ``` ```yml spring: cloud: nacos: discovery: namespace: 492a7d5d-237b-46a1-a99a-fa8e # 此处填id ``` 临时实例和非临时实例 临时实例 采用心跳检测,服务挂掉之后,会直接删除 非临时实例,采用nacos主动询问,挂掉后会报不健康, 不会删除 Eureka 采用pull nacos 采用pull + push, 主动推送变更消息 ```yml spring: cloud: nacos: discovery: ephemeral: false ``` nacos 配置管理 放入热更新的配置 新建配置 DataID: userservice-dev.yaml Group: DEFAULT_GROUP 配置文件的内容 ```yaml pattern: dateformat: yyyy-MM-dd HH:mm:ss ``` nacos 中的配置文件会和application.yml中合并 顺序: bootstrap.yml->nacos配置文件->application.yml 1. 引入nacos配置管理依赖 ```yml com.alibaba.cloud spring-cloud-starter-alibab-nacos-config ``` 2. 在userserives 中的resource目录中添加一个`bootstrap.yml`文件,这个文件是引导文件,优先级高于`application.yml` ```yml spring: applicatioin: name: userservice # 服务名字 profiles: active: dev # 环境 cloud: nacos: server-addr: localhost:8848 # nacos地址 config: file-extension: yaml # 文件后缀名 ``` 3. 测试配置是否正常 在controller中测试 ```java @Slf4j @RestController @RequestMapping("/user") @RefreshScope public claass UserController{ @Atuowired private UserService userService; @Value("${pattern.dataformat}") private String dataformat; @GetMapping("now") public String now(){ return LocalDataTime.now().format(DataTimeFormatter.ofPattern(dataformat)); } } ``` 4. 在`@Value`注入的变量所在类上添加注解,添加`@RefreshScope`注解 5. 用`ConfigurationProperties`会自动刷新 ```java @Data @Component //注册为bean 都可以使用 @ConfigConfigurationProperties(prefix="pattern") public calss PatternProperties{ private String dataformate; } ``` 多环境共享配置 在nacos中新建配置,为`userservice.yaml` ```yaml pattern: envSharedValue: 环境共享属性 ``` 在Config / PatternProperties 中 ```java @Data @Component //注册为bean 都可以使用 @ConfigConfigurationProperties(prefix="pattern") public calss PatternProperties{ private String envSharedValue; } ``` 优先级 ``` profile.yaml -> 服务名称.yaml -> 本地配置 ``` nacos集群搭建 1. 搭载mysql集群并初始化数据表 新建一个数据库,命名为nacos,而后导入下面的sql ...... 2. 下载解压nacos,修改配置多个,不同端口,其他全相同,分别启动 进入nacos的config目录,修改配置文件cluster.conf.example,重命名为cluster.conf 添加节点信息 ``` 127.0.0.1:8844 127.0.0.1:8845 127.0.0.1:8846 ``` 打开mysql的数据源(去掉#) 密码账号还有url `db.num=1` 分别更改三个server.port 启动 `startup.cmd` 3. nginx 负载均衡 在最后追加 config/nginx.conf ```conf upstream nacos-cluster{ server 127.0.0.1:8845 server 127.0.0.1:8846 server 127.0.0.1:8847 } server{ listen: 80; server_name localhost; location /nacos{ # 访问/nacos这个路径 proxy_pass http://nacos-cluster; } } ``` ### Eureka ![](src/2023_12_14_17_08_22.png) eurekaApplication order 访问两个user服务 user user 服务端 ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-server ``` 启动类加入`@EnableEurekaSServer`注解 ```yml server: port: 10086 spring: application: name: eurekaserver # 服务名称 eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka/ # 地址信息 ``` 客户端 引入eurekaclient依赖`变化` 在application.yml添加名称和地址`不变` 仍为`defaultZone: http://127.0.0.1:10086/eureka/ # 地址信息` 使用和服务配置 修改路径 ```java String url="http://localhost:8081/user" 改为 String url="http://userservice/user" ``` 启动项目中加入`@LoadBalance` 不是给启动项加,给RestTemplate 类加 ## 负载均衡 Ribbon ### Ribbon IRule 自定义 1. 在application类中定义一个新的rule ```java @Bean public IRule randomRule(){ return new RandomRule(); } 对所有微服务都随机(这个类中的) ``` 2. 在配置文件中修改 ```yml userservice: ribbon: NFLoadBalancecrRuleClassName: com.netflix.loadbalancer.RandomRule ``` 对某个指定的微服务而言的(这个类中的) 饥饿加载 启动时候就创建类 ```yml ribbon: eager-load: enable: true clients: userservice # 针对这个类 ``` ## 网关 Gateway SpringGateWay 网关 请求转发 安全认证 流量控制 负载均衡 降级熔断 参数校验 请求转发+请求过滤 先nginx 再 网关 SpringGateWay 实现原理 通过过滤器来处理请求 流程 路由判断 请求过滤 服务处理 后端处理 响应过滤 后端处理后再次进行过滤 响应返回 SpringCloud GateWay 断言 如果客户端发送的请求满足了断言的条件,则映射到指定的路由器 可以在配置文件中写明 可以对Cookies 时间 Header Method Path Query 配置 一个路由规则匹配多个断言 如果一个请求可以匹配多个路由,则映射第一个匹配成功的路由 如何实现动态路由 结合`Nacos or Eureka `注册中心,将配置写到注册中心,进而避免重启 SpringCloud GateWay 过滤器 分类 第一种分类方法 Pre 请求到微服务之前,可以进行拦截和修改 Post 请求完之后,可以修改内容或者响应头 第二种分类方法 GatewayFilter 局部过滤器 应用在单个路由 GlobalFilter 全局 应用在所有路由器上的过滤器 SpringCloud GateWay 支持限流吗 支持 基于Redis 配置全局异常处理 实现 ErrorWebExceptionHandler 并重写 handler 方法 ### GateWay springcloud中网关的实现包括两种 - gateway 响应式 性能更好 - zuul 阻塞式编程,基于servlet ```xml org.springframework.cloud spring-cloud-starter-gateway com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` 创建主类 ```java public class GatewayApplication{ public static void main(String[] args){ SpringApplication.run(GatewayApplication.class,args); } } ``` 编写路由配置以及nacos地址 ```yml server: port: 10010 # 网关端口 spring: application: name: gateway # 服务名称 cloud: nacos: server-addr: localhost:8848 #nacos地址 gateway: routes: - id: user-service # 路由id,唯一即可 uri: lb://userservice # lb 就是负载均衡 后面跟上服务名称 loadbalance # uri: http://127.0.0.1:8081 # 固定地址 predicates: # 断言 - Path=/user/** ``` AddRequstHeader GatewayFilter Factory ```yml spring: cloud: gateway: routes: filters: - AddRequestHeader=Truth,Itcast is freaking awsome! ``` ```java @FeignClient("userservice") public interface UserClient{ GetMapping("/user/{id}") User findById(@PathVariablke("id") Long id,RequestHeader(value="truth",required=false)String truth); } ``` or 对于所有的都过滤 ```yml spring: cloud: gateway: default-filters: - AddRequestHeader=Truth,Itcast is freaking awsome! ``` 全局过滤器 GlobalFilter 通过重写,进而自定义 自定义全局顾虑器,判断参数中是否有authorization,有的话判断是不是admin,同时满足,则放行 新建一个类`AuthorizeFilter` ```java @Component public class AuthorizeFilter implements GlobalFilter,Ordered{ @Override public Mono filter(ServerWebExchange exchange,GatewayFilterChain chain){ ServerHttpRequest request=exchange.getRequest(); MultiValueMap params=request.getQueryParams(); String auth=params.getFirst("authorization"); if("admin".equal(auth)){ return chain.filter(exchange); } exchange.getResponse().setStatusCode(HttoStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } @Override public int getOrder(){ return -1; } } ``` order 值越小,优先级越高,执行顺序越往前 GlobalFilter 通过实现Ordered接口,或者添加@Order注解来指定Order 路由过滤器和dedfaultFilter的order由Spring指定,默认是按照从1递增 当过滤器的order值一样时,会按照`defaultFilter>路由过滤器>GlobalFilter`的顺序执行 ### 跨域问题 ```yml spring: cloud: gateway: globalcors: add-to-simple-url-handler-mapping: true #解决询问的options请求被拦截 crosssConfigurations: '[/**]': allowedOrigins: # 允许那些跨域请求 -"http://localhost:8090" -"http://www.leyou.com" allowedMethods: # 允许的ajax请求方式 -"GET" -"POST" -"DELETE" -"PUT" -"OPTIONS" allowedHeaders: "*" # 允许在请求中携带的头信息 allowCredentials: true # 是否允许携带cookie maxAge: 360000 #跨域有效期检测 ``` ## 分布式搜索 ES ### ES lucene 开源java语言的搜索引擎类库 1. 易拓展 2. 高性能 1. 只限于java开发 2. 学习曲线陡峭 3. 不支持水平拓展 elasticseacrch 1. 支持分布式,可以水平开发 2. 提供restful接口,可以被任何语言调用 install ```sh docker network create es-net docker load -i es.tar docker - run -d \ --name es\ -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \ -e "discovery.type=single-node" \ -v es-data:/usr/share/elasticsearch/data\ -v es-plugs:/usr/share/elasticsearch/plugins \ --privileged \ --network es-net\ -p 9200:9200 \ -p 9300:9300 \ elasticsearch:7.12.1 ``` `-e "cluster.name=es-docker-cluster"` 设置集群 使用 ```json GET / #测试es是否连接 POST /_analyze { "text": "黑马学习java太棒了", "analyzer":"ik_max_word" //ik_min_word } ``` kibana 给elasticsearch提供可视化界面 安装 ``` docker run -d \ --name kibana -e ELASTICSEARCH_HOSTS=http://es:9200 \ --netwotk=es-net \ -p 5601:5601 \ libana:7.12.1 ``` libana版本要和es版本一样 ik分词器 默认的对中文支持很差 分词器 在线安装 ``` docker exec -it elasticsearch /bin/bash ./bin/elasticsearch-plug install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip exit docker restart elasticsearch ``` 离线安装 ``` docker volume inspect es-plugins 之后将安装包放到指定路径 重启容器 ``` 词库 用户可以自己拓展词典,可以配置停用的词典 在ik分词器的config目录的`analyzer.cfg.xml`文件中 ## 消息队列 rabbit MQ SpringAMQP Kafka ### RabbitMQ SpringAMQP是基于RabbitMQ封装的一套模板 message queue 消息队列,也就是broker 异步通信 right: 提高吞吐量 解耦 wrong: 依赖broker 架构复杂,不利于追踪 安装 官网 docker 使用 ```xml org.springframework.boot spring-boot-starter-amqp ``` ```yml spring: rabbitmq: host: 192.168.150.101 port: 5672 virtual-host: / # 虚拟主机 usrname: itcast password: 123321 ``` 新建测试类,进行测试 ```java @RunWith(SpringRunner.class) @SpingBootTest public class SpringAmpqTest{ @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSimpleQueue(){ String queueName ="simple..queue"; String message="hello,Spring amqp!"; rabbitTemplate.converAndSend(queueName,message); } } ``` 接收消息 ```java @Component public class SpringRabbitListener{ @RabbitListener(queues="simple.queue") public void listenSimpleQueue(String msg){ System.out.println("消费者收到消息:"+msg); } } ``` 修改application.yml 设置preFetch的值,可以控制预取消息的上限 ```yml spring: rabbitmq: listener: simple: prefetch: 1 # 默认好像是无穷大 ``` workqueue ```java @Component public class SpringRabbitListener{ @RabiitListener(queues="simple.queue"); public void ListenWorkQueue1(String msg) throw EXception{ Systemctl.out.println("消费者1接收消息:"+msg); Thread.sleep(20); } @RabiitListener(queues="simple.queue"); public void ListenWorkQueue2(String msg) throw EXception{ Systemctl.out.println("消费者2接收消息:"+msg); Thread.sleep(200); } ``` exchange 引入交换机 ![](http://leaweihou.site:1003/photobed/2023_12_20_22_36_48.png) ```java @Configuration public class FanoutConfig{ @Bean public FanoutExchange fanoutExchange()}{ return new FanoutExchange("itcast.fanout"); } @Bean public Queue fanoutQueue1(){ return new Queue("fanout.queue1"); } @Bean public Binding fanoutBinding1(Queue fanoutQueue1,FanoutExchange fanoutExchane){ return BindingBuilder .bind(fanoutQueue1) .to(fanoutExchane) } @Bean public Binding fanoutBinding1(Queue fanoutQueue1,FanoutExchange fanoutExchane){ return BindingBuilder .bind(fanoutQueue2) .to(fanoutExchane) } @RabbitListener(queues="fanout.queue1") public void listenFanoutQueue1(String msg){ System.out.println(msg); } @RabbitListener(queue="fanout.queue2") public void listenFanoutQueue2(String msg){ System.out.println(msg); } } ``` Test ```java @Test public void testSendFanoutExchange(){ String exchangeName="itcast.fanout"; String message="hello"; rabbitTemplate.convertAndSend(exchangeName,"".message); } ``` 简单声明 && 路由 ```java @RabbitListener(bindings=@QueueBinding( value=@Queue(name="direct.queue1"); exchange=@Exchange(name="itcast.direct",type=ExchangeTypes.DIRECT). key={"red","blue"} )) public void listenDirectQueue1(String msg){ System.out.println(msg); } @RabbitListener(bindings=@QueueBinding( value=@Queue(name="direct.queue2"); exchange=@Exchange(name="itcast.direct",type=ExchangeTypes.DIRECT). key={"yellow","blue"} )) public void listenDirectQueue1(String msg){ System.out.println(msg); } ``` testSendFanoutExchange Test ```java @Test public void testSendFanoutExchange(){ String exchangeName="itcast.direct"; String message="hello"; rabbitTemplate.convertAndSend(exchangeName,"blue".message); } ``` topicExchange 多个单词,以 `.` 分开 `# 0个或者多个` `* 代指一个` ex: 1. china.# 2. #.news ```java @RabbitListener(bindings=@QueueBinding( value=@Queue(name="direct.queue1"); exchange=@Exchange(name="itcast.topic",type=ExchangeTypes.DIRECT). key="china.#" )) public void listenDirectQueue1(String msg){ System.out.println(msg); } @RabbitListener(bindings=@QueueBinding( value=@Queue(name="direct.queue2"); exchange=@Exchange(name="itcast.topic",type=ExchangeTypes.DIRECT). key="#.news" )) public void listenDirectQueue1(String msg){ System.out.println(msg); } ``` Test ```java @Test public void testSendFanoutExchange(){ String exchangeName="itcast.direct"; String message="hello"; rabbitTemplate.convertAndSend(exchangeName,"china.news".message); } ``` ### 消息转换器 jackson 可以让发送的数据为json类型,而不是java序列化的 jackson ```xml com.fasterxml.jackson.core jackson-databind ``` 启动类 ```java @Bean public MessageConverter messageConverter(){ return new Jackson2JsonMessageConverter(); } ``` 消费者,监听object.queue队列并消费信息 ```java @RabbitListener(queues="object.queue") public void listenObjectQueue(Map msg){ System.out.println(msg); } ``` ## 不全85-142