Spring

spring是一个容器,它帮助我们管理java对象。它的核心思想是Inversion of Control,意思是把对象的创建,赋值,管理工作都交给代码之外的容器实现,这样做的好处就是解耦合。

那么解耦合的好处又是什么呢?当我们需要对代码的一些部分进行修改时(比如将数据库从mysql换成oracle),只需要修改spring配置文件即可,不光降低了修改的工作量,更为重要的一点是热插拔(改变代码要生效必须重启服务器,而该配置文件不需要)。servlet其实就是IOC的一个体现,它是由Tomcat容器创建。

实现IOC的方式有多种,DI(Dependency Injection)是用的最广泛的一个,spring实现IOC的方式就是DI,它的底层是通过反射来创建对象。

【什么对象应该放到spring容器中,让它帮我们管理?】

dao类,service类,controller类和工具类。

【什么对象不应该放到spring容器中呢?】

实体类对象,因为这些对象来自数据库。另外servlet、listener以及filter这些对象已经放在tomcat容器中了,因此也不需要放入spring容器中。

1. 初步使用

首先创建一个接口以及它的实现类(比如MyService接口,实现类名为MyServiceImple)。

1.1 创建spring配置文件

在resources目录下创建【applicationContext.xml文件】(spring配置文件的规范名称)

配置文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>

<!--beans是根标签,spring把java对象视为一个个的bean-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<!--一个bean代表一个java类,spring通过id找到这个类,class是这个类的全限定名称-->
<bean id="MyService" class="com.aaron.service.impl.MyServiceImple"/>
<!--这里注册完后,相当于spring会帮我们创建【一个】该类的对象(如果要多个该类的对象,就声明多个该类的bean标签),并将其存在spring容器中
即:MyService obj = new MyServiceImple();
然后会将该对象放入一个map中: springMap.put(id, obj), 本例为springMap.put("MyService",obj);
-->

</beans>

1.2 创建容器对象ApplicationContext

1
2
3
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");//从classes文件夹中读取配置文件,创建spring容器,【spring容器创建完毕后,会为所有bean创建对象,并保存在容器中】
MyService myService = (MyService) ac.getBean("MyService"); //获取spring帮我们实例化的实现类对象,getBean的参数为applicationContext.xml中声明的bean的id
myService.speak(); //调用成功

可以调用ac.getBeanDefinitionCount();获取当前spring容器中的对象总数。ac.getBeanDefinitionNames();可以获取spring容器中所有对象的名称数组。

1.3 使用spring创建非自定义类对象

比如想让spring创建一个日期类

  1. 把日期类的bean注册到spring容器

    1
    <bean id="mydate" class="java.util.Date"/>
  2. 获取日期类对象

    1
    2
    ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");//读取配置文件,创建spring容器
    Date d = (Date) ac.getBean("mydate");

1.4 用配置文件给spring创建的对象赋值(需要经常改动的部分用此法)

该方法最大的优点,就是代码和赋值完全是分开的,改代码的值不用重启服务器。

1.4.1 设值set注入

其原理就是Spring调用了对象的set方法给对象的属性赋值,使用该方法的前提是对象有set方法,且【属性名】和set方法的【参数名】一致。

在bean标签中用property标签对属性逐个赋值。

1
2
3
4
<bean id="MyService" class="com.aaron.service.impl.MyServiceImple">
<property name="AttributeName1" value="AttributeValue1"/>
<property name="AttributeName2" value="AttributeValue2"/>
</bean>

【赋值引用类型属性】

  1. 将要赋值的引用类型的bean标签注册beans标签中

  2. 利用ref赋值引用类型属性

1
2
3
4
<bean id="SomeObject" class="com.aaron.service.impl.SomeObject"/>
<bean id="MyService" class="com.aaron.service.impl.MyServiceImple">
<property name="AnObject" ref="SomeObject"/> <!--ref中的参数就是引用类型的bean id-->
</bean>

1.4.2 构造注入

如同它的名字,其原理是spring调用对象的构造方法给对象属性赋值。注意,和设值注入一样,构造方法的参数名要和其对应的属性名保持一致,这样才方便spring通过标签中的name定位到对应的属性。

