微服务网关spring cloud gateway入门详解

1.API网关

API 网关是一个处于应用程序或服务( REST API 接口服务)之前的系统,用来管理授权、访问控制和流量限制等,这样 REST API 接口服务就被 API 网关保护起来,对所有的调用者透明。因此,隐藏在 API 网关后面的业务系统就可以专注于创建和管理服务,而不用去处理这些策略性的基础设施。
1.1 API网关的职能
请求接入:作为所有api接口服务请求的统一接入点。
业务聚合:作为所有后端业务服务的聚合点。
中介策略:实现安全、验证、路由、过滤、限流等策略。
统一管理:对所有api服务和策略统一管理。

1.2 API网关的分类与对比
微服务网关spring cloud gateway入门详解_第1张图片

目前主流的成熟api网关如上图所示,openresty基于ngix和lua脚本,kong在openresty的基础上结合了数据库(postgreSql或Cassandra数据库)实现,这两个网关都需要lua脚本配合实现, spring cloud gateway和zuul是基于java开发,所以对于我们java开发人员相对方便,性能对比openresty>kong>spring cloud gateway≈zuul2.x>zuul1.x。
2 spring cloud gateway
spring cloud gateway是Spring官方基于Spring 5.0,Spring Boot 2.0和Project Reactor等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供一种简单而有效的统一的API路由管理方式。Spring Cloud Gateway作为Spring Cloud生态系中的网关,目标是替代zuul1.x。(由于zuul1.x基于同步的io实现,性能有限,虽然目前zuul2.x的版本已经基于netty实现了异步非阻塞式的通信,但是spring cloud 并没有集成zuul2.x版本) ,gateway使用 Netty 实现异步 IO非阻塞式的通信,支持长连接,基于响应式编程,从而实现了一个简单、比 Zuul 1.x 更高效的、与 Spring Cloud 紧密配合的 API 网关。
2.1spring cloud gateway和zuul1.x的性能对比
微服务网关spring cloud gateway入门详解_第2张图片

通过模拟两组不同的并发量,可以看到spring cloud gateway比zuul 的性能提升了1.5–2倍,低并发的情况下两者差距不是很明显,但随着并发量的增加,spring cloud gateway的性能优势会越明显。

2.2 spring cloud gateway 底层架构实现
spring cloud gateway基于spring 5、spring boot 2和Reactor类库构建,使用Netty作为运行时环境,比较完美的支持异步非阻塞编程。Netty使用非阻塞的IO,线程处理模型建立在主从Reactor多线程模型上。

Reactor是 Spring 5 中响应式编程的基础,一个新的响应式编程库。提供了一个非阻塞的,高并发的基于健壮的Netty框架的网络运行API,响应式编程是一种关注于数据流data streams)和事件传递的异步编程方式,当你做一个带有一定延迟的才能够返回的io操作时,不会阻塞,而是立刻返回一个流,并且订阅这个流,当这个流上产生了返回数据,可以立刻得到通知并调用回调函数处理数据。
Webflux是Reactor中的一个核心概念。Webflux中有两个重要的类Flux和Mono,它们都提供了丰富的操作符。一个Flux对象代表一个包含0或者N个元素的响应式序列,元素可以是普通对象、数据库查询的结果、http响应体,甚至是异常。而一个Mono对象代表0或者1个元素的结果。下图就是一个Flux类型的数据流,Flux往流上发送了3个元素,Subscriber通过订阅这个流来接收通知。类似于java8中的Stream流。
微服务网关spring cloud gateway入门详解_第3张图片
2.3 webflux入门
微服务网关spring cloud gateway入门详解_第4张图片
从以上spring官方给出的对比图可以看出,webflux是一种区别于传统spring mvc的新型编程思想,摒弃了传统的servlet api, 基于响应式编程原理,进而提升性能,是未来的一种趋势。

2.3.1 Webflux 的三大特性

