Spring IoC概述
Spring IoC阐述
- 控制反转是一种通过描述(在Java中可以是XML或者注解)并通过第三方去产生或获得特定对象的方式。
- 主动创建对象,责任归于开发者;被动创建对象,责任归于IoC容器。
- 降低了开发难度,对模块解耦,有利于测试。
Spring IoC容器
Spring IoC容器的设计
-
主要基于BeanFactory和ApplicationContext两个接口,前者是最底层接口,后者是前者的子接口,在之前的功能上做了许多扩展。
-
初始化容器,并获得特定对象
ApplicationContext context = new ClassPathXmlApplicationContext("ApplicationContext.xml"); Person2 p2 = (Person2) context.getBean("p2"); p2.say();Spring IoC容器的初始化和依赖注入
Spring IoC容器的初始化和依赖注入
- Bean的初始化和依赖注入在Spring IoC容器中是两大步骤,它是在初始化后,才进行依赖注入。
Bean的初始化步骤:
- Resource定位,定位配置文件。
- BeanDefinition的载入,加载配置文件生成对应的POJO实例。
- BeanDefinition的注册,将POJO实例放入IoC容器中。
- 完成以上3步,Bean在IoC容器中得到了初始化,但没有完成依赖注入,所以Bean还不能完全使用。
- SpringBean有一个配置选项:lazy-init,表示是否初始化SpringBean,默认值为default,实际值是false,IoC会自动初始化Bean。如果值为true,当使用getBean()方法时,Bean才初始化。
Spring Bean的生命周期
- Spring IoC容器本质是管理Bean
- BeanPostProcessor接口的方法针对每一个Bean
- DisposableBean接口的方法针对IoC容器,使用destory方法执行销毁
- BeanNameAware,BeanFactoryAware,ApplicationContextAware,InitializingBean接口的方法,和Bean自定义的初始化、销毁方法是针对单个Bean
装配Spring Bean
依赖注入的3种方式
- 构造器注入,使用constructor-arg标签
- setter注入,使用property标签
- 接口注入,获取外界资源时使用
装配Bean概述
- 装配Bean就是将开发Bean放入IoC容器中
- 在XML中显示配置,当使用第三方类库时,因为无法修改里面的代码,所以使用XML方式配置
- 在Java的接口和类中实现配置,能避免XML配置泛滥
- 隐式Bean的发现机制和自动装配原则,基于约定优于配置的原则,推荐使用该方式,能减少程序开发者的决定权,简单又灵活
通过XML配置装配Bean
- 装配简易值:使用bean标签定义一个Bean,id属性为该Bean的编号,不是必须的属性。如果没有,Spring将采用“全限定名#{number}”格式生成编号。因为自动生成的编号较为繁琐,因此都会定义id。class属性为全限定名,通过反射生成该类的Class对象。property标签定义类的属性,name属性定义名称,value属性定义值或使用ref属性,引用其他的Bean。
- 装配集合:List属性使用list标签,通过value标签设值;Map属性使用map标签,通过entry标签设值,使用key和value属性设值;Properties属性使用props标签,通过prop元素设值,key属性设值;Set属性使用set标签,通过value标签设值;Array属性使用array标签,通过value标签设值;当集合使用的是类对象时,使用ref替换value。
- 命名空间配置:需要引入对应的命名空间和XML模式(XSD)文件。
通过注解装配Bean
- Spring中,提供两种方式让IoC容器发现Bean;1.组件扫描;2.自动装配
- 使用@Component装配Bean,IoC会把这个类扫描生成Bean实例,value属性代表这个类的Bean的id,如果不写,会默认为是首字母小写的类名。
- @Value代表值的注入。
- @ComponentScan:注解配置类,代表进行扫描,默认是扫描当前包的路径,POJO报名和它保持一致才能扫描,否则没有。存在两个配置项,basePackages扫描到包,basePackageClasses扫描到类。采用多个@ComponentScan去定义对应的包时,配置的Bean会生成多个实例。因为basePackages可读性更好,优先使用。
- @Autowired:用在属性、参数上,自动装配,Spring先完成Bean的定义和生成,然后在寻找对应的资源。Spring会根据类型去寻找定义的Bean然后将其注入。当某个接口下有多个实现类时,会产生自动装配的歧义性,不知道装配哪个类,抛出BeansException。
- @Primary:注解在类上,自动装配时,让Spring优先注入该类。只能解决首要性问题,并不能解决选择性问题。
- @Qualifier:用在属性上,自动装配时是按名称注入。
- @Bean:用在方法上,将方法返回的对象作为Bean放入IoC容器。可使用initMethod和destroyMethod配置项定义Bean的初始化和销毁方法。
- 装配的混合使用:对于自己开发的类,使用注解;对于第三方包或者服务的类,使用XML方式。
- @ImportResource:注解在配置类上,用于引入配置文件,该注解内容是一个数组,因此可以配置多个XML文件。
- @Import:注解在配置类上,用于注入其他的配置类,因为所有配置都放在一个配置类里面会造成配置复杂,内容是数组,可以注入多个配置类。
- 在XML文件中,可以使用import标签引入其他XML文件;可以使用context:component-scan标签代替@ComponentScan功能;暂时不支持使用XML加载Java配置类。
使用Profile
- @Profile:注解在数据源上,内容设为“dev”代表用于开发,“test”代表用于测试。
- 使用XML定义Profile:在XML中beans根标签使用profile=“dev“属性,会使整个配置文件的Bean都放在dev的Profile下。因此可在XML文件中使用多个beans根标签+profile属性来配置多个Profile。
- 启动Profile:使用**@ActiveProfiles(”?“)**,注解在类上,来指定加载哪个Profile。
加载属性(properties)文件
-
使用属性文件可有效减少硬编码,修改环境时只需修改配置文件即可,提高操作遍历性。
-
@PropertySource,注解在配置类上,用于加载配置文件,配置项:name,配置这次属性配置的名称;value,字符数组,配置多个属性文件;ignoreResourceNotFound,boolean值,当找不到配置文件时是否抛出异常,默认为false;encoding,编码,默认为”“。使用:
String url = context.getEnvironment().getProperty("jdbc.database.url");
只能获得对应的配置属性,没有解析属性占位符的能力。
-
Spring推荐使用PropertySourcesPlaceholderConfigurer属性文件解析类去解析对应的属性文件,并可通过占位符引用对应的配置。占位符引用${jdbc.database.url}
-
使用XML方式加载属性文件:
<context:property-placeholder ignore-resource-not-found="true" location="classpath:database-config.properties, XXX-config.properties"/>
条件化装配Bean
-
@Condition:注解在方法上,当属性文件不存在的时候,就不需要装配Bean。
@Condition({DataSourceCondition.class}) public DataSource getDataSource() { }
注解中的类需要实现Condition接口。
Bean的作用域
- 有4种作用域:1.单例,默认选项,整个应用中,只有一个Bean实例。2.原型,每次从IoC容器获取Bean时,Spring都会创建一个实例。3.会话,在Web应用中使用,会话过程中只创建一个实例。4.请求,在Web应用中使用,一次请求创建一个实例,不同的请求会创建不同的实例。
- @Scope:注解在类上,表示该Bean的作用域为单例。
使用Spring表达式(Spring EL)
-
Spring EL比上述的注入方式更加强大。
-
属性文件的读取中使用的是“$”,Spring EL使用“#”读取。
@Value("#{1}") private Long id; @Value("#{'role_name_1'}") private String roleName; @Value("#{role.id}") private Long roleId; @Value("#{role.getNote?.toString()}")//问号含义是判断是否返回为null,如果不是就调用后面方法 private String note;
-
使用类的静态常量和方法
@Value("#{T(Math).PI}") private double pi; @Value("#{T(Math).random()}") private double random;
-
Spring EL运算
@Value("#{role.id + 1}") private int num; @Value("#{role.roleName + role.note}") private String str; @Value("#{role.id == 1}") private boolean flag; @Value("#{role.id > 1 ? 5 : 1}") private int max;
面向切面编程
面向切面编程的术语
- 切面:相当于一个拦截器类
- 通知:切面开启后,切面中的方法
- 引入:引入允许我们在现有的类中添加自定义的类和方法
- 切点:被切面拦截的方法就是一个切点
- 连接点:是一个判断条件,可指定哪些方法是切点
- 织入:生成代理对象的过程
使用@AspectJ注解开发Spring AOP
-
Spring只能支持方法拦截的AOP
-
@Aspect:注解在类上,表示该类为一个切面类
-
@Before:前置通知,@After:后置通知,@Around:环绕通知,强大并灵活,但可控性不强,在不需要大量改变业务逻辑时,不需要使用。@AfterReturning,返回通知,@AfterThrowing:异常通知。都是注解在方法上。
-
连接点:Spring通过正则表达式判断需要拦截的方法
@Before("execution(* com.ssm.RoleService.printRole(..))")
execution:代表执行方法的时候会触发。
**:代表任意返回类型。
com.ssm.RoleService:代表类的全限定名。
printRole:被拦截方法的名称。
(…):代表任意参数。
-
@Pointcut:注解在方法上,定义为切点,可以减少正则表达式的重复书写
@Pointcut("execution(* com.ssm.RoleService.printRole(..))") public void print(){} @Before("print()") public void before(){}
-
测试AOP:在配置类上使用**@EnableAspectJAutoProxy**开启AspectJ框架的自动代理,然后通过@Bean得到一个被@Aspect注解的切面类对象。
@Configuration @EnableAspectJAutoProxy @ComponentScan("com.ssm.aop") public class AopConfig { @Bean public RoleAspect getAspect() { return new RoleAspect(); } }
在XML文件定义切面
<!-- 需要导入aop的命名空间 --> <aop:aspectj-autoproxy/>//等同于@EnableAspectJAutoProxy
-
给通知传递参数
public void printRole(Role role, int sort) { } //将切点方法中的参数role和sort通过args属性传递给了通知方法 @Before("execution(* com.ssm.RoleService.printRole(..))" + "&& args(role, sort)" ) public void before(Role role, int sort){ }
-
引入:在切面中引入其他类,调用引入类方法,对原方法进行增强操作
public interface RoleVerifier { public boolean verify(Role role); } //使用verify方法检测对象是否为空 public class RoleVerifierImpl implements RoleVerifier { public boolean verify(Role role) { return role != null; } } @DeclareParents(value = "com.ssm.impl.RoleServiceImpl+", defaultImpl = "RoleVerifierImpl.class") public RoleVerifier roleVerifier;
@DeclareParents:注解在属性上,表示引入的增强类对象
value = “com.ssm.impl.RoleServiceImpl+”:表示对RoleServiceImpl类进行增强,相当于为RoleServiceImpl类引入另一个新的接口,该接口为增强类的接口(RoleVerifier)。
defaultImpl = “RoleVerifierImpl.class”:指定增强类接口的实现类,这里RoleVerifierImpl做为RoleVerifier接口的实现类。
具体使用:
//得到被代理类对象 RoleService roleService = context.getBean("RoleService.class"); //将被代理对象转换为增强类对象,因为RoleServiceImpl实现了两个接口,所以可以相互转换 RoleVerifier roleVerifier = (RoleVerifier)roleService; //使用增强类方法,判断角色是否为空 if (roleVerifier.verify(role)) { //再执行切面拦截方法 roleService.printRole(role); }
使用XML配置开发Spring AOP
-
通过XML配置切面以及多个通知
<!-- 不定义切点时,表达式需要多次书写 --> <aop:config> <!-- 引用切面 --> <aop:aspect ref="xmlAspect"> <!-- 定义前置通知 --> <aop:before method="before" pointcut="execution(* com.ssm.RoleService.printRole(..))"/> <!-- 其他通知的定义上同 --> </aop:aspect> </aop:config>
<!-- 定义切点时 --> <aop:config> <!-- 引用切面 --> <aop:aspect ref="xmlAspect"> <aop:pointcut id="printRole" expression="execution(* com.ssm.RoleService.printRole(..))"/> <!-- 定义前置通知,引入切点 --> <aop:before method="before" pointcut-ref="printRole"/> <!-- 其他通知的定义上同 --> </aop:aspect> </aop:config>
-
传递参数
<!-- 给通知传递参数 --> <aop:pointcut id="printRole" expression="execution(* com.ssm.RoleService.printRole(..)) and args(role)"/>
-
引入自定义的类或方法
<aop:declare-parents types-matching="com.ssm.impl.RoleServiceImpl+" implement-interface="com.ssm.RoleVerifier" default-impl="com.ssm.RoleVerifierImpl"/>
多个切面
-
当使用多个切面拦截同一个方法时,切面的通知执行顺序是无序的。
-
@Order(n):注解在切面类上,可以使多个切面按照n从小到大按顺序执行通知。
@Aspect //@Order(1) 实现了Ordered接口,代替了@Order注解 public class Aspect1 implements Ordered { @Override public int getOrder() { return 1; } }
-
在XML定义顺序
<aop:aspect ref="aspect1" order="1"> ... </aop:aspect>
小结
- AOP是Spring两大核心之一,通过AOP可以把一些比较公共的方法抽取出来,减少开发者的工作量。
Spring和数据库编程
传统的JDBC代码的弊端
- 即使是执行一条简单的SQL,也需要有许多的操作,而且包含大量的try…catch…finally…语句,造成代码泛滥,可读性下降。
配置数据资源
- 使用简单数据库配置:Spring提供的类是SimpleDriverDataSource,只是一个很简单的数据库连接的应用,不支持数据库连接池,一般只用于测试。
- 使用第三方数据库连接池:比如DBCP数据库连接池。
jdbcTemplate
- 是Spring为解决传统JDBC代码的弊端,所提供的解决方案,但不算成功。
MyBatis-Spring项目
-
配置SqlSessionFactoryBean,提供SqlSessionFactoryBean去支持SqlSessionFactory的配置,然后产生SqlSession。
引入MyBatis的配置文件,使得SqlSessionFactoryBean的配置不全部依赖于Spring提供的规则,导致配置复杂。
<bean id="SqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:sqlMapConfig.xml"/> </bean>
MyBatis配置文件
<configuration> <!-- 配置设置 --> <settings> <!-- 开启缓存 --> <setting name="cacheEnabled" value="true"/> <!-- 自动生成主键 --> <setting name="useGeneratedKeys" value="true"/> ... </settings> <!-- 别名配置 --> <typeAliases> <typeAlias alias="role" type="com.ssm.Role"/> </typeAliases> <!-- 指定映射器路径 --> <mappers> <mapper resource="com/ssm/mapper/RoleMapper.xml"/> </mappers> </configuration>
-
配置MapperFactoryBean,因为RoleMapper是一个接口,不是类,没有办法产生实例,所以需要对映射器接口进行配置,可以通过配置MapperFactoryBean实现我们想要的Mapper。
<bean id="roleMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"> <!-- 指定被扫描为Mapper的接口类 --> <property name="mapperInterface" value="com.ssm.mapper.RoleMapper"/> <property name="SqlSessionFactory" ref="SqlSessionFactory"/> </bean>
mapperInterface是映射器的接口。
当项目比较大时,一个个配置Mapper会造成配置量大的问题,不利于开发。
-
配置MapperScannerConfiger:通过扫描的形式进行配置Mapper类
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.ssm.mapper"/> <property name="SqlSessionFactoryBeanName" value="sqlSessionFactory"/> <!-- 指定标注才扫描成为Mapper --> <property name="annotationClass" value="org.springframework.stereotype.Repository"/> </bean>
basePackage:指定让Spring自动扫描什么包。
annotationClass:如果类被**@Repository**注解标识了,才会对其进行扫描,注册成为Mapper。
SqlSessionFactoryBeanName:指定在Spring中定义的SqlSessionFactory的Bean的名称。
makerInterface:指定实现了什么接口就认为是Mapper,需要提供一个公共的接口去标记。
@Repository:注解在类上,标志这是一个DAO层,当Spring扫描时,将有该注解的接口扫描为Mapper对象,然后放入Spring IoC容器中。
深入Spring数据库事务管理
Spring数据库事务管理器的设计
-
Spring中数据库事务是通过PlatformTransactionManager这个事务管理器进行管理的,支持事务的TransactionTemplate模板是Spring提供的事务管理器的模板。
-
1.TransactionTemplate中有一个PlatformTransactionManager接口的属性,事务的创建、提交和回滚都是通过PlatformTransactionManager接口来完成。
2.当事务发生异常时,默认所有的异常都会回滚,但可以通过配置去修改某个异常是否需要回滚。
3.当没有异常时,提交事务。
配置事务管理器
-
Mybatis框架使用最多的是DataSourceTransactionManager数据源事务管理器。
-
在XML配置文件中配置事务管理器,需要在XML中加入事务命名空间
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- 注入数据库连接池 --> <property name="dataSource" ref="dataSource"/> </bean>
在事务管理器中注入数据源,表示你将数据库事务委托给该事务管理器管理。
-
Spring中可使用声明式事务和编程式事务,声明式事务又分为XML配置和注解事务,推荐使用注解@Transactional。
-
用Java配置方式实现Spring数据库事务:
需在配置类中实现TransactionManagementConfigurer接口的annotationDrivenTransactionManager方法Spring会把该方法返回的事务管理器作为程序中的事务管理器。
@Configuration @ComponentScan("com.ssm.*") @EnableTransactionManagement public class JavaConfig implements TransactionManagementConfigurer { @Override @Bean(name = "transactionManager") public PlatformTransactionManager annotationDrivenTransactionManager() { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; } }
@EnableTransactionManagement:注解在配置类上,表示使用事务管理器。
声明式事务
-
Transactional的配置项:编程式事务允许的自定义接口–TransactionDefinition,可以又XML或者注解**@Transactional**进行配置。
使用声明式事务需要配置注解驱动:
<tx:annotation-driven transaction-manager="tansactionManager"/>
-
声明式事务的约定流程:
@Transactional注解可使用在类或方法上,IoC容器初始化时,Spring读入这个注解或XML配置的事务信息,并放入事务定义类(TransactionDefinition接口的子类)里面。运行时,Spring拦截带有注解的方法或类里的所有方法,进行事务操作,运用了Spring AOP技术,开发者只需编写业务代码和配置事务属性。
首先Spring通过事务管理器创建事务,将事务定义类中的属性根据配置在事务上设置,然后执行开发者提供的业务代码,最后提交或者回滚事务。
数据库的相关知识
-
数据库事务ACID特性
1.原子性:整个事务的所有操作,要么全部完成,要么全部不完成,不会停留在中间环节,如果发生错误会回滚。
2.一致性:事务必须始终保持系统处于一致的状态。
3.隔离性:两个事务间的隔离程度。
4.持久性:事务完成后,数据持久保存在数据库中,不会回滚。
-
隔离级别:隔离级别在一定程度上可以减少丢失更新
按照SQL的标准规范,把隔离级别分为4层:
1.脏读:允许一个事务去读取另一个事务没提交的数据。
2.读/写提交:一个事务只能读取另一个事务已经提交的数据。消除了脏读的问题。但会造成不可重复读问题。
3.可重复读:针对数据库同一条记录而言,使得同一条数据库记录的读/写按照一个序列化进行操作,不会产生交叉情况,保证了一条数据的一致性。消除了不可重复读问题,造成幻读问题。
4.序列化:让SQL按照顺序读/写的方式,能消除数据库事务间产生数据不一致问题。
选择隔离级别和传播行为
-
选择隔离级别:从脏读到序列化,系统性能会直线下降。
为了提高并发,压制脏读,都会选择读/写操作的方式设置事务。但会带来数据不一致性问题。
当并发量不大或根本不用考虑时,使用序列化隔离级别。
-
传播行为:指方法之间的调用事务策略的问题。一个方法调用另一个方法,可以对事务的特性进行传播配置,成为传播行为。
Spring中的传播行为的类型,通过一个枚举类去定义,该类为:org.springframework.transaction.annotation.Propagation,共有7种传播行为。
REQUIRED:最常用,也是默认传播行为。含义:方法调用时,如果当前不存在事务,就启动事务;如果存在,就沿用下来。
REQUIRES_NEW:无论是否存在当前事务,方法都会在新的事务中运行。
NESTED:嵌套事务,如果调用方法时抛出异常,只回滚自己内部执行的SQL,不会回滚主方法的SQL。
@Transactional的自调用失效问题
-
注解@Transactional的底层实现是Spring AOP技术,使用的是动态代理,这意味这对于静态方法和非public方法,使用@Transactional是失效的。
-
自调用。自己的一个方法调用自己的另一个方法的过程,不会产生代理对象,所以@Transactional会失效。
解决:1.使用两个类存放方法。2.从容器中获取接口的代理对象。
典型错误用法的剖析
- 错误使用Service:当我们想插入两个角色时,我们可能会使用两次对应Service的插入方法,这是很常见的情况,但这样会使两个插入操作不在一个事务当中,而是开启了两个事务。
- 过长时间占用事务:与数据库无关的操作应该放在Controller中。使事务完成相关业务操作后,立即释放数据库事务资源,避免长时间占用数据库事务,导致系统性能下降。
- 错误捕捉异常:对于业务代码中的try…catch,发生异常时,我们应该抛出异常,而不是做其他处理,这样才符合Spring事务管理的约定流程。