1
2
3
4
<bean>
<constructor-arg name="AttributeName1" value="AttributeValue1"/> <!--普通属性赋值-->
<constructor-arg name="AttributeName2" ref="AttributeBeanId"/> <!--引用类型属性赋值-->
</bean>

1.4.3 自动注入

对象某一属性类型为引用类型时,可用此法赋值

【byName】

添加一个对应的引用类型的bean,该bean的id与对象中该引用类型的属性名相同,这时使用autowired绑定即可自动给引用类型对象赋值。

1
2
3
<bean id="Student" class="com.aaron.service.impl.MyServiceImple" autowire="byName">
<property name="name" value="xxx"/> <!--基本类型还是要自己赋值,引用类型应用autowire后会自动赋值-->
</bean>

【byType】

添加一个对应的引用类型的bean,该bean的class与引用类型为同源关系时

1
2
3
<bean id="Student" class="com.aaron.service.impl.MyServiceImple" autowire="byType">
<property name="name" value="xxx"/> <!--基本类型还是要自己赋值,引用类型应用autowire后会自动赋值-->
</bean>

同源是指:

  1. 两个类型完全一致
  2. 两个类型为父子关系
  3. 两个类型为实现关系

2. 多配置文件

小项目使用一个spring配置文件够了,但是一旦项目规模上来,spring配置文件的信息太多久会造成读取缓慢。另外一般一个项目是由多人共同开发的,大家一起往一个配置文件读写就会存在冲突问题,因此最好使用多配置文件。

多文件分配方案有多种,我们以一个功能模块一个配置文件为例

  1. 为每一个模块创建一个sprint-xxx.xml配置文件(它们必须位于同一目录),比如spring-module1.xml,spring-module2.xml

  2. 创建一个主配置文件applicationContext.xml,它的作用就是包含其他部分配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <?xml version="1.0" encoding="UTF-8"?>

    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">


    <import resource="classpath:spring-module1.xml"/>
    <import resource="classpath:spring-module2.xml"/>
    <!--或者使用更方便的通配符-->
    <import resource="classpath:spring-*.xml"/>

    </beans>

3. 使用注解创建对象以及属性赋值(不需要经常改动的部分用此法,常用)

【注意】spring版本大于3.2.3才支持java1.8,报错的话要么升级spring版本,要么降低java版本。

  1. 必须先用maven加入spring-aop依赖。
  2. 给类加上不同功能的spring注解
1
2
3
4
5
6
7
@Component(value = "MyServiceImple") //这个value就相当于配置文件中bean的id
public class MyServiceImple implements MyService {
@Override
public void speak() {
System.out.println("hi");
}
}
  1. 在spring配置文件中加入组件扫描器标签,说明注解在项目中的位置
1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="com.aaron.service.impl"/> <!--【注意】打<com再按tap可顺带把xmlns:context引入,否则要手动输入。该配置使得spring会去扫描impl包以及其包下所有类的spring注解,对它们做相应的解析-->

</beans>
  1. 使用,跟之前一样

    1
    2
    3
    ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); 
    MyServiceImple obj = (MyServiceImple) ac.getBean("MyServiceImple");
    obj.speak();

3.1 不同功能的spring注解

  1. @Repository() 用于创建dao层对象
  2. @Service() 用于创建service对象
  3. @Controller() 用于创建controller对象

它们的使用与@Component()相同,不过都有专门针对某层的额外功能。而@Component()是用于除了以上三种情况的其他任何情况。

3.2 属性赋值

3.2.1 基本类型赋值

直接在实现类中,属性的上方利用注解:@value(value = "xxx")即可赋值,此时不需要set方法。

1
2
3
4
5
6
7
8
9
@Component("student")
public class Student {

@Value(value = "lisa")
String name;

@Value(value = "20")
int age;
}

或者也可以在set方法上注解。

3.2.2 引用类型赋值

先要给引用类型的类定义加上注解(代表将它的bean注册到spring容器中),然后在其他类中,属性为该引用类型的上方加上autowired(默认byType)即可。

1
2
3
4
5
6
7
8
9
10
11
12
@Component("student")
public class Student {

@Value(value = "lisa")
String name;

@Value(value = "20")
int age;

@Autowired
IDcard iDcard;
}