异步非阻塞
众所周知,SpringMVC是同步阻塞的IO模型,资源浪费相对来说比较严重,当我们在处理一个比较耗时的任务时,例如:上传一个比较大的文件,首先,服务器的线程一直在等待接收文件,等到接收完毕,才开始将文件写入磁盘,在写入磁盘的过程中,线程又要等待文件写完才能干其他的事情,这样就会浪费资源。
Spring WebFlux就是来解决这问题的,Spring WebFlux可以做到异步非阻塞。还是上面上传文件的例子,Spring WebFlux是这样做的:线程发现文件还没准备好,就先去做其它事情,当文件准备好之后,通知这根线程来处理,当接收完毕写入磁盘的时候(根据具体情况选择是否做异步非阻塞),写入完毕后通知这根线程再来处理。这样就会极大的避免了资源的浪费。
响应式(reactive)函数编程
支持函数式编程,得益于对于reactive-stream的支持(通过reactor框架来实现的)。
不再拘束于Servlet容器
大部分应用都运行于Servlet容器之中,例如我们大家最为熟悉的Tomcat, Jetty…等等。而现在Spring WebFlux不仅能运行于传统的Servlet容器中(前提是容器要支持Servlet3.1,因为非阻塞IO是使用了Servlet3.1的特性),还能运行在支持NIO的Netty和Undertow中。

2.3.2 Webflux的原理流程
微服务网关spring cloud gateway入门详解_第5张图片

2.3.3 Webflux的编程步骤
Webflux编程大致主要有三个阶段:开始阶段的创建、中间阶段的处理和最终阶段的消费。
创建阶段:创建 Mono 和 Flux对象
中间阶段:中间阶段的 Mono 和 Flux 的方法主要有 filter、map、flatMap、then、zip、reduce 等。这些方法使用方法和java8 Stream 中的方法类似。
结束阶段:结束阶段就是 Mono 或 Flux调用 subscribe 方法。如果在 Web Flux 接口中开发,直接返回 Mono 或 Flux 即可,不用调用subscribe方法。Web Flux 框架会为我们完成最后的 Response 输出工作。

2.3.4 webflux入门程序

  1. pom文件添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
  1. 编写简单的webflux
 	@Test
    public void testMono(){
        Mono.just("tom").subscribe(System.out::println);
    }
    @Test
    public void testFlux() {
        //just方法创建一个反应式的流
        Flux.just("joye", "jack", "allen")
                .filter(s -> s.length() > 3)
                .map(s -> s.concat("@qq.com"))
                .subscribe(System.out::println);
    }

2.4 spring cloud gateway核心概念
2.4.1 路由Route
路由为一组断言与一组过滤器的集合,他是网关的一个基本组件。是网关转发路径规则的体现。
2.4.2 断言Predicate
在将web请求和路由进行匹配时,用到的就是断言predicate,决定请求走哪一个路由。GateWay内置了多种类型的Predicate(10种)。可以通过配置的方式直接使用,也可以组合使用多个路由predicate, 以下是gateway内置的断言。
微服务网关spring cloud gateway入门详解_第6张图片
2.4.3 过滤器Filter
用于拦截和链式处理web请求,实现横切的、与应用无关的需求。可在代理请求处理之前(pre)执行,也可以在请求之后(post)执行。pre类型过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等;post类型过滤器可以做响应内容、响应头的修改、日志输出、流量监控等。除了pre和post的区分,根据作用范围还可以分为针对单个路由的gateway filter和针对所有路由的global gateway filter。
简单的来说,gateway 的过滤器可以总结为三种,默认的过滤器(gateway内置),自定义单个路由的过滤器gateway filter(有两种方式:1、实现GatewayFilter, Ordered接口。2、继承过滤器工厂类AbstractGatewayFilterFactory),全局过滤器(实现GlobalFilter, Ordered)。
以下是gateway内置的过滤器:内置的过滤器工厂有22个实现类,根据过滤器工厂的用途来划分,可以分为以下几种:Header、Parameter、Path、Body、Status、Session、Redirect、Retry、RateLimiter和Hystrix等。

微服务网关spring cloud gateway入门详解_第7张图片

2.5 spring cloud gateway 配置路由的两种方式

2.5.1 yml文件配置(常用)
微服务网关spring cloud gateway入门详解_第8张图片

2.5.2 Java代码编写路由规则
这种方式一般不常用,配置太麻烦而且存在硬编码问题。

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
  return builder.routes()
    .route("blog", r -> 
           r.path("/gateway/user-service/**").uri("http://ip:port/path"))
    .build();
}

2.6 spring cloud gateway过滤器的使用

Spring cloud gateway作为网关,我们在业务开发中,路由的编写,和过滤器的定义是必不可少的,路由实现了网关到子系统的桥接作用,上面已经做了简单的介绍,过滤器主要涉及到网关到子系统之间复杂的业务处理,包括统一登录,统一请求体处理,统一响应内容包装,统一异常处理等等。
Spring cloud gateway网关给我们提供了22个已经实现的过滤器工厂,参考上文spring cloudgateway重要概念过滤器介绍,这22个内置的过滤器我们可以直接使用,配置在yml文件中即可,参考上文yml文件配置。
然而,在具体的业务场景中,这22个可能都不能满足我们的业务,此时需要我们单独编写过滤器,根据具体业务,gateway提供了两种过滤器类型,全局过滤器和局部过滤器。

