//使用官网的启动方式
public class demo implements WebApplicationInitializer {
@Override
public void onStartup(javax.servlet.ServletContext servletCxt) throws ServletException {
//创建一个web容器
AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
//手动注册配置类
ac.register(AppConfig.class);
/**
* 我觉得这个地方进行容器的刷新毫无作用,甚至还会报错
* 此时你进行刷新,springMVC的容器并没有往springMVC独有的一些beanDefiniton/beanPostProcessor
* 所以如果此时你扫描到的类使用了某些MVC才有的beanPostProcessor就会报错
* 而且当Tomcat第一次接收请求会执行DispatcherServlet的init方法,在该方法中又进行了一次refersh()
* 所以我觉得此处的refersh()方法毫无用处。
*/
ac.refresh();
/**
* DispatcherServlet本质上就如其名字所展示的那样是一个Java Servlet。
* 同时它是Spring MVC中最为核心的一块——前端控制器。它主要用来拦截符合要求的外部请求,
* 并把请求分发到不同的控制器去处理,根据控制器处理后的结果,生成相应的响应发送到客户端。
* DispatcherServlet作为统一访问点,主要进行全局的流程控制
*/
DispatcherServlet servlet = new DispatcherServlet(ac);
ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}
上述的代码完全替代了web.xml中的配置,讲一下为什么
* 为什么启动 tomcat 加载该容器能执行该类的onStartup方法
* 因为 spi 和 servlet3.0 以后提供的一种规范
* 如果你把这个项目交给tomcat管理,tomcat会自动去找jar包下的META-INF/services/目录下找一个ServletContainerLnitializer文件
* 然后加载该文加中的指定的类
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
/**
* tomcat会执行该方法,并且servlet提供了一个注解 @HandlesTypes(WebApplicationInitializer.class)
* 这个注解的作用就是在执行这个类的方法时候,动态找到注解上的接口的所有实现类,然后放到set<Class<?>> webAppInitializerClasses 集合中
*
*/
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<>();
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
//一些servlet容器为我们提供了无效的类
//我们需要对这webAppInitializerClasses中的类进行剔除
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
//对所有的类进行排序
AnnotationAwareOrderComparator.sort(initializers);
//按照顺序执行所有类的onStartUp()方法,所以就会加载到我们用来初始化spring容器的onStartup方法
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
至于所有onStartup()方法做了什么,有兴趣自己看
DispathcherServlet中并没有init()方法,该方法存在于他的父类HttpServletBean中
/**
* 通过init()方法来对springMVC的容器进行初始化
*/
public final void init() throws ServletException {
//获取Web.xml里面的servlet的init-param
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
//为给定类型的所有属性注册给定的自定义属性编辑器,提供了设置和获取属性值,它有对应的BeanWrapperImpl
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
//可以根据一个资源地址加载文件资源。classpath:这种方式指定SpringMVC框架bean配置文件的来源
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
} catch (BeansException ex) {
}
}
//调用子类继续对容器进行初始化
initServletBean();
}
继续看如何初始化
/**
* FrameworkServlet子类对HttpServletBean方法的重写,在任何bean属性设定好之后调用
* 创建此servlet的WebApplicationContext。
*/
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
//在控制台打印容器初始化已经开始了
if (this.logger.isInfoEnabled()) {
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
}
//记录当前的系统时间
long startTime = System.currentTimeMillis();
try {
//初始化SpringMVC容器
this.webApplicationContext = initWebApplicationContext();
/**
* 此方法将在设置任何bean属性之后调用
* 并且加载WebApplicationContext。默认实现为空;
*/
initFrameworkServlet();
}
if (this.logger.isInfoEnabled()) {
//拿到启动所耗用时间
long elapsedTime = System.currentTimeMillis() - startTime;
//控制台打印启动所耗用时间
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
elapsedTime + " ms");
}
}
继续追踪
protected WebApplicationContext initWebApplicationContext() {
/**
* 获取根节点上下文,通过ContextLoaderListener加载,服务器启动便加载
* tomcat的加载顺序是context-param >> listener >> filter >> servlet。
*/
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
//在DispatcherServlet进行创建时,会传递一个ApplicationContext
if (this.webApplicationContext != null) {
wac = this.webApplicationContext;
//判断容器的类型是否是ConfigurableWebApplicationContext(web容器)
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
//确定此应用程序上下文是否处于活动状态,即是否已刷新至少一次且尚未关闭
if (!cwac.isActive()) {
//如果springMVC容器的parent属性为null,则设置parent的值为spring容器,父子容器
if (cwac.getParent() == null) {
cwac.setParent(rootContext);
}
//初始化springMVC容器,进行refresh()操作,所以这也证实了前面的refresh()操作多余
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (!this.refreshEventReceived) {
/**
* 这是通过springMVC进行初始化的重点!!!!
* 如果初始化容器成功
* 就去初始化了SpringMVC的九大组件
*/
onRefresh(wac);
}
if (this.publishContext) {
//将容器作为servlet容器的属性发布。
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
"' as ServletContext attribute with name [" + attrName + "]");
}
}
return wac;
}
何进行初始化的
WebApplicationInitializer的实现类中把ContextLoaderListener监听器注册给Tomcat的 ServletContext -> contextInitialized()完成了父容器的初始化
ContextLoaderListener继承了ContextLoader,并实现了ServletContextListener接口中的contextInitialized方法。
web容器初始化时调用contextInitialized方法通知listener,继而调用ContextLoader中的initWebApplicationContext方法,初始化spring容器。
在这个init方法中,主要做了两件事:
1)创建容器实例。
2)设置初始化配置,读取配置文件、重启IOC容器(BeanFactory)加载各个资源文件和bean等。然后,将ServletContext中的spring根容器设置为本容器实例。
此时,spring父容器中的bean都已准备就绪。
重点看一下onRefresh()方法,基本上初始化流程差不多,进行一个分析即可
protected void initStrategies(ApplicationContext context) {
/**
* SpringMVC的九大组件
*/
/**
* 用于处理上传请求
* 将普通的Request包装成MultipartHttpServletRequest
* 可以通过getFile() 直接获得文件,如果是多个文件上传,还可以通过调用getFileMap
* 得到Map<FileName,File> 这样的结构。
*/
initMultipartResolver(context);
/**
* 在上面我们有看到ViewResolver 的resolveViewName()方法,需要两个参数。那么第
* 二个参数Locale是从哪来的呢,这就是LocaleResolver 要做的事了。和国际化相关,可以理解为设置编码
*/
initLocaleResolver(context);
/**
* 从名字便可看出,这个类是用来解析主题的。主题,就是样式,图片以及它们所形成的
* 显示效果的集合
*/
initThemeResolver(context);
/**
* HandlerMapping是用来查找Handler的,也就是处理器,具体的表现形式可以是类也
* 可以是方法。比如,标注了@RequestMapping 的每个 method 都可以看成是一个
* Handler,由 Handler 来负责实际的请求处理。 HandlerMapping 在请求到达之后,
* 它的作用便是找到请求相应的处理器Handler和Interceptors。
* 这一步完事后 handlerMappings 中最少存放了2个对象
*/
initHandlerMappings(context);
/**
* 从名字上看,这是一个适配器。因为SpringMVC中Handler可以是任意形式的,只要
* 能够处理请求便行, 但是把请求交给 Servlet 的时候,就需要根据传递过去的具体类型(方法、类)
* 找到对应的 HandlerAdapter然后执行handler的方法
*/
initHandlerAdapters(context);
/**
* 从这个组件的名字上看,这个就是用来处理Handler过程中产生的异常情况的组件。 具
* 体来说,此组件的作用是根据异常设置ModelAndView, 之后再交给 render()方法进行
* 渲 染 , 而 render() 便 将 ModelAndView 渲 染 成 页 面 。 不 过 有 一 点 ,
* HandlerExceptionResolver 只是用于解析对请求做处理阶段产生的异常,而渲染阶段的
* 异常则不归他管了,这也是Spring MVC 组件设计的一大原则分工明确互不干涉。
*/
initHandlerExceptionResolvers(context);
/**
* 这个组件的作用,在于从 Request 中获取 viewName. 因为 ViewResolver 是根据
* ViewName 查找 View, 但有的 Handler 处理完成之后,没有设置 View 也没有设置
* ViewName,便要通过这个组件来从Request中查找viewName,就是处理完成之后,跳转到发送request请求的页面
*/
initRequestToViewNameTranslator(context);
/**
* 从方法的定义就可以看出,Controller层返回的String类型的视图名viewName,( return "xxxx" )
* 最终会在这里被解析成为View。View 是用来渲染页面的,也就是说,它会将程序返回
* 的参数和数据填入模板中,最终生成 html 文件 直接显示给用户
* ViewResolver会找到渲染所用的模板(使用什么模板来渲染?)和所用的技术(如JSP)填入参数
*/
initViewResolvers(context);
/**
* 用于重定向Redirect时的参数数据传递,比如,在处理用户订单提交时,为
* 了避免重复提交,可以处理完post请求后redirect到一个get请求,这个get请求可以
* 用来显示订单详情之类的信息。这样做虽然可以规避用户刷新重新提交表单的问题,但
* 是在这个页面上要显示订单的信息,那这些数据从哪里去获取呢,因为redirect重定向
* 是没有传递参数这一功能的,如果不想把参数写进url(其实也不推荐这么做,url有长度
* 限制不说,把参数都直接暴露,感觉也不安全), 那么就可以通过flashMap来传递。只
* 需 要 在 redirect 之 前 , 将 要 传 递 的 数 据 写 入 request ( 可 以 通 过
* ServletRequestAttributes.getRequest() 获 得 ) 的 属 性
* OUTPUT_FLASH_MAP_ATTRIBUTE 中,这样在 redirect 之后的 handler 中 Spring 就
* 会自动将其设置到Model中,在显示订单信息的页面上,就可以直接从Model 中取得
* 数据了。而FlashMapManager就是用来管理FlashMap的。
*/
initFlashMapManager(context);
}
说一个重点,我们在最开始的时候new DispatcherServlet(),而这个类有一个静态代码块,所以在创建的时候一定会执行
static {
try {
/**
* DispatcherServlet类加载的时候就会执行该静态方法
* 创建一个文件读取器,通过读取路径下的 DispatcherServlet.properties文件
* 把 key = value 封装到 defaultStrategies的properties对象中
* 保存了8组信息,在init()方法的时候会用到
*/
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
} catch (IOException ex) {
throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
}
}
重点分析一下initHandlerMappings(context)方法
/**
* 建立当前ApplicationContext中的所有controller和url的对应关系
*/
private void initHandlerMappings(ApplicationContext context) {
// 初始化记录 HandlerMapping 对象的属性变量为null
this.handlerMappings = null;
// 根据属性detectAllHandlerMappings决定是检测所有的 HandlerMapping 对象,还是使用指定名称的 HandlerMapping 对象
if (this.detectAllHandlerMappings) {
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
/**
* 从容器及其祖先容器查找所有类型为 HandlerMapping 的 HandlerMapping 对象,
* spring提供的扩展点,通过实现 requestHandlerMapping类 在注册进spring ioc容器中,
* 就可以实现自定义请求类型
*
* springboot就是通过 import配置类,配置类中 @bean 把扩展的各种handlerMapping、Adapters.....等,
* 添加进spring ioc容器,再通过该方法那出来,放到web容器中,子容器
*/
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
//如果其不为空
if (!matchingBeans.isEmpty()) {
//记录到 handlerMappings
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
//根据order属性排序
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
} else {
try {
/**
* 获取名称为 handlerMapping 的 HandlerMapping bean 并记录到 handlerMappings
*/
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
} catch (NoSuchBeanDefinitionException ex) {
}
}
/**
* 除非你手动扩展handlerMapping,不然从容器是获取不到的
* 然后是从DefaultStrategies集合中获取,熟悉不,就是静态代码块中注册那些类
*/
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isDebugEnabled()) {
logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default");
}
}
}
springMVC的创建流程大致重点差不多都分析了,下篇文章将会分析service()方法,也就是对请求的处理,最后再说一下我对父子容器的理解
spring父子容器
* 当一个项目中引入Spring和SpringMVC这两个框架,其实就是2个容器,Spring是父容器,SpringMVC是其子容器,
* 子容器可以访问父容器对象,而父容器不可以访问子容器对象
* 我们可以理解为springMVC容器中拥有spring容器的引用,对外暴露的是springMVC的容器
* 而且这两个容器是两个独立的容器互不干涉,拥有着自己独立的属性
* 也就是说我们平常用的所有注解扫描及实例化完成的bean,全都存放在MVC的容器中。所以不会出现问题
* 但是如果把所有的实例化bean放到spring容器中就会报错,因为MVC容器中的单例池为空找不到controller的bean,更找不到映射
*
*
* 在扩展一下springboot是如何启动项目的
* springboot的启动方式有两种 1.jar 2.war
* jar包方式的启动 Tomcat t = new Tomcat(80);
* ...把spring容器存放到tomcat中.....
* t.start();
* 也就是说内嵌了一个tomcat,执行了自己的tomcat来启动,
*
* war包方式启动 启动类实现一个 springApplicationInitializer接口 这个接口又实现了 WebApplicationInitializer 接口
* 也是通过SPI规范来放到外部的Tomcat