要指定byName的话,按如下方式

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component("student")
public class Student {

@Value(value = "lisa")
String name;

@Value(value = "20")
int age;

@Autowired
@Qualifier(value = "beanId") //指定beanId
IDcard iDcard;
}

默认的byType有时会产生歧义,这时加上byName即可解决。

4. AOP

想要给当前已经存在类或者方法增加额外(非主要)功能时,或者要给项目中的多个类或方法增加相同的功能时,用AOP。

面向切面编程(Aspect Oriented Programming),底层原理是动态代理(若目标类有接口用jdk动态代理,若没接口用cglib的动态代理),因为动态代理的使用方式比较灵活,它所做的就是提供动态代理使用的规范。Aspect指的是【非业务】功能,即不影响主业务正常进行的辅助性功能,比如计算某个业务方法的运行时间,打印某个业务处理的日志信息,这一类【非业务】功能可以理解为对原功能的增强,通过动态代理来实现这类功能,可以在不改变原业务方法的基础上增加功能,满足开闭原则。

常见的切面功能有:日志、事务、统计信息,参数检查,权限验证

切面三要素:

  1. JoinPoint(连接点),将切面功能和业务功能连接起来的位置,实际上就是业务层的方法(即调用切面功能的方法)。
  2. PointCut(切入点),连接点的集合。
  3. Advice,表示切面功能执行的时机,比如是在业务层方法执行前还是执行后。

spring虽然内置了aop框架,但是比较笨重,所以我们一般使用另外一个专门的aop框架: aspectJ,spring中也集成了这个框架。

4.1 aspectJ的使用

一般使用注解的方式(也可以通过xml文件配置)。

4.1.1 切面的执行时间:Advice

1
2
3
4
5
@Before //目标类方法执行后切面方法执行
@AfterReturning //
@Around
@AfterThrowing
@After

4.1.2 切面执行的位置(切入点pointCut)

使用切入点表达式,它的原型为

1
2
3
4
5
6
execution(
modifiers-pattern? //访问权限类型
ret-type-pattern //返回值类型(*)
declaring-type-pattern?name-pattern(param-pattern) //包名类名(可选),方法名(参数类型和参数个数)(*)
throw-pattern? //抛出异常类型
)

其中?代表可选。

该表达式声明了哪些包中的哪些类中的哪些方法要增加切面功能,表达式中的参数都是pattern,也就是说他们可以用通配符来表示

*”代表0~多个任意字符,

..”用在方法参数中代表任意多个参数,用在包名后代表当前包及其子包路径

+”用在类名后表示当前类及其子类,用在接口后表示当前接口及其实现类

举例:

execution(public * *(..)) 将切入点指定为任意public方法

execution(* set*(..)) 将切入点指定为函数名称以“set”开头的所有方法

execution(* com.xyz.service.*.*(..))

4.2 基于注解的AOP使用示例

4.2.1 JoinPoint

在切面方法中,可以利用joinPoint参数(必须是第一个参数)获取到该切面方法附着的主方法【在执行时】的全部信息。

1
2
3
4
5
6
7
8
9
@Before(value = "execution(* com.example.CRM.*.*(..))")
public void beforeFunction(JoinPoint jp){
System.out.println(jp.getSignature());
System.out.println(jp.getArgs());
System.out.println(jp.getKind());
System.out.println(jp.getSourceLocation());
System.out.println(jp.getStaticPart());
System.out.println(jp.getThis());
}

4.2.2 before

  1. 在spring配置文件中声明自动代理生成器<aop:aspectj-autoproxy/>

  2. 确定要使用切面方法的函数(假定要在People类中speak函数执行前调用切面方法),这个函数也叫做joinPoint

1
2
3
4
5
6
7
8
9
10
11
@Component(value = "people") 
public class People {

public String name;
public int age;

public void speak(String name, int age){
System.out.println("My name is "+name+", I'm "+age+" years old");
}
}

  1. 创建切面类(在普通类的上面加上注解@Aspect),并在切面类中写切面功能方法,这些方法的上面要加上advice注解比如@before,和切入点表达式execution
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component(value = "test")
@Aspect
public class testAOP {
/**
* 【Before前置通知】,value为切入点表达式,代表该切面功能执行的位置
* 特点:
* 1. 前置通知的切面方法会在目标方法执行前执行
* 2. 不会改变目标方法的执行结果
* 3. 不会影响目标方法的执行
* 4. public修饰
* 5. 没有返回值
*/
@Before(value = "execution(* com.example.CRM.*.*(..))")
public void beforeFunction(){
System.out.println(new Date());
}
}

  1. 切面类和目标类都要注册为bean

  2. 通过spring获取到目标类对象,调用对应的方法后就可看到效果(本例中在speak之前打印了当前时间,说明切面方法执行成功)