2.6.1 全局过滤器
所有配置的路由都会经过该过滤器处理,全局过滤器的使用比较简单,只需编写一个全局过滤器类即可,方式如下

@Component  //该注解或者@Configuration将组件注入容器
public class TokenGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //---todo  过滤器业务处理
        /*
        * gateway基于webflux链式编程:
        * 区别于zuul过滤器,zuul过滤器中会有pre前置处理方法,和post后置方法
        * gateway的过滤器只有这一个方法,前置和后置都在此方法中处理,以调用.then()方法为界,之前的编写前置处理逻辑
        * 之后的编写后置处理逻辑。
        * 具体可以参考,cloud-gateway中ModifyRequestGlobalFilter和ModifyResponseGlobalFilter这两个全局过滤器
        * */
        return null;
    }
    @Override
    public int getOrder() {
        //过滤器的执行顺序
        return 0;
    }
}

2.6.2 局部过滤器
针对单个模块或者部分配置了该过滤器的路由使用。局部过滤器的实现有两种方式,以下方式仅供参考。
第一步:先编写一个过滤器,参考以下代码

public class MyGatewayFilter implements GatewayFilter, Ordered {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyGatewayFilter.class);
    private static final String TIME = "Time";
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        exchange.getAttributes().put(TIME,System.currentTimeMillis());
        return chain.filter(exchange).then(
             Mono.fromRunnable(() -> {
                 Long start = exchange.getAttribute(TIME);
                 if(start !=null){
                     try {
                         Thread.sleep(1000);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     LOGGER.info("request uri:" + exchange.getRequest().getURI() + ", Time:" + (System.currentTimeMillis() - start) + "ms");
                 }
             })
        );
    }
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

第二步:编写过滤器工厂。这里类名必须严格以GatewayFilterFactory的方式结尾,下面第三步yml文件配置的时候需要用到。

/*过滤器工厂*/
@Component
public class MyGatewayFilterFactory extends AbstractGatewayFilterFactory {
    @Override
    public GatewayFilter apply(Object config) {
        return new MyGatewayFilter();
    }
}

第三步:将自定义的过滤器工厂配置yml文件中的具体路由中。

spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service #lb代表从注册中心获取服务
          predicates:
            - Path=/user-service/**
          filters:
            - My  #自定义过滤器  这里和过滤器工厂名称保持一致,提取前半部分

2.7 spring cloud gateway 限流
限流作为网关最基本的功能,spring cloud gateway官方已经提供了一个默认的过滤器工厂RequestRateLimiterGatewayFilterFactory这个类,直接使用即可,该方案使用Redis和lua脚本实现令牌桶限流的方式。具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中。并且支持,ip限流,用户限流,接口限流三种方式,下面以ip限流为例。
首先在网关项目的pom文件中引入redis的reactive依赖,代码如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifatId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

其次,在yml配置文件中添加以下的配置:

filters:
  - name: RequestRateLimiter #限流
   args:
      redis-rate-limiter.replenishRate: 10  #允许用户每秒平均处理多少个请求
      redis-rate-limiter.burstCapacity: 20  #令牌桶的总容量,允许在一秒钟内完成的最大请求数
      key-resolver: "#{@ipKeyResolver}"  #用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。

最后,编写网关限流配置类。

/*
* spring cloud 网关限流配置类
* */
@Configuration
public class RateLimiterConfig {
    /*
    * IP限流
    * */
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    }
   /*
    *用户限流:使用这种方式限流,请求路径中必须携带userId参数
    * 
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
    }
    
    * 接口限流:获取请求地址的uri作为限流key
    * 
    @Bean
    public KeyResolver apiKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getPath().value());
    }*/
}

2.8 spring cloud gateway熔断
熔断主要保护的是网关服务,当下游接口负载很大,或者接口不通等其他原因导致超时,如果接口不熔断的话将会影响到下游接口得不到喘息,网关也会因为超时连接一直挂起,很可能因为一个子系统的问题导致整个系统的雪崩。所以我们的网关需要设计熔断,当熔断器打开时,网关将返回一个降级的应答。Spring Cloud Gateway的熔断可以基于Hystrix实现。
首先在pom文件中添加以下依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

其次,在yml配置文件中,新增熔断配置。

