本文详细介绍了SSM框架的主要核心原理,包括Spring、Spring MVC和MyBatis的工作机制分析和源码解读。首先,Spring框架通过控制反转(IOC)和依赖注入(DI)实现了对象的管理,支持多种Bean作用域和自动装配模式,确保了应用程序的灵活性与可维护性。其次,Spring MVC处理请求的工作流程和注解配置使得开发简单高效。最后,MyBatis作为持久层框架,通过映射文件与会话管理实现了对数据库操作的封装。整体上,SSM框架共同支持现代Java应用的开发需求。
1 String Framework
1.1 IOC和DI
- 控制反转(IOC):Spring容器使用工厂模式来创建所需要的对象,使得不需要自己去创建,直接调用Spring提供的对象即可。
- 依赖注入(DI):Spring使用Java Bean对象的setter方法或者带参数的构造方法,在创建所需对象时将其属性自动设为所需要的值。
- 构造器依赖注入:容器触发一个类的构造器。该类有一系列参数,每个参数代表一个对其他类的依赖。
- Setter方法注入:容器通过调用无参构造器或无参static工厂方法实例化bean之后,调用该bean的setter方法。
1.2 Bean
1.2.1 作用域
Spring框架支持以下五种bean的作用域:
- singleton:bean在每个Spring IOC容器中只有一个实例。
- prototype:一个bean的定义可以有多个实例。
- request:每次http请求都会创建一个bean,该作用域仅在基于web的Spring ApplicationContext情形下有效。
- session:在一个HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。
- application:属于应用程序域。应用程序启动时bean创建,应用程序销毁时bean 销毁。该作用域仅在基于web的ServletContext。
1.2.2 自动装配模式
Spring的自动装配功能:无须在Spring配置文件中描述Java Bean之间的依赖关系(如配置<property>
、<constructor-arg>
)。
自动装配模式:
- no:Spring框架的默认设置,在该设置下自动装配关闭。开发者需要自行在bean定义中用标签明确的设置依赖关系 。
- byName:该选项可根据bean名称设置依赖关系 。 当向一个bean中自动装配一个属性时,容器将根据bean的名称自动在在配置文件中查询一个匹配的bean。 若找到则装配这个属性,若没找到则报错。
- byType:该选项可以根据bean类型设置依赖关系 。 当向一个bean中自动装配一个属性时,容器将根据 bean的类型自动在在配置文件中查询一个匹配的bean。 若找到则装配这个属性,若没找到则报错。
- constructor:构造器的自动装配和byType模式类似,但仅适用于与有构造器相同参数的bean。若在容器中没有找到与构造器参数类型一致的bean,则会抛出异常 。
- default:该模式自动探测使用构造器自动装配或byType自动装配。 首先会尝试找合适的带参数的构造器,若找到则用构造器自动装配;若在bean内部没有找到相应的构造器或无参构造器,容器就会自动选择byType自动装配方式 。
【例】如下注入
可以改造为
1.2.3 生命周期
1.2.3.1 doGetBean()源码
Spring框架在创建的bean时都会调用AbstractBeanFactory类中的doGetBean()
方法。
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
// 获取beanName。有三种形式:原始的beanName、加了&、别名
final String beanName = transformedBeanName(name);
Object bean;
// Eagerly check singleton cache for manually registered singletons.
// 是否已经创建了
Object sharedInstance = getSingleton(beanName);
// 已经创建了——若没有构造参数,进入该方法;如果有构造参数,往else走,即不获取bean,而直接创建bean
if (sharedInstance != null && args == null) {
if (logger.isTraceEnabled()) {
if (isSingletonCurrentlyInCreation(beanName)) {
logger.trace("Returning eagerly cached instance of singleton bean '" + beanName +
"' that is not fully initialized yet - a consequence of a circular reference");
} else {
logger.trace("Returning cached instance of singleton bean '" + beanName + "'");
}
}
// 若为普通bean,直接返回;若为FactoryBean,返回其getObject
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
} else {
// Fail if we're already creating this bean instance:
// We're assumably within a circular reference.
// 没创建过bean或者是多例的情况或者有参数的情况
// 创建过Prototype的bean会循环引用。需抛出异常,单例才尝试解决循环依赖的问题
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
// Check if bean definition exists in this factory.
BeanFactory parentBeanFactory = getParentBeanFactory();
// 父容器存在,本地没有当前beanName,从父容器取
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
// Not found -> check parent.
// 处理后,如果是加&,就补上&
String nameToLookup = originalBeanName(name);
if (parentBeanFactory instanceof AbstractBeanFactory) {
return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
nameToLookup, requiredType, args, typeCheckOnly);
} else if (args != null) {
// Delegation to parent with explicit args.
return (T) parentBeanFactory.getBean(nameToLookup, args);
} else if (requiredType != null) {
// No args -> delegate to standard getBean method.
return parentBeanFactory.getBean(nameToLookup, requiredType);
} else {
return (T) parentBeanFactory.getBean(nameToLookup);
}
}
if (!typeCheckOnly) {
// typeCheckOnly为false,将beanName放入alreadyCreated中
markBeanAsCreated(beanName);
}
try {
// 获取BeanDefinition
final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
// 抽象类检查
checkMergedBeanDefinition(mbd, beanName, args);
// Guarantee initialization of beans that the current bean depends on.
// 如果有依赖的情况,先初始化依赖的bean
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
for (String dep : dependsOn) {
// 检查是否循环依赖,a依赖b,b依赖a。包括传递的依赖,比如a依赖b,b依赖c,c依赖a
if (isDependent(beanName, dep)) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
}
// 注册依赖关系
registerDependentBean(dep, beanName);
try {
// 初始化依赖的bean
getBean(dep);
} catch (NoSuchBeanDefinitionException ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"'" + beanName + "' depends on missing bean '" + dep + "'", ex);
}
}
}
// Create bean instance.
// 如果是单例
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
// 创建bean
return createBean(beanName, mbd, args);
} catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
});
// 若为普通bean,直接返回;若为FactoryBean,返回其getObject
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
} else if (mbd.isPrototype()) {
// It's a prototype -> create a new instance.
Object prototypeInstance = null;
try {
// 加入prototypesCurrentlyInCreation,说明正在创建
beforePrototypeCreation(beanName);
//创建bean
prototypeInstance = createBean(beanName, mbd, args);
} finally {
// 移除prototypesCurrentlyInCreation,说明已经创建结束
afterPrototypeCreation(beanName);
}
// 若为普通bean,直接返回;若为FactoryBean,返回其getObject
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
} else {
String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
} finally {
afterPrototypeCreation(beanName);
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
} catch (IllegalStateException ex) {
throw new BeanCreationException(beanName,
"Scope '" + scopeName + "' is not active for the current thread; consider " +
"defining a scoped proxy for this bean if you intend to refer to it from a singleton",
ex);
}
}
} catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
throw ex;
}
}
// Check if required type matches the type of the actual bean instance.
if (requiredType != null && !requiredType.isInstance(bean)) {
try {
T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType);
if (convertedBean == null) {
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
return convertedBean;
} catch (TypeMismatchException ex) {
if (logger.isTraceEnabled()) {
logger.trace("Failed to convert bean '" + name + "' to required type '" +
ClassUtils.getQualifiedName(requiredType) + "'", ex);
}
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
}
return (T) bean;
}
1.2.3.2 七个阶段详解
一、处理名称,检查缓存
- 处理别名,将别名解析为实际名称
- 对FactoryBean也会特殊处理,如果以
&
开头表示要获取FactoryBean本身,否则表示要获取其产品 - 针对单例对象会检查一级、二级、三级缓存
- singletonFactories:三级缓存,存放单例工厂对象
- earlySingletonObjects:二级缓存,存放单例工厂的产品对象
- 如果发生循环依赖,产品是代理;无循环依赖,产品是原始对象
- singletonObjects:一级缓存,存放单例成品对象
二、处理父子容器
- 如果当前容器根据名字找不到这个bean,此时若父容器存在,则执行父容器的
getBean()
流程 - 父子容器的bean名称可以重复
三、处理dependsOn
- 如果当前bean有通过dependsOn指定了非显式依赖的bean,这一步会提前创建这些dependsOn的bean
- 所谓非显式依赖,指两个bean之间不存在直接依赖关系,但需要控制它们的创建先后顺序
四、选择scope策略
- 对于singleton scope:首先到单例池去获取bean,如果有则直接返回,没有再进入创建流程
- 对于prototype scope:每次都会进入创建流程
- 对于自定义scope,例如request:首先到request域获取bean,若有则直接返回,没有再进入创建流程
五、创建bean
- 创建bean实例
要点 | 总结 |
---|---|
AutowiredAnnotationBeanPostProcessor | ① 优先选择带@Autowired 注解的构造;② 若有唯一的带参构造,也会入选 |
采用默认构造 | 如果上面的后处理器和BeanDefiniation都没找到构造,采用默认构造,即使是私有的 |
- 依赖注入
要点 | 总结 |
---|---|
AutowiredAnnotationBeanPostProcessor | 识别 @Autowired 及@Value 标注的成员,封装为 InjectionMetadata进行依赖注入 |
CommonAnnotationBeanPostProcessor | 识别 @Resource 标注的成员,封装为InjectionMetadata进行依赖注入 |
AUTOWIRE_BY_NAME | 根据成员名字找bean对象,修改mbd的propertyValues,不会考虑简单类型的成员 |
AUTOWIRE_BY_TYPE | 根据成员类型执行resolveDependency找到依赖注入的值,修改mbd 的propertyValues |
applyPropertyValues | 根据mbd的propertyValues进行依赖注入(即xml中<property name ref|value/> ) |
- 初始化
要点 | 总结 |
---|---|
内置Aware接口的装配 | 包括BeanNameAware、BeanFactoryAware等 |
扩展Aware接口的装配 | 由ApplicationContextAwareProcessor解析,执行时机在 postProcessBeforeInitialization |
@PostConstruct |
由CommonAnnotationBeanPostProcessor解析,执行时机在postProcessBeforeInitialization |
InitializingBean | 通过接口回调执行初始化 |
initMethod | 根据BeanDefinition得到的初始化方法执行初始化,即<bean init-method> 或 @Bean(initMethod) |
创建AOP代理 | 由AnnotationAwareAspectJAutoProxyCreator创建,执行时机在postProcessAfterInitialization |
- 注册可销毁bean
- 判断并登记可销毁bean
- 判断依据
- 如果实现了DisposableBean或AutoCloseable接口,则为可销毁bean
- 如果自定义了destroyMethod,则为可销毁 bean
- 如果采用
@Bean
没有指定destroyMethod,则采用自动推断方式获取销毁方法名(close,shutdown) - 如果有
@PreDestroy
标注的方法,则执行该销毁方法
- 存储位置
- singleton scope的可销毁bean存储于beanFactory的成员中
- 自定义scope的可销毁bean存储于对应的域对象中
- prototype scope不会存储,需自己找到此对象销毁
- 存储时都会封装为DisposableBeanAdapter类型对销毁方法的调用进行适配
六、类型转换处理
- 如果
getBean()
的requiredType参数与实际得到的对象类型不同,会尝试进行类型转换
七、销毁bean
- 销毁时机
- singleton bean的销毁在ApplicationContext.close时,此时会找到所有DisposableBean的名字,逐一销毁
- 自定义scope bean的销毁在作用域对象生命周期结束时
- prototype bean的销毁可以通过自己手动调用
AutowireCapableBeanFactory.destroyBean()
方法执行销毁
- 同一bean中不同形式销毁方法的调用次序
- 优先后处理器销毁,即
@PreDestroy
- 其次DisposableBean接口销毁
- 最后destroyMethod销毁(包括自定义名称、推断名称、AutoCloseable接口多选一)
- 优先后处理器销毁,即
1.2.4 Bean线程安全
当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这些线程会并发执行该请求对应的业务逻辑(成员方法),如果该处理逻辑中有对该单列状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。
Spring框架并没有对单例bean进行任何多线程的封装处理,关于单例bean的线程安全和并发问题需要开发者自行解决。
Spring bean并没有可变的状态(比如Service类和DAO类),所以某种程度上Spring的单例bean是线程安全的。
但如果bean有多种状态(如View Model对象),就需要自行保证线程安全。最浅显的解决办法为将多态bean的作用域由singleton变更为prototype。
1.3 常用的Spring注解
@Component
:用于服务类。@Service
@Repository
@Controller
@Autowired
:用于在bean中自动装配依赖项。通过类型来实现自动注入bean。和@Qualifier
配合使用可实现根据name注入bean。@Qualifier
:和@Autowired
配合使用,在同一类型的bean有多个的情况下可以实现根据name注入的需求。@Scope
:用于配置bean的范围。@Configuration
、@ComponentScan
、@Bean
:用于基于 java 的配置。@Aspect
、@Before
、@After
、@Around
、@Pointcut
:用于切面编程 (AOP)
1.4 事务
1.4.1 Spring事务管理
Spring支持编程式事务管理和声明式事务管理两种方式。
- 编程式事务管理:需要使用TransactionTemplate来进行实现,这种方式实现对业务代码有侵入性,因此在项目中很少使用。
- 声明式事务管理:建立在AOP之上。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,即目标方法开始前加入一个事务,在执行完目标方法后根据执行情况提交或回滚事务。
声明式事务最大的优点是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过@Transactional
便可将事务规则应用到业务逻辑中。
1.4.2 事务失效的场景
- 因为Spring事务基于代理实现,所以某个添加了
@Transactional
的⽅法仅当被代理对象调⽤时,该注解才会⽣效。如果使用的是目标对象调用,则@Transactional
会失效 - 若某个⽅法为private,则
@Transactional
也会失效。因为底层cglib基于⽗⼦类实现,⼦类不能重载⽗类的private⽅法,所以⽆法很好地利⽤代理导致注解失效。 - 如果在业务中对异常进行了捕获处理,出现异常后Spring框架无法感知到异常,
@Transactional
也会失效。因为@Transactional
中的属性Rollback配置的默认异常为runTimeException,设置为Exception即可避免失效。
1.4.3 事务传播行为
- PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务;如果当前存在事务,就加入该事务。该设置是最常用的设置。
- PROPAGATION_SUPPORTS:支持当前事务。如果当前存在事务,就加入该事务;如果当前不存在事务,就以非事务执行。
- PROPAGATION_MANDATORY:支持当前事务(必须有事务)。如果当前存在事务,就加入该事务;如果当前不存在事务,则抛出异常。
- PROPAGATION_REQUIRES_NEW:创建新事务。无论当前是否存在事务,都创建新事务。
- PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作。如果当前存在事务,就把当前事务挂起。
- PROPAGATION_NEVER:以非事务方式执行。如果当前存在事务,则抛出异常。
- PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。
1.5 AOP
AOP(面向切面编程)作为面向对象的一种补充,用于将那些与业务无关,却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块称为切面(Aspect)。可减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。
在实际项目中自己写AOP的场景其实很少,但很多框架的功能底层都是AOP,例:
- 统一日志处理
- 需求:是谁、在什么时间、修改了数据(修改之前和修改之后)、删除了什么数据、新增了什么数据
- 要求:方法命名要规范
- 实现步骤:
- 定义切面类
- 使用环绕通知,根据方法名和参数记录到表中
- Spring内置的事务处理
1.6 JDK动态代理、CGLIB
Spring中AOP的底层基于动态代理来实现。常见的动态代理技术有两种:JDK动态代理、CGLIB
- JDK动态代理只能对实现了接口的类生成代理,而不能针对类
- CGLIB针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法进行增强,但是因为采用的是继承,所以该类或方法不应声明为final(对于final类或方法无法进行继承)
Spring选用JDK或CGLIB的原则:
- 当bean实现接口时,会用JDK代理模式
- 当bean没有实现接口,会用CGLIB实现
- 可以强制使用CGLIB:在SpringBoot项目配置注解
@EnableAspectJAutoProxy(proxyTargetClass = true)
如下图所示
1.7 循环依赖
定义:A依赖B的同时,B依赖A。在创建A对象的同时需要使用的B对象,在创建B对象的同时需要使用到A对象。
如下所示
@Component
public class A {
private B b;
public A(){
System.out.println("A的构造方法执行了...");
}
@Autowired
public void setB(B b) {
this.b = b;
System.out.println("给A注入B");
}
}
@Component
public class B {
private A a;
public B(){
System.out.println("B的构造方法执行了...");
}
@Autowired
public void setA(A a) {
this.a = a;
System.out.println("给B注入了A");
}
}
对象的创建过程会产生死循环,如下所示:
Spring框架可以自行解决大部分循环依赖,但仍有一部分循环依赖需手动进行解决,如构造方法的循环依赖
如下所示
@Component
public class A {
// B成员变量
private B b;
public A(B b){
System.out.println("A的构造方法执行了...");
this.b = b ;
}
}
@Component
public class B {
// A成员变量
private A a;
public B(A a){
System.out.println("B的构造方法执行了...");
this.a = a ;
}
}
/* main方法程序 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
@Autowired
private A a ;
@Test
public void testTransfer() throws Exception {
System.out.println(a);
}
}
解决方案:使用 @Lazy
注解。添加注解后就不会真正地注入真实对象,该注入对象会被延迟加载,此时注入的是一个代理对象。
@Component
public class A {
// B成员变量
private B b;
public A(@Lazy B b){
System.out.println("A的构造方法执行了...");
this.b = b ;
}
}
1.8 三级缓存
Spring通过三级缓存来解决循环依赖,如下所示
缓存 | 源码名称 | 作用 |
---|---|---|
一级缓存 | singletonObjects | 单例池。缓存已经经历了完整声明周期、已经初始化完成的bean对象 |
二级缓存 | earlySingletonObjects | 缓存早期的bean对象(生命周期还未走完) |
三级缓存 | singletonFactories | 缓存ObjectFactory(对象工厂,用于创建某个对象) |
1.8.1 singletonObjects
一级缓存singletonObjects用于实现singleton scope,如下图所示
一级缓存解决不了循环依赖问题,会出现下图所示的循环:
1.8.2 earlySingletonObjects
二级缓存earlySingletonObjects的作用:充当中间人的作用,打破上述的循环。
步骤:
- 实例化A得到A的原始对象
- 将A的原始对象存储到二级缓存(earlySingletonObjects)中
- 需要注入B。B对象在一级缓存中不存在,此时实例化B,得到原始对象B
- 将B的原始对象存储到二级缓存中
- 需要注入A。从二级缓存中获取A的原始对象
- B对象创建成功
- 将B对象加入到一级缓存中
- 将B注入给A,A创建成功
- 将A对象添加到一级缓存中
1.8.3 singletonFactories
上述场景引发的问题:如果A的原始对象注入给B属性a之后,A的原始对象进行AOP产生了一个代理对象,此时对于A而言,其Bean对象应是AOP之后的代理对象,但B的属性a对应的并不是AOP之后的代理对象。因此产生了冲突,即最终单例池中存放的A对象(代理对象)和B依赖的A对象不是同一个。
三级缓存singletonFactories可用于解决上述问题。其存储的是某个beanName对应的ObjectFactory,在bean的生命周期中,生成完原始对象后就会构造一个ObjectFactory存入singletonFactories中,后期其他的Bean可以通过调用该ObjectFactory对象的getObject方法获取对应的Bean。
整体的解决循环依赖问题的思路如下所示:
步骤:
- 实例化A,得到原始对象A,并且同时生成一个原始对象A对应的ObjectFactory对象
- 将ObjectFactory对象存储到三级缓存中
- 需要注入B。发现B对象在一级缓存和二级缓存都不存在,并且三级缓存中也不存在B对象所对应的ObjectFactory对象
- 实例化B,得到原始对象B,并且同时生成一个原始对象B对应的ObjectFactory对象,然后将该ObjectFactory对象也存储到三级缓存中
- 需要注入A。发现A对象在一级缓存和二级缓存都不存在,但在三级缓存中存在A对象所对应的ObjectFactory对象
- 通过A对象所对应的ObjectFactory对象创建A对象的代理对象
- 将A对象的代理对象存储到二级缓存中
- 将A对象的代理对象注入给B,B对象执行后面的生命周期阶段,最终B对象创建成功
- 将B对象存储到一级缓存中
- 将B对象注入给A,A对象执行后面的生命周期阶段,最终A对象创建成功,将二级缓存的A的代理对象存储到一级缓存中
注意:
1、后面的生命周期阶段会按照本身的逻辑进行AOP, 在进行AOP之前会判断是否已经进行了AOP,如果已经进行了AOP就不会进行AOP操作了。
2、singletonFactories : 缓存的是一个ObjectFactory,主要用来去生成原始对象进行了AOP之后得到的代理对象,在每个Bean的生成过程中,都会提前暴露一个工厂,这个工厂可能用到,也可能用不到,如果没有出现循环依赖依赖本bean,那么这个工厂无用,本bean按照自己的生命周期执行,执行完后直接把本bean放入singletonObjects中即可,如果出现了循环依赖依赖了本bean,则另外那个bean执行ObjectFactory提交得到一个AOP之后的代理对象(如果没有AOP,则直接得到一个原始对象)。
- 只有一级缓存和三级缓存是否可行
【答】不行,每次从三级缓存中拿到ObjectFactory对象执行getObject()
方法会产生新的代理对象,因为A是单例的,所有这里要借助二级缓存来解决这个问题,将产生的对象放到二级缓存中去。后续操作时从二级缓存中拿,没必要再执行一遍objectFactory.getObject()
方法再产生一个新的代理对象,由此保证始终只有一个代理对象。
总结:如果没有AOP确实可以两级缓存就解决循环依赖的问题,如果加上AOP两级缓存是无法解决的,因为会使得每次执行objectFactory.getObject()
方法都产生一个新的代理对象,所以还要借助另一个缓存来保存产生的代理对象。
2 SpringMVC
2.1 执行流程
- 用户发送出请求到前端控制器DispatcherServlet
- DispatcherServlet收到请求调用HandlerMapping(处理器映射器)
- HandlerMapping找到具体的处理器(可查找xml配置或注解配置),生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet
- DispatcherServlet调用HandlerAdapter(处理器适配器)
- HandlerAdapter经过适配调用具体的处理器(Handler/Controller)
- Controller执行完成返回ModelAndView对象
- HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet。
- DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)
- ViewReslover解析后返回具体View(视图)
- DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)
- DispatcherServlet响应用户
2.2 常用的Spring MVC注解
@RequestMapping
:用于映射请求路径。可以定义在类上和方法上,用于类上则表示类中的所有的方法都是以该地址作为父路径@RequestBody
:注解实现接收http请求的json数据,将json转换为java对象@RequestParam
:指定请求参数的名称@PathViriable
:从请求路径下中获取请求参数传给方法(形如/user/{id}
)@ResponseBody
:注解实现将controller方法返回对象转化为json对象响应给客户端@RequestHeader
:获取指定的请求头数据
2.3 拦截器和过滤器
- 拦截器(Interceptor):依赖于SpringMVC框架(web框架),为面向切面编程(AOP)的运用。
- 过滤器(Filter):依赖于Servlet容器,基于函数回调,可以对几乎所有请求进行过滤
2.4 全局异常处理器
可以直接使用Spring MVC中的全局异常处理器对异常进行统一处理,此时Contoller方法只需要编写业务逻辑代码,无需考虑异常处理代码。
需要使用到两个注解:@Controlleradvice
、@ ExceptionHandler
3 MyBatis
3.1 执行流程
- 读取 MyBatis 配置文件:mybatis-config.xml为MyBatis的全局配置文件,配置了MyBatis的运行环境等信息,例如数据库连接信息。
- 加载映射文件。即SQL映射文件,该文件配置了操作数据库的SQL语句,需要在MyBatis配置文件mybatis-config.xml中加载。
- mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。
- 构造会话工厂:通过MyBatis的环境等配置信息构建会话工厂SqlSessionFactory。
- 创建会话对象:由会话工厂创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。
- Executor执行器:MyBatis底层定义了一个Executor接口来操作数据库,其将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。
- MappedStatement对象:在Executor接口的执行方法中有一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。
- 输入参数映射:输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。
- 输出结果映射:输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输出结果映射过程类似于JDBC对结果集的解析过程。
3.2 动态SQL
Mybatis的动态SQL允许在Xml映射文件内以标签的形式编写动态SQL语句,完成逻辑判断和动态拼接sql的功能。
共有9种动态sql标签:<trim>
、<where>
、<set>
、<foreach>
、<if>
、<choose>
、<when>
、<otherwise>
、<bind>
执行原理:使用OGNL从SQL参数对象中计算表达式的值,根据表达式的值动态拼接SQL,以此来完成动态SQL的功能。
3.3 #{}和${}的区别
#{}
是预编译处理,${}
是字符串替换- Mybatis在处理
#{}
时,会将其替换为?
号,调用PreparedStatement的set方法来赋值 - Mybatis在处理
${}
时,会直接将${}
替换成变量的值。 - 使用
#{}
可以有效的防止SQL注入,提高系统安全性。
3.4 获取生成的主键
使用<insert>
标签中的useGeneratedKeys和keyProperty属性来获取生成的主键
- useGeneratedKeys:是否获取自动增长的主键值,true表示获取
- keyProperty:指定将获取的主键值封装到哪个属性里
如下所示:
<insert id = "saveUser" useGeneratedKeys = "true" keyProperty="id">
insert into tb_user(user_name, password, gender, addr)
values (#{username}, #{password}, #{gender}, #{addr})
</insert>
3.5 属性名和字段名不一致问题
当实体类中的属性名和表中的字段名不一致时,有以下3种解决方法
- 在查询的SQL语句中定义字段名的别名,让字段名的别名和实体类的属性名一致
<select id="getOrder" parameterType="int" resultType="com.heima.pojo.Order">
select order_id id,order_no orderno,order_price price from orders
</select>
- 通过
<resultMap>
来映射字段名和实体类属性名的一一对应的关系
<select id="getOrder" parameterType="int" resultMap = "orderResultMap">
select order_id id,order_no orderno,order_price price from orders
</select>
<resultMap type="com.heima.pojo.Order" id="orderResultMap">
<!-- 用id属性来映射主键字段 -->
<id property="id" column="order_id">
<!-- 用result属性来映射非主键字段,property为实体类属性名,column为数据库表中的属性 -->
<result property ="orderno" column="order_no" />
<result property ="price" column="order_price" />
</resultMap>
- 开启MyBatis驼峰命名自动匹配映射
<settings>
<setting name="mapUnderscoreToCamelCase" value="true" /> <!-- 开启驼峰命名自动映射 -->
</settings>
3.6 多表查询
MyBatis有以下2种方式实现多表查询:
- 编写多表关联查询的SQL语句,使用
<resultMap>
建立ResultMap表结果集映射,其子标签有<association>
和<collection>
<resultMap id="Account_User_Map" type="com.heima.entity.Account">
<id property="id" column="id"></id>
<result property="uid" column="uid"></result>
<result property="money" column="money"></result>
<association property="user">
<id property="id" column="uid"></id>
<result property="username" column="username"></result>
<result property="birthday" column="birthday"></result>
<result property="sex" column="sex"></result>
<result property="address" column="address"></result>
</association>
</resultMap>
<!--public Account findByIdWithUser(Integer id);-->
<select id="findByIdWithUser" resultMap="Account_User_Map">
select a.*, username, birthday, sex, address from account a, user u where a.UID = u.id and a.ID = #{id} ;
</select>
- 将多表查询分解为多个单表查询:使用ResultMap表的子标签
<association>
和<collection>
标签的select属性指定另外一条SQL的定义去执行, 然后执行结果会被自动封装
<resultMap id="Account_User_Map" type="com.heima.entity.Account" autoMapping="true">
<id property="id" column="id"></id>
<association property="user" select="com.heima.dao.UserDao.findById" column="uid" fetchType="lazy"></association>
</resultMap>
<!--public Account findByIdWithUser(Integer id);-->
<select id="findByIdWithUser" resultMap="Account_User_Map">
select * from account where id = #{id}
</select>
3.7 延迟加载
MyBatis仅支持association关联对象和collection关联集合对象的延迟加载,association指一对一查询,collection指一对多查询。在MyBatis配置文件中可以配置lazyLoadingEnabled
是否启用延迟加载(默认关闭)。
<settings>
<setting name = "lazyLoadingEnabled" value = "true"> <!-- 开启延迟加载 -->
</settings>
实现原理:使用CGLIB创建目标对象的代理对象,调用目标方法时进入拦截器方法。例如调用order.getUser().getUserName()
时,拦截器invoke()
方法发现order.getUser()
是null,则会单独发送事先保存好的查询关联User对象的SQL来查询User,随后调用order.setUser(user)
,使得order的对象user属性非空,由此完成order.getUser().getUserName()
方法的调用。
3.8 批量插入
MyBatis实现批量插入数据的步骤:
- MyBatis的接口方法参数需要定义为集合类型
List<User>
public abstract void saveUsers(List<User> users);
- 在映射文件中通过
<forEach>
标签遍历集合,获取每一个元素作为<insert>
语句的参数值
<!-- 批量插入用户 -->
<insert id="savaUsers" parameterType="java.util.List">
insert into tb_user(user_name,password)
values
<foreach collection="list",item="user",index="index",separator=",">
(#{user.userName}, #{user.password})
</foreach>
</insert>
3.9 MyBatis缓存
3.9.1 一级缓存
基于PerpetualCache的HashMap本地缓存(Local Cache)。其存储作用域为Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存。
使用同一个sqlSession对象获取两次UserMapper对象,进行两次用户数据的查询。控制台的输出结果如下所示:
只执行了一次SQL语句,说明第二次查询的时候使用了缓存数据。
3.9.2 二级缓存
基于namespace和mapper的作用域,不依赖于SQL session,默认也采用PerpetualCache,HashMap存储。
默认情况下二级缓存没有开启。开启二级缓存的方法如下所示:
- 全局配置文件
<settings>
<setting name="cacheEnabled" value="true"/> <!-- 开启二级缓存 -->
</settings>
- 映射文件中,使用
<cache/>
标签让当前mapper生效二级缓存
<mapper namespace="com.itheima.mapper.UserMapper">
<cache/> <!-- 二级缓存生效 -->
<select id="selectAll" resultType="user">
select *
from tb_user;
</select>
<select id="selectById" resultType="user">
select *
from tb_user where id = #{id};
</select>
</mapper>
运行程序进行测试,控制台输出结果如下所示:
只进行了一次查询,说明数据进入到了二级缓存中。
- 对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有
<select>
中的缓存将被清除。
注意:
- 被二级缓存的数据需实现Serializable接口
- 只有会话提交或者关闭以后,一级缓存中的数据才会转移到二级缓存中
- 可自定义存储源,如Ehcache。
4 其他
- POJO(Plain Ordinary Java Object) :简单java对象。POJO是最常见最多变的对象,是一个中间对象(POJO持久化后即为PO,直接用它传递、传递过程中即为DTO,直接用来对应表示层即为VO)。
- VO(View Object):视图对象,主要对应界面显示的数据对象。其作用为把某个指定页面(或组件)的所有数据封装起来。若一个DTO对应一个VO,则DTO等价于VO;但若一个DTO对应多个VO,则展示层需将VO转换为服务层对应方法所要求的DTO后传送给服务层,从而达到服务层与展示层解耦的效果。对于一个WEB页面或SWT、SWING的界面,可用一个VO对象对应整个界面的值。
- BO(Business Object):业务对象,主要用于将业务逻辑封装为一个对象。该对象可包括一个或多个其他对象。例如一个简历(包含教育经历、工作经历、社会关系等),可将教育经历、工作经历、社会关系分别对应一个PO,建立一个对应简历的BO对象处理简历,每个BO包含这些PO,由此即可针对BO处理业务逻辑。BO包括包含方法的实体对象(Entity Object)和不包含方法的值对象(VO)。
- DTO(Data Transfer Object):数据传输对象,主要用于远程调用等需要大量传输对象的地方。例如一张表有100个字段,那么对应的PO就有100个属性,但是界面上只需显示10个字段,客户端用WEB service来获取数据,没必要将整个PO对象传递到客户端,此时即可用只有这10个属性的DTO来传递结果到客户端,这样也不会暴露服务端表结构。到达客户端以后,若用该对象来对应界面显示,则此时其身份就转为VO。(此处泛指用于展示层与服务层之间的数据传输对象)
- DO(Domain Object):领域对象,从现实世界中抽象出来的有形或无形的业务实体,用来接收数据库对应的实体,是一种抽象化的数据状态。介于数据库与业务逻辑之间,一般在业务逻辑层(Service)对数据库(SQL) 进行访问时用于接收数据。常命名为xxxDO,其中xxx为数据表名。DO是Entity的一种,是与数据库存在某种映射关系的Entity。
- PO(Persistent Object):持久化对象,跟持久层(常为关系型数据库)的数据结构形成一一对应的映射关系,若持久层为关系型数据库,则数据表中的每个字段(或若干个)对应PO的一个(或若干个)属性。即一个PO就是数据库中的一条记录,由此可以把一条记录作为一个对象处理,且可以方便的转为其他对象。