SpringCloud---Nacos

Nacos服务的注册

想要使用项目工程里的服务, 必须要在nacos注册和发现服务

  1. pom添加注册中心依赖

  1. yml文件配置

通过Feign实现远程服务调用

1. pom文件中添加Feign依赖

image-20211204121435731

注意: 加在原调用子工程里

2. 启动类添加@EnableFeignClients注解

image-20211204121449668

3. 新建FeignService接口

image-20211204121507393

package com.jt.consumer.service;

import com.jt.consumer.service.factory.ProviderFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/*@FeignClient描述的接口, 用于定义远程调用规范
 * 其中: name属性的值为远端服务名
 * 同时也会将这个名字作为 RemoteProviderService 接口实现类的对象的Bean名字*/
@FeignClient(name = "sca-provider", contextId = "remoteProviderService", fallbackFactory = ProviderFallbackFactory.class)
public interface RemoteProviderService {


    /*Feign接口是基于方法上的@GetMapping等注解的value属性值
     * 后续会基于这个值调用远端服务的某个方法*/
    @GetMapping("/provider/echo/{msg}")
    String echoMessage(@PathVariable("msg") String msg);  //@PathVariable("msg") JDK1.8之前要定义url里的参数

}

4. 实现调用

  1. 基于GetMapping/PostMapping等注解的Value值调用到具体服务里的目标方法

    • @FeignClient里的参数
      • name: 调用服务的注册名
      • contextId: 定义实现类的对象的Bean名字
      • fallbackFactory: 远程服务回调工厂
  2. 用户访问FeignConsumerController原调用方法

package com.jt.consumer.controller;

import com.jt.consumer.service.RemoteProviderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/consumer/")
public class FeignConsumerController {

    @Autowired
    private RemoteProviderService remoteProviderService;

    /*在此方法中基于Feign实现远程服务调用
    * 外界: http://localhost:8090/consumer/echo/xxx*/
    @RequestMapping("/echo/{msg}")
    public String doFeignEcho(@PathVariable("msg") String msg) {
        return remoteProviderService.echoMessage(msg);
    }
}

  1. 通过@FeignClient描述的接口去调用远程provider服务里的方法

实现远程服务回调

当调用远程服务时出现故障中断, 调用不到, 前端就会报出500错误, 可是使用远程服务回调, 解决报错问题
  1. 通过@FeignClient注解里的fallbackFactory属性定义远程回调工厂接口类名称
  2. 新建ProviderFallbackFactory实现类实现FallbackFactory
package com.jt.consumer.service.factory;

import com.jt.consumer.service.RemoteProviderService;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;
/*远程服务回调工程,当远端服务不可用时,可以
 *通过此工厂创建一个service对象,返回一些友好信息*/
@Component
public class ProviderFallbackFactory implements FallbackFactory<RemoteProviderService> {

    @Override
    public RemoteProviderService create(Throwable throwable) {

//       return new RemoteProviderService() {//匿名内部类
//           @Override
//           public String echoMessage(String msg) {
//               return "服务忙,稍等片刻再访问";
//           }
//       };
        //JDK8中的lambda表达式
        return (msg) -> {
            return "服务器维护中,稍等片刻";
        };
    }
}
注意: 可以使用lambda表达式进行简写
  1. 当用户访问consumer调用provider出现错误时, 会回调到consumer配置的远程回调工厂实现类, 而不会报错

基于RestTemPlate实现远程调用

要调用的Provider服务里的方法

package com.jt.provider.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/*创建一个服务提供方对象
* 处理服务消费端请求*/
@RestController
public class ProviderController {

    @Value("${server.port}")
    private String server;

    /*
     * 请求处理对象是通过方法处理请求
     * 当前方法主要用于实现一个字符串的回显
     * 向客户端/服务端返回一个字符串*/
    @GetMapping("/provider/echo/{msg}")
    public String doRestEcho1(@PathVariable String msg) throws InterruptedException {
//        Thread.sleep(5000);
        return server+"hello" + msg;
    }
}

RestTemPlate实现远程调用

