Spring MVC

Spring Framework 框架图

下图是 Spring 官网的一个架构图,介绍下其组成部分:

../_images/Spring-Framework-Runtime.png

核心容器

由spring-core、spring-beans、spring-context、spring-context-support和spring-expression模块组成:

  • spring-core 和 spring-beans 提供框架的基础部分,包括 IOC 功能,BeanFactory 是一个复杂的工厂模式的实现,将配置和特定的依赖从实际程序逻辑中解耦。

  • context 模块建立在 core 和 beans 模块的基础上,增加了对国际化的支持、事件广播、资源加载和创建上下文,ApplicationContext 是 context 模块的重点。

  • spring-context-support 提供对常见第三个库的支持,集成到 spring 上下文中,比如缓存(ehcache,guava)、通信(javamail)、调度(commonj,quartz)、模板引擎等(freemarker,velocity)。

  • spring-expression 模块提供了一个强大的表达式语言用来在运行时查询和操作对象图,这种语言支持对属性值、属性参数、方法调用、数组内容存储、集合和索引、逻辑和算数操作及命名变量,并且通过名称从 spring 的控制反转容器中取回对象。

AOP 和服务器工具

  • spring-aop 模块提供面向切面编程实现

  • 单独的 spring-aspects 模块提供了 aspectj 的集成和适用。

  • spring-instrument 提供一些类级的工具支持和 ClassLoader 级的实现,用于服务器。spring-instrument-tomcat 针对 tomcat 的 instrument 实现。

消息组件

包含了spring-messaging模块,从spring集成项目中抽象出来,比如Messge、MessageChannel、MessageHandler及其他用来提供基于消息的基础服务。

数据访问/集成

数据访问和集成层由 JDBC、ORM、OXM、JMS 和 Transaction 模块组成。

  • spring-jdbc 模块提供了不需要编写冗长的 JDBC 代码和解析数据库厂商特有的错误代码的 JDBC 抽象出。

  • spring-orm 模块提供了领先的对象关系映射 API 集成层,如 JPA、Hibernate 等。

  • spring-oxm 模块提供抽象层用于支持 Object/XML maping 的实现,如 JAXB、XStream 等。

  • spring-jms 模块包含生产和消费消息的功能,从 Spring4.1开始提供集成 spring-messaging 模块。

  • spring-tx 模块提供可编程和声明式事务管理。

Web

Web层包含spring-web、spirng-webmvc、spring-websocket和spring-webmvc-portlet模块组成。

  • spring-web 模块提供了基本的面向 web 开发的集成功能,例如多文件上传、使用 servert listeners 和 web 开发应用程序上下文初始化 IOC 容器。也包含 HTTP 客户端以及 spring 远程访问的支持的 web 相关部分。

  • spring-webmvc 包含 spring 的 model-view-controller 和 REST web services 实现的 Web 应用程序。

  • spring-webmvc-portlet 模块提供了 MVC 模式的 portlet 实现,protlet 与 Servlet 的最大区别是请求的处理分为 action 和 render 阶段,在一个请求中,action 阶段只执行一次,但 render 阶段可能由于用户的浏览器操作而被执行多次。

测试

spring-test模块支持通过组合Junit或TestNG来进行单元测试和集成测试,提供了连续的加载ApplicationContext并且缓存这些上下文。

使用Spring Context

使用ClassPathXmlApplicationContext:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("META-INF/spring/spring-main.xml");
A a = context.getBean(A.class);

直接使用 DefaultListableBeanFactory:

Resource resource = new ClassPathResource("spring-core.xml");
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
reader.loadBeanDefinitions(resource);
MyBean myBean = (MyBean) beanFactory.getBean("myBean");
myBean.sayHello();

区分ApplicationContext and ServletContext java - ApplicationContext and ServletContext - Stack Overflow

使用Bean

Spring 基于 Ioc 和 DI 的方式 创建 & 装配 Bean :

  • 控制反转(Inversion of Control): 使用者不自己创建依赖的对象, 而交由第三方(IoC容器)创建. 从IOC容器中获取(和自动注入).而不必由用户调用 new 来创建 Bean 对象, 通过 IoC 则可以减少它们之间的耦合度.

  • 依赖注入(Dependency Injection): 将依赖对象传递给使用者. 在 Spring 中, bean 的装配是依赖注入的具体行为,依赖注入的时候需要根据 bean 的名称或类型等进行装配。

创建Bean的几种方式

基于注解

① 基于 @Component

  1. 通过注解方式创建容器:

    @Configuration
    @ComponentScan
    public interface ThisIsConfig {
    }
    • @Configuration 来标注该接口是用于定义配置的, Spring 会视为该java文件为一个xml配置
    • @ComponentScan Spring 将会扫描该类所在的包下的所有 bean注解(@Component, @Service等等), 等同于在 Spring的xml里写:
      <context:component-scan base-package="com.bigdata"></context:component-scan>
      如果要指定要扫描的包的路径(而不是 这个类所在的包) 可以用 @ComponentScan(value="包路径") 指定;
  2. 带有 @Component注解的类被Ioc方式创建:
  3. 通过 @Autowired 用 DI 方式进行装配:

关于@Component,@Service,@Controler,@Repository注解
这几个注解都是同样的功能,被注解的类将会被 Spring 容器创建单例对象。
@Component : 侧重于通用的Bean类
@Service:标识该类用于业务逻辑
@Controler:标识该类为Spring MVC的控制器类
@Repository: 标识该类是一个实体类,只有属性和Setter,Getter