4.2.3 AfterReturning

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component(value = "test")
@Aspect
public class testAOP {

/**
* 【后置通知】
* 特点:
* 1. 在目标方法执行后执行
* 2. 可以获取到目标方法的返回值,利用该值来做一些功能
* 3. 切面方法有Object类型参数,该参数存放目标方法的返回值
* 4. 注解中有returning属性,其值与切面方法中自定义的返回值形参名字相同
*/

@AfterReturning(value = "execution(* com.example.CRM.*.*(..))", returning = "obj")
public void afterFunction(JoinPoint jp, Object obj){
System.out.println(obj);
}
}

4.2.4 ProceedingJoinPoint(最强)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Component(value = "test")
@Aspect
public class testAOP {


/**
* 【环绕通知】
* 特点:
* 1. 在目标方法执行前后都能通知,是【功能最强】的通知,因为它对目标方法有【完全】的控制权
* 2. 环绕通知的切面方法必须有返回值,一般为Object类型,该返回值就是目标方法的返回值
* 3. 切面方法有ProceedingJoinPoint类型参数,该参数继承自JoinPoint,可直接当JoinPoint用
* 4. 能够控制目标方法最终是否被调用执行
* 5. 修改目标方法返回值
*/

@Around(value = "execution(* com.example.CRM.*.*(..))")
public void aroundFunction(ProceedingJoinPoint pjp){
//0. 在目标方法执行前加功能
System.out.println("Function before");

//1. 调用目标方法并获取返回值(可以不在Around修饰的切面方法中调用pjp的proceed方法,那就相当于切面方法把目标方法拦截下并取消其执行了)
Object result = pjp.proceed();

//2. 在目标方法执行后加功能
System.out.println("Function after");

//3. 返回修改后的目标方法的值(相当于目标方法就返回result+1)
return result+1;
}
}

环绕通知经常用来做事务,目标方法启动前开启事务,关闭后提交事务。

4.2.5 AfterThrowing

在目标方法抛出异常时就会执行该切面方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component(value = "test")
@Aspect
public class testAOP {
/**
* 【异常通知】
* 特点:
* 1. 在目标方法抛出异常时会执行该切面方法
* 2. 可用于监控方法是否发出异常,如果发出异常就启动通知进程(通知工程师来维护,或者通知用户操作错误)
* 3. 切面方法有一个形参类型为Exception
* 3. 注解有一个属性throwing,其值必须与Exception形参名保持一致
*/

@AfterThrowing(value = "execution(* com.example.CRM.*.*(..))",throwing = "ex")
public void throwFunction(Exception ex){
System.out.println("Exception occurred");
}
}

4.2.6 After

如果说AfterThrowing相当于catch语句,After就相当于finally语句。

1
2
3
4
@After(value = "execution(* com.example.CRM.*.*(..))")
public void throwFunction(){
System.out.println("Exception occurred");
}

4.2.7 PointCut

如果一个目标方法(pointCut)要绑定多个切面方法,那么可以给该pointCut起个别名。

1
2
@Pointcut(value = "execution(* com.example.CRM.*.*(..))")
public void pointCutName(){} //该函数名就是目标方法的别名

然后其他切面方法都可以用别名更方便的关联到该pointCut。

1
2
3
4
@After(value = "pointCutName()") 
public void throwFunction(){
System.out.println("Exception occurred");
}

5. Spring集成MyBatis

利用spring的ioc技术,将MyBatis框架中的对象交给spring统一管理,这样开发人员几乎无需知道MyBatis的具体使用方法,只需要会一个spring框架即可。

回忆MyBatis的使用过程,先要配置主配置文件和mapper文件,然后创建SqlSessionFactory对象,通过它获取到SqlSession,然后再通过SqlSession的getMapper方法获取到对应的dao对象。之前主配置文件中有定义一个连接池,该连接池功能比较弱,spring可以帮我们替换掉它。

