spring源码------一个请求在spring中的处理过程(请求的处理链HandlerExecutionChain的选择)代码及流程图说明 (3)

本文主要是介绍spring源码------一个请求在spring中的处理过程(请求的处理链HandlerExecutionChain的选择)代码及流程图说明 (3),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

      • 前提
      • 1.`HandlerMapping`何时被封装到`handlerMappings`
      • 2. `HandlerExecutionChain`对象
      • 3 如何获取`HandlerExecutionChain`对象的
        • 3.1 进入到`AbstractHandlerMapping`的`getHandler`方法
          • 3.2.1 获取`handler`
            • 3.2.1.1 `getHandlerInternal`方法的实现类
            • 3.2.1.2 寻找内部`HandlerMethod`的`getHandlerInternal`方法(这里举例的是`RequestMappingHandlerMapping`)
          • 3.2.2 如何根据请求的路径获取`HandlerMethod`
            • 3.2.2.1 注册列表`mappingRegistry`是何时完成映射的注册的
          • 3.2.3 生成`HandlerExecutionChain`对象的`getHandlerExecutionChain`
      • 4.总结

前提

 前面已经讲过了从Servlet规范到FrameworkServlet,以及从FrameworkServlet规范到DispatcherServlet。其中在DispatcherServlet这个类中讲到了doDispatch方法,其中的第一个重要步骤就是,调用getHandler方法根据请求对象获取合适的HandlerExecutionChain

	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {//判断handlerMappings是否为空,默认不会为空的if (this.handlerMappings != null) {//迭代handlerMappingsfor (HandlerMapping mapping : this.handlerMappings) {//调用HandlerMapping的getHandler方法获取handler,这里只获取到第一个合适的HandlerExecutionChain,所以有顺序优先性HandlerExecutionChain handler = mapping.getHandler(request);if (handler != null) {return handler;}}}return null;}

 这里有两个关键的东西:

  1. 包含HandlerMapping对象集合的handlerMappings
  2. 返回的HandlerExecutionChain

1.HandlerMapping何时被封装到handlerMappings

handlerMappings是在容器刷新上下文的时候会调用的,具体的可以看看这个spring什么时候以及何时初始化web应用相关上下文的(FrameworkServlet,DispatcherServlet)。
这里直接看initStrategies方法中的initHandlerMappings方法逻辑。

	private void initHandlerMappings(ApplicationContext context) {this.handlerMappings = null;//是否获取容器中的所有的HandlerMappingif (this.detectAllHandlerMappings) {//获取容器中的所有的HandlerMappingMap<String, HandlerMapping> matchingBeans =BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);//如果存在HandlerMapping,则进行排序if (!matchingBeans.isEmpty()) {this.handlerMappings = new ArrayList<>(matchingBeans.values());// We keep HandlerMappings in sorted order.AnnotationAwareOrderComparator.sort(this.handlerMappings);}}else {try {//获取上下文中的bean的名字是handlerMapping的类型为HandlerMapping的beanHandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);this.handlerMappings = Collections.singletonList(hm);}catch (NoSuchBeanDefinitionException ex) {// Ignore, we'll add a default HandlerMapping later.}}//如果容器中没有,则用默认的,一般情况下不会到这一波,关于默认的配置会读取DispatcherServlet.properties配置文件中的配置if (this.handlerMappings == null) {this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);if (logger.isTraceEnabled()) {logger.trace("No HandlerMappings declared for servlet '" + getServletName() +"': using default strategies from DispatcherServlet.properties");}}}

 整体的逻辑还是比较简单的,就是从容器中获取所有的HandlerMapping类型的bean封装到一个集合中,然后在进行排序。

2. HandlerExecutionChain对象

HandlerExecutionChain主要是一个由Object类型的(默认情况下是HandlerMethod对象)的handler字段跟两个由HandlerInterceptor对象组成的拦截器数组跟集合组成的

public class HandlerExecutionChain {//默认情况下这个Object是HandlerMethod类型的private final Object handler;@Nullableprivate HandlerInterceptor[] interceptors;@Nullableprivate List<HandlerInterceptor> interceptorList;
}

3 如何获取HandlerExecutionChain对象的

 我们直接进入到DispatcherServlet类的getHandler方法中。

	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {//判断handlerMappings是否为空,默认不会为空的if (this.handlerMappings != null) {//迭代handlerMappingsfor (HandlerMapping mapping : this.handlerMappings) {//调用HandlerMapping的getHandler方法获取handler,这里只获取到第一个合适的HandlerExecutionChain,所以有顺序优先性HandlerExecutionChain handler = mapping.getHandler(request);if (handler != null) {return handler;}}}return null;}

 这里需要注意的一点事在循环遍历HandlerMapping对象集合的时候,如果找到了第一个合适的就会停下来,直接返回。所以一般自定义的实现类都会实现Ordred接口或者贴上@Order注解来指定对应的顺序。

3.1 进入到AbstractHandlerMappinggetHandler方法

HandlerMappinggetHandler方法由AbstractHandlerMapping实现的。

		public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {//获取handlerObject handler = getHandlerInternal(request);//如果没有找到合适的处理器,就用默认的正常情况下是nullif (handler == null) {handler = getDefaultHandler();}//如果没有handel则直接返回if (handler == null) {return null;}// Bean name or resolved handler?//如果对应的HandlerMethod是string类型的则需要确认是不是Bean,并寻找对应的beanif (handler instanceof String) {String handlerName = (String) handler;handler = obtainApplicationContext().getBean(handlerName);}//添加符合当前请求路径的拦截器到一个新的HandlerExecutionChain对象并返回HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);if (logger.isTraceEnabled()) {logger.trace("Mapped to " + handler);}else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) {logger.debug("Mapped to " + executionChain.getHandler());}//这个部分的逻辑不太清楚,大概跟跨域请求相关的,如果handler有跟跨域先关的配置(实现了CorsConfigurationSource接口)if (hasCorsConfigurationSource(handler)) {CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null);CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);config = (config != null ? config.combine(handlerConfig) : handlerConfig);executionChain = getCorsHandlerExecutionChain(request, executionChain, config);}return executionChain;}

 这个方法的逻辑就是获取导一个请求处理器对象:

  1. 调用由子类来实现的getHandlerInternal方法,获取一个合适的Object类型的handler(默认情况下是AbstractHandlerMethodMapping实现的返回的是HandlerMethod类型)
  2. 如果没有合适的则用默认的,默认情况下是默认的也是null
  3. 如果是handler是null,则直接结束当前方法
  4. 如果hanler是String类型的,则可能表示的是一个bean的名称,所以前去上下文中去取
  5. 根据请求的路径获取handler中的合适的拦截器,然后加入到创建的HandlerExecutionChain对象中,并返回
  6. 关于跨域先关的处理逻辑,这一部分没太理解
3.2.1 获取handler

 在上面的步骤中最关键的就是第一步的获取合适请求处理器。第一步的方法getHandlerInternal是在AbstractHandlerMapping中定义的由子类来实现的一个方法。

3.2.1.1 getHandlerInternal方法的实现类

 在spring中AbstractHandlerMapping类的getHandlerInternal有很多实现比如AbstractUrlHandlerMappingAbstractHandlerMethodMapping等,但是哪一个才是默认的呢。这里直接说。
 在Spring中有一个类DelegatingWebMvcConfiguration,这个类会被容器管理,而这个类的父类WebMvcConfigurationSupport中会用@Bean注解注入多个AbstractHandlerMapping的子类到到容器中比如,RequestMappingHandlerMappingBeanNameUrlHandlerMappingSimpleUrlHandlerMapping等。


@Beanpublic BeanNameUrlHandlerMapping beanNameHandlerMapping(......return mapping;}@Beanpublic RequestMappingHandlerMapping requestMappingHandlerMapping(ContentNegotiationManager mvcContentNegotiationManager,FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();mapping.setOrder(0);mapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));mapping.setContentNegotiationManager(mvcContentNegotiationManager);mapping.setCorsConfigurations(getCorsConfigurations());......return mapping;}

 接下来的分析我们选取的是RequestMappingHandlerMapping这个类进行分析的,其他类的这个方法的目的都是一样的,大同小异,可以自己查看源码

3.2.1.2 寻找内部HandlerMethodgetHandlerInternal方法(这里举例的是RequestMappingHandlerMapping

 在RequestMappingHandlerMapping中实现的getHandlerInternal方法回先调用父类AbstractHandlerMethodMapping的实现,然后执行之际的额外逻辑。所以先看看AbstractHandlerMethodMapping的逻辑

	protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {//获取映射查找路径 比如 localhost/one/two 对应的lookupPath就是/one/twoString lookupPath = getUrlPathHelper().getLookupPathForRequest(request);//将请求的路径加入到请求对象中request.setAttribute(LOOKUP_PATH, lookupPath);//获取读锁this.mappingRegistry.acquireReadLock();try {//根据路径查找handlerMethodHandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);//如果handlerMethod不是null,并且中的bean是容器中的Bean则需要先获取return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);}finally {//释放锁this.mappingRegistry.releaseReadLock();}}

 主要逻辑就是先获取请求的请求路径,然后先获取一个读锁根据请求的路径去找对应的HandlerMethod,找到之后就返回,没有找到就返回null,最后释放读锁。

3.2.2 如何根据请求的路径获取HandlerMethod

 接下来的逻辑就是如何根据请求的路径来获取HandlerMethod了。

	protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {//Match对象保存了路径跟对应的handlerMethod的包装类List<Match> matches = new ArrayList<>();//根据路径查找对应匹配的路径集合,mappingRegistry会在AbstractHandlerMethodMapping初始化之后设置,MappingRegistry是一个注册表对象,它维护所有到处理程序方法的映射List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);//对应的匹配的路径集合不为空,将路径跟请求跟对应的handleMethod保存到Match中if (directPathMatches != null) {addMatchingMappings(directPathMatches, matches, request);}//匹配器为空说明没有合适的mapping跟当前的request匹配,就把所有的mapping跟当前请求进行匹配选择if (matches.isEmpty()) {// No choice but to go through all mappings...addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);}//如果找到了合适的就进行下一步处理if (!matches.isEmpty()) {//获取一个MatchComparatorComparator<Match> comparator = new MatchComparator(getMappingComparator(request));//进行排序matches.sort(comparator);//选出最佳的匹配Match bestMatch = matches.get(0);if (matches.size() > 1) {if (logger.isTraceEnabled()) {logger.trace(matches.size() + " matching mappings: " + matches);}if (CorsUtils.isPreFlightRequest(request)) {return PREFLIGHT_AMBIGUOUS_MATCH;}Match secondBestMatch = matches.get(1);//如果存在相同的路径则报错if (comparator.compare(bestMatch, secondBestMatch) == 0) {Method m1 = bestMatch.handlerMethod.getMethod();Method m2 = secondBestMatch.handlerMethod.getMethod();String uri = request.getRequestURI();throw new IllegalStateException("Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");}}//设置匹配的处理方法request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);//从mappingInfo中获取URI模板变量、矩阵变量和可产生的媒体类型handleMatch(bestMatch.mapping, lookupPath, request);//返回HandlerMethodreturn bestMatch.handlerMethod;}else {//没有就将所有的匹配路径交给子类在遍历一边,如果还是寻找不出来,就根据不同情况抛出对应的异常return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);}}

 现在对逻辑进行分析:

  1. 从处理方法跟路径的注册表中,根据请求的路径获取对应的映射集合
  2. 如果匹配的路径不为空将对应的路径跟匹配的HandlerMethod加入到路径跟处理器的包装类集合matches
  3. 如果matches为空,说明没有合适的mapping匹配的,因此要遍历所有的mapping进行寻找
  4. 如果找到了,先获取一个合适MatchComparator(用于后面的匹配HandlerMethod用),然后将matches中的第一个跟第二个进行比较获取合适的HandlerMethod。获取到合适的之后将对应的HandlerMethod设置到request的属性中
  5. 如果没有找到,就将所有的匹配路径交给子类在遍历一边,如果还是寻找不出来,就根据不同情况抛出对应的异常
3.2.2.1 注册列表mappingRegistry是何时完成映射的注册的

 在上面的方法中一个核心的对象就是MappingRegistry这个注册列表。这个对象保存了对应的请求跟处理方法和其他配置。
AbstractHandlerMethodMapping实现了InitializingBeanafterPropertiesSet方法,在初始化之后会调用这个方法。

	public void afterPropertiesSet() {//初始化映射的方法initHandlerMethods();}protected void initHandlerMethods() {//确定应用程序上下文中候选bean的名称for (String beanName : getCandidateBeanNames()) {//如果bean的名称不是以scopedTarget开头,这个开头的bean是被代理的有作用域的bean,这种情况下需要排除if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {//处理候选beanprocessCandidateBean(beanName);}}//初始化handlerMethodhandlerMethodsInitialized(getHandlerMethods());}protected String[] getCandidateBeanNames() {//是object类型的都拿出来return (this.detectHandlerMethodsInAncestorContexts ?BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :obtainApplicationContext().getBeanNamesForType(Object.class));}protected void processCandidateBean(String beanName) {Class<?> beanType = null;try {//获取bean的Class对象beanType = obtainApplicationContext().getType(beanName);}catch (Throwable ex) {// An unresolvable bean type, probably from a lazy bean - let's ignore it.if (logger.isTraceEnabled()) {logger.trace("Could not resolve type for bean '" + beanName + "'", ex);}}//如果bean不为空,并且是符合的类型,其中的isHandler在RequestMappingHandlerMapping实现if (beanType != null && isHandler(beanType)) {//对bean进行注册detectHandlerMethods(beanName);}}protected boolean isHandler(Class<?> beanType) {//bean上面有Controller注解或者RequestMapping类型注解return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));}

 上面逻辑中主要就是在上下文中寻找所有的bean,然后对bean进行筛选需要满足以下条件的bean:

  1. bean是不是被代理出来的有作用域的bean
  2. bean的Class对象不能为null
  3. bean的Class对象有Controller注解或者RequestMapping类型注解(注意这里的isHandler具体是现在RequestMappingHandlerMapping中)

 然后就是对bean的注册处理了,处理的方法就在detectHandlerMethods方法中

	protected void detectHandlerMethods(Object handler) {//如果对应的handler是String类型的,则从上下文中获取对应的name为handler 的bean 的类型,不是String则直接获取对应的Class对象Class<?> handlerType = (handler instanceof String ?obtainApplicationContext().getType((String) handler) : handler.getClass());//如果类型不是nullif (handlerType != null) {//获取原始Class,因为这个Class可能是经过CGLIB代理过后的Class<?> userType = ClassUtils.getUserClass(handlerType);//获取对应的Class中的贴有RequestMapping或者派生于RequestMapping注解的方法,然后处理封装为RequestMappingInfo对象,跟对饮给的处理方法组成一个map对象//RequestMappingInfo对象包含注解中的一些参数设置信息Map<Method, T> methods = MethodIntrospector.selectMethods(userType,(MethodIntrospector.MetadataLookup<T>) method -> {try {return getMappingForMethod(method, userType);}catch (Throwable ex) {throw new IllegalStateException("Invalid mapping on handler class [" +userType.getName() + "]: " + method, ex);}});if (logger.isTraceEnabled()) {logger.trace(formatMappings(userType, methods));}//对方法进行迭代,然后注册methods.forEach((method, mapping) -> {//获取可调用的方法Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);//注册到mappingRegistry对象中registerHandlerMethod(handler, invocableMethod, mapping);});}}

 这里的逻辑就是从对应的请求处理类的Class对象中获取所有的RequestMapping注解的方法转化成一个MethodRequestMappingInfo组成的map集合,然后进行处理。具体的获取方法跟封住逻辑可以自行查看,都是对反射的利用。
 上面就是对应的MappingRegistry注册列表的生成逻辑了。

3.2.3 生成HandlerExecutionChain对象的getHandlerExecutionChain

 现在我们要回到AbstractHandlerMappinggetHandler方法了。上面的逻辑都是分析的这个方法中的getHandlerInternal方法的逻辑,现在看下一个关键步骤HandlerExecutionChain对象的生成方法getHandlerExecutionChain
 这个其实比较简单就是对interceptor的一个筛选过程。

protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {//检查handler是不是HandlerExecutionChain子类,不是则需要创建HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));//获取请求的路径String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, LOOKUP_PATH);//循环对应的HandlerInterceptorfor (HandlerInterceptor interceptor : this.adaptedInterceptors) {//如果是MappedInterceptor,需要对路径进行匹配不是的则直接添加if (interceptor instanceof MappedInterceptor) {MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;//检查路径是不是以下请求“/actuator/*”或者“/error”,不是则将对应的MappedInterceptor加入到HandlerExecutionChain中if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {chain.addInterceptor(mappedInterceptor.getInterceptor());}}else {chain.addInterceptor(interceptor);}}return chain;}

4.总结

整体的逻辑有部分代码省略了,但是逻辑还是有点多的,这里做一个大概的总结:

  1. DispatchServletinitHandlerMappings方法中,会从容器中获取到所有的HandlerMapping类型的bean,然后按照顺序进行排序
  2. DispatchServletgetHandler方法中,会调用对应的HandlerMappinggetHandler方法获取HandlerExecutionChain对象
    2.1 进入到对应的HandlerMappinggetHandler方法(这里spring中的HandlerMappinggetHandler方法都是调用的AbstractHandlerMapping类的),回先调用子类实现的getHandlerInternal方法获取合适的处理对象(这里举例的是RequestMappingHandlerMapping
    2.2 获取到了对应的请求处理对象后,用将请求对象封装在一个HandlerExecutionChain对象中,并将合适的拦截器集合也封装在一起,然后返回一个HandlerExecutionChain

这篇关于spring源码------一个请求在spring中的处理过程(请求的处理链HandlerExecutionChain的选择)代码及流程图说明 (3)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/783321

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Zookeeper安装和配置说明

一、Zookeeper的搭建方式 Zookeeper安装方式有三种,单机模式和集群模式以及伪集群模式。 ■ 单机模式:Zookeeper只运行在一台服务器上,适合测试环境; ■ 伪集群模式:就是在一台物理机上运行多个Zookeeper 实例; ■ 集群模式:Zookeeper运行于一个集群上,适合生产环境,这个计算机集群被称为一个“集合体”(ensemble) Zookeeper通过复制来实现

如何选择适合孤独症兄妹的学校?

在探索适合孤独症儿童教育的道路上,每一位家长都面临着前所未有的挑战与抉择。当这份责任落在拥有孤独症兄妹的家庭肩上时,选择一所能够同时满足两个孩子特殊需求的学校,更显得尤为关键。本文将探讨如何为这样的家庭做出明智的选择,并介绍星贝育园自闭症儿童寄宿制学校作为一个值得考虑的选项。 理解孤独症儿童的独特性 孤独症,这一复杂的神经发育障碍,影响着儿童的社交互动、沟通能力以及行为模式。对于拥有孤独症兄

无人叉车3d激光slam多房间建图定位异常处理方案-墙体画线地图切分方案

墙体画线地图切分方案 针对问题:墙体两侧特征混淆误匹配,导致建图和定位偏差,表现为过门跳变、外月台走歪等 ·解决思路:预期的根治方案IGICP需要较长时间完成上线,先使用切分地图的工程化方案,即墙体两侧切分为不同地图,在某一侧只使用该侧地图进行定位 方案思路 切分原理:切分地图基于关键帧位置,而非点云。 理论基础:光照是直线的,一帧点云必定只能照射到墙的一侧,无法同时照到两侧实践考虑:关