② 基于 @Bean

@Configuration
@ComponentScan
public class SwaggerConfig {
@Bean
public SwaggerSpringMvcPlugin customImplementation() {
...
}
}

@Bean 注解在这里的意思是 : 该方法会返回一个 SwaggerSpringMvcPlugin 类型的 bean

二者区别

Component vs Bean 的区别,参考下面的 「 [[#注解(Annotation)]] 」一章

基于XML

① 基于构造器: 下面的类JedisPortsFactory 具有一个构造器(该构造器 有两个参数: config 和 autoFlush)
config 引用到了另一个bean, autoFlush 是个boolean型

<bean id = "jedisPortsFactory" class="com.bigdata.console.tools.online.JedisPortsFactory">
<constructor-arg name="config" ref="jedisEvictionPoolConfig"/>
<constructor-arg name="autoFlush" value="true"/>
</bean>

② 基于 setter: CommonsMultipartResolver 要有property对应的 Setter方法

<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="defaultEncoding" value="UTF-8" />
<property name="maxUploadSize" value="-1" />
</bean>

③ 基于静态工厂: 指定 工厂类的class, 适用于 静态工厂方法:

<bean id="jp_featurePv" class="com.bigdata.consoleJedisPortsFactory" factory-method="getJedisMSServers">
<constructor-arg type="java.lang.String" value="xxx"/>
</bean>

④ 基于动态工厂: 指定 动态工厂的bean 和方法, 下面的例子中工厂方法 getJedisMSServers 有一个字符串型的参数, 适用于动态工厂方法:

<bean id="jp_featurePv" factory-bean="jedisPortsFactory" factory-method="getJedisMSServers">
<constructor-arg type="java.lang.String" value="xxx"/>
</bean>

Bean的属性

无论是基于@Component 还是 Xml 创建的 Bean ,属性都是通用的:

scope

  • scope=”singleton”: 单例, Spring 在每次需要时都返回同一个bean实例
  • scope=”prototype”: Spring 在每次需要时都产生一个新的 bean 实例
  • scope=”request”
  • scope=”session”

如果使用 @Service、@Controller … 等注解创建 Bean:

  • @Component 注解默认实例化的对象是单例,如果想声明成多例对象可以使用@Scope(“prototype”)
  • @Repository 默认单例
  • @Service 默认单例
  • @Controller 默认单例

autowire

  • autowire=”byName”: 只能用于setter注入。比如我们有方法“setHelloApi”,则“byName”方式Spring容器将查找名字为helloApi的Bean并注入
  • autowire=”no”: 意思是 Spring 将不自动装配这个Bean,必须明确指定依赖
<bean id="bean" class="bean.HelloApiDecorator" autowire="byName"/>

depends-on

Spring保证该Bean所依赖的其他bean已经初始化, 用<ref>元素建立对其他bean的依赖关系, Sprign 会确保创建 bean的顺序:

<bean id="helloApi" class="helloworld.HelloImpl"/>
<bean id="decorator" class="helloworld.HelloApiDecorator"
depends-on="helloApi">
<property name="helloApi"><ref bean="helloApi"/></property>
</bean>

lookup-method

单例模式的beanA需要引用另外一个非单例模式的beanB,为了在我们每次引用的时候都能拿到最新的beanB

<bean id="prototypeBean" class="bean.PrototypeBean" scope="prototype"/>
<bean id="singletonBean" class="bean.SingletonBean">
<!-- SingletonBean.getBean()方法被代理 -->
<lookup-method name="getBean" bean="prototypeBean"/>
</bean>

下面是java代码

public abstract class SingletonBean{
// 抽象方法, 每次获取一个新的PrototypeBean实例
protected abstract PrototypeBean getBean();
}

ApplicationContext app = new ClassPathXmlApplicationContext("classpath:resource/applicationContext.xml");
SingletonBean single= (SingletonBean)app.getBean("singletonBean");
single.getBean(); // 每次返回一个新的PrototypeBean

Bean的初始化/销毁回调

Spring-Bean-Lifecycle

基于代码

InitializingBean接口为bean提供了属性初始化后的处理方法,它只包括afterPropertiesSet方法,凡是继承该接口的类,在bean的属性初始化后都会执行该方法:

public class ExampleBean implements InitializingBean {
public void afterPropertiesSet() {
// do some initialization work
}
}

DisposableBean接口为bean提供销毁方法

public class ExampleBean implements DisposableBean {
public void destroy() {
// do some destruction work
}
}

基于XML配置

<bean id="helloWorld"
class="com.dropNotes.HelloWorld"
init-method="init" destroy-method="destroy">
<property name="message" value="Hello World!"/>
</bean>

上面的init-method属性和 destroy-method属性, 指定了HelloWorld类的初始化/销毁回调方法名字, 接下来在HelloWorld类中定义无参的方法即可.

何时调用

ApplicationContext.registerShutdownHook()被调用时

IOC 和 DI

上面提到了 Spring 基于 Ioc 和 DI 的方式创建 & 装配 Bean,总结一下 IoC 和 DI 的常用注解 :

  • IoC 创建 Bean:@Bean、@Component、@Service、@Controller、@Repository …
  • DI 注入 Bean:@Autowired、@Resource …

上面注解具体的区别参考下面的 「 [[#注解(Annotation)]] 」一章

Spring 如何实现 IOC 和 DI

创建 bean(IoC),以 XML 方式为例,伪码:

//解析<bean .../>元素的 id 属性得到该字符串值为“courseDao”  
String idStr = "courseDao"; 
 
//解析<bean .../>元素的class属性得到该字符串“com.xx.Dao.impl.CourseDaoImpl”  
String classStr = "com.xx.Dao.impl.CourseDaoImpl";  

//利用反射创建对象  
Class<?> cls = Class.forName(classStr);  
Object obj = cls.newInstance();  

//放入Spring容器保存
container.put(idStr, obj);

构造器注入(DI)实现的伪码:

// 通过反射获取当前类所有的构造方法信息(Constructor 对象)
Constructor<?>[] candidates = beanClass.getDeclaredConstructors();

// 设置构造方法参数实例
Object[] argsToUse = new Object[parameterTypes.length];
argsToUse[i] = getBean(beanNames.get(i));

// 使用带有参数的 Constructor 对象实现实例化 Bean
return constructorToUse.newInstance(argsToUse);

Autowired 注入(DI)实现的伪码:

// 通过反射得到当前类所有的字段信息(Field 对象)
Field[] fields = bean.getClass().getDeclaredFields();

// 判断字段是否有 @Autowired 注解
Annotation ann = field.getAnnotation(Autowired.class);

// 设置字段可连接,相当于将非 public(private、default、protect)更改为 public field.setAccessible(true);

// 通过反射设置字段的值
field.set(bean, getBean(field.getName()));

@ref: Spring 中的反射与反射的原理 - 掘金

使用AOP

AOP(Aspect Oriented Program) ,面向切面编程:
主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。

AOP的一些概念

  • 连接点(Jointpoint)连接点是能够插入切面的一个点,连接点可能是类初始化,可以是调某方法时,抛出异常时,修改某字段时
  • 切入点(Pointcut):一组连接点集合
  • 通知(Advice):定义在连接点上“要做什么”,以及“何时去做”,包括前置通知(before advice)、后置通知(after advice)、环绕通知(around advice)
  • 切面(Aspect):可以认为是”通知”和”切入点”的集合

  • 目标对象(Target Object):需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为“被通知对象”;由于Spring AOP 通过代理模式实现,从而这个对象永远是被代理对象,在AOP中表示为“对谁做”;

  • AOP代理(AOP Proxy):AOP框架使用代理模式创建的对象,从而实现在连接点处插入通知(即应用切面),就是通过代理来对目标对象应用切面。在Spring中,AOP代理可以用JDK动态代理或CGLIB代理实现,而通过拦截器模型应用切面。

  • 织入(Weaving):织入是一个过程,是将切面应用到目标对象从而创建出AOP代理对象的过程,织入可以在编译期、类装载期、运行期进行。

  • 引入(inter-type declaration):为已有的类添加额外新的字段或方法,Spring 允许引入新的接口(必须对应一个实现)到所有被代理对象(目标对象), 在 AOP 中表示为“做什么”;

基于XML配置aspect

<bean id="aspect" class="cn.javass.spring.chapter6.aop.HelloWorldAspect"/>
<aop:config>
<!-- 定义了一个id="pointcut"的切点, 范围是com.javass包下的所有类 -->
<aop:pointcut id="pointcut" expression="execution(* cn.javass..*.*(..))"/>
<!-- 定义切面的集合, ref="aspect"表示要引入"aspect"这个bean -->
<aop:aspect ref="aspect">
<!-- 定义一个切点, 包括用哪些切点, 以及在切点处要插入aspect.beforeAdvice()方法 -->
<aop:before pointcut-ref="pointcut" method="beforeAdvice"/>
<!-- 定义另一个切点, 在切点处要插入aspect.afterFinallyAdvice()方法 -->
<aop:after pointcut="execution(* cn.javass..*.*(..))" method="afterFinallyAdvice"/>
</aop:aspect>
</aop:config>

基于注解配置aspect

下面的代码定义一个切面(@Aspect): 哪里切入(@Pointcut), 切入的行为(@Advice)

@Aspect
public class ControllerLogAspect { //定义了一个切面

// 定义切点"logPointCut", 在哪些类里切入
@Pointcut("execution(public * com.xxx.*.controller..*.*(..)) && " +
"!execution(public * com.xxx.*.controller..CheckController.*(..))")
public void logPointCut() {
}

// 环绕通知
@Around("logPointCut()")
public void advice(ProceedingJoinPoint joinPoint){

}

// 前置通知, 在切点"logPointCut"之前
@Before("logPointCut()")
public void doBefore(JoinPoint joinPoint) {
}

// 后置通知
@AfterReturning(returning = "ret", pointcut = "logPointCut()")
public void doAfter(Object ret) throws Throwable {
}
}

Spring 如何实现 AOP

Spring 通过 jdk 动态代理cglib 动态代理实现 AOP.

Spring 的 AOP 是通过 Java 语言提供的 代理(Proxy)模式 实现的, Java 语言的代理包括如下 2种方式: JDK 动态代理, Cglib 动态代理. 实现过程参考 @link [[../12.Java/Java-Tutorials.14.代理(Proxy)]]

Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib ,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:

../_images/JavaEE.SpringMVC-2023-05-08-1.png

Spring AOP vs AspectJ

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,

如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。

DispatcherServlet

Servlet 规范、Servlet 容器、Spring 实现的 DispatcherServlet 关系,参考 JavaEE.Tomcat 第一节

要使用Spring MVC只需要在web.xml(Java Servlet 规范里Java Web项目的部署描述符文件)里增加一个Servlet:

<servlet>
<servlet-name>comment</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:appcontext-core-web.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>comment</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>

DispatcherServlet 工作流程

DispatcherServlet处理一次 Req 的流程,伪码:


(1)Request → DispatcherServlet

(2)DispatcherServlet 从 HandleMapping[] 查找匹配, 返回 HandlerExecutionChain { HandlerInterceptor1,HandleInterceptor2..}

(3)HandleInterceptor → HandleAdaptor → Controller

(4)Controller 返回 ModelAndView → ViewResolver

(5)View

  • 对于 Spring MVC 程序来说, 首先调用的是 DispatcherServlet.service(ServletRequest, ServletResponse), 实现是在 HttpServlet.service(ServletRequest req, ServletResponse resp), 这个方法里把 ServletRequest 对象转换为 HttpServletRequest, 在这个方法里又调用进了 FrameworkServlet.service(HttpServletRequest req, HttpServletResponse resp), 在这个方法里如果 method!=PATCH 则调用进 super.service(HttpServletRequest, HttpServletResponse), 也就是 HttpServletservice(HttpServletRequest, HttpServletResponse), 这里根据不同的 method 调用不同的 doX() 方法

  • 以 GET 方法为例,调用 this.doGet(), 因为在 FrameworkServlet 重写了 doGet(), 所以这里调用的代码是 FrameworkServlet.doGet(), 在这个方法里调用了FrameworkServlet.processRequest(), 然后又调用了this.doService(),

  • DispatcherServlet 重写了 doService(), 所以最终调用到 DispatcherServlet.doService(), 该方法逻辑大致如下:
void doDispatch(HttpServletRequest request, HttpServletResponse response)  {
HandlerExecutionChain mappedHandler = getHandler(processedRequest);
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
}
  1. getHandler()主要就是通过this.handlerMappings中的HandlerMapping实例来对具体request映射一个handler(Spring MVC中的Controller类) ;
  2. 如果看过this.handlerMappings的初始化,便知道HandlerMapping的具体实现有3个:
    • RequestMappingHandlerMapping : 用来映射Controller和URL
    • BeanNameUrlHandlerMapping
    • SimpleUrlHandlerMapping
  3. ……
  4. ……

DispatcherServlet 工作流程
上图中组件处理顺序分别是:

  • Dispatcher Servlet分发器
  • Handler Mapping 处理器映射
  • Controller 控制器
  • ModelAndView 模型和视图对象
  • ViewResolver 视图解析器

@ref SpringMVC 工作原理详解

拦截器(Interceptor)

处理器映射处理过程配置的拦截器,必须实现 org.springframework.web.servlet包下的 HandlerInterceptor接口。
这个接口定义了三个方法:
preHandle(..),它在处理器实际执行 之前 会被执行;
postHandle(..),它在处理器执行 完毕 以后被执行;
afterCompletion(..),它在 整个请求处理完成 之后被执行。

通过xml定义拦截器

<beans>
<bean id="handlerMapping" class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
<property name="interceptors">
<list>
<ref bean="officeHoursInterceptor"/>
</list>
</property>
</bean>

<bean id="officeHoursInterceptor" class="samples.TimeBasedAccessInterceptor">
<property name="openingTime" value="9"/>
<property name="closingTime" value="18"/>
</bean>
<beans>

通过注解定义拦截器

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleInterceptor());
registry.addInterceptor(new ThemeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*");
}

}

控制器(Controller)

传入类型

  • @RequestParam注解: @RequestParam(value = "client_id") String appId
  • Model类型: 这种通常返回String类型的view路径
  • HttpServletResponse:
  • HttpServletRequest:

返回类型

  • 返回ModelAndView: 返回视图return new ModelView("/view/111", map)
    • 通过ModelAndView也可以重定向: return new ModelAndView("redirect:/controller2");
    • 如果modelView是以参数传入的: model.setViewName("forward:index.jsp"); return model;
  • 返回RedirectView: 专门用来处理转发的视图, 见后面的代码.
  • 返回String: 返回字符串可以指定逻辑视图名, 通过视图解析器解析为物理视图地址
    • 通过String也可以重定向: return "redirect:/resource/page2.jsp";
    • 如果Controller带有@ResponseBody注解, 可以直接返回String字面值;
  • 以json返回对象: 借助@ResponseBody注解, 项目导入Jackson.jar, 并且在Spring配置文件启用了<mvc:annotation-driven /> 1
  • 返回Map:
    • 借助@ResponseBody注解, return new HashMap<>();会返回一个json
    • 没有@ResponseBody注解, map.put("key1", "value-1"); return map;, 在jsp页面中可直通过${key1}获得到值
  • 返回void: 需要通过形参传入request和response
    • 使用request转向页面: request.getRequestDispatcher("index.html").forward(request, response);
    • 通过response页面重定向: response.sendRedirect("http://www.xxx.com");
    • forward和Redirect的区别: forward是由Servlet直接转给另一个Controller处理, Redirect相当于302, 返回给浏览器, 然后浏览器再发一次新的请求到Controller2
    • 通过response指定响应结果:
      • 返回json: response.setContentType("application/json;charset=utf-8"); response.getWriter().write("this_is_json");
      • 返回Html: response.getWriter().println("<title>HelloWorld</title></head><body>");

用RedirectAttributes带参跳转:

@RequestMapping("/")
public RedirectView hello(RedirectAttributes attrs) {
attrs.addAttribute("message", "hello");
attrs.addFlashAttribute("username", "sudoz");
return new RedirectView("hello");
}
@RequestMapping("hello")
Map<String, String> hello(@ModelAttribute("username") String username,
@ModelAttribute("message") String message) {
Map<String, String> map = Maps.newHashMap();
map.put("username", username);
map.put("message", message);
return map;
}

1 mvc:annotation-driven是一种简写形式,完全可以手动配置替代这种简写形式,<mvc:annotation-driven />会自动注册DefaultAnnotationHandlerMappingAnnotationMethodHandlerAdapter 两个bean,是Spring MVC为@Controllers分发请求所必须的。
并提供了:数据绑定支持,@NumberFormatannotation支持,@DateTimeFormat支持,@Valid支持,读写XML的支持(JAXB),读写JSON的支持(Jackson)。

Spring是如何处理返回类型的?

DispatchServlet.viewResolvers的类型是List<ViewResolver>, Controller返回的类型转给DispatchServlet, 最终交给不同的ViewResolver处理的

视图(View)

所有web应用的MVC框架都提供了视图相关的支持。Spring提供了一些视图解析器,它们让你能够在浏览器中渲染模型,并支持你自由选用适合的视图技术而不必与框架绑定到一起。
Spring原生支持JSP视图技术、Velocity模板技术和XSLT视图等。

有两个接口在Spring处理视图相关事宜时至关重要,分别是视图解析器接口ViewResolver和视图接口本身View。
视图解析器ViewResolver负责处理视图名与实际视图之间的映射关系。
视图接口View负责准备请求,并将请求的渲染交给某种具体的视图技术实现。

使用ViewResolver接口解析视图

Spring MVC中所有控制器的处理器方法都必须返回一个逻辑视图的名字,无论是显式返回(比如返回一个String、View或者ModelAndView)还是隐式返回(比如基于约定的返回)。
Spring中的视图由一个视图名标识,并由视图解析器来渲染。Spring有非常多内置的视图解析器。

资源(Resource)

Resource接口

Resource接口提供了足够的抽象,足够满足我们日常使用。而且提供了很多内置Resource实现:ByteArrayResource、InputStreamResource 、FileSystemResource 、UrlResource 、ClassPathResource、ServletContextResource、VfsResource等。

路径通配符

  • ?匹配一个字符,如config?.xml将匹配config1.xml
  • *匹配零个或多个字符串,如cn/*/config.xml将匹配cn/javass/config.xml,但不匹配匹配cn/config.xml
  • **匹配路径中的零个或多个目录,如cn/**/config.xml
// 加载Resource例子1:
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
//只加载一个绝对匹配Resource,且通过ResourceLoader.getResource进行加载
Resource[] resources=resolver.getResources("classpath:META-INF/INDEX.LIST");

// 加载Resource例子2:
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
//将加载多个绝对匹配的所有Resource
//将首先通过ClassLoader.getResources("META-INF")加载非模式路径部分
//然后进行遍历模式匹配
// classpath*: 用于加载类路径(包括jar包)中的所有匹配的资源
Resource[] resources=resolver.getResources("classpath*:META-INF/INDEX.LIST");

静态资源

静态资源包括:HTML、CSS、JS、图像、视频、PDF/Office等不需要服务器端处理的文件。

静态资源文件的位置:

  • Java Web默认的静态资源文件夹是 src/main/webapp/
  • Spring Boot自动将src/main/resource/下的「/static」「/public」「/resources」「/META-INF/resources」识别为资源文件夹。 下面的css可以通过访问http://localhost:8080/css/a.css获取
    Project Root
    └─src
    └─ main
    └─ resources
    ├─ static
    | └─ css
    | └─ a.css
    ├─ public
    | └─ css
    | └─ b.css
    ├─ resources
    | └─ css
    | └─ b.css
    └─ META-INF
    └─ resources
    └─ css
    └─ d.css

异常处理(Exception)

  1. Controller的匹配. 除了value指定url, 还可以通过product指定MIME-TYPE(参考网络协议HTTP)
  2. 调试的时候需要注意, cURL实际是使用了Accept: */*, 浏览器发出的请求是Accept:text/html
@RequestMapping(value = "/return-text-plain", produces = MimeTypeUtils.TEXT_PLAIN_VALUE)
@ResponseBody
public String returnPlainText() throws SomeException {
throw new SomeException();
}

How to自定义Error页面:

@Configuration
public class CustomDefaultErrorViewConfiguration {

@Autowired
private ThymeleafViewResolver thymeleafViewResolver;

@Bean
public View error() throws Exception {
return thymeleafViewResolver.resolveViewName("custom-error-page/error", Locale.CHINA);
}

}

注解(Annotation)

Spring注解

IOC 注解: @Service, @Controller, @Repository, @Component, @Bean

  • @Service: 用于注解Service层, 默认是单例的
  • @Controller: 定义控制器类一般这个注解在类中,通常方法需要配合注解 @RequestMapping
  • @RestController相当于@ResponseBody@Controller的合集, 默认是单例的
  • @Repository用于注解DAO,这个注解修饰的DAO类会被ComponetScan发现并配置,同时也不需要为它们提供xml配置项
  • 如果一个类不好归类, 则使用 @Component 注解
  • The default scope for the bean is a singleton

  • @Bean: 区别与上面的注解,@Component 注解作用于类,而 @Bean 注解作用于方法, 该方法必须返回一个类型对象, 该对象被注册为 Spring 上下文中的 bean, 注意方法名字将会作为 bean 的 ID, 相当于在 xml 中定义 <bean>

    • @Bean(initMethod=”aa”,destroyMethod=”bb”): 指定 aa 和 bb 方法分别在在构造之后/销毁之前执行

Spring 会自动扫描 base-package 指定的包下面用 @Service 注解的所有类, 并注册到 beans 容器里.
需要在 Spring 配置文件里增加: <context:component-scan base-package="com.xxx.product.core.service"/> 来说明启用自动扫描

DI 注解: @Autowired, @Resource, @Inject, @Primary

  • @Autowired: 可以写在属性上, 和 setter 方法上, 或者构造函数上, 默认按照类型进行装配
  • @Autowired 和 @Inject: 通过 AutowiredAnnotationBeanPostProcessor 来实现依赖注入, 顺序:
    1. 按照类型匹配
    2. 使用限定符进行类型限定
    3. 按照名称匹配
  • @Resource: 使用 CommonAnnotationBeanPostProcessor 来实现注入, 顺序:
    1. 按照名称匹配
    2. 按照类型匹配
    3. 使用限定符进行类型限定

➤ 区别二者:

  • @Resource 并不是 Spring 的注解,它的包是 javax.annotation.Resource, Spring 也支持该注解的注入;
  • 两者都可以写在字段和 setter 方法上。两者如果都写在字段上,那么就不需要再写 setter 方法;

@ComponentScan & @Component

  • @Component: 使用在类上, 表示可以被 @ComponentScan 标注的类扫描到
  • @ComponentScan: 使用在类上, 可以扫描到 @Component 注解的类

比较: @Configuration + @Bean 的方式需要在@Configuration 的类里定义”返回每种 Bean 类型的方法”, @ComponentScan + @Component 的方式省去了定义方法返回 Bean 的类型
@Configuration, @ComponentScan, @Component 注解通常联合起来使用, 免去了在 xml 里定义 bean, 也不必写 @Bean

@Component
public class CompactDisc {
}

@Component
public class MediaPlayer {
private CompactDisc cd;

@Autowired
public CDPlayer(CompactDisc cd) { this.cd = cd; }
}

@Configuration
@ComponentScan
public class CDPlayerConfig {
}

// 使用扫描到的Bean:
@ContextConfiguration(classes=CDPlayerConfig.class)
public class Test {
@Autowired
private MediaPlayer player;

@Autowired
private CompactDisc cd;
}

@ContextConfiguration & @Configuration

  • @Configuration: 用于类上, 说明这个类可以使用 Spring IoC 容器作为 bean 定义的来源, 相当于在 xml 中定义 <beans>
  • @ContextConfiguration(classes=KnightConfig.class) 使用在类上, 表示使用 @Configuration 标注的类当作 bean 的定义来源
@Configuration
public class TextEditorConfig {
@Bean
public TextEditor textEditor(){
return new TextEditor( spellChecker() );
}
@Bean
public SpellChecker spellChecker(){
return new SpellChecker( );
}
}
// 上面的等同于在xml里定义了两个<bean>

// 使用从@Configuration标注类里注入的bean
@ContextConfiguration(classes=KnightConfig.class,loader=AnnotationConfigContextLoader.class)
public class Test {
@Autowired
TextEditor textEditor;

@Autowired
SpellChecker spellChecker;
}

@Transcational, @Cacheable

  • @Transcational : 事务处理
  • @Cacheable : 数据缓存

@Scope

默认是@Scope("singleton")单例的, 此外还有:

  • singleton 单例的
  • prototype 表示每次获得bean都会生成一个新的对象
  • request 表示在一次http请求内有效
  • session 表示在一个用户会话内有效

@Qualifier

public class CarFactory
{
@Autowired
@Qualifier("ImplementedClass")
private AbstractClass a;
}

当抽象类AbstractClass的实现类有多个时, 如果没有Qualifier注解则会报错, 因为Spring不知道应该注入哪个类型, 注意@Qualifier()括号里是类的名字

@Aspect

  • @After @Before. @Around 定义切面,可以直接将拦截规则(切入点 PointCut)作为参数
  • @PointCut : 专门定义拦截规则 然后在 @After @Before. @Around 中调用
  • @EnableAaspectJAutoProxy : 开启Spring 对 这个切面(Aspect )的支持

JDK注解

  • @Resource: 可以写在属性上, 和setter方法上, 默认按照名称进行装配

Spring中的线程安全性

本节参考: 聊一聊Spring中的线程安全性 | SylvanasSun’s Blog

Spring 作为一个 IOC/DI 容器,帮助我们管理了许许多多的“bean”。但其实,Spring 并没有保证这些对象的线程安全,需要由开发者自己编写解决线程安全问题的代码。

Spring对每个bean提供了一个scope属性来表示该bean的作用域。它是bean的生命周期。例如,一个scopesingleton的bean,在第一次被注入时,会创建为一个单例对象,该对象会一直被复用到应用结束。

singleton:默认的scope,每个scope为singleton的bean都会被定义为一个单例对象,该对象的生命周期是与Spring IOC容器一致的(但在第一次被注入时才会创建)。
prototype:bean被定义为在每次注入时都会创建一个新的对象。
request:bean被定义为在每个HTTP请求中创建一个单例对象,也就是说在单个请求中都会复用这一个单例对象。
session:bean被定义为在一个session的生命周期内创建一个单例对象。
application:bean被定义为在ServletContext的生命周期中复用一个单例对象。
websocket:bean被定义为在websocket的生命周期中复用一个单例对象。

我们交由Spring管理的大多数对象其实都是一些无状态的对象,这种不会因为多线程而导致状态被破坏的对象很适合Spring的默认scope,每个单例的无状态对象都是线程安全的(也可以说只要是无状态的对象,不管单例多例都是线程安全的,不过单例毕竟节省了不断创建对象与GC的开销)。

无状态的对象即是自身没有状态的对象,自然也就不会因为多个线程的交替调度而破坏自身状态导致线程安全问题。无状态对象包括我们经常使用的DO、DTO、VO这些只作为数据的实体模型的贫血对象,还有Service、DAO和Controller,这些对象并没有自己的状态,它们只是用来执行某些操作的。例如,每个DAO提供的函数都只是对数据库的CRUD,而且每个数据库Connection都作为函数的局部变量(局部变量是在用户栈中的,而且用户栈本身就是线程私有的内存区域,所以不存在线程安全问题),用完即关(或交还给连接池)。

有人可能会认为,我使用 scope=request 作用域不就可以避免每个请求之间的安全问题了吗?这是完全错误的,因为 Controller 默认是单例的,一个 HTTP 请求是会被多个线程执行的,这就又回到了线程的安全问题。当然,你也可以把 Controller 的 scope 改成 prototype,实际上 Struts2就是这么做的,但有一点要注意,Spring MVC 对请求的拦截粒度是基于每个方法的,而 Struts2是基于每个类的,所以把 Controller 设为多例将会频繁的创建与回收对象,严重影响到了性能。

通过阅读上文其实已经说的很清楚了,Spring 根本就没有对 bean 的多线程安全问题做出任何保证与措施。对于每个 bean 的线程安全问题,根本原因是每个 bean 自身的设计。不要在 bean 中声明任何有状态的实例变量或类变量,如果必须如此,那么就使用 ThreadLocal 把变量变为线程私有的,如果 bean 的实例变量或类变量需要在多个线程之间共享,那么就只能使用 synchronizedlockCAS 等这些实现线程同步的方法了。ThreadLocal @link [[../12.Java/Java-并发.02.ThreadLocal]] & Servlet 规范的线程安全 @link JavaEE.Servlet

本文作者为 SylvanasSun(sylvanas.sun@gmail.com),首发于 SylvanasSun’s Blog
原文链接:https://sylvanassun.github.io/2017/11/06/2017-11-06-spring_and_thread-safe/
(转载请务必保留本段声明,并且保留超链接。)

@Async注解实现异步方法

• 定义线程池:

@Configuration
public class ExecutorConfig {
@Bean("customExecutor-1")// 自定义线程池1
public Executor customExecutor1() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);//核心池大小
executor.setMaxPoolSize(6);//最大线程数
executor.setKeepAliveSeconds(60);//线程空闲时间
executor.setQueueCapacity(10);//队列程度
executor.setThreadNamePrefix("customExecutor-1-");//线程前缀名称
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());//配置拒绝策略
executor.setAllowCoreThreadTimeOut(true);// 允许销毁核心线程
executor.initialize();
return executor;
}
}

• @Async定义异步的service方法:

@Service
public class AsyncService {
// 例:无返回值的异步方法
// 使用上面定义的线程池
@Async("customExecutor-1")
public void noReturnMethod() {
String tName = Thread.currentThread().getName();
System.out.println("current thread name : " + tName);
System.out.println("noReturnMethod end");
}

// 例:有返回值的异步方法
// 使用默认 SimpleAsyncTaskExecutor
@Async
public Future<String> withReturnMethod() {
String tName = Thread.currentThread().getName();
System.out.println("current thread name : " + tName);
return new AsyncResult<>("aaa");
}
}

• 使用异步service

@RestController
@RequestMapping("/api/async/test/")
public class AsyncController {
@Autowired
AsyncService asyncService;

// 无返回值
@GetMapping("/noReturn")
public String noReturn() {
asyncService.noReturnMethod();
return "success";
}

// 有返回值
@GetMapping("/withReturn")
public String withReturn() {
Future<String> future = asyncService.withReturnMethod();
try {
String res = future.get();// 阻塞获取返回值
System.out.println("res = " + res);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return "success";
}
}

对“约定优于配置”的支持

约定优于配置(convention over configuration),也称作按约定编程,是一种软件设计范式,旨在减少软件开发人员需做决定的数量,获得简单的好处,而又不失灵活性。
本质是说,开发人员仅需规定应用中不符约定的部分。例如,如果模型中有个名为Sale的类,那么数据库中对应的表就会默认命名为sales。只有在偏离这一约定时,例如将该表命名为”products_sold”,才需写有关这个名字的配置。
许多新的框架使用了约定优于配置的方法,包括:Spring,Ruby on Rails,Kohana PHP,Grails,Grok,Zend Framework,CakePHP,symfony,Maven,ASP.NET MVC,Web2py(MVC),Apache Wicket。
比如Maven对目录做了”约定优于配置”的设定:

src/main/resources: 资源文件目录;
src/main/java: Java源码目录;
src/main/webapp: web应用文件目录(当打包为war时),如WEB-INF/web.xml

对JDBC的支持

Spring主要提供JDBC模板方式、关系数据库对象化方式和SimpleJdbc方式三种方式来简化JDBC编程,这三种方式就是Spring JDBC的工作模式:

  • JDBC模板方式:Spring JDBC框架提供以下几种模板类来简化JDBC编程,实现GoF模板设计模式,将可变部分和非可变部分分离,可变部分采用回调接口方式由用户来实现:如JdbcTemplate、NamedParameterJdbcTemplate、SimpleJdbcTemplate。
  • 关系数据库操作对象化方式:Spring JDBC框架提供了将关系数据库操作对象化的表示形式,从而使用户可以采用面向对象编程来完成对数据库的访问;如MappingSqlQuery、SqlUpdate、SqlCall、SqlFunction、StoredProcedure等类。这些类的实现一旦建立即可重用并且是线程安全的。

JDBC模板

<!--数据源的配置 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql:///spring"></property>
<property name="username" value="root"></property>
<property name="password" value=""></property>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
public class JdbcTemplateTest {
private static JdbcTemplate jdbcTemplate;

@Test
public void testQuery() {
String sql = "select * from INFORMATION_SCHEMA.SYSTEM_TABLES";
jdbcTemplate.query(sql, new RowCallbackHandler() {
@Override
public void processRow(ResultSet rs) throws SQLException {
String value = rs.getString("TABLE_NAME");
System.out.println("Column TABLENAME:" + value);
}
});
}

@Test
public void testUpdate() {
jdbcTemplate.update("insert into test(name) values('name1')");
jdbcTemplate.update("delete from test where name=?", new Object[]{"name2"});
jdbcTemplate.update("update test set name='name3' where name=?", new Object[]{"name1"});
}
}

关系数据库对象化

对MyBatis的支持

参考mybatis-spring – MyBatis-Spring | 第二章 入门 @ref

1. 引入mybatis-spring依赖

如果使用 Maven 作为构建工具,仅需要在 pom.xml 中加入以下代码即可:

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.1</version>
</dependency>

2. SqlSessionFactoryBean & Mapper

要和 Spring 一起使用 MyBatis,需要在 Spring 应用上下文中定义至少两样东西:一个 SqlSessionFactory 和至少一个数据映射器。

  • 增加 sqlSessionFactory 的 bean,注意 sqlSessionFactory 还需要一个数据源(DataSource),下面的例子用了 DruidDataSource

  • 这里使用了 MapperScannerConfigurer, 它将会查找类路径下的映射器并自动将它们创建成 MapperFactoryBean

  • (可选)增加 transactionManager 的 bean, 开启 Spring 事务

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="maxActive" value="10"/>
<property name="minIdle" value="5"/>
</bean>

<!-- 要注意 SqlSessionFactory 需要一个 dataSource -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
<property name="mapperLocations" value="classpath:mapper/**/*.xml"/>
</bean>

<!--事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>

<!--定义注解驱动事务-->
<tx:annotation-driven transaction-manager="transactionManager"/>

<!-- 配置扫描包,加载mapper代理对象 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.kuaizhan.kzweixin.dao.mapper"/>
</bean>

对Transaction的支持

  • @Transactional(value="transactionManagerPrimary", isolation = Isolation.DEFAULT, propagation = Propagation.REQUIRED)
    • value: 事务管理器
    • 隔离级别(isolation):
      • DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是:READ_COMMITTED。
      • READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。
      • READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值
      • REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。
      • SERIALIZABLE:提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
    • 传播行为(Propagation):所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。
      • REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是最常见的选择。
      • SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
      • MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
      • REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
      • NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
      • NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
      • NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED。

Spring MVC Step by Step @Deprecated

  1. Pom.xml
    • build - resources # 定义资源文件?
  2. webapp/WEB-INF/web.xml
    • context-param: contextConfigLocation=classpath:spring/appcontext-.xml # 指定Spring配置路径
    • listener: # listen优先级>Servlet
      • ContextLoaderListener=org.springframework.web.context.ContextLoaderListener
      • RequestContextListener=org.springframework.web.context.request.RequestContextListener
    • servlet: org.springframework.web.servlet.DispatcherServlet
      • init-param: contextConfigLocation=classpath:appcontext-core-web.xml # 指定Servlet配置路径
  3. Spring配置xml: 默认去找classpath下的application-Context.xml,这是一种约定优于配置的概念
    • context:property-placeholder: 指定*.properties位置
    • mvc:interceptors // 定义拦截器
    • mvc:annotation-driven // 注册DefaultAnnotationHandlerMapping/AnnotationMethodHandlerAdapter, 用于支持@Controller等注解风格
    • mvc:resources # css/js/htm等静态资源映射
    • 增加View解析器:
      • bean id=”velocityConfigurer” class=”org.springframework.web.servlet.view.velocity.VelocityConfigurer”
      • bean id=”viewResolver” class=”org.springframework.web.servlet.view.velocity.VelocityViewResolver”
    • 增加多数据源
      • bean id=”parentDataSource” class=”org.springframework.jdbc.datasource.DriverManagerDataSource”
      • bean id=”adminDataSource” parent=”parentDataSource” # 数据源1
      • bean id=”userDataSource” parent=”parentDataSource” # 数据源2
      • bean id=”dataSource” class=”com.frogking.datasource.DynamicDataSource” # 多数源映射关系, property增加上面两个bean
      • bean id=”sessionFactory” class=”org.springframework.orm.hibernate3.LocalSessionFactoryBean”

附: Configuration XML说明

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring/appcontext-*.xml</param-value>
</context-param>

<listener id="ContextLoaderListener">
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<listener id="RequestContextListener">
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>

<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/api/2/*</url-pattern>
</filter-mapping>

<servlet>
<servlet-name>comment</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:appcontext-core-web.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>comment</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>

<error-page>
<error-code>400</error-code>
<location>/error.jsp</location>
</error-page>