filters:
# 熔断降级配置
- name: Hystrix
  args:
    name : default
    fallbackUri: 'forward:/defaultfallback'
# hystrix 信号量隔离,3秒后自动超时
hystrix:
  command:
    default:
      execution:
        isolation:
          strategy: SEMAPHORE
          thread:
            timeoutInMilliseconds: 3000

最后,新建一个降级默认处理方法即可,如下内容仅供参考:

	/*
    * 熔断降级默认返回数据
    * */
    @RequestMapping("/defaultfallback")
    public Map<String,String> defaultfallback(){
        System.out.println("port = " + port);
        System.out.println("address = " + address);
        System.out.println("降级操作...");
        Map<String,String> map = new HashMap<>();
        map.put("resultCode","fail");
        map.put("resultMessage","服务异常");
        map.put("resultObj","null");
        return map;
    }
 

2.9 spring cloud gateway 统一异常处理
spring Cloud Gateway网关的异常处理和传统的springmvc异常处理是有很大区别的,原因在于gateway使用webflux实现,两者底层的运行容器并不相同,Spring Cloud Gateway中的全局异常处理不能直接用@ControllerAdvice来处理,所以需要通过跟踪异常信息的抛出,找到对应的源码,自定义一些处理逻辑来符合业务的需求。
网关一般都是给接口做代理转发的,后端对应的都是REST API,返回数据格式都是JSON。如果不做处理,当发生异常时,Gateway默认给出的错误信息是页面,不方便前端进行异常处理。所以需要对异常信息进行处理,返回JSON格式的数据给客户端。
**示例:**修改前抛出异常

{
  "timestamp": "2020-03-12T05:34:05.356+0000",
  "path": "/app/insertCompany",
  "status": 500,
  "error": "Internal Server Error",
  "message": "发生错误!"
}

示例: 配置全局异常捕获后抛出异常

{
  "code": 500,
  "message": "系统繁忙,请稍后重试。"
}

统一异常的实现

  1. 自定义异常处理配置
@Configuration
public class ExceptionConfig {

    /**
     * 自定义异常处理
     */
    @Primary
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public ErrorWebExceptionHandler errorWebExceptionHandler(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                                             ServerCodecConfigurer serverCodecConfigurer) {
        JsonExceptionHandler jsonExceptionHandler = new JsonExceptionHandler();
        jsonExceptionHandler.setViewResolvers(viewResolversProvider.getIfAvailable(Collections::emptyList));
        jsonExceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
        jsonExceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
        return jsonExceptionHandler;
    }

}

  1. 全局异常处理监听器
public class JsonExceptionHandler implements ErrorWebExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(JsonExceptionHandler.class);

    /**
     * MessageReader
     */
    private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();

    /**
     * MessageWriter
     */
    private List<HttpMessageWriter<?>> messageWriters = Collections.emptyList();

    /**
     * ViewResolvers
     */
    private List<ViewResolver> viewResolvers = Collections.emptyList();

    /**
     * 存储处理异常后的信息
     */
    private ThreadLocal<Map<String, Object>> exceptionHandlerResult = new ThreadLocal<>();

    /**
     * 参考AbstractErrorWebExceptionHandler
     */
    public void setMessageReaders(List<HttpMessageReader<?>> messageReaders) {
        Assert.notNull(messageReaders, "'messageReaders' must not be null");
        this.messageReaders = messageReaders;
    }

    /**
     * 参考AbstractErrorWebExceptionHandler
     */
    public void setViewResolvers(List<ViewResolver> viewResolvers) {
        this.viewResolvers = viewResolvers;
    }

    /**
     * 参考AbstractErrorWebExceptionHandler
     */
    public void setMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
        Assert.notNull(messageWriters, "'messageWriters' must not be null");
        this.messageWriters = messageWriters;
    }

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        // 按照异常类型进行处理
        HttpStatus httpStatus;
        String body;
        if (ex instanceof NotFoundException) {
            httpStatus = HttpStatus.NOT_FOUND;
            body = "很抱歉!你访问的页面不存在。";
        } else if (ex instanceof ResponseStatusException) {
            ResponseStatusException responseStatusException = (ResponseStatusException) ex;
            httpStatus = responseStatusException.getStatus();
            body = responseStatusException.getMessage();
        } else {
            httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
            body = "系统异常,请稍后重试";
        }
        //封装响应体,此body可修改为自己的jsonBody
        Map<String, Object> result = new HashMap<>(2, 1);
        result.put("httpStatus", httpStatus);

        String msg = "{\"code\":" + httpStatus + ",\"message\": \"" + body + "\"}";
        result.put("body", msg);
        //错误记录
        ServerHttpRequest request = exchange.getRequest();
        log.error("[全局异常处理]异常请求路径:{},记录异常信息:{}", request.getPath(), ex.getMessage());
        //参考AbstractErrorWebExceptionHandler
        if (exchange.getResponse().isCommitted()) {
            return Mono.error(ex);
        }
        exceptionHandlerResult.set(result);
        ServerRequest newRequest = ServerRequest.create(exchange, this.messageReaders);
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse).route(newRequest)
                .switchIfEmpty(Mono.error(ex))
                .flatMap((handler) -> handler.handle(newRequest))
                .flatMap((response) -> write(exchange, response));

    }

    /**
     * 参考DefaultErrorWebExceptionHandler
     */
    protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
        Map<String, Object> result = exceptionHandlerResult.get();
        return ServerResponse.status((HttpStatus) result.get("httpStatus"))
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(BodyInserters.fromObject(result.get("body")));
    }

    /**
     * 参考AbstractErrorWebExceptionHandler
     */
    private Mono<? extends Void> write(ServerWebExchange exchange,
                                       ServerResponse response) {
        exchange.getResponse().getHeaders()
                .setContentType(response.headers().getContentType());
        return response.writeTo(exchange, new ResponseContext());
    }

    /**
     * 参考AbstractErrorWebExceptionHandler
     */
    private class ResponseContext implements ServerResponse.Context {

        @Override
        public List<HttpMessageWriter<?>> messageWriters() {
            return JsonExceptionHandler.this.messageWriters;
        }

        @Override
        public List<ViewResolver> viewResolvers() {
            return JsonExceptionHandler.this.viewResolvers;
        }
    }
}