因此,我们需要spring帮我们创建以下对象:

  1. 创建连接池对象(我们使用阿里的druid连接池)
  2. SqlSessionFactory对象
  3. dao对象

5.1 使用步骤

5.1.1 MyBatis部分

5.1.1.1 创建实体类和其对应的dao接口

5.1.1.2 创建对应的mapper文件

5.1.1.3 创建mybatis主配置文件

mybatis.xml放在resources目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd"> <!--约束文件-->

<configuration>

<!--setting控制mybatis的全局行为-->
<settings>
<!--输出mybatis日志-->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

<!--设置别名-->
<typeAliases>
<package name="com.example.domain"/>
</typeAliases>

<!--定位mapper文件-->
<mappers>
<package name="com.example.dao"/> <!--让mybatis可以解析dao包下所有xml文件-->
</mappers>
</configuration>

可以看到现在mybatis主配置文件中没有配置连接数据库的相关信息,这是因为我们要更换其他的数据源,要在其他地方配置。

5.1.2 spring配置

5.1.2.1 配置DataSource

将其他数据源(如阿里的druid)依赖导入,在spring的配置文件中加入

1
2
3
4
5
6
7
8
9
10
11
<!--声明数据源-->
<bean id="myDataSource"
class="com.alibaba.druid.pool.DruidDataSource"
init-method="init"
destroy-method="close">
<!--使用set注入给DruidDataSource提供连接数据库信息-->
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value=""/>
<property name="maxActive" value="20"/> <!--最大连接数-->
</bean>

5.1.2.2 配置SqlSessionFactoryBean

1
2
3
4
5
6
7
8
 <!--声明mybatis提供的SqlSessionFactory类-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

<property name="dataSource" ref="myDataSource"/>

<!--configLocation属性为Resource类型,读取value指定的位置的配置文件-->
<property name="configLocation" value="classpath:mybatis.xml"/>
</bean>

5.1.2.3 配置dao对象bean

1
2
3
4
5
6
7
8
9
10
<!--创建dao对象,该配置内部调用getMapper()生成每一个dao接口的代理对象。【该bean不用id】-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--指定SqlSessionFactory对象bean的id-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!--
指定dao接口所在包名,MapperScannerConfigurer会扫描这个包中所有接口,为它们一一
调用getMapper方法生成对应的dao对象,并将它们放到spring容器中
-->
<property name="basePackage" value="com.example.dao, com.example.dao2"/>
</bean>

【重点】这一步结束后,spring容器中就会存在所有的dao对象,它们的bean id为接口名字首字母小写(比如dao接口名为UserDao,那么spring帮我们创建的实现类名叫userDao)。

5.1.3 service层

一般项目中是service层对象调用dao层对象访问数据库。

5.1.3.1 创建service接口及其实现类

接口的实现类放到子包impl中。

5.1.3.2 将service实现类注册到spring

只需在service接口实现类中添加两个注解

1
2
3
4
5
@Service(value = "UserServiceImpl")
public class UserServiceImpl implements UserService {

@Autowired
private UserDao userDao;

5.1.3.3 通过service类调用dao对象完成数据库操作

1
2
3
4
5
6
7
8
9
10
@Test
public void test02(){
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
UserServiceImpl usi = (UserServiceImpl) ac.getBean("UserServiceImpl");
List<User> userList = usi.queryALlUsers();
for (User u :
userList){
System.out.println(u);
}
}

【重点】spring整合mybatis后,事务自动提交,不需要我们手动调commit方法了。

5.2 配置文件分离

将属性配置文件和spring配置文件分开,方便维护。如之前数据库信息部分可以改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--把数据库的配置信息写到独立的文件jdbc.properties中,并将该文件位置告诉spring-->
<context:property-placeholder location="classpath:jdbc.properties"/>


<!--声明数据源-->
<bean id="myDataSource"
class="com.alibaba.druid.pool.DruidDataSource"
init-method="init"
destroy-method="close">
<!--使用set注入给DruidDataSource提供连接数据库信息-->
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>

然后创建一个jdbc.properties文件,将数据库信息写进去

1
2
3
4
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=
jdbc.driverClassName=com.mysql.cj.jdbc.Driver

6. 事务

什么是事务?

事务是一组sql语句,它们要不都执行,要不都不执行。

不同数据库sql语法不一样,事务相关的语句逻辑也不一样,spring做的就将它们全部统一了。实际开发中,事务一般放在service层的方法上,因为只有业务方法中才会调用多个dao方法,执行多个sql语句,需要用到事务。

spring把每个数据库访问技术对应的事务处理类都创建好了:

  1. DataSourceTransactionManager代表mybatis
  2. HibernateTransactionManager代表hibernate

我们只需要声明要使用的事务处理类的bean即可,比如用mybatis:<bean id="xx" class="...DataSourceTransactionManager">

6.1 配置事务

说明某个方法需要什么样的事务,即配置事务属性

  1. 事务的隔离级别

    read_uncommitted,read_committed,repeatable_read,serializable

    mysql默认为repeatable_read,oracle默认为read_committed

  2. 事务的传播行为

    propagation_required,表示本方法必须在事务内执行(spring默认)

    propagation_requires_new,

    propagation_supports

  3. 事务提交/回滚的时机

    a. 业务方法执行成功,没有异常抛出,spring会提交事务

    b. 业务方法抛出【运行时异常】,spring调用回滚

    c. 业务方法抛出【非运行时异常】,提交事务

6.2 使用事务

  1. 声明对应数据库访问技术的事务管理器对象

  2. 开始事务注解驱动(告诉spring我们采用注解的方式管理事务)

    spring会对所有被@Transactional注解的方法使用事务,其实现原理就是aop的Around advice,在被注解的方法执行前开启事务,执行后关闭事务。

1
2
3
4
5
6
7
8
9
10
11
<!--spring事务-->

<!--1. 声明事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--将数据源指定为当前连接的数据库-->
<property name="dataSource" ref="myDataSource"/>
</bean>

<!--2. 开启事务注解驱动,告诉spring我们用注解管理事务对象,其中transaction-manager为上面事务管理器对象的id-->
<tx:annotation-driven transaction-manager="transactionManager"/>
<!--注意annotation-driven有多个,选择tx结尾的那个-->
  1. 在要用的方法(必须为public)加上事务注解。这一步完成后,UserServiceImpl中所有方法都使用事务了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Service(value = "UserServiceImpl")
    public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;


    @Transactional //事务注解
    @Override
    public int addUser(User user) { //返回本次sql影响的行数
    int re = userDao.insertUser(user);
    return re;
    }

    @Override
    public List<User> queryALlUsers() {
    List<User> userList = userDao.selectUser();
    return userList;
    }
    }

    @Transactional也可以加在类上面,效果是该类中所有的方法都会开启事务。

    6.3 使用aspectj配置事务(针对大型项目)

    该法是完全在xml文件中配置事务,达到了事务和代码完全分离,因此适合大型项目。

    1. 加入spring-aspects依赖
    2. 声明事务管理器对象(与上一小节相同)
    3. 配置事务属性(隔离级别,传播行为,超时等)
    4. 配置aop,指定哪些类要创建代理
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <!--spring事务,aspectj实现-->

    <!--1. 声明事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--将数据源指定为当前连接的数据库-->
    <property name="dataSource" ref="myDataSource"/>
    </bean>

    <!--2. 配置业务方法的事务属性,其中transaction-manager为上面事务管理器对象的id-->
    <tx:advice id="transactionInterceptor" transaction-manager="transactionManager">
    <tx:attributes>
    <tx:method name="buy" isolation="DEFAULT" read-only="false"/> <!--每一个该标签代表一个方法的事务配置,name就是方法的名称(可用通配符,非全限定名称,就是方法本名)-->
    <tx:method name="sell" isolation="DEFAULT" read-only="true"/> <!--只指定方法名,不指定全限定名称,那有重名方法怎么办?下面还要配置解决-->
    </tx:attributes>
    </tx:advice>

    <!--3. 配置aop-->
    <aop:config>
    <!--配置切入点表达式:指定在哪些包中使用事务-->
    <!--id为切入点表达式的名称,expression为切入点表达式,指定哪些类要使用事务-->
    <aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))"/> <!--execution中表达式代表:所有名称带service的包中的所有类中的所有方法-->

    <!--配置增强器,将pointCut和advice关联起来-->
    <!--advice-ref的值为第2步tx advice的id-->
    <!--pointcut-ref的值为本步骤中pointcut的id-->
    <aop:advisor advice-ref="transactionInterceptor" pointcut-ref="servicePt"/>
    </aop:config>

