Spring系列(一)Spring MVC bean 解析、注册、实例化流程源码剖析
最近在使用Spring MVC过程中遇到了一些问题,网上搜索不少帖子后虽然找到了答案和解决方法,但这些答案大部分都只是给了结论,并没有说明具体原因,感觉总是有点不太满意。
更重要的是这些所谓的结论大多是抄来抄去,基本源自一家,真实性也有待考证。
那作为程序员怎么能知其所以然呢?
此处请大家内心默读三遍。
用过Spring 的人都知道其核心就是IOC和AOP,因此要想了解Spring机制就得先从这两点入手,本文主要通过对IOC部分的机制进行介绍。
在开始阅读之前,先准备好以下实验材料。
IDEA 是一个优秀的开发工具,如果还在用Eclipse的建议切换到此工具进行。
IDEA有很多的快捷键,在分析过程中建议大家多用Ctrl+Alt+B快捷键,可以快速定位到实现函数。
Spring bean的加载主要分为以下6步:
查看源码第一步是找到程序入口,再以入口为突破口,一步步进行源码跟踪。
Java Web应用中的入口就是web.xml。
在web.xml找到ContextLoaderListener ,此Listener负责初始化Spring IOC。
contextConfigLocation参数设置了bean定义文件地址。
下面是ContextLoaderListener的官方定义:
翻译过来ContextLoaderListener作用就是负责启动和关闭Spring root WebApplicationContext。
具体WebApplicationContext是什么?开始看源码。
从源码看出此Listener主要有两个函数,一个负责初始化WebApplicationContext,一个负责销毁。
继续看initWebApplicationContext函数。
在上面的代码中主要有两个功能:
进入CreateWebAPPlicationContext函数
进入determineContextClass函数。
进入configureAndReFreshWebApplicaitonContext函数。
WebApplication Context有很多实现类。 但从上面determineContextClass得知此处wac实际上是XmlWebApplicationContext类,因此进入XmlWebApplication类查看其继承的refresh()方法。
沿方法调用栈一层层看下去。
获取beanFactory。
beanFactory初始化。
加载bean。
读取XML配置文件。
XmlBeanDefinitionReader读取XML文件中的bean定义。
继续查看loadBeanDefinitons函数调用栈,进入到XmlBeanDefinitioReader类的loadBeanDefinitions方法。
最终将XML文件解析成Document文档对象。
上一步完成了XML文件的解析工作,接下来将XML中定义的bean注册到webApplicationContext,继续跟踪函数。
用BeanDefinitionDocumentReader对象来注册bean。
解析XML文档。
循环解析XML文档中的每个元素。
下面是默认命名空间的解析逻辑。
不明白Spring的命名空间的可以网上查一下,其实类似于package,用来区分变量来源,防止变量重名。
这里我们就不一一跟踪,以解析bean元素为例继续展开。
解析bean元素,最后把每个bean解析为一个包含bean所有信息的BeanDefinitionHolder对象。
接下来将解析到的bean注册到webApplicationContext中。接下继续跟踪registerBeanDefinition函数。
跟踪registerBeanDefinition函数,此函数将bean信息保存到到webApplicationContext的beanDefinitionMap变量中,该变量为map类型,保存Spring 容器中所有的bean定义。
Spring 实例化bean的时机有两个。
一个是容器启动时候,另一个是真正调用的时候。
相信用过Spring的同学们都知道以上概念,但是为什么呢?
继续从源码角度进行分析,回到之前XmlWebApplication的refresh()方法。
可以看到获得beanFactory后调用了 finishBeanFactoryInitialization()方法,继续跟踪此方法。
预先实例化单例类逻辑。
获取bean。
doGetBean中处理的逻辑很多,为了减少干扰,下面只显示了创建bean的函数调用栈。
创建bean。
判断哪种动态代理方式实例化bean。
不管哪种方式最终都是通过反射的形式完成了bean的实例化。
我们继续回到doGetBean函数,分析获取bean的逻辑。
上面方法中首先调用getSingleton(beanName)方法来获取单例bean,如果获取到则直接返回该bean。方法调用栈如下:
getSingleton方法先从singletonObjects属性中获取bean 对象,如果不为空则返回该对象,否则返回null。
那 singletonObjects保存的是什么?什么时候保存的呢?
回到doGetBean()函数继续分析。如果singletonObjects没有该bean的对象,进入到创建bean的逻辑。处理逻辑如下:
下面是判断容器中有没有注册bean的逻辑,此处beanDefinitionMap相信大家都不陌生,在注册bean的流程里已经说过所有的bean信息都会保存到该变量中。
如果该容器中已经注册过bean,继续往下走。先获取该bean的依赖bean,如果镩子依赖bean,则先递归获取相应的依赖bean。
依赖bean创建完成后,接下来就是创建自身bean实例了。
获取bean实例的处理逻辑有三种,即Singleton、Prototype、其它(request、session、global session),下面一一说明。
如果bean是单例模式,执行此逻辑。
获取单例bean,如果已经有该bean的对象直接返回。如果没有则创建单例bean对象,并添加到容器的singletonObjects Map中,以后直接从singletonObjects直接获取bean。
把新生成的单例bean加入到类型为MAP 的singletonObjects属性中,这也就是前面singletonObjects()方法中获取单例bean时从此Map中获取的原因。
Prototype是每次获取该bean时候都新建一个bean,因此逻辑比较简单,直接创建一个bean后返回。
从相应scope获取对象实例。
判断scope,获取实例函数逻辑。
在相应scope中设置实例函数逻辑。
以上就是Spring bean从无到有的整个逻辑。
从源码角度分析 bean的实例化流程到此基本接近尾声了。
回到开头的问题,ContextLoaderListener中初始化的WebApplicationContext到底是什么呢?
通过源码的分析我们知道WebApplicationContext负责了bean的创建、保存、获取。其实也就是我们平时所说的IOC容器,只不过名字表述不同而已。
本文主要是讲解了XML配置文件中bean的解析、注册、实例化。对于其它命名空间的解析还没有讲到,后续的文章中会一一介绍。
希望通过本文让大家在以后使用Spring的过程中有“一切尽在掌控之中”的感觉,而不仅仅是稀里糊涂的使用。
spring mvc mybatis 整合 大体步骤
一、简单说明
用到的框架:spring、springmvc,mybatis
开发工具:eclipse,apache-tomcat-6.0.39
jar包管理:maven
开发过程
一、建立工程
1、引入相关jar包:
dependencies
!--测试包 --
dependency
groupIdjunit/groupId
artifactIdjunit/artifactId
version4.8.1/version
scopetest/scope
/dependency
!-- servlet的jar包 添加scopeprovided/scope, 因为provided表明该包只在编译和测试的时候用--
dependency
groupIdjavax.servlet/groupId
artifactIdservlet-api/artifactId
version2.5/version
scopeprovided/scope
/dependency
/dependencies
二、引入mybatis相关内容并测试
1、引入JAR包
properties
project.build.sourceEncodingUTF-8/project.build.sourceEncoding
maven.build.timestamp.formatyyyyMMddHHmmss/maven.build.timestamp.format
mybatis.version3.3.1/mybatis.version
/properties
dependencies
!--测试包 --
dependency
groupIdjunit/groupId
artifactIdjunit/artifactId
version4.8.1/version
scopetest/scope
/dependency
!-- servlet的jar包 添加scopeprovided/scope,因为provided表明该包只在编译和测试的时候用--
dependency
groupIdjavax.servlet/groupId
artifactIdservlet-api/artifactId
version2.5/version
scopeprovided/scope
/dependency
dependency
groupIdorg.mybatis/groupId
artifactIdmybatis/artifactId
version${mybatis.version}/version
/dependency
dependency
groupIdmysql/groupId
artifactIdmysql-connector-java/artifactId
version5.1.25/version
/dependency
/dependencies
这里面在pom里面使用了properties 标签重点看一下,在这个环节添加了数据库连接的jar包和mybatis的jar包。这里要想可以操作数据库需要编程式的读取配置文件一般放在classpath下面。这里取名为conf.xml.这个里面主要完成2件事:
a:对数据库4个基本信息的配置, b:引入mapper.xml文件。
?xml version="1.0" encoding="UTF-8"?
!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" ""
configuration
environments default="development"
environment id="development"
transactionManager type="JDBC" /
!-- 配置数据库连接信息 --
dataSource type="POOLED"
property name="driver" value="com.mysql.jdbc.Driver" /
property name="url" value="jdbc:mysql://localhost:3306/SSM_XML"/
property name="username" value="root" /
property name="password" value="root" /
/dataSource
/environment
/environments
mappers
!-- 注册userMapper.xml文件,--
mapper resource="im/fenqi/study/user/mapper/UserMapper.xml"/
/mappers
/configuration
下面对这个步骤里面的内容进行详细说明:
1、创建im.fenqi.study.user.entity.User实体对象,这里导入一个lombok插件,这个插件要安装(具体百度),可以不用显示的写set/get方法。
2、创建im/fenqi/study/user/mapper/UserMapper.xml文件,这个里面是对数据库进行操作一般和 im.fenqi.study.user.mapper.UserMapper配对使用,里面注意点。
标签mapper里面的namespace的值就是配对的JAVA类im.fenqi.study.user.mapper.UserMapper
标签resultMap:是为了解决实体类的属性名字和数据库字段名字不一致的问题。主键和其他键是有区别的。
resultMap里面定义的字段和属性在对应的表和实体必须有对应的内容否则会报错。
标签trim:可以处理user(user_id,user_name,password)等问题,里面的子标签为:
prefix:前缀覆盖并增加其内容
suffix:后缀覆盖并增加其内容
prefixOverrides:前缀判断的条件
找到最前面的和条件一样的内容然后变成prefix里面的内容
例:trim prefix="where" prefixOverrides="AND"
/trim
根据条件判断trim里面的字符串为"AND id=#{id} AND name like #{name}"
找到最前面的AND(prefixOverrides的内容)替换成where(prefix内容)
如果没有prefixOverrides的内容就直接加在前面。
suffixOverrides:后缀判断的条件
和prefixOverrides类似。
标签if:比较简单,if test="判断条件"/if
标签sql:
sql id="Base_Column_List"
id, user_id,user_name,password
/sql
使用场景:
select include refid="Base_Column_List" / from user
需要返回的字段很多的时候不用每次都写一遍
3、创建im.fenqi.study.user.mapper.UserMapper类这个和上面的文件是成对出现的,这是一个接口接口里面的方法名和UserMapper.xml里面的id要一致
4、测试类userTest
在本地测试主要使用SqlSession对象二这个对象需要使用SqlSessionFactory对象才能创建,下面先说SqlSessionFactory对象是怎么得到的
a:获取配置文件config.xml的输入流:
String resource = "/conf.xml";
//从classpath里面寻找
InputStream resourceAsStream = Object.class.getResourceAsStream(resource);
b:获取SqlSessionFactory对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
c:获取SqlSession对象:
SqlSession session = factory.openSession();
d:得到dao层的对象
UserMapper userMapper = session.getMapper(UserMapper.class);
d:插入数据
User user = new User();
user.setUserId(UUID.randomUUID().toString());
user.setUserName("zw1");
user.setPassword("123456");
int count = userMapper.saveUserInfo(user);
session.commit();
System.out.println("insert:"+count);
三、引入spring框架并测试
首先需要引入必要的jar包,主要是spring必要的包和spring支持mybatis,jdbc的包
!--spring基础的包 --
dependency
groupIdorg.springframework/groupId
artifactIdspring-core/artifactId
version${spring.version}/version
/dependency
dependency
groupIdorg.springframework/groupId
artifactIdspring-beans/artifactId
version${spring.version}/version
/dependency
dependency
groupIdorg.springframework/groupId
artifactIdspring-context/artifactId
version${spring.version}/version
/dependency
dependency
groupIdorg.springframework/groupId
artifactIdspring-expression/artifactId
version${spring.version}/version
/dependency
!--spring+mybatis+jdbc--
dependency
groupIdorg.mybatis/groupId
artifactIdmybatis-spring/artifactId
version1.2.1/version
/dependency
dependency
groupIdorg.springframework/groupId
artifactIdspring-jdbc/artifactId
version${spring.version}/version
/dependency
这个时候conf.xml这个配置文件已经不需要了,统一在spring.xml里面进行配置
1、配置数据库连接
?xml version="1.0" encoding="UTF-8" ?
beans xmlns=""
xmlns:xsi=""
xmlns:p=""
xmlns:aop=""
xmlns:context=""
xmlns:tx=""
xsi:schemaLocation="
"
bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"
property name="driverClassName" value="com.mysql.jdbc.Driver"/property
property name="url" value="jdbc:mysql://localhost:3306/SSM_XML"/property
property name="username" value="root"/property
property name="password" value="root"/property
/bean
/beans
当然这个配置可以写在properties文件里面(用到再说)
2、spring引入以后主要目的就是利用IOC容器进行创建对象,没有加入spring之前SqlSessionFactory、SqlSession、UserMapper 对象的创建都是我们硬编码实现,spring就是为了解决这些问题,最后直接返回UserMapper 给我们使用。其他的事情都在配置里完成。这里面具体操作流程以后会详细分析
3、创建SqlSessionFactory
创建这个对象需要数据源和mapper.xml配置文件的位置
!--创建 sqlSessionFactory,给了数据源和配置路径 --
bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"
property name="dataSource" ref="dataSource"/property
property name="mapperLocations" value="classpath:im/fenqi/study/*/mapper/*.xml"/property
/bean
4、创建UserMapper 对象(这一部分还要重点研究)
需要SqlSessionFactory对象创建SqlSession对象,然后创建UserMapper 对象,所以需要SqlSessionFactory和UserMapper 接口的位置,MapperScannerConfigurer是扫描包,不要每次都创建MapperFactoryBean
!-- 创建Mapper层的对象 --
bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"
property name="basePackage" value="im.fenqi.study.*.mapper" /
property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/property
/bean
5、测试获取对象
ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");
UserMapper bean = context.getBean("userMapper",UserMapper.class);
ListUser userByUser = bean.getUserByUser(null);
6、这个测试还可以使用spring自带的test类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/spring.xml")
public class springtest {
@Autowired
private UserMapper userMapper;
@Test
public void save()
{
User user = new User("zw1", 20);
userMapper.save(user);
}
四、引入springmvc框架
首先引入springmvc的JAR包
!--springMVC相关框架 --
dependency
groupIdorg.springframework/groupId
artifactIdspring-web/artifactId
version${spring.version}/version
/dependency
dependency
groupIdorg.springframework/groupId
artifactIdspring-webmvc/artifactId
version${spring.version}/version
/dependency
这个时候需要配置web.xml,也要配置springmvc.xml
1、配置web.xml
添加加载spring.xml的监听器和spring文件路径
?xml version="1.0" encoding="UTF-8"?
web-app xmlns:xsi="" xmlns="" xsi:schemaLocation=" " version="2.5"
listener
listener-classorg.springframework.web.context.ContextLoaderListener/listener-class
/listener
context-param
param-namecontextConfigLocation/param-name
param-valueclasspath:spring.xml/param-value
/context-param
servlet
servlet-namespringmvc/servlet-name
servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class
init-param
param-namecontextConfigLocation/param-name
param-valueclasspath:springmvc.xml/param-value
/init-param
/servlet
servlet-mapping
servlet-namespringmvc/servlet-name
url-pattern/*/url-pattern
/servlet-mapping
/web-app
配置Dispatcherservlet和springmvx.xml配置文件的位置
2、配置springmvc.xml文件
springmvc.xml主要配置控制器、handlerMapper、HandlerAdapter、渲染器
?xml version="1.0" encoding="UTF-8"?
beans xmlns=""
xmlns:xsi=""
xmlns:context=""
xmlns:mvc=""
xsi:schemaLocation="
"
!-- 注解扫描,以包为单位 --
context:component-scan base-package="im.fenqi.study"/
!--创建控制器 --
bean id="/user.do" class="im.fenqi.study.user.rest.UserController"
property name="userService" ref="userService"/property
/bean
!--创建handlermapper --
bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/bean
!--创建handlerAdapter --
bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/bean
!-- 配置渲染器 --
bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"
property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/
property name="prefix" value="/WEB-INF/jsp/"/
property name="suffix" value=".jsp"/
/bean
/beans
3、编写控制器控制器和配置的类型要一致
public class UserController implements Controller{
private UserService userService;
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView modelAndView = new ModelAndView();
ListUser userList = userService.getUserByUser(null);
modelAndView.addObject("user", userList.get(0));
modelAndView.setViewName("mypage");
return modelAndView;
}
}
五、整合过程中出现的问题
1、IOC容器。
如果我使用XML配置IOC容器,自己定义一个类这个类里面的属性想用IOC容器里面的对象初始化,必须提供set/get方法。
2、JSP不能使用el表达式,这个问题要解决
这个问题是缺少jar包的问题要想jsp使用起来不出现问题需要添加下面的包,最上面的包之前添加过了
!-- servlet的jar包 添加scopeprovided/scope,因为provided表明该包只在编译和测试的时候用--
dependency
groupIdjavax.servlet/groupId
artifactIdservlet-api/artifactId
version2.5/version
scopeprovided/scope
/dependency
!--JSP --
dependency
groupIdjavax.servlet.jsp/groupId
artifactIdjsp-api/artifactId
version2.2/version
scopeprovided/scope
/dependency
dependency
groupIdjavax.servlet/groupId
artifactIdjstl/artifactId
version1.2/version
/dependency
dependency
groupIdtaglibs/groupId
artifactIdstandard/artifactId
version1.1.2/version
/dependency
3、mybatis和spring的配置过程还要具体分析
A:这个里面有很多种配置这一次就详细说明一下我们这里使用的sqlSessionFactory和 MapperScannerConfigurer
sqlSessionFactory:创建的时候需要dataSource和mapper.xml的配置文件
MapperScannerConfigurer:可以整体扫描,提供基础的包名可以使用通配符,需要sqlSessionFactoryBeanName用来创建sqlSessionFactory对象再创建SqlSession对象再通过SqlSession对象创建各个mapper对象。
详解Spring mvc工作原理及源码分析
Model 模型层 (javaBean组件 = 领域模型(javaBean) + 业务层 + 持久层)
View 视图层( html、jsp…)
Controller 控制层(委托模型层进行数据处理)
springmvc是一个web层mvc框架,类似struts2。
springmvc是spring的部分,其实就是spring在原有基础上,又提供了web应用的mvc模块。
实现机制:
struts2是基于过滤器实现的。
springmvc是基于servlet实现的。
运行速度:
因为过滤器底层是servlet,所以springmvc的运行速度会稍微比structs2快。
struts2是多例的
springmvc单例的
参数封装:
struts2参数封装是基于属性进行封装。
springmvc是基于方法封装。颗粒度更细。
⑴ 用户发送请求至DispatcherServlet。
⑵ DispatcherServlet收到请求调用HandlerMapping查询具体的Handler。
⑶ HandlerMapping找到具体的处理器(具体配置的是哪个处理器的实现类),生成处理器对象及处理器拦截器(HandlerExcutorChain包含了Handler以及拦截器集合)返回给DispatcherServlet。
⑷ DispatcherServlet接收到HandlerMapping返回的HandlerExcutorChain后,调用HandlerAdapter请求执行具体的Handler(Controller)。
⑸ HandlerAdapter经过适配调用具体的Handler(Controller即后端控制器)。
⑹ Controller执行完成返回ModelAndView(其中包含逻辑视图和数据)给HandlerAdaptor。
⑺ HandlerAdaptor再将ModelAndView返回给DispatcherServlet。
⑻ DispatcherServlet请求视图解析器ViewReslover解析ModelAndView。
⑼ ViewReslover解析后返回具体View(物理视图)到DispatcherServlet。
⑽ DispatcherServlet请求渲染视图(即将模型数据填充至视图中) 根据View进行渲染视图。
⑾ 将渲染后的视图返回给DispatcherServlet。
⑿ DispatcherServlet将响应结果返回给用户。
(1)前端控制器DispatcherServlet(配置即可)
功能:中央处理器,接收请求,自己不做任何处理,而是将请求发送给其他组件进行处理。DispatcherServlet 是整个流程的控制中心。
(2)处理器映射器HandlerMapping(配置即可)
功能:根据DispatcherServlet发送的url请求路径查找Handler
常见的处理器映射器:BeanNameUrlHandlerMapping,SimpleUrlHandlerMapping,
ControllerClassNameHandlerMapping,DefaultAnnotationHandlerMapping(不建议使用)
(3)处理器适配器HandlerAdapter(配置即可)
功能:按照特定规则(HandlerAdapter要求的规则)去执行Handler。
通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展多个适配器对更多类型的处理器进行执行。
常见的处理器适配器:HttpRequestHandlerAdapter,SimpleControllerHandlerAdapter,AnnotationMethodHandlerAdapter
(4)处理器Handler即Controller(程序猿编写)
功能:编写Handler时按照HandlerAdapter的要求去做,这样适配器才可以去正确执行Handler。
(5)视图解析器ViewReslover(配置即可)
功能:进行视图解析,根据逻辑视图名解析成真正的视图。
ViewResolver负责将处理结果生成View视图,ViewResolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。
springmvc框架提供了多种View视图类型,如:jstlView、freemarkerView、pdfView...
(6)视图View(程序猿编写)
View是一个接口,实现类支持不同的View类型(jsp、freemarker、pdf...)
引入相关依赖:spring的基本包、springmvc需要的spring-webmvc,日志相关的slf4j-log4j12,jsp相关的jstl、servlet-api、jsp-api。
因为DispatcherServlet本身就是一个Servlet,所以需要在web.xml配置。
一、使用默认加载springmvc配置文件的方式,必须按照以下规范:
①命名规则:-servlet.xml ==== springmvc-servlet.xml
②路径规则:-servlet.xml必须放在WEB-INF下边
二、如果要不按照默认加载位置,则需要在web.xml中通过标签来指定springmvc配置文件的加载路径,如上图所示。
将自定义的 Controller 处理器配置到 spring 容器中交由 spring 容器来管理,因为这里的 springmvc.xml 配置文件中处理器映射器配置的是 BeanNameUrlHandlerMapping ,根据名字可知这个处理器映射器是根据 bean (自定义Controller) 的 name 属性值url去寻找执行类 Handler(Controller) , 所以bean的name属性值即是要和用户发送的请求路径匹配的 url 。
根据视图解析路径:WEB-INF/jsps/index.jsp
功能:根据bean(自定义Controller)的name属性的url去寻找执行类Controller。
功能:自定义的处理器(Controller)实现了Controller接口时,适配器就会执行Controller的具体方法。
SimpleControllerHandlerAdapter会自动判断自定义的处理器(Controller)是否实现了Controller接口,如果是,它将会自动调用处理器的handleRequest方法。
Controller接口中有一个方法叫handleRequest,也就是处理器方法。
因此,自定义的Controller要想被调用就必须实现Controller接口,重写Controller接口中的处理器方法。