2.10 SPRING CLOUD GATEWAY整合SPRING SESSION
4.10.1 Spring session介绍
微服务项目开发中,Session会话管理是一个很重要的部分,用于存储与记录用户的状态或相关的数据;通常情况下session交由servlet容器(例如tomcat)来负责存储和管理,但是我们的微服务项目分布式部署在多台机器,则session管理存在很大的问题:比如,1、多台tomcat之间无法共享session,比如用户在tomcat A服务器上已经登录了,但当负载均衡跳转到tomcat B时,由于tomcat B服务器并没有用户的登录信息,session就失效了,用户就退出了登录;2、一旦tomcat容器关闭或重启也会导致session会话失效;因此如果项目部署在多台tomcat中,就需要解决session共享的问题。
之前在用zuul作网关的时候,我们选择了整合spring session1.x来统一管理会话。Spring Session 是Spring家族中的一个子项目,Spring Session提供了用于管理用户会话信息的API和实现。它把servlet容器实现的httpSession替换为spring session,专注于解决 session管理问题,Session信息存储在Redis中,可简单快速且无缝的集成到我们的应用中,从而完美的实现了session共享的功能。
4.10.2 Gateway网关整合spring session
Spring cloud gateway整合spring session,需要spring session2.0以上版本的支持,因为spring cloud gateway基于webflux实现,webflux中的session概念由传统的httpsession转换成websession(webflux摒弃了servlet api)。Websession需要spring session2.0版本的支持。
第一步,pom文件引入依赖

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
 </dependency>
 <dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

第二步,yml文件中配置 redis的相关信息
Redis的配置有两种方式,直连和哨兵监听模式,具体可参考网关配置文件。
主要配置redis的链接地址,端口,密码,数据库(可不配置,默认为0号库)
第三步,Spring boot项目的启动类上添加@EnableRedisWebSession
注意:这里gateway是基于webflux所以添加@ EnableRedisWebSession注解 ,如果是传统的springmvc项目添加@ @EnableRedisHttpSession注解。

4.10.3 Gateway中session的简单使用
这里以gateway过滤器中获取session,以及修改session属性为例。

@Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("this is a pre filter");
        return exchange.getSession().flatMap(webSession -> {
            log.info("websession: {}", webSession.getId());
            webSession.getAttributes().put(webSession.getId(), "aaaa");
            return chain.filter(exchange);
        }).then(Mono.fromRunnable(() -> {
            log.info("this is a post filter");
        }));
    }

请求经过filter的时候,会将 webSession.getAttributes().put(webSession.getId(), “aaaa”);存入redis,用PostMan发起一个请求,然后查看redis,可以看到刚才保存进去的数据。

你可能感兴趣的:(微服务网关spring cloud gateway入门详解)