1. 在启动类构建RestTemPlate对象,并注入Bean

    /*
     * 构建RestTemplate对象,基于此对象实现远程服务调用.
     * 因为spring容器初始化时,没有创建这个对象,所以我们
     * 自己创建,然后交给spring管理.
     * 在Spring中配置第三方的Bean时,可以通过@Bean的方式
     * 对指定方法进行描述,然后在方法内部进行对象的创建和配置.
     * @Bean描述的方法,其返回值会交给spring管理,spring会给
     * 这个bean一个默认的名字,默认为方法名,这个名字对应的bean
     * 的作用域为单例作用域
     */

    //@Bean("rt")
    //@Scope("singleton")
    //@ConditionalOnMissingBean //spring容器没有这个bean时它才会生效.
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

2. 新建Controller类写业务测试

    /*从spring容器获取一个RestTemplate对象
     * 基于这个对象调用远端服务*/
    @Autowired
    private RestTemplate restTemplate;

    /*此方法中通过一个对象调用远端sca-provider中的服务
    * 访问: http:localhost:8090/consumer/doRestEcho1*/
    @GetMapping("/consumer/doRestEcho1")
    public String doRestEcho01() {
        //1.定义调用的远端服务url
        String url = "http://localhost:8081/provider/echo/" + appName;
        //2.基于restTemplate对象中的相关方法进行服务调用
        return restTemplate.getForObject(url, String.class);

    }
  1. 描述

     这是单服务的调用, 使用固定的URL
    

RestTemPlate - loadBalancerClient 简化远程调用

1. 启动类构建RestTemPlate-LoadBalancerClient对象

image-20211205165411243

    /*
    * 使用@LoadBalanced描述RestTemplate对象时
    * 假如使用RestTemplate对象发起远程服务调用, 底层会对这个请求拦截
    * 拦截到此请求后,会基于LoadBalancerClient对象获取服务实例
    * 然后进行负载均衡方式的调用*/
    @Bean
    public RestTemplate loadBalancedRestTemplate() {
        return new RestTemplate();
    }

2. Controller里写业务实现调用和负载均衡

    //此对象负责从nacos服务中发现和获取服务实例
    @Autowired
    private LoadBalancerClient loadBalancerClient;

    /*在01的基础之上实现负载均衡
    * 基于loadBalancerClient*/
    @GetMapping("/consumer/doRestEcho02")
    public String doRestEcho02() {
        //1.获取provider服务实例
        ServiceInstance serviceInstance = loadBalancerClient.choose("sca-provider");  //sca-provider: nacos服务列表中的服务名
        //2.定义调用的远端服务url
        String url = String.format("http://%s:%s/provider/echo/%s", serviceInstance.getHost(), serviceInstance.getPort(), appName);
        System.out.println("request url:" + url);
        //3.基于restTemplate对象中的相关方法进行服务调用
        return restTemplate.getForObject(url, String.class);
    }

3. 描述

  • 使用loadBalancerClient.choose获取服务实例信息
  • 使用了URL的拼接, 通过动态获取端口号, 实现了负载均衡

RestTemPlat - loadBalancerClient 简化

1. 基于构建的RestTemPlat - loadBalancerClient对象,添加@LoadBalanced拦截器注解

image-20211204121614334

2. Controller写业务

    //此对象负责进行负载均衡方式的调用
    @Autowired
    private RestTemplate loadBalancedRestTemplate;

    /*此方法对02实现了简化操作
    * 简化: 基于loadBalancerClient获取服务实例的过程
    * 说明: 为RestTemplate对象注入拦截器, 在底层拦截器中实现服务实例的获取
    * RestTemplate对象通过使用@LoadBalancd注解进行描述,为RestTemplate对象注入拦截器*/
    @GetMapping("/consumer/doRestEcho03")
    public String doRestEcho03() {
        String serviceID = "sca-provider";
//        String url = "http://"+serviceID+"/provider/echo/" + appName;
        String url = String.format("http://%s/provider/echo/%s", serviceID,appName);
        return loadBalancedRestTemplate.getForObject(url, String.class);
    }

3. 描述

  • 简化了获取provider服务实例代码, 底层自己写了这段代码
  • 通过@LoadBalanced注解拦截到请求, 访问到要调用的服务方法里

