《Spring 5 官方文档》18. Web MVC 框架(一)

  1. 云栖社区>
  2. 博客>
  3. 正文

《Spring 5 官方文档》18. Web MVC 框架(一)

青衫无名 2017-05-18 14:45:39 浏览3043
展开阅读全文

18.1  Spring Web MVC 框架的介绍

Spring Web模型视图控制器(MVC)框架是围绕一个DispatcherServlet设计的,它将请求分派给处理程序,具有可配置的处理程序映射,视图解析,区域设置,本地化和主题解析,并且支持上传文件。默认的处理是基于注解@Controller 和@RequestMapping,提供一系列灵活的处理方法。随着Spring 3.0的推出,通过@PathVariable或者其他注解,@Controller机制开始允许你去创建 Rest风格的web站点和应用。

在Spring Web MVC 和 Spring中一条关键的准则是“对扩展开放,对修改关闭”

在Spring Web MVC中一些核心类的方法被标注为final,由于开发者不能用自已的方法去覆盖这些方法,这并不是任意的,而是特别考虑到这个原则。

对于这个准则的解释,请参考Seth Ladd的Expert Spring Web MVC和Web Flow; 具体参见第一版第117页的“A Look At Design”一节。 或者参见

Bob Martin, The Open-Closed Principle (PDF)

当你使用Spring MVC时,你不能在final方法增加切面。例如,你不能在AbstractController.setSynchronizeOnSession()增加切面,有关AOP 代理的更多信息以及为什么不能再Final方法增加切面,查看第7.6.1节“了解AOP代理”

在Spring Web MVC中,您可以使用任何对象作为命令或表单支持对象;你不需要实现一个特别架构接口或者基类。Spring数据绑定非常灵活:例如,你可以使用程序将类型不匹配当作验证错误而不是系统错误。 因此,您不需要将您的业务对象的属性复制为简单的无格式的字符串,仅用于处理无效提交,或者正确转换字符串。 相反,通常最好直接绑定到您的业务对象。

Spring 的视图处理也是相当灵活,控制器通常负责准备具有数据和选择视图名称的模型映射,但它也可以直接写入响应流并完成请求。视图名称解析可通过文件扩展或Accept标头内容类型协商进行高度配置,通过bean名称,属性文件或甚至自定义的ViewResolver实现。模型(MVC中的M)是一个Map接口,可以完全提取视图技术,你可以直接与基于模板的渲染技术(如JSP和FreeMarker)集成,或直接生成XML,JSON,Atom和许多其他类型的内容。 模型Map可以简单地转换成适当的格式,如JSP请求属性或FreeMarker模板模型。

18.1.1 Spring Web MVC的特点

 

Spring Web 流程

Spring Web 流程 (SWF)的目的是成为最好的Web页面应用流程管理方案,SWF与Servlet 和Portlet 环境中的Spring MVC和JSF等现有框架集成。如果你有一个这样的业务流程,使用会话模型比纯粹的请求要优,那么SWF可能是一个选择。

SWF允许您将逻辑页面流作为在不同情况下可重用的自包含模块捕获,因此非常适合构建引导用户通过驱动业务流程的受控导航的Web应用程序模块。

更多关于SWF的信息,请点击Spring Web Flow website.

