Spring Cloud Zuul案例

wen java案例 1

本文目录导读:

Spring Cloud Zuul案例

  1. 项目结构
  2. 父POM文件
  3. Zuul网关服务模块
  4. 服务提供者模块
  5. Eureka注册中心(可选)
  6. 测试用例
  7. 测试命令
  8. 关键特性说明

我将为您提供一个完整的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: 错误处理

主要功能

  1. 请求路由转发
  2. 认证与授权
  3. 日志记录
  4. 熔断降级
  5. 负载均衡
  6. 响应处理

这个案例展示了Spring Cloud Zuul的核心功能,可以作为实际项目的参考模板。

抱歉,评论功能暂时关闭!