本文目录导读:

我将为您提供一个完整的Spring Cloud Zuul网关案例,包含服务注册、路由转发、过滤器等功能。
项目结构
zuul-gateway-demo
├── pom.xml
├── src/main/java/com/example/zuul/
│ ├── ZuulGatewayApplication.java
│ ├── config/
│ │ └── ZuulConfig.java
│ ├── filter/
│ │ ├── TokenFilter.java
│ │ ├── LogFilter.java
│ │ └── ErrorFilter.java
│ └── controller/
│ └── FallbackController.java
└── src/main/resources/
└── application.yml
父POM文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>zuul-gateway-demo</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<modules>
<module>zuul-server</module>
<module>service-provider</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Zuul网关服务模块
1 Zuul网关POM文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>zuul-gateway-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>zuul-server</artifactId>
<dependencies>
<!-- Zuul网关依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- Eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Ribbon负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<!-- Hystrix熔断 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!-- Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
2 启动类
package com.example.zuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy // 启用Zuul网关
@EnableDiscoveryClient // 启用服务发现
public class ZuulGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulGatewayApplication.class, args);
}
}
3 配置文件application.yml
server:
port: 8769
spring:
application:
name: zuul-gateway
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
zuul:
# 路由配置
routes:
# 用户服务
user-service:
path: /api/user/**
serviceId: service-user
stripPrefix: false
# 订单服务
order-service:
path: /api/order/**
serviceId: service-order
stripPrefix: false
# 直接URL路由
direct-url:
path: /external/**
url: http://www.example.com
# 排除某些路径
ignored-patterns:
- /admin/**
# 路由前缀
prefix: /api
# 是否添加主机头
add-host-header: true
# 是否设置自定义sensitive头
sensitive-headers:
- Cookie
- Set-Cookie
# 是否启用重试
retryable: true
# 是否允许所有路由共享Headera
strip-prefix: true
# Ribbon配置
ribbon:
# 连接超时
ConnectTimeout: 3000
# 读取超时
ReadTimeout: 5000
# 最大自动重试次数
MaxAutoRetries: 1
# 切换实例最大重试次数
MaxAutoRetriesNextServer: 1
# Hystrix熔断配置
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 60000
# Actuator端点
management:
endpoints:
web:
exposure:
include: '*'
4 过滤器实现
package com.example.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* 令牌验证过滤器
*/
@Slf4j
@Component
public class TokenFilter extends ZuulFilter {
/**
* 过滤器类型
* pre: 路由之前
* routing: 路由之时
* post: 路由之后
* error: 发送错误调用
*/
@Override
public String filterType() {
return "pre";
}
/**
* 过滤顺序
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 是否执行该过滤器
*/
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String requestURI = request.getRequestURI();
// swagger路径等不需要过滤
if (requestURI.contains("/api/user/login") ||
requestURI.contains("/api/user/register")) {
return false;
}
// 排除swagger相关
if (requestURI.contains("swagger") || requestURI.contains("api-docs")) {
return false;
}
return true;
}
/**
* 具体过滤逻辑
*/
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
log.info("=============== Token Filter ===============");
log.info("请求方法: {},请求路径: {}", request.getMethod(), request.getRequestURI());
// 获取token
String token = request.getHeader("token");
// 简单验证token
if (StringUtils.isEmpty(token)) {
log.warn("token为空,拒绝访问");
ctx.setSendZuulResponse(false); // 不进行路由
ctx.setResponseStatusCode(401); // 设置响应码
ctx.setResponseBody("{\"code\": 401, \"message\": \"未授权访问\"}");
return null;
}
// 模拟验证token
if (!"valid-token".equals(token)) {
log.warn("token无效,拒绝访问");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody("{\"code\": 401, \"message\": \"token无效\"}");
return null;
}
log.info("token验证通过");
ctx.addZuulRequestHeader("userId", "123456"); // 向下游传递用户信息
return null;
}
}
5 日志过滤器
package com.example.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* 日志过滤器
*/
@Slf4j
@Component
public class LogFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE; // "pre"
}
@Override
public int filterOrder() {
return 1; // 在TokenFilter之后执行
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
long startTime = System.currentTimeMillis();
ctx.set("startTime", startTime);
log.info("日志过滤器 - 请求: {} {}", request.getMethod(), request.getRequestURI());
log.info("请求头信息:");
java.util.Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
log.info(" {}: {}", headerName, headerValue);
}
return null;
}
}
6 响应后置过滤器
package com.example.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
/**
* 响应过滤器
*/
@Slf4j
@Component
public class ResponseFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.POST_TYPE; // "post"
}
@Override
public int filterOrder() {
return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletResponse response = ctx.getResponse();
Long startTime = (Long) ctx.get("startTime");
if (startTime != null) {
long duration = System.currentTimeMillis() - startTime;
log.info("请求处理耗时: {} ms", duration);
}
// 添加响应头
response.addHeader("X-Gateway", "Zuul");
response.addHeader("X-Request-Id", java.util.UUID.randomUUID().toString());
return null;
}
}
7 错误过滤器
package com.example.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
/**
* 错误过滤器
*/
@Slf4j
@Component
public class ErrorFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.ERROR_TYPE; // "error"
}
@Override
public int filterOrder() {
return -1; // 最先执行
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
Throwable throwable = ctx.getThrowable();
if (throwable != null) {
log.error("Zuul过滤器异常: ", throwable);
ctx.setResponseStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
ctx.setResponseBody("{\"code\": 500, \"message\": \"网关内部错误\"}");
}
return null;
}
}
8 熔断降级处理
package com.example.zuul.controller;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
/**
* 服务降级处理
*/
@Component
public class ServiceFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
return "*"; // 所有路由都启用降级
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "OK";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
String response = "{\"code\": 503, \"message\": \"服务暂时不可用,请稍后重试\"}";
return new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8));
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
return headers;
}
};
}
}
9 网关配置类
package com.example.zuul.config;
import com.example.zuul.filter.LogFilter;
import com.example.zuul.filter.TokenFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Zuul网关配置
*/
@Configuration
public class ZuulConfig {
/**
* 创建Token过滤器
*/
@Bean
public TokenFilter tokenFilter() {
return new TokenFilter();
}
/**
* 创建日志过滤器
*/
@Bean
public LogFilter logFilter() {
return new LogFilter();
}
}
服务提供者模块
1 服务提供者POM文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>zuul-gateway-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>service-provider</artifactId>
<dependencies>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
2 启动类
package com.example.provider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class ServiceProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceProviderApplication.class, args);
}
}
3 配置文件
server:
port: 8081
spring:
application:
name: service-user
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
management:
endpoints:
web:
exposure:
include: '*'
4 用户服务Controller
package com.example.provider.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserController {
/**
* 用户登录(不需要token)
*/
@PostMapping("/login")
public Map<String, Object> login(@RequestBody Map<String, String> params) {
log.info("用户登录请求: {}", params);
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "登录成功");
result.put("data", "登录成功后的token");
return result;
}
/**
* 获取用户信息(需要token)
*/
@GetMapping("/info/{id}")
public Map<String, Object> getUserInfo(@PathVariable("id") String id,
HttpServletRequest request) {
String userId = request.getHeader("userId");
log.info("获取用户信息,请求ID: {}, 用户ID: {}", id, userId);
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("userId", id);
result.put("username", "张三");
result.put("email", "zhangsan@example.com");
return result;
}
/**
* 用户列表
*/
@GetMapping("/list")
public Map<String, Object> getUserList(@RequestParam(required = false) Integer page,
@RequestParam(required = false) Integer size) {
log.info("查询用户列表,page: {}, size: {}", page, size);
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("data", new Object[]{
new HashMap<String, Object>() {{
put("id", 1);
put("username", "张三");
}},
new HashMap<String, Object>() {{
put("id", 2);
put("username", "李四");
}}
});
return result;
}
}
5 订单服务(另一个服务的示例)
package com.example.provider.controller;
import org.springframework.web.bind.annotation.*;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/order")
public class OrderController {
/**
* 创建订单
*/
@PostMapping("/create")
public Map<String, Object> createOrder(@RequestBody Map<String, Object> order) {
log.info("创建订单: {}", order);
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("orderId", "ORD" + System.currentTimeMillis());
result.put("message", "订单创建成功");
return result;
}
/**
* 查询订单
*/
@GetMapping("/{orderId}")
public Map<String, Object> getOrder(@PathVariable("orderId") String orderId) {
log.info("查询订单: {}", orderId);
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("orderId", orderId);
result.put("amount", 199.00);
result.put("status", "已支付");
return result;
}
}
Eureka注册中心(可选)
1 Eureka Server POM
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
2 Eureka Server配置
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka/
测试用例
package com.example.zuul;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
@SpringBootTest
public class ZuulGatewayTest {
private RestTemplate restTemplate = new RestTemplate();
/**
* 测试登录(不需要token)
*/
@Test
public void testLogin() {
String url = "http://localhost:8769/api/api/user/login";
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
String requestBody = "{\"username\":\"zhangsan\",\"password\":\"123456\"}";
HttpEntity<String> entity = new HttpEntity<>(requestBody, headers);
ResponseEntity<String> response = restTemplate.exchange(
url, HttpMethod.POST, entity, String.class);
System.out.println("响应状态: " + response.getStatusCode());
System.out.println("响应内容: " + response.getBody());
}
/**
* 测试获取用户信息(需要token)
*/
@Test
public void testGetUserInfo() {
String url = "http://localhost:8769/api/api/user/info/123";
HttpHeaders headers = new HttpHeaders();
headers.set("token", "valid-token");
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
url, HttpMethod.GET, entity, String.class);
System.out.println("响应状态: " + response.getStatusCode());
System.out.println("响应内容: " + response.getBody());
}
/**
* 测试无token访问(应该被拒绝)
*/
@Test
public void testWithoutToken() {
String url = "http://localhost:8769/api/api/user/info/123";
ResponseEntity<String> response = restTemplate.exchange(
url, HttpMethod.GET, null, String.class);
System.out.println("响应状态: " + response.getStatusCode());
System.out.println("响应内容: " + response.getBody());
}
}
测试命令
# 启动Eureka Server
mvn spring-boot:run -pl eureka-server
# 启动服务提供者
mvn spring-boot:run -pl service-provider
# 启动Zuul网关
mvn spring-boot:run -pl zuul-server
# 测试接口
curl -X POST http://localhost:8769/api/api/user/login \
-H "Content-Type: application/json" \
-d '{"username":"zhangsan","password":"123456"}'
curl -X GET http://localhost:8769/api/api/user/info/123 \
-H "token: valid-token"
curl -X GET http://localhost:8769/api/api/user/info/123
关键特性说明
路由配置说明
- serviceId: 服务的唯一标识
- path: 路由匹配的路径表达式
- stripPrefix: 是否去除前缀
- url: 直接URL路由
过滤器类型
- pre: 路由之前执行
- routing: 路由时执行
- post: 路由之后执行
- error: 错误处理
主要功能
- 请求路由转发
- 认证与授权
- 日志记录
- 熔断降级
- 负载均衡
- 响应处理
这个案例展示了Spring Cloud Zuul的核心功能,可以作为实际项目的参考模板。