命名空间

  1. 新建命名空间

    image-20211204121634547

  2. 克隆配置

    image-20211204121645485

    image-20211204121655841

    image-20211204121706658

  3. 配置yml

    image-20211204121718882

  • 不配置的话会默认使用public空间的配置

分组配置

  1. 新建配置, 修改分组IP

image-20211204121731996

  1. 修改yml

image-20211204121742467

  • yml配置上分组IP, 就会使用配置好分组的配置

公共配置

  1. 新建公共配置, 命名分组

image-20211204121754284

  1. 修改yml文件

image-20211204121806085

  1. 新建Controller测试

image-20211204121816763

/*测试公共配置*/
@RefreshScope
@RestController
public class ProviderSecretController {

    /*很多配置中有一些相同的配置
    * */

    @Value("${app.secret: 123456}")
    private String secret;

    @GetMapping("/provider/secret")
    public String doGetSecret() {
        return "the secret is" + secret;
    }
}

  • 注意:
    • 共享配置可以通过下标[0],[1],[2],配置多个
    • 如果组名为默认名DEFAULT_GROUP, 不用写, 如果不是, 需要在下面定义group组名

基于日志对象进行日志输出测试

  1. provider新建ProviderLogController
package com.jt.provider.controller;

import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/*基于这个controller演示配置中心的作用
* 基于日志对象进行日志输出测试*/
@Slf4j  //lombok提供的注解
@RestController
public class ProviderLogController {
    //创建一个日志对象
    //org.slf4j.Logger(Java中的日志API规范)

    //记住: 以后只要Java中使用日志对象, 就采用下面这种方式创建
    //log对象所在类上使用了@Slf4j 注解, 就不需要再手动创建log对象, 底层自己创建了这个对象
//    private static Logger log = LoggerFactory.getLogger(ProviderLogController.class);

    @GetMapping("/provider/log/doLog01")
    public String doLog01() {   // trace < debug < info < warn < error
        log.trace("====trace====");//跟踪
        log.debug("====debug====");//调试
        log.info("====info====");//常规信息
        log.warn("====warn====");
        log.error("====error====");
        return "log config test";
    }
}
  1. yml文件配置日志输出等级

    默认输出等级是info, 只有比他等级高的日志才会输出, 通过配置yml文件, 修改日志输出等级
    每次修改都需要重启服务器, 很不方便
    

image-20211204121835946

将日志配置配置到配置中心

  1. 添加配置中心的依赖

image-20211204121845798

  1. nacos配置管理, 新建配置

image-20211204121858792

  1. ID要和服务名完全相同, 选择YAML类型

image-20211204121913321

  注意: 配置内容没有提示,最好在IDEA的yml文件里写完再复制过来
  1. 点击发布, 添加配置到yml文件
image-20211204121927794
  1. 更改yml文件名

image-20211204121942637

  启动服务时会读取applcation.yml文件, 如果想使用配置中心, 必须要改名为bootstrap.yml
  不然不会读取配置中心的内容, 还会使用applcation.yml默认的配置
  1. 注意

    • 要注释掉配置中心的日志配置

      • 因为启动服务, 会先读取配置文件, 如果有相同的配置不同的等级, 配置中心不会生效
    • 配置内容会保存在nacos配置的数据库中

测试获取配置中心的内容

  1. 本类中添加业务代码
    /*
     * 请问这个配置何时读取?logLevel属性初始化时.
     * 请问logLevel属性何时初始化呢?对象构建时
     * 假如希望logLevel属性值,与配置中心这个配置的值始终是同步的怎么办?
     * 只要修改配置中心的内容,就重新重建对象,然后属性会重新初始化.
     */
    @Value("${logging.level.com.jt}")
    private String logLevel;

    @GetMapping("/provider/log/doLog02")
    public String doLog02() {
        log.info("log.level is {}", logLevel);  //这里{}表示占位符
        return "log level is" + logLevel;
    }
  会返回配置中心的日志等级

image-20211204122002425

  1. 当更改配置中心配置等级时

image-20211204122012302

  log对象会发生变化, 但是返回的日志等级信息不会变化,
  因为: 在log对象初始化时获取到的配置中心的信息, 构造函数只创建一次对象
  更改配置中心时, 不会再创建对象, 所以日志等级信息不会再改变

