Nacos服务的注册
想要使用项目工程里的服务, 必须要在nacos注册和发现服务
- pom添加注册中心依赖
- yml文件配置
通过Feign实现远程服务调用
1. pom文件中添加Feign依赖
注意: 加在原调用子工程里
2. 启动类添加@EnableFeignClients注解
3. 新建FeignService接口
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. 实现调用
-
基于GetMapping/PostMapping等注解的Value值调用到具体服务里的目标方法
- @FeignClient里的参数
- name: 调用服务的注册名
- contextId: 定义实现类的对象的Bean名字
- fallbackFactory: 远程服务回调工厂
- @FeignClient里的参数
-
用户访问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);
}
}
- 通过@FeignClient描述的接口去调用远程provider服务里的方法
实现远程服务回调
当调用远程服务时出现故障中断, 调用不到, 前端就会报出500错误, 可是使用远程服务回调, 解决报错问题
- 通过@FeignClient注解里的fallbackFactory属性定义远程回调工厂接口类名称
- 新建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表达式进行简写
- 当用户访问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);
}
-
描述
这是单服务的调用, 使用固定的URL
RestTemPlate - loadBalancerClient 简化远程调用
1. 启动类构建RestTemPlate-LoadBalancerClient对象
/*
* 使用@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拦截器注解
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注解拦截到请求, 访问到要调用的服务方法里
命名空间
-
新建命名空间
-
克隆配置
-
配置yml
- 不配置的话会默认使用public空间的配置
分组配置
- 新建配置, 修改分组IP
- 修改yml
- yml配置上分组IP, 就会使用配置好分组的配置
公共配置
- 新建公共配置, 命名分组
- 修改yml文件
- 新建Controller测试
/*测试公共配置*/
@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组名
基于日志对象进行日志输出测试
- 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";
}
}
-
yml文件配置日志输出等级
默认输出等级是info, 只有比他等级高的日志才会输出, 通过配置yml文件, 修改日志输出等级 每次修改都需要重启服务器, 很不方便
将日志配置配置到配置中心
- 添加配置中心的依赖
- nacos配置管理, 新建配置
- ID要和服务名完全相同, 选择YAML类型
注意: 配置内容没有提示,最好在IDEA的yml文件里写完再复制过来
- 点击发布, 添加配置到yml文件
![image-20211204121927794](https://gitee.com/ZaoQiShaYiTian/img/raw/master/202112051655194.png)
- 更改yml文件名
启动服务时会读取applcation.yml文件, 如果想使用配置中心, 必须要改名为bootstrap.yml
不然不会读取配置中心的内容, 还会使用applcation.yml默认的配置
-
注意
-
要注释掉配置中心的日志配置
- 因为启动服务, 会先读取配置文件, 如果有相同的配置不同的等级, 配置中心不会生效
-
配置内容会保存在nacos配置的数据库中
-
测试获取配置中心的内容
- 本类中添加业务代码
/*
* 请问这个配置何时读取?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;
}
会返回配置中心的日志等级
- 当更改配置中心配置等级时
log对象会发生变化, 但是返回的日志等级信息不会变化,
因为: 在log对象初始化时获取到的配置中心的信息, 构造函数只创建一次对象
更改配置中心时, 不会再创建对象, 所以日志等级信息不会再改变
-
需要添加@RefreshScope注解
如果页面有需要输出配置中心的信息的代码, 必须添加这个注解
用来告诉系统, 更改了配置文件, 重新创建controller对象, 对log对象重新初始化
测试创建定时任务
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秒执行一次
}
}
测试获取配置中心本地缓存的配置
设置了命名空间的分组, 并使用了这个分组, 这个分组配置了本地缓存
- 新建Controller类写业务
/*基于此Controller测试是否启用本地缓存的配置(这个配置从配置中心获取)*/
@RefreshScope //告诉系统底层, 修改了配置中心的配置, 需要重新构建对象
@RestController
public class ProviderCacheController {
@Value("${useLocalCache:false}")
private boolean useLocalCache;
@RequestMapping("/provider/cache01")
public String doUseLocalCache01(){
return "useLocalCache'value is " + useLocalCache;
}
- 前端发送请求会返回本地缓存的配置
- 当更改配置中心配置时, 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的问题
注意: 如果更新了数据库, 本地缓存不更新, 还会出现数据不统一问题