spring mvc convention over configuration 之 RequestToViewNameTranslator
1. RequestToViewNameTranslator简介
在springmvc中很多地方都是约定优于配置的,比如这种写法:
@Controller
public class IndexAction { @RequestMapping("/index.htm")
public void index(){ System.out.println("首页"); } }
这个Handler方法并没有返回视图名,这个时候该怎么办呢,springmvc提供了一个RequestToViewNameTranslator接口就是专门为了解决这种情况的,在没有指定视图名字的时候会调用这个接口的实现来得到要使用的视图名。
RequestToViewNameTranslator : 用于处理没有返回视图名时的情况下如何得到一个默认的视图名。
2. RequestToViewNameTranslator原理分析
那么这个接口是在什么时候被调用的呢,来看一下DispatcherServlet里面的代码:
在DispatcherServlet中有一个变量用来存储当没有返回视图名时要使用的RequestToViewNameTranslator的:
初始化代码:
REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME是一个常量,我们如果要自定义的话beanName一定要和这个值一致:
上面被调用的getDefaultStrategy(ApplicationContext context, Class<T> strategyInterface)方法:
/**
*
* 策略模式,根据传入的策略接口返回默认的策略对象,即根据传入的一个Interface的class类得到其默认的实现类。
*
* 当然啦,默认的实现应该只有一个,那这里为什么还要进行检查呢?继续耐心往下看...
*
* Return the default strategy object for the given strategy interface.
* <p>The default implementation delegates to {@link #getDefaultStrategies},
* expecting a single object in the list.
* @param context the current WebApplicationContext
* @param strategyInterface the strategy interface
* @return the corresponding strategy object
* @see #getDefaultStrategies
*/
protected <T> T getDefaultStrategy(ApplicationContext context, Class<T> strategyInterface) {
List<T> strategies = getDefaultStrategies(context, strategyInterface);
if (strategies.size() != 1) {
throw new BeanInitializationException(
"DispatcherServlet needs exactly 1 strategy for interface [" + strategyInterface.getName() + "]");
}
return strategies.get(0);
}
在getDefaultStrategies获取并实例化策略对象返回:
/**
*
* 根据传入的策略接口来创建一个策略对象列表,即根据传入的一个接口可以得到一大波的对象,但是它是怎么知道这两个怎么对应起来的呢?
* 这是因为在同一个包(org.springframework.web.servlet)下有一个叫做DispatcherServlet.properties的文件记录着接口和对象的映射关系。
*
* Create a List of default strategy objects for the given strategy interface.
* <p>The default implementation uses the "DispatcherServlet.properties" file (in the same
* package as the DispatcherServlet class) to determine the class names. It instantiates
* the strategy objects through the context's BeanFactory.
* @param context the current WebApplicationContext
* @param strategyInterface the strategy interface
* @return the List of corresponding strategy objects
*/
@SuppressWarnings("unchecked")
protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
//获取接口的全路径类名
String key = strategyInterface.getName();
//以接口的全路径类名为key,得到其对应的策略对象
String value = defaultStrategies.getProperty(key);
if (value != null) {
//上面的策略对象如果有多个的话,是以逗号来进行分割,所以这里就相当于按逗号split
String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
//用于装返回结果的
List<T> strategies = new ArrayList<T>(classNames.length);
//然后将上面的分割出的String(这个String其实是实现类的全路径类名)依次遍历进行实例化传入装入strategies以便返回
for (String className : classNames) {
try {
Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
Object strategy = createDefaultStrategy(context, clazz);
strategies.add((T) strategy);
}
catch (ClassNotFoundException ex) {
throw new BeanInitializationException(
"Could not find DispatcherServlet's default strategy class [" + className +
"] for interface [" + key + "]", ex);
}
catch (LinkageError err) {
throw new BeanInitializationException(
"Error loading DispatcherServlet's default strategy class [" + className +
"] for interface [" + key + "]: problem with class file or dependent class", err);
}
}
return strategies;
}
else {
return new LinkedList<T>();
}
}
这个defaultStrategies是个什么鬼呢:
/**
*
* 这个是相对于DispatcherServlet为basePath的资源路径:DispatcherServlet.properties
*
* Name of the class path resource (relative to the DispatcherServlet class)
* that defines DispatcherServlet's default strategy names.
*/
private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties"; //配置文件加载到内存中
private static final Properties defaultStrategies; static {
/*
* 这下面啰里啰嗦一大堆的意思就是这个存储策略映射的文件是程序内部使用的,并不提供开发人员自定义。
*/
// Load default strategy implementations from properties file.
// This is currently strictly internal and not meant to be customized
// by application developers.
try {
//初始化defaultStrategies
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load 'DispatcherServlet.properties': " + ex.getMessage());
}
}
OK,现在已经很明朗了,去看看这个DispatcherServlet.properties究竟长啥样。
位置:
内容:
# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers. org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver # 就是这一行定义了策略接口到策略对象的映射,一直没太搞明白策略模式到底是个什么鬼,现在看起来感觉也就那样吧... o(╯□╰)o
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager
OK,我们分析完了这个东西究竟是怎么来的,再来分析一下它是怎么被调用的:
/**
*
* 将viewNameTranslator又封装了一层方法,根据提供的HttpServletRequest得到一个默认的视图名字
*
* Translate the supplied request into a default view name.
* @param request current HTTP servlet request
* @return the view name (or {@code null} if no default found)
* @throws Exception if view name translation failed
*/
protected String getDefaultViewName(HttpServletRequest request) throws Exception {
return this.viewNameTranslator.getViewName(request);
}
这个getDefaultViewName(HttpServletRequest request)方法又在什么情况下会被调用呢,大概有两种情况,一个是正常的处理:
在DispatcherServlet的doDispatch方法中有一句话:
applyDefaultViewName(processedRequest, mv);
这个方法的实现:
/**
*
* 当没有视图名的时候,使用viewNameTranslator得到一个视图名设置进去
*
* Do we need view name translation?
*/
private void applyDefaultViewName(HttpServletRequest request, ModelAndView mv) throws Exception {
if (mv != null && !mv.hasView()) {
mv.setViewName(getDefaultViewName(request));
}
}
总结一下:
在正常的处理流程时会使用到viewNameTranslator来防止视图为空。
第二种情况是在HandlerExceptionResolver处理异常的情况下:
/**
*
* 根据配置的HandlerExceptionResolvers来得到一个ModelAndView以决定异常发生时应该如何处理
*
* Determine an error ModelAndView via the registered HandlerExceptionResolvers.
* @param request current HTTP request
* @param response current HTTP response
* @param handler the executed handler, or {@code null} if none chosen at the time of the exception
* (for example, if multipart resolution failed)
* @param ex the exception that got thrown during handler execution
* @return a corresponding ModelAndView to forward to
* @throws Exception if no error ModelAndView found
*/
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception { // Check registered HandlerExceptionResolvers...
ModelAndView exMv = null;
// 调用所有的异常处理器,知道有人能处理
for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
} if (exMv != null) {
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
// 当异常没有明确的指定返回的视图名字的时候就要借助于RequestToViewNameTranslator来得到一个默认的视图名字
// We might still need view name translation for a plain error model...
if (!exMv.hasView()) {
exMv.setViewName(getDefaultViewName(request));
}
if (logger.isDebugEnabled()) {
logger.debug("Handler execution resulted in exception - forwarding to resolved error view: " + exMv, ex);
}
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
} // 如果没有HandlerExceptionResolver能够处理,就将异常继续往上抛
throw ex;
}
OK,分析完了这个东西是如何被调用的,再来看一下它的代码实现:
RequestToViewNameTranslator策略接口的代码分析:
package org.springframework.web.servlet; import javax.servlet.http.HttpServletRequest; /**
*
* 当处理的handler方法没有明确的返回视图名的时候,就会采用这个接口来得到视图名。
* 这个接口采用了策略模式,会根据不同的情况使用不同的实现。
*
* Strategy interface for translating an incoming
* {@link javax.servlet.http.HttpServletRequest} into a
* logical view name when no view name is explicitly supplied.
*
* @author Rob Harrop
* @author Juergen Hoeller
* @since 2.0
*/
public interface RequestToViewNameTranslator { /**
*
* 将HttpServletRequest转换为一个String类型的视图名字。
*
* Translate the given {@link HttpServletRequest} into a view name.
* @param request the incoming {@link HttpServletRequest} providing
* the context from which a view name is to be resolved
* @return the view name (or {@code null} if no default found)
* @throws Exception if view name translation fails
*/
String getViewName(HttpServletRequest request) throws Exception; }
默认实现类(即策略对象)的代码实现:
package org.springframework.web.servlet.view; import javax.servlet.http.HttpServletRequest; import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.RequestToViewNameTranslator;
import org.springframework.web.util.UrlPathHelper; /**
*
*
* RequestToViewNameTranslator类只是最简单的将请求的URI转换为视图名字。
*
* 我们可以指定一个简单的viewNameTranslator,在没有明确指定返回的视图名字的时候就会使用默认的实现,
* 默认的实现就是这个类...
*
* 这个默认的转换规则是将头部和尾部的斜线以及扩展名去掉,然后可以配置前缀和后缀,这样子在处理完视图名字返回的时候会加上头尾的。
*
* 使用到的一些参数都可以使用setter来进行设置,具体的方法是在配置文件中配置的时候传入即可。
*
* 栗子:
*
* http://localhost:8080/gamecast/display.html --> display
* http://localhost:8080/gamecast/displayShoppingCart.html --> displayShoppingCart
* http://localhost:8080/gamecast/admin/index.html --> admin/index
*
* 规则:
* 1. 取contextPath后面的requestURI
* 2. 去掉头尾的斜线和.后面的东西
* 3. 加上prefix和suffix返回作为视图名字
*
* {@link RequestToViewNameTranslator} that simply transforms the URI of
* the incoming request into a view name.
*
* <p>Can be explicitly defined as the {@code viewNameTranslator} bean in a
* {@link org.springframework.web.servlet.DispatcherServlet} context.
* Otherwise, a plain default instance will be used.
*
* <p>The default transformation simply strips leading and trailing slashes
* as well as the file extension of the URI, and returns the result as the
* view name with the configured {@link #setPrefix prefix} and a
* {@link #setSuffix suffix} added as appropriate.
*
* <p>The stripping of the leading slash and file extension can be disabled
* using the {@link #setStripLeadingSlash stripLeadingSlash} and
* {@link #setStripExtension stripExtension} properties, respectively.
*
* <p>Find below some examples of request to view name translation.
* <ul>
* <li>{@code http://localhost:8080/gamecast/display.html} &raquo; {@code display}</li>
* <li>{@code http://localhost:8080/gamecast/displayShoppingCart.html} &raquo; {@code displayShoppingCart}</li>
* <li>{@code http://localhost:8080/gamecast/admin/index.html} &raquo; {@code admin/index}</li>
* </ul>
*
* @author Rob Harrop
* @author Juergen Hoeller
* @since 2.0
* @see org.springframework.web.servlet.RequestToViewNameTranslator
* @see org.springframework.web.servlet.ViewResolver
*/
public class DefaultRequestToViewNameTranslator implements RequestToViewNameTranslator { private static final String SLASH = "/"; //在转换完后要加上的前缀
private String prefix = ""; //在转换完后要加上的后缀
private String suffix = ""; //转换完后的要使用的分隔符
private String separator = SLASH; //是否去掉前面头部的斜线
private boolean stripLeadingSlash = true; //是否去掉尾部的斜线
private boolean stripTrailingSlash = true; //是否要去掉扩展名
private boolean stripExtension = true; //工具类,在这里用来取出request URI
private UrlPathHelper urlPathHelper = new UrlPathHelper(); /**
* Set the prefix to prepend to generated view names.
* @param prefix the prefix to prepend to generated view names
*/
public void setPrefix(String prefix) {
this.prefix = (prefix != null ? prefix : "");
} /**
* Set the suffix to append to generated view names.
* @param suffix the suffix to append to generated view names
*/
public void setSuffix(String suffix) {
this.suffix = (suffix != null ? suffix : "");
} /**
* Set the value that will replace '{@code /}' as the separator
* in the view name. The default behavior simply leaves '{@code /}'
* as the separator.
*/
public void setSeparator(String separator) {
this.separator = separator;
} /**
* Set whether or not leading slashes should be stripped from the URI when
* generating the view name. Default is "true".
*/
public void setStripLeadingSlash(boolean stripLeadingSlash) {
this.stripLeadingSlash = stripLeadingSlash;
} /**
* Set whether or not trailing slashes should be stripped from the URI when
* generating the view name. Default is "true".
*/
public void setStripTrailingSlash(boolean stripTrailingSlash) {
this.stripTrailingSlash = stripTrailingSlash;
} /**
* Set whether or not file extensions should be stripped from the URI when
* generating the view name. Default is "true".
*/
public void setStripExtension(boolean stripExtension) {
this.stripExtension = stripExtension;
} /**
* Set if URL lookup should always use the full path within the current servlet
* context. Else, the path within the current servlet mapping is used
* if applicable (i.e. in the case of a ".../*" servlet mapping in web.xml).
* Default is "false".
* @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath
*/
public void setAlwaysUseFullPath(boolean alwaysUseFullPath) {
this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath);
} /**
* Set if the context path and request URI should be URL-decoded.
* Both are returned <i>undecoded</i> by the Servlet API,
* in contrast to the servlet path.
* <p>Uses either the request encoding or the default encoding according
* to the Servlet spec (ISO-8859-1).
* @see org.springframework.web.util.UrlPathHelper#setUrlDecode
*/
public void setUrlDecode(boolean urlDecode) {
this.urlPathHelper.setUrlDecode(urlDecode);
} /**
* Set if ";" (semicolon) content should be stripped from the request URI.
* @see org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean)
*/
public void setRemoveSemicolonContent(boolean removeSemicolonContent) {
this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent);
} /**
* Set the {@link org.springframework.web.util.UrlPathHelper} to use for
* the resolution of lookup paths.
* <p>Use this to override the default UrlPathHelper with a custom subclass,
* or to share common UrlPathHelper settings across multiple web components.
*/
public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
Assert.notNull(urlPathHelper, "UrlPathHelper must not be null");
this.urlPathHelper = urlPathHelper;
} /**
*
* 传入一个HttpServletRequest,根据这个请求的URI计算出返回的视图名字.
*
* Translates the request URI of the incoming {@link HttpServletRequest}
* into the view name based on the configured parameters.
* @see org.springframework.web.util.UrlPathHelper#getLookupPathForRequest
* @see #transformPath
*/
@Override
public String getViewName(HttpServletRequest request) {
//得到request URI
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
//然后转换路径并加入可自定义的prefix和suffix
return (this.prefix + transformPath(lookupPath) + this.suffix);
} /**
*
* 将这个请求的URI的斜线和扩展名干掉,比如传入/index.htm,会将头部的斜线和.之后的htm干掉,返回的是index
*
* Transform the request URI (in the context of the webapp) stripping
* slashes and extensions, and replacing the separator as required.
* @param lookupPath the lookup path for the current request,
* as determined by the UrlPathHelper
* @return the transformed path, with slashes and extensions stripped
* if desired
*/
protected String transformPath(String lookupPath) {
String path = lookupPath;
// 干掉头部的斜线分隔符,默认是要干掉的
if (this.stripLeadingSlash && path.startsWith(SLASH)) {
path = path.substring(1);
}
// 干掉尾部的斜线分隔符,默认是干掉
if (this.stripTrailingSlash && path.endsWith(SLASH)) {
path = path.substring(0, path.length() - 1);
}
//干掉扩展名,默认是干掉
if (this.stripExtension) {
path = StringUtils.stripFilenameExtension(path);
}
/* 如果分隔符不是斜线的话,就将所有的斜线转换为分隔符,上面对separator进行初始的时候是直接separator=SLASH的,
* 所以如果不使用setSeparator(String separator)来自定义分隔符的话这一句是永远不会被执行的
* * */
if (!SLASH.equals(this.separator)) {
path = StringUtils.replace(path, SLASH, this.separator);
}
return path;
} }
3. 自定义RequestToViewNameTranslator
当没有明确指定返回视图名时使用我们自己的RequestToViewNameTranslator来进行处理。
新建一个Class实现RequestToViewNameTranslator接口:
package org.cc1100100.springmvc.study_001; import javax.servlet.http.HttpServletRequest; import org.springframework.web.servlet.RequestToViewNameTranslator; /**
*
* 自定义的RequestToViewNameTranslator
*
* @author chenjc20326
*
*/
public class FooRequestToViewNameTranslator implements RequestToViewNameTranslator{ public String getViewName(HttpServletRequest request) throws Exception {
//凡是没有明确返回视图名的一律跳转到defaultPage页面
return "defaultPage";
} }
然后将其在springmvc的配置文件中声明:
<!-- 配置自定义的RequestToViewNameTranslator -->
<bean name="viewNameTranslator" class="org.cc1100100.springmvc.study_001.FooRequestToViewNameTranslator" />
然后就可以用啦,再当handler方法没有返回视图名的时候就会调用FooRequestToViewNameTranslator来进行处理。
参考资料:
1. spring-webmvc-4.3.2源代码。
spring mvc convention over configuration 之 RequestToViewNameTranslator的更多相关文章
- Spring MVC小结
Spring MVC项目搭建 添加依赖 (省略) Spring MVC配置类 @Configuration @EnableWebMvc @ComponentScan("com.sjx.spr ...
- Spring MVC学习总结(6)——一些Spring MVC的使用技巧
APP服务端的Token验证 通过拦截器对使用了 @Authorization 注解的方法进行请求拦截,从http header中取出token信息,验证其是否合法.非法直接返回401错误,合法将to ...
- 实现WebMvcConfigurer接口扩展Spring MVC的功能
前言: 先查看WebMvcConfigurer接口中都定义了哪些内容 public interface WebMvcConfigurer { default void configurePathMat ...
- Unit Testing of Spring MVC Controllers: Configuration
Original Link: http://www.petrikainulainen.net/programming/spring-framework/unit-testing-of-spring-m ...
- 精尽Spring MVC源码分析 - RequestToViewNameTranslator 组件
该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...
- Spring MVC 学习笔记 spring mvc Schema-based configuration
Spring mvc 目前支持5个tag,分别是 mvc:annotation-driven,mvc:interceptors,mvc:view-controller, mvc:resources和m ...
- spring mvc DispatcherServlet详解之前传---FrameworkServlet
做项目时碰到Controller不能使用aop进行拦截,从网上搜索得知:使用spring mvc 启动了两个context:applicationContext 和WebapplicationCont ...
- Spring MVC 学习总结(二)——控制器定义与@RequestMapping详解
一.控制器定义 控制器提供访问应用程序的行为,通常通过服务接口定义或注解定义两种方法实现. 控制器解析用户的请求并将其转换为一个模型.在Spring MVC中一个控制器可以包含多个Action(动作. ...
- Spring MVC 学习总结(一)——MVC概要与环境配置
一.MVC概要 MVC是模型(Model).视图(View).控制器(Controller)的简写,是一种软件设计规范,用一种将业务逻辑.数据.显示分离的方法组织代码,MVC主要作用是降低了视图与业务 ...
随机推荐
- 2nd 词频统计效能测试
词频统计效能测试 使用性能分析工具分析结果如下 :
- 设计模式PHP篇(三)————装饰器模式
简单的用php实现了装饰器模式: <?php /** *简单的装饰器模式 */ class PrintText { protected $decorators = []; public func ...
- Delphi编程防止界面卡死的方法经验分享
Delphi编程防止界面卡死的方法经验分享! 1.循环里面防止界面卡死的方法可以使用Application.ProcessMessages: 例如下列方法: var n: Integ ...
- FZU2128_最长子串
题目说给你一个长串,要你选一个最长子串,不包括任何一个给定串为子串. 建立一个自动机,每个点保存的信息为当前这个状态为结尾最长可以有多长? 然后....就可以了... #include <ios ...
- C++解析(9):关于const和引用的疑问
0.目录 1.关于const的疑问 2.关于引用的疑问 2.1 引用与指针 2.2 从C++语言与C++编译器角度看引用 2.3 从工程项目开发看引用 3.小结 1.关于const的疑问 const什 ...
- Now or later UVALive - 3211(2-SAT 最小值最大化)
emmm...去吃早饭了... rujia讲的很好.. 最小值最大化问题,,,二分枚举答案 设x1.x2为同一个集合中的元素,y1.y2为另一个集合中的元素,如果x1与y1之差小于mid,那么如果 ...
- 【BZOJ4516】生成魔咒(后缀自动机)
[BZOJ4516]生成魔咒(后缀自动机) 题面 BZOJ Description 魔咒串由许多魔咒字符组成,魔咒字符可以用数字表示.例如可以将魔咒字符 1.2 拼凑起来形成一个魔咒串 [1,2]. ...
- 延长xss的攻击(转)
XSS 的本质仍是一段脚本.和其他文档元素一样,页面关了一切都销毁.除非能将脚本蔓延到页面以外的地方,那样才能获得更长的生命力. 庆幸的是,从 DOM 诞生的那一天起,就已为我们准备了这个特殊的功能, ...
- 【arc080F】Prime Flip
Portal --> arc080_f Solution 这题的话..差分套路题(算吗?反正就是想到差分就很好想了qwq) (但是问题就是我不会这种套路啊qwq题解原话是:&quo ...
- CSU 多校训练第二场 J Pinemi Puzzles
传送门:http://acm.csu.edu.cn:20080/csuoj/problemset/problem?pid=2279 题意: 代码: #include <set> #incl ...