Spring 的Web模块包含许多独特的web支持特性:

  • 明确并分离的角色.每个角色-控制器,验证器,命令对象,构建对象,模型对象,分发器,映射处理器,视图解析等等都是完全的一个特定对象
  • 框架和应用程序类作为JavaBeans的强大而直接的配置。 此配置功能包括跨上下文的简单引用,例如从Web控制器到业务对象和验证器。
  • 可适配,无入侵,灵活,定义您需要的任何控制器方法签名,可能使用给定方案的参数注释之一(例如@RequestParam,@RequestHeader,@PathVariable等)。
  • 可重用的业务代码,不需要重复,使用现有的业务对象作为命令或表单对象,而不是仿照它们来扩展特定的框架基类。
  • 自定义绑定和验证,类型不匹配作为应用程序级验证错误,保持违规值,本地化日期和数字绑定等,而不是只使用仅包含字符串的表单对象进行手动解析和转换为业务对象。
  • 自定义的处理程序映射和视图解析,从简单的URL配置策略到复杂的,特制的策略,Spring比Web MVC框架更灵活,这些框架需要特定的技术。
  • 灵活的模型转换,具有名称/值的模型传输Map支持与任何视图技术的轻松集成。
  • 本地,时区,主题自定义,支持具有或不具有Spring标签库的JSP,支持JSTL,支持FreeMarker而不需要额外的网桥等等。
  • 一个简单而强大的JSP标签库,被称为Spring标签库,为数据绑定和主题等功能提供支持。 自定义标签允许在标记代码方面具有最大的灵活性。 有关标签库描述符的信息,请参见附录Chapter 40, spring JSP Tag Library
  • 在Spring 2.0中引入的JSP表单标签库,使得在JSP页面中的写入表单更容易。 有关标签库描述符的信息,请参见附录Chapter 41, spring-form JSP Tag Library
  • Bean的生命周期范围限定在当前的HTTP请求或HTTP Session中。 这不是Spring MVC本身的一个特定功能,而是Spring MVC使用的WebApplicationContext容器。 这些bean范围在Section 3.5.4, “Request, session, application, and WebSocket scopes”

 

18.1.2 其他MVC实现的可插拔性

对于某些项目,非Spring MVC实现更为可取。许多团队希望利用他们现有的技能和工具投资,例如使用JSF。

如果您不想使用Spring的Web MVC,但打算利用Spring提供的其他解决方案,您可以轻松地将您选择的Web MVC框架与Spring集成。通过其ContextLoaderListener简单地启动一个Spring根应用程序上下文,并通过任何动作对象中的ServletContext属性(或Spring的各自的帮助方法)访问它。没有涉及“插件”,因此不需要专门的集成。从Web层的角度来看,您只需使用Spring作为库,将根应用程序上下文实例作为入口点。

即使没有Spring的Web MVC,您的注册bean和Spring的服务也可以在您的指尖。在这种情况下,Spring不会与其他Web框架竞争。它简单地解决了纯Web MVC框架从bean配置到数据访问和事务处理的许多方面。所以您可以使用Spring中间层和/或数据访问层来丰富您的应用程序,即使您只想使用JDBC或Hibernate的事务抽象。

18.2 分发

Spring的Web MVC框架与许多其他Web MVC框架一样,以请求为驱动,围绕一个中央Servlet设计,将请求发送给控制器,并提供了其他促进Web应用程序开发的功能。然而, Spring 的DispatcherServlet 做得更多.它和 Spring IoC 容器整合一起,它允许你使用Spring 每个特性.

Spring Web MVC DispatcherServlet的请求处理工作流程如下图所示。 对设计模式熟悉的读者将会认识到,DispatcherServlet是“前端控制器”设计模式的表达(这是Spring Web MVC与许多其他领先的Web框架共享的模式)。

下图,在Spring Web MVC 中请求处理流程

mvc

DispatcherServlet是一个实际的Servlet(它继承自HttpServlet基类),因此在Web应用程序中被声明。 您需要使用URL映射来映射要DispatcherServlet处理的请求。 以下是Servlet 3.0+环境中的标准Java EE Servlet配置:


 public class MyWebApplicationInitializer implements WebApplicationInitializer {
 @Override
 public void onStartup(ServletContext container) {
 ServletRegistration.Dynamic registration = container.addServlet("example", new DispatcherServlet());
 registration.setLoadOnStartup(1);
 registration.addMapping("/example/*");
 }
 }
 

 

在前面的示例中,以/ example开头的所有请求都将由名为Example的DispatcherServlet实例处理。

WebApplicationInitializer是由Spring MVC提供的接口,可确保您的基于代码的配置被检测并自动用于初始化任何Servlet 3容器。这个名为AbstractAnnotationConfigDispatcherServletInitializer的接口的抽象基类实现通过简单地指定其servlet映射和列出配置类来更容易地注册DispatcherServlet,甚至建议您设置Spring MVC应用程序。有关更多详细信息,请参阅基于代码的Servlet容器初始化。

