1. Ribbon 负载均衡
1.1 Ribbon 的概念
Ribbon 是 Netflix 发布的负载均衡器,它有助于控制 HTTP 和 TCP 的客户端的行为。为 Ribbon 配置服务提供者地址后,Ribbon 就可基于某种负载均衡算法,自动地帮助服务消费者去请求。Ribbon 默认为我们提供了很多负载均衡算法,例如轮询、随机等。当然,我们也可为 Ribbon 实现自定义的负载均衡算法。
1.2 Ribbon 的使用
在之前的项目中,我们启动了一个 zt-service-provider,然后通过 DiscoveryClient 来获取服务实例信息,然后获取 ip 地址和端口号来访问。
但是实际环境中,我们往往会开启很多个 zt-service-provider 的集群。此时我们获取的服务列表中就会有多个服务实例,到底该访问哪一个实例呢?
一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。
不过 Eureka 中已经帮我们集成了负载均衡组件 Ribbon,接下来,我们就来使用 Ribbon 实现负载均衡。
1.2.1 启动两个服务提供方实例
首先参照之前启动两个 zt-eureka 的步骤,启动两个 zt-service-provider,端口号分别为 8081 和 8083
1.2.2 在服务消费方中开启负载均衡
-
修改 zt-service-customer 的引导类,在 RestTemplate 的配置方法上添加 @LoadBalanced 注解
@SpringBootApplication @EnableDiscoveryClient public class ZtServiceCustomerApplication { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ZtServiceCustomerApplication.class, args); } }
-
修改 UserController 中的调用方式,不再通过 ip 地址和端口号调用,而是直接通过服务名称调用
@Controller @RequestMapping("user") public class UserController { @Autowired private RestTemplate restTemplate; @GetMapping @ResponseBody public User queryUserById(@RequestParam("id") Integer id) { String baseUrl = "http://service-provider/user/" + id; return this.restTemplate.getForObject(baseUrl, User.class); } }
-
启动 zt-service-customer 项目
-
打开浏览器访问以下地址
http://localhost:8080/user?id=2
1.3 源码跟踪
为什么我们只输入了服务名称就可以访问了呢?显然有人帮我们根据服务名称,获取到了服务实例的 ip 地址和端口。它就是 LoadBalancerInterceptor,也就是负载均衡器。
在如下代码打断点:
一路源码跟踪:RestTemplate.getForObject --> RestTemplate.execute --> RestTemplate.doExecute:
继续跟入:AbstractClientHttpRequest.execute --> AbstractBufferingClientHttpRequest.executeInternal --> InterceptingClientHttpRequest.executeInternal --> InterceptingClientHttpRequest.execute:
继续跟入:LoadBalancerInterceptor.intercept方法
继续跟入 execute 方法:发现获取了8082端口的服务
再跟下一次,发现获取的是8081:
1.4 Ribbon 负载均衡策略
1.4.1 默认负载均衡策略
Ribbon 默认的负载均衡策略是简单的轮询,我们可以测试一下。
-
在刚才的源码中我们看到拦截中是使用 RibbonLoadBalanceClient 来进行负载均衡的,其中有一个 choose 方法,找到 choose 方法的接口方法,是这样介绍的:
现在这个就是负载均衡获取实例的方法。
-
测试类
@SpringBootTest @RunWith(SpringRunner.class) public class LoadBalanceTest { @Autowired private RibbonLoadBalancerClient loadBalancerClient; @Test public void testLoadBalance() { for (int i = 0; i < 10; i++) { ServiceInstance serviceInstance = loadBalancerClient.choose("service-provider"); System.out.println(serviceInstance.getHost()+":"+serviceInstance.getPort()); } } }
运行结果:
DESKTOP-5474MPD:8084 DESKTOP-5474MPD:8083 DESKTOP-5474MPD:8084 DESKTOP-5474MPD:8083 DESKTOP-5474MPD:8084 DESKTOP-5474MPD:8083 DESKTOP-5474MPD:8084 DESKTOP-5474MPD:8083 DESKTOP-5474MPD:8084 DESKTOP-5474MPD:8083
符合了我们的预期推测,确实是轮询方式。
1.4.2 修改负载均衡策略
我们是否可以修改负载均衡的策略呢?
继续跟踪源码,发现这么一段代码:
我们看看这个 rule 是谁:
这里的 rule 默认值是一个 RoundRobinRule,看类的介绍,也就是轮询的意思:
我们注意到,这个类其实是实现了接口 IRule 的,查看一下:
定义负载均衡的规则接口,它有以下实现:
SpringBoot 也帮我们提供了修改负载均衡规则的配置入口,在 zt-service-consumer 的 application.yml 中添加配置:
service-provider:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
再次测试,运行结果:
DESKTOP-5474MPD:8084
DESKTOP-5474MPD:8084
DESKTOP-5474MPD:8084
DESKTOP-5474MPD:8083
DESKTOP-5474MPD:8084
DESKTOP-5474MPD:8083
DESKTOP-5474MPD:8083
DESKTOP-5474MPD:8083
DESKTOP-5474MPD:8083
DESKTOP-5474MPD:8084
2. Hystrix 容错管理
2.1 Hystrix 的概念
Hystrix 是由 Netflix 开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性、容错性与局部应用的弹性,是一个实现了超时机制和断路器模式的工具类库。
2.2 雪崩问题
微服务中,服务间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的调用链路。如图,一次业务请求,需要调用 A、P、H、I 四个服务,这四个服务又可能调用其它服务:
如果此时,某个服务出现异常:
微服务 I 发生异常,请求阻塞,用户不会得到响应,则 tomcat 的这个线程不会释放,请求要排队处理,于是越来越多的用户请求到来,越来越多的线程会阻塞:
服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪崩效应。
这就好比,一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为种种原因无法使用,那么就会造成整台车无法装配,陷入等待零件的状态,直到零件到位,才能继续组装。 此时如果有很多个车型都需要这个零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。一个零件的波及范围不断扩大。
Hystix 解决雪崩问题的手段有两个:
- 线程隔离
- 服务熔断
2.3 线程隔离和服务降级
2.3.1 线程隔离原理
原来是请求直接调用服务,现在 Hystrix 做了一个优化:Hystrix 为每个服务分配一个小的线程池。假如请求调用服务出现问题,可以继续调用该服务的其他线程,但如果都出现问题,线程池已满调用将被立即拒绝,默认不采用排队,加速失败判定时间。
也就是说用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行服务降级处理。
服务降级:优先保证核心服务,而非核心服务不可用或弱可用。
触发 Hystix 服务降级的情况:
- 线程池已满
- 请求超时
线程隔离和服务降级的好处:
- 用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果
- 服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个服务对应的线程池中的资源,对其它服务没有响应
2.3.2 服务降级实践
-
打开 zt-service-customer 工程
-
引入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
-
设置超时
默认情况下,请求在超过1秒后都会返回错误信息,这是因为 Hystix 的默认超时时长为1,我们可以通过配置修改这个值:
hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 6000 # 设置hystrix的超时时间为 6000ms
-
开启熔断
在引导类上添加注解 @EnableCircuitBreaker
@EnableCircuitBreaker @SpringBootApplication @EnableDiscoveryClient public class ZtServiceCustomerApplication { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ZtServiceCustomerApplication.class, args); } }
在微服务中,经常会引入上面的三个注解,于是 Spring 就提供了一个组合注解:@SpringCloudApplication
@SpringCloudApplication public class ZtServiceCustomerApplication { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ZtServiceCustomerApplication.class, args); } }
-
编写降级逻辑
我们改造 zt-service-customer,当服务的调用出现故障,我们希望快速失败,给用户一个友好提示。因此需要提前编写好失败时的降级处理逻辑。
采用局部熔断方法:
@Controller @RequestMapping("user") public class UserController { @Autowired private RestTemplate restTemplate; @GetMapping @ResponseBody @HystrixCommand(fallbackMethod = "fallBack") // 指定局部熔断方法 public String queryUserById(@RequestParam("id") Integer id) { String baseUrl = "http://service-provider/user/" + id; return this.restTemplate.getForObject(baseUrl, String.class); } /** * 局部熔断方法:要和被熔断的方法返回值和参数列表一致 * @return */ public String fallBack(Integer id) { return "服务器正忙,请稍后重试!"; } }
采用全局熔断方法:
@Controller @RequestMapping("user") @DefaultProperties(defaultFallback = "fallBack") // 指定一个类的全局熔断方法 public class UserController { @Autowired private RestTemplate restTemplate; @GetMapping @ResponseBody @HystrixCommand public String queryUserById(@RequestParam("id") Integer id) { String baseUrl = "http://service-provider/user/" + id; return this.restTemplate.getForObject(baseUrl, String.class); } /** * 全局熔断方法:返回值类型要被熔断的方法一致,参数列表必须为空 * @return */ public String fallBack() { return "服务器正忙,请稍后重试!"; } }
-
启动测试
-
启动 zt-eureka 和 zt-service-customer 两个项目,不启动服务提供方项目
-
打开浏览器访问下面地址
http://localhost:8080/user?id=1
-
2.4 服务熔断
2.4.1 服务熔断原理
熔断机制的原理很简单,像家里的电路熔断器,如果电路发生故障能立刻熔断电路,避免发生整个电路的灾难。在分布式系统中应用这一模式之后,服务调用方可以自己进行判断某些服务反应慢或者存在大量超时的情况时,能够主动熔断,防止整个系统被拖垮。
不同于电路熔断只能断,不能自动重连,Hystix 可以实现弹性容错,当情况好转之后,可以自动重连。
通过断路的方式,可以将后续请求直接拒绝掉,一段时间之后允许部分请求通过,如果调用成功则回到电路闭合状态,否则继续断开。
熔断状态机3个状态:
- Closed:关闭状态,所有请求都正常访问。
- Open:打开状态,所有请求都会被降级。Hystix 会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值是 50%,请求次数最少不低于 20 次。
- Half Open:半开状态,open 状态不是永久的,打开后会进入休眠时间(默认是 5S)。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,则会完全关闭断路器,否则继续保持打开,再次进行休眠计时
2.4.2 服务熔断实践
-
修改熔断策略
circuitBreaker: requestVolumeThreshold: 10 sleepWindowInMilliseconds: 10000 errorThresholdPercentage: 50
- requestVolumeThreshold:触发熔断的最小请求次数,默认 20
- errorThresholdPercentage:触发熔断的失败请求最小占比,默认 50%
- sleepWindowInMilliseconds:休眠时长,默认是 5000 毫秒
-
修改 zt-service-customer 项目中的 UserController,让 queryUserById 方法中 id =1 就抛异常
@Controller @RequestMapping("user") @DefaultProperties(defaultFallback = "fallBack") // 指定一个类的全局熔断方法 public class UserController { @Autowired private RestTemplate restTemplate; @GetMapping @ResponseBody @HystrixCommand public String queryUserById(@RequestParam("id") Integer id) { if (id == 1) { throw new RuntimeException(); } String baseUrl = "http://service-provider/user/" + id; return this.restTemplate.getForObject(baseUrl, String.class); } /** * 全局熔断方法:返回值类型要被熔断的方法一致,参数列表必须为空 * * @return */ public String fallBack() { return "服务器正忙,请稍后重试!"; } }
-
启动 zt-eureka,zt-service-provider,zt-service-customer 三个项目
-
打开浏览器
访问 id 为 1的请求 10 次,就会触发熔断。断路器会断开,一切请求都会被降级处理。
http://localhost:8080/user?id=1
此时你访问 id 为 2 的请求,会发现返回的也是失败,而且失败时间很短。
http://localhost:8080/user?id=2
3. Feign 服务调用
在前面的学习中,我们使用了 Ribbon 的负载均衡功能,大大简化了远程调用时的代码:
String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
如果就学到这里,你可能以后需要编写类似的大量重复代码,格式基本相同,无非参数不一样。有没有更优雅的方式,来对这些代码再次优化呢?
这就是我们接下来要学的 Feign 的功能了。
3.1 Feign 的概念
Feign 是一个 Netflix 开发得声明式、模块化的 HTTP 客户端,其灵感来自 Retrofit、JAXRS-2.0 以及 WebSocket。Feign 可以帮助我们更加便捷、优雅地调用 HTTP API。
在 Spring Cloud 中,使用 Feign 非常简单——创建一个接口,并在接口上添加一些注解,代码就完成了。Feign 支持多种注解,例如 Feign 自带的注解或者 JAX-RS 注解等。
Spring Cloud 对Feign 进行了增强,使 Feign 支持了 Spring MVC 注解,并整合了 Ribbon 和 Eureka,从而让 Feign 的使用更加方便。
3.2 Feign 快速入门
-
打开 zt-service-customer 项目
-
导入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
开启 Feign 功能,修改启动类,添加注解 @EnableFeignClients,删除 RestTemplate
@SpringCloudApplication @EnableFeignClients public class ZtServiceCustomerApplication { public static void main(String[] args) { SpringApplication.run(ZtServiceCustomerApplication.class, args); } }
Feign 已经自动集成了 Ribbon 负载均衡的 RestTemplate。所以,此处不需要再注册 RestTemplate。
-
创建 Feign 的客户端
-
创建 com.zt.client 包
-
创建 UserClient 接口
@FeignClient("service-provider") public interface UserClient { @GetMapping("user/{id}") User queryUserById(@PathVariable("id") Integer id); }
-
-
修改 UserController
@Controller @RequestMapping("user") public class UserController { @Autowired private UserClient userClient; @GetMapping @ResponseBody public String queryUserById(@RequestParam("id") Integer id) { return userClient.queryUserById(id).toString(); } }
-
启动 zt-eureka,zt-service-provider,zt-service-customer 三个项目
-
打开浏览器访问下面地址
http://localhost:8080/user?id=1
3.3 Feign 集成 Ribbon 负载均衡
Feign 中本身已经集成了 Ribbon 依赖和自动配置,因此我们不需要额外引入依赖,也不需要再注册 RestTemplate 对象。
3.4 Feign 集成 Hystrix 容错管理
-
Feign 默认也有对 Hystrix 的集成,只不过,默认情况下是关闭的。我们需要通过下面的参数来开启:
feign: hystrix: enabled: true
-
定义一个类 UserClientFallback,实现刚才编写的 UserClient,作为 fallback 的处理类
@Component public class UserClientFallback implements UserClient { @Override public User queryUserById(Integer id) { User user = new User(); user.setUserName("服务器正忙,请稍后重试!"); return user; } }
-
在 UserFeignClient 中,指定刚才编写的实现类
@FeignClient(value = "service-provider",fallback = UserClientFallback.class) public interface UserClient { @GetMapping("user/{id}") User queryUserById(@PathVariable("id") Integer id); }
-
启动测试
-
启动 zt-eureka 和 zt-service-customer 两个项目,不启动服务提供方项目
-
打开浏览器访问下面地址
http://localhost:8080/user?id=1
-
4. Zuul 网关
4.1 Zuul 的概念
Zuul 是 Netflix 开源的微服务网关,它可以和 Eureka、Ribbon、hystrix 等组件配合使用。Zuul 的核心是一系列过滤器。这些过滤器完成以下功能:
-
身份认证和安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求。
-
审查与监控:在边缘位置追踪有意义的数据和统计结果,从而带来精确的生产视图。
-
动态路由:动态地将请求路由到不同的后端集群。
-
压力测试:逐渐增加指向集群的流量,以了解性能。
-
负责分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求。
-
静态响应处理:在边缘位置直接建立部分响应,避免其转发到内部集群。
-
多区域弹性:跨越 AWS Region 进行请求路由,旨在实现 ELB(Elastic Load Blancing)使用的多样化,以及让系统的边缘更贴近系统的使用者。
Spring Cloud 对 Zuul 进行了整合与增强。目前,Zuul 使用默认 HTTP 客户端是 Apache HTTP Client,也可使用 RestClient 或 okhttp3.OkHttpClient。如果想要使用 RestClient,可以设置 ribbon .restclient.enabled=true;想要使用 okhttp3.OkHttpClient,可以设置 ribbon.okhttp.enabled=true。
4.2 Zuul 加入后的架构
不管是来自于客户端(PC 或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过 Zuul 这个网关,然后再由网关来实现鉴权、动态路由等等操作。Zuul 就是我们服务的统一入口。
4.3 Zuul 快速入门
4.3.1 创建工程
-
打开 IDEA
-
Spring Initializr --> Next
-
填写项目信息 --> Next
-
添加依赖 --> Next
-
填写项目位置 --> Finish
4.3.2 Zuul 的配置
-
打开 application.yaml,编写配置
server: port: 10010 #服务端口 spring: application: name: zt-zuul #指定服务名
-
开启 Zuul 功能,修改引导类,添加 @EnableZuulProxy 注解
@SpringBootApplication @EnableZuulProxy public class ZtZuulApplication { public static void main(String[] args) { SpringApplication.run(ZtZuulApplication.class, args); } }
-
打开 application.yaml,编写路由规则,我们将符合 path 规则的一切请求,都代理到 url 参数指定的地址
zuul: routes: service-provider: # 这里是路由 id,随意写 path: /service-provider/** # 这里是映射路径 url: http://localhost:8084 # 映射路径对应的实际 url 地址
-
启动 zt-eureka,zt-service-provider,zt-service-customer,zt-zuul 四个项目
-
打开浏览器,访问下面地址
http://localhost:10010/service-provider/user/1
相当于代理到:
http://localhost:8084/user/1
4.4 面向服务的路由
在刚才的路由规则中,我们把路径对应的服务地址写死了。如果同一服务有多个实例的话,这样做显然就不合理了。我们应该根据服务的名称,去 Eureka 注册中心查找服务对应的所有实例列表,然后进行动态路由。
下面就优化一下 zt-zuul 项目:
-
引入 Eureka 客户端依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
-
添加 Eureka 配置,获取服务信息
eureka: client: registry-fetch-interval-seconds: 5 # 获取服务列表的周期:5s service-url: defaultZone: http://localhost:10086/eureka
-
开启 Eureka 客户端功能
@SpringBootApplication @EnableZuulProxy @EnableEurekaClient public class ZtZuulApplication { public static void main(String[] args) { SpringApplication.run(ZtZuulApplication.class, args); } }
-
修改映射配置,通过服务名称获取
因为已经有了 Eureka 客户端,我们可以从 Eureka 获取服务的地址信息,因此映射时无需指定 IP 地址,而是通过服务名称来访问,而且 Zuul 已经集成了 Ribbon 的负载均衡功能。
zuul: routes: service-provider: # 路由id path: /service-provider/** # 映射路径 serviceId: service-provider # 服务名称
-
启动 zt-eureka,zt-service-provider,zt-service-customer,zt-zuul 四个项目
-
打开浏览器,访问下面地址
http://localhost:10010/service-provider/user/1
4.5 简化路由配置
原来的配置:
zuul:
routes:
service-provider: # 路由id
path: /service-provider/** # 映射路径
serviceId: service-provider # 服务名称
简化的配置:
zuul:
routes:
service-provider: /service-provider/** # 服务名称 + 映射路劲
补充:默认情况下,一切服务的映射路径就是服务名本身。例如服务名为:service-provider
,则默认的映射路径就是:/service-provider/**
。也就是说,刚才的映射规则我们完全不配置也是可以的。
4.6 路由前缀
zuul:
routes:
service-provider: /service-provider/**
service-customer: /service-customer/**
prefix: /api
我们通过zuul.prefix=/api
来指定了路由的前缀,这样在发起请求时,路径就要以 /api 开头,例如:
http://localhost:10010/api/service-provider/user/1
4.7 过滤器
Zuul 作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过 Zuul 提供的过滤器来实现的。
4.7.1 ZuulFilter
ZuulFilter 是过滤器的顶级父类。在这里我们看一下其中定义的 4 个最重要的方法:
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();// 来自IZuulFilter
Object run() throws ZuulException;// IZuulFilter
}
- shouldFilter:返回一个 Boolean 值,判断该过滤器是否需要执行。返回 true 执行,返回 false 不执行。
- run:过滤器的具体业务逻辑。
- filterType:返回字符串,代表过滤器的类型。包含以下 4 种:
- pre:请求在被路由之前执行
- route:在路由请求时调用
- post:在 route 和 errror 过滤器之后调用
- error:处理请求时发生错误调用
- filterOrder:通过返回的 int 值来定义过滤器的执行顺序,数字越小优先级越高。
4.7.2 过滤器执行生命周期
正常流程:
- 请求到达首先会经过 pre 类型过滤器,而后到达 route 类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达 post 过滤器,而后返回响应。
异常流程:
- 整个过程中,pre 或者 route 过滤器出现异常,都会直接进入 error 过滤器,在 error 处理完毕后,会将请求交给 post 过滤器,最后返回给用户。
- 如果是 error 过滤器自己出现异常,最终也会进入 post 过滤器,将最终结果返回给请求客户端。
- 如果是 post 过滤器出现异常,会跳转到 error 过滤器,但是与 pre 和 route 不同的是,请求不会再到达 post 过滤器了。
4.7.3 过滤器使用场景
场景非常多:
- 请求鉴权:一般放在 pre 类型,如果发现没有访问权限,直接就拦截了
- 异常处理:一般会在 error 类型和 post 类型过滤器中结合来处理。
- 服务调用时长统计:pre 和 post 结合使用。
4.8 自定义过滤器
接下来我们来自定义一个过滤器,模拟一个登录的校验。基本逻辑:如果请求中有 token 参数,则认为请求有效,否则返回登陆失败的提示信息。
-
创建 com.zt.filter 包
-
创建 LoginFilter
@Component public class LoginFilter extends ZuulFilter { /** * 过滤器类型,前置过滤器 * @return */ @Override public String filterType() { return "pre"; } /** * 过滤器的执行顺序 * @return */ @Override public int filterOrder() { return 10; } /** * 该过滤器是否生效 * @return */ @Override public boolean shouldFilter() { return true; } /** * 登陆校验逻辑 * @return * @throws ZuulException */ @Override public Object run() throws ZuulException { // 获取 zuul 提供的上下文对象 RequestContext context = RequestContext.getCurrentContext(); // 从上下文对象中获取请求对象 HttpServletRequest request = context.getRequest(); // 获取 token信息 String token = request.getParameter("token"); // 判断 if (StringUtils.isBlank(token)) { // 过滤该请求,不对其进行路由 context.setSendZuulResponse(false); // 设置响应状态码,401 context.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED); // 设置响应信息 context.setResponseBody("request error!"); } // 返回 null 值,代表该过滤器什么都不做 return null; } }
-
启动项目,打开浏览器测试
没有 token 参数,访问失败
带有 token 参数,访问成功