image-20211204122021658

  1. 需要添加@RefreshScope注解

    如果页面有需要输出配置中心的信息的代码, 必须添加这个注解

用来告诉系统, 更改了配置文件, 重新创建controller对象, 对log对象重新初始化

image-20211204122033746

测试创建定时任务

package com.jt.common.util;

import java.util.Timer;
import java.util.TimerTask;

/**单线程定时任务工具类*/

public class TimerUtils {
    public static void main(String[] args) {
        //1. 构建执行任务的对象
        /*Timer对象创建时会创建一个线程, 并且为线程分配一个队列*/
        Timer timer = new Timer();
        //2. 构建任务对象
        TimerTask task1 =
                new TimerTask() {

            //重写里面的run方法
            @Override
            public void run() {
                System.out.println(System.currentTimeMillis());   //打印当前时间的毫秒值
                //timer.cancel();  结束任务调度
            }
        };
        //3. 定时执行任务

        timer.schedule(task1,   //要执行的任务
                1000,       //1秒后开始执行
                1000);      //每隔1秒执行一次
    }
}

测试获取配置中心本地缓存的配置

设置了命名空间的分组, 并使用了这个分组, 这个分组配置了本地缓存

image-20211204122053604

  1. 新建Controller类写业务
/*基于此Controller测试是否启用本地缓存的配置(这个配置从配置中心获取)*/


@RefreshScope  //告诉系统底层, 修改了配置中心的配置, 需要重新构建对象
@RestController
public class ProviderCacheController {
    @Value("${useLocalCache:false}")
    private boolean useLocalCache;

    @RequestMapping("/provider/cache01")
    public String doUseLocalCache01(){
        return "useLocalCache'value is   " + useLocalCache;
    }
  1. 前端发送请求会返回本地缓存的配置
  2. 当更改配置中心配置时, useLocalCache对象也会改变

测试构建本地缓存对象(基于jvm中的一个对象储存从数据库中获取的数据)

使用ArrayList集合创建对象

private List<String> cache = new ArrayList<>();

@RequestMapping("/provider/cache02")
public List<String> doUseLocalCache02() {
    if (cache.isEmpty()) {  //cache为空的时候
       //假设这部分信息从数据库获取的
       //不希望每次获取信息都要从数据库查, 将这部分信息存到本地缓存中
       List<String> cates = Arrays.asList("A", "B", "C");
       cache.addAll(cates);
       }
    return cache;
}
当前端第一次获取数据时, 会从数据库获取, 然后储存到cache对象,再次获取这个数据, 会直接从cache对象获取 

如果出现多个线程同时访问时, 会出现同时写入cache的问题, ArrayList是不安全的,
可能会出内容的重复, 或者数据为null

使用CopyOnWriteArrayList<>()集合创建对象

   /*CopyOnWriteArrayList是一个线程安全的list集合, 允许多个线程执行并发更新, 但是只能有一个更新成功*/
    private List<String> cache = new CopyOnWriteArrayList<>();     //乐观锁   CAS算法(比较和交换)

    @RequestMapping("/provider/cache02")
    public List<String> doUseLocalCache02() {
        if (!useLocalCache){
            return Arrays.asList("A", "B", "C");
        }
        if (cache.isEmpty()) {          //双重验证
            synchronized (cache) {
                if (cache.isEmpty()) {  //cache为空的时候

                    //假设这部分信息从数据库获取的
                    //不希望每次获取信息都要从数据库查, 将这部分信息存到本地缓存中
                    List<String> cates = Arrays.asList("A", "B", "C");
                    cache.addAll(cates);
                }
            }
        }
        return cache;
    }
}
  • CopyOnWriteArrayList集合: 是一个线程安全的list集合, 允许多个线程执行并发更新, 但是只能有一个更新成功
  • 使用双重验证: 判断cache是否为空, 第一层验证如果为空, 多个线程可能会同时进入访问, 又使用第二层验证
  • synchronized悲观锁: 第一层验证失败, 使用悲观锁只允许一个线程访问, 进入第二层验证, 就不会出现缓存数据重复或者为null的问题

注意: 如果更新了数据库, 本地缓存不更新, 还会出现数据不统一问题

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

偶尔也吹晚风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值