7. web项目使用spring

可以在servlet的service方法中通过ApplicationContext从spring容器中获取想要的对象

1
2
3
4
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
SomeObject so = (SomeObject) ac.getBean("SomeObject");
}

但是ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); 相当于创建对象然后放到容器中,那么每次网页请求都要重新创建对象放到容器中,这显然是不科学的。显然ApplicationContext这个对象在整个项目的运行过程中只需要创建一次,然后将其放到全局作用域ServletContext中就够了,

可以用监听器来实现。当全局作用域对象被创建时,创建ApplicationContext并将其存入全局作用域对象,(spring框架提供了一个监听器,本例我们使用它)。

  1. 加入spring-web依赖,里面有监听器

  2. 将监听器注册到web.xml

    1
    2
    3
    <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
  3. 修改监听器默认加载spring配置文件位置

    监听器默认/WEB-INF/applicationContext.xml为spring配置文件位置,我们将其改掉,同样在web.xml下配置:

    1
    2
    3
    4
    <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value> <!--会找到resources下的applicationContext.xml-->
    </context-param>

这就完成了,后面我们的代码中不需要写ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); 这句话了,每次web容器启动时,会自动读取applicationContext.xml并创建所有bean声明过的对象放到spring容器中。

1
2
3
4
5
6
7
8
9
10
11
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ServletContext sc = getServletContext();
WebApplicationContext ac = WebApplicationContextUtils.getRequiredWebApplicationContext(sc); //WebApplicationContext等同于ApplicationContext

UserServiceImpl usi = (UserServiceImpl) ac.getBean("UserServiceImpl");
List<User> userList = usi.queryALlUsers();
for (User u :
userList) {
System.out.println(u);
}
}

最终版spring配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

<context:component-scan base-package="com.example"/> <!--扫描bast-package下所有注解,要用注解先要有这一句配置-->
<aop:aspectj-autoproxy/> <!--自动为切面方法生成代理-->


<!--
把数据库的配置信息写到独立的文件jdbc.properties中,并将该文件位置告诉spring
jdbc.properties内容如下:
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=
jdbc.driverClassName=com.mysql.cj.jdbc.Driver
然后在数据源声明中,更改声明方式如下:
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
-->
<context:property-placeholder location="classpath:jdbc.properties"/>


<!--声明数据源-->
<bean id="myDataSource"
class="com.alibaba.druid.pool.DruidDataSource"
init-method="init"
destroy-method="close">
<!--使用set注入给DruidDataSource提供连接数据库信息-->
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>

<!--声明mybatis提供的SqlSessionFactory类-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="myDataSource"/>

<!--configLocation属性为Resource类型,读取value指定的位置的配置文件-->
<property name="configLocation" value="classpath:mybatis.xml"/>
</bean>

<!--创建dao对象,该配置内部调用getMapper()生成每一个dao接口的代理对象-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--指定SqlSessionFactory对象的id-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!--
指定dao接口所在包名,MapperScannerConfigurer会扫描这个包中所有接口,为它们一一
调用getMapper方法生成对应的dao对象,并将它们放到spring容器中
-->
<property name="basePackage" value="com.example.dao"/>
</bean>

<!--spring事务-->

<!--1. 声明事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--将数据源指定为当前连接的数据库-->
<property name="dataSource" ref="myDataSource"/>
</bean>

<!--2. 开启事务注解驱动,告诉spring我们用注解管理事务对象,其中transaction-manager为上面事务管理器对象的id-->
<tx:annotation-driven transaction-manager="transactionManager"/>
<!--注意annotation-driven有多个,选择tx结尾的那个-->

</beans>

bug解决

1. invalid bound statement

这是因为mapper文件没有被编译(一般是由于mapper文件没有放在resources目录中,而是放到dao包内),在pom.xml的build标签下加上

1
2
3
4
5
6
7
8
9
10
<resources>
<resource>
<!--此处配置到java是因为mapper.xml文件在java目录-->
<directory>src/main/java</directory>
<includes>
<include>**/*</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>