DispatcherServlet是一个实际的Servlet(它继承自HttpServlet基类),因此在Web应用程序的web.xml中声明。您需要通过使用同一web.xml文件中的URL映射来映射要DispatcherServlet处理的请求。这是标准的Java EE Servlet配置;以下示例显示了这样的DispatcherServlet声明和映射:

<web-app>
	<servlet>
		<servlet-name>example</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>example</servlet-name>
		<url-pattern>/example/*</url-pattern>
	</servlet-mapping>

</web-app>


如第3.15节“ApplicationContext的附加功能”中所述,Spring中的ApplicationContext实例可以被限定。 在Web MVC框架中,每个DispatcherServlet都有自己的WebApplicationContext,它继承了已经在根WebApplicationContext中定义的所有bean。 根WebApplicationContext应该包含应该在其他上下文和Servlet实例之间共享的所有基础架构bean。 这些继承的bean可以在特定于servlet的范围内被覆盖,您可以在给定的Servlet实例本地定义新的范围特定的bean。

18.2. Spring Web MVC中的典型上下文层次结构

mvc context hierarchy

在初始化DispatcherServlet时,Spring MVC将在Web应用程序的WEB-INF目录中查找名为[servlet-name] -servlet.xml的文件,并创建在那里定义的bean,覆盖使用相同名称定义的任何bean的定义 在全球范围内。 请考虑以下DispatcherServlet Servlet配置(在web.xml文件中):

<web-app>
	<servlet>
		<servlet-name>golfing</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>golfing</servlet-name>
		<url-pattern>/golfing/*</url-pattern>
	</servlet-mapping>
</web-app>

使用上述Servlet配置,您将需要在应用程序中有一个名为/WEB-INF/golfing-servlet.xml的文件; 该文件将包含您所有的Spring Web MVC特定组件(bean)。 您可以通过Servlet初始化参数更改此配置文件的确切位置(有关详细信息,请参阅下文)。 单个DispatcherServlet方案也可能只有一个根上下文。

mvc root context

这可以通过设置一个空的ContextConfigLocation servlet init参数进行配置,如下所示:

<web-app>
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/root-context.xml</param-value>
	</context-param>
	<servlet>
		<servlet-name>dispatcher</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value></param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>dispatcher</servlet-name>
		<url-pattern>/*</url-pattern>
	</servlet-mapping>
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
</web-app>

WebApplicationContext是普通ApplicationContext的扩展,它具有Web应用程序所需的一些额外功能。 它与正常的ApplicationContext不同之处在于它能够解析主题(参见第18.9节“使用主题”),并且知道它与哪个Servlet相关联(通过连接到ServletContext)。 WebApplicationContext绑定在ServletContext中,并且通过在RequestContextUtils类上使用静态方法,您可以随时查找WebApplicationContext,如果您需要访问它。 请注意,我们可以通过基于Java的配置实现相同的方式:


 public class GolfingWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
 protected Class<?>[] getRootConfigClasses() {
 // GolfingAppConfig defines beans that would be in root-context.xml
 return new Class[] { GolfingAppConfig.class };
 }
@Override
 protected Class<?>[] getServletConfigClasses() {
 // GolfingWebConfig defines beans that would be in golfing-servlet.xml
 return new Class[] { GolfingWebConfig.class };
 }
@Override
 protected String[] getServletMappings() {
 return new String[] { "/golfing/*" };
 }
}

 

18.2.1 WebApplicationContext中的特殊Bean类型

Spring DispatcherServlet使用特殊的bean来处理请求并呈现适当的视图。 这些bean是Spring MVC的一部分。 您可以通过在WebApplicationContext中简单配置一个或多个选择要使用的特殊bean。 但是,您最初不需要这样做,因为Spring MVC维护一个默认bean列表,如果您没有配置任何内容。 更多的在下一节。 首先看下表列出DispatcherServlet依赖的特殊bean类型。

表18.1. 在 WebApplicationContext中的特殊bean类型

Bean type Explanation
HandlerMapping 根据一些标准将传入的请求映射到处理程序和前处理程序和后处理程序列表(处理程序拦截器),其细节由HandlerMapping实现而异。 最流行的实现支持注释控制器,但其他实现也存在。
HandlerAdapter 帮助DispatcherServlet调用映射到请求的处理程序,而不管实际调用哪个处理程序。 例如,调用带注释的控制器需要解析各种注释。 因此,HandlerAdapter的主要目的是屏蔽DispatcherServlet和这些细节
HandlerExceptionResolver 映射视图的异常,也允许更复杂的异常处理代码。
ViewResolver 将基于逻辑字符串的视图名称解析为实际的View类型。
LocaleResolver & LocaleContextResolver 解决客户端正在使用的区域设置以及可能的时区,以便能够提供国际化的视图
ThemeResolver 解决您的Web应用程序可以使用的主题,例如,提供个性化的布局
MultipartResolver 解析multi-part请求,以支持从HTML表单处理文件上传。
FlashMapManager 存储并检索可以用于将属性从一个请求传递到另一个请求的“输入”和“输出”FlashMap,通常是通过重定向。

18.2.2 默认DispatcherServlet 配置

如上一节中针对每个特殊bean所述,DispatcherServlet会维护默认使用的实现列表。此信息保存在包org.springframework.web.servlet中的文件DispatcherServlet.properties中。

所有特殊豆都有一些合理的默认值。不久之后,您将需要自定义这些bean提供的一个或多个属性。例如,将InternalResourceViewResolver设置的前缀属性配置为视图文件的父位置是很常见的。

无论细节如何,在这里了解的重要概念是,一旦您在WebApplicationContext中配置了一个特殊的bean(如InternalResourceViewResolver),您可以有效地覆盖该特殊bean类型所使用的默认实现列表。例如,如果配置了InternalResourceViewResolver,则会忽略ViewResolver实现的默认列表。

在第18.16节“配置Spring MVC”中,您将了解配置Spring MVC的其他选项,包括MVC Java配置和MVC XML命名空间,这两者都提供了一个简单的起点,并且对Spring MVC的工作原理几乎不了解。无论您如何选择配置应用程序,本节中介绍的概念都是基础的,应该对您有所帮助。

18.2.3 DispatcherServlet 处理序列

在您设置了DispatcherServlet并且针对该特定DispatcherServlet启动了一个请求后,DispatcherServlet将按如下所示开始处理请求:

在请求中搜索并绑定WebApplicationContext作为控件和进程中的其他元素可以使用的属性。默认情况下,它将在DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE键下绑定。
语言环境解析器被绑定到请求,以使进程中的元素能够解决在处理请求时使用的区域设置(渲染视图,准备数据等)。如果您不需要语言环境解析,则不需要它。
主题解析器被绑定到使得诸如视图之类的元素确定要使用哪个主题的请求。如果不使用主题,可以忽略它。
如果指定了多部分文件解析器,则会检查该请求的多部分;如果找到多部分,则请求被包装在一个MultipartHttpServletRequest中,以便进程中的其他元素进一步处理。有关多部分处理的更多信息,请参见第18.10节“Spring的多部分(文件上传)支持”。
搜索适当的处理程序。如果找到处理程序,则执行与处理程序(预处理程序,后处理程序和控制器)关联的执行链,以便准备模型或呈现。
如果返回模型,则呈现视图。如果没有返回模型(可能是由于预处理程序或后处理程序拦截请求,可能是出于安全原因),因为请求可能已经被满足,所以不会呈现任何视图。
在WebApplicationContext中声明的处理程序异常解析程序在处理请求期间提取异常。使用这些异常解析器允许您定义自定义行为来解决异常。

Spring DispatcherServlet还支持返回由Servlet API指定的最后修改日期。确定特定请求的最后修改日期的过程很简单:DispatcherServlet查找适当的处理程序映射,并测试发现的处理程序是否实现了LastModified接口。如果是,则LastModified接口的long getLastModified(request)方法的值将返回给客户端。

您可以通过将Servlet初始化参数(init-param元素)添加到web.xml文件中的Servlet声明来自定义单独的DispatcherServlet实例。有关支持的参数列表,请参见下表。

表18.2. DispatcherServlet 初始化参数

参数 解释
contextClass 实现WebApplicationContext的类,它实例化了这个Servlet使用的上下文。 默认情况下,使用XmlWebApplicationContext。
contextConfigLocation 传递给上下文实例(由contextClass指定)以指示可以找到上下文的字符串。 该字符串可能包含多个字符串(使用逗号作为分隔符)来支持多个上下文。 在具有两次定义的bean的多个上下文位置的情况下,优先级最高。
namespace WebApplicationContext的命名空间。 默认为[servlet-name] -servlet。

18.3 实现Controllers

控制器提供对通常通过服务接口定义的应用程序行为的访问。控制器解释用户输入并将其转换为由视图表示给用户的模型。 Spring以非常抽象的方式实现控制器,使您能够创建各种各样的控制器。

Spring 2.5引入了一种基于注释的编程模型,用于使用诸如@RequestMapping,@RequestParam,@ModelAttribute等注释的MVC控制器。以这种风格实现的控制器不必扩展特定的基类或实现特定的接口。此外,它们通常不直接依赖于Servlet API,但是如果需要,您可以轻松地配置对Servlet设施的访问。

@Controller
public class HelloWorldController {

@RequestMapping( “/ HelloWorld” 的)
public String helloWorld(Model model){
model.addAttribute(“message”,“Hello World!”);

return “helloWorld”;
}
}
您可以看到,@Controller和@RequestMapping注释允许灵活的方法名称和签名。在这个特殊的例子中,该方法接受一个Model并返回一个视图名称作为一个String,但是可以使用各种其他的方法参数和返回值,如本节稍后所述。 @Controller和@RequestMapping和许多其他注释构成了Spring MVC实现的基础。本节介绍这些注释以及它们在Servlet环境中最常用的注释。

18.3.1 使用@Controller定义控制器

@Controller注释表示特定的类用于控制器的角色。 Spring不需要扩展任何控制器基类或引用Servlet API。 但是,如果需要,您仍然可以参考Servlet特定的功能。

@Controller注释作为注释类的构造型,表示其作用。 调度程序扫描这些注释类的映射方法,并检测@RequestMapping注释(请参阅下一节)。

您可以使用调度程序上下文中的标准Spring bean定义来明确定义带注释的控制器bean。 但是,@Controller构造型还允许自动检测,与Spring通用支持对齐,用于检测类路径中的组件类并自动注册它们的bean定义。要启用自动检测这些带注释的控制器,您可以向组态添加组件扫描。 使用spring-context模式,如以下XML代码片段所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context
		http://www.springframework.org/schema/context/spring-context.xsd">

	<context:component-scan base-package="org.springframework.samples.petclinic.web"/>
	<!-- ... -->
</beans>

18.3.2 使用@RequestMapping映射请求

您可以使用@RequestMapping注释将诸如/约会的URL映射到整个类或特定的处理程序方法。 通常,类级注释将特定的请求路径(或路径模式)映射到表单控制器上,其他方法级注释缩小了特定HTTP请求方法(“GET”,“POST”等)的主映射,或 HTTP请求参数条件。

Petcare示例中的以下示例显示了使用此注释的Spring MVC应用程序中的控制器:

@Controller
@RequestMapping("/appointments")
public class AppointmentsController {

	private final AppointmentBook appointmentBook;

	@Autowired
	public AppointmentsController(AppointmentBook appointmentBook) {
		this.appointmentBook = appointmentBook;
	}

	@RequestMapping(method = RequestMethod.GET)
	public Map<String, Appointment> get() {
		return appointmentBook.getAppointmentsForToday();
	}

	@RequestMapping(path = "/{day}", method = RequestMethod.GET)
	public Map<String, Appointment> getForDay(@PathVariable @DateTimeFormat(iso=ISO.DATE) Date day, Model model) {
		return appointmentBook.getAppointmentsForDay(day);
	}

	@RequestMapping(path = "/new", method = RequestMethod.GET)
	public AppointmentForm getNewForm() {
		return new AppointmentForm();
	}

	@RequestMapping(method = RequestMethod.POST)
	public String add(@Valid AppointmentForm appointment, BindingResult result) {
		if (result.hasErrors()) {
			return "appointments/new";
		}
		appointmentBook.addAppointment(appointment);
		return "redirect:/appointments";
	}
}
在上面的例子中,@RequestMapping用在很多地方。 第一个用法是类型(类)级别,这表示此控制器中的所有处理程序方法都相对于/约会路径。 get()方法还有一个@RequestMapping细化:它只接受GET请求,这意味着/appointments 的HTTP GET调用此方法。 add()有一个类似的细化,getNewForm()将HTTP方法和路径的定义组合成一个,以便通过该方法处理appointments /新的GET请求。
getForDay()方法显示了@RequestMapping:URI模板的另一种用法。 (参见“URI模板模式”一节)。
类级别上的@RequestMapping不是必需的。 没有它,所有的路径都是绝对的,而不是相对的。 PetClinic示例应用程序的以下示例显示了使用@RequestMapping的多操作控制器:
@Controller
public class ClinicController {

	private final Clinic clinic;

	@Autowired
	public ClinicController(Clinic clinic) {
		this.clinic = clinic;
	}

	@RequestMapping("/")
	public void welcomeHandler() {
	}

	@RequestMapping("/vets")
	public ModelMap vetsHandler() {
		return new ModelMap(this.clinic.getVets());
	}

}
上述示例不指定GET与PUT,POST等,因为@RequestMapping默认映射所有HTTP方法。 使用@RequestMapping(method = GET)或@GetMapping来缩小映射。

组合@RequestMapping变体

 Spring Framework 4.3引入了@RequestMapping注释的以下方法级组合变体,有助于简化常见HTTP方法的映射,并更好地表达注释处理程序方法的语义。 例如,@GetMapping可以被读取为GET @RequestMapping。
  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

以下示例显示了使用已组合的@RequestMapping注释简化的上一节中的AppointmentsController的修改版本。

@Controller
@RequestMapping("/appointments")
public class AppointmentsController {

	private final AppointmentBook appointmentBook;

	@Autowired
	public AppointmentsController(AppointmentBook appointmentBook) {
		this.appointmentBook = appointmentBook;
	}

	@GetMapping
	public Map<String, Appointment> get() {
		return appointmentBook.getAppointmentsForToday();
	}

	@GetMapping("/{day}")
	public Map<String, Appointment> getForDay(@PathVariable @DateTimeFormat(iso=ISO.DATE) Date day, Model model) {
		return appointmentBook.getAppointmentsForDay(day);
	}

	@GetMapping("/new")
	public AppointmentForm getNewForm() {
		return new AppointmentForm();
	}

	@PostMapping
	public String add(@Valid AppointmentForm appointment, BindingResult result) {
		if (result.hasErrors()) {
			return "appointments/new";
		}
		appointmentBook.addAppointment(appointment);
		return "redirect:/appointments";
	}
}

@Controller 和AOP 代理

在某些情况下,控制器可能需要在运行时用AOP代理进行装饰。 一个例子是如果您选择在控制器上直接使用@Transactional注释。 在这种情况下,对于控制器,我们建议使用基于类的代理。 这通常是控制器的默认选项。 但是,如果控制器必须实现不是Spring Context回调的接口(例如InitializingBean,* Aware等),则可能需要显式配置基于类的代理。 例如,使用<tx:annotation-driven />,更改为<tx:annotation-driven proxy-target-class =“true”/>。

转载自 并发编程网 - ifeve.com

网友评论

登录后评论
0/500
评论
青衫无名
+ 关注