Spring 是一个分层的 JavaSE/EEfull-stack(一站式) 轻量级开源框架。

spring概述

​ Spring是一个开源框架,Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson 在其著作 Expert One-On-One J2EE Development and Design 中阐述的部分理念和原型衍生而来。它是 为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使 用者选择使用哪一个组件,同时为 J2EE 应用程序开发提供集成的框架。Spring 使用基本的 JavaBean 来完成以前只可能由 EJB 完成的事情。然而,Spring 的用途不仅限于服务器端的开发。从简单性、 可测试性和松耦合的角度而言,任何 Java 应用都可以从 Spring 中受益。Spring 的核心是控制反转 (IoC)和面向切面(AOP)。简单来说,Spring 是一个分层的 JavaSE/EEfull-stack(一站式) 轻量级开源框架。

EE 开发分成三层结构:

  • WEB层:Spring MVC.
  • 业务层:Bean 管理:(IOC)
  • 持久层:Spring 的 JDBC 模板.ORM 模板用于整合其他的持久层框架

为什么会用spring

方便解耦,简化开发

​ Spring 就是一个大工厂,可以将所有对象创建和依赖关系维护,交给 Spring 管理。

AOP 编程的支持

​ Spring 提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等功能

声明式事务的支持

​ 只需要通过配置就可以完成对事务的管理,而无需手动编程

方便程序的测试

​ Spring 对 Junit4 支持,可以通过注解方便的测试 Spring 程序

方便集成各种优秀框架

​ Spring 不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如:Struts、Hibernate、 MyBatis、Quartz 等)的直接支持

降低JavaEE API 的使用难度

​ Spring 对 JavaEE 开发中非常难用的一些 API(JDBC、JavaMail、远程调用等),都提供了封装, 使这些 API 应用难度大大降低

IOC:反转控制

将重建对象的方法进行反转,之前~pojo对象的创建等是由开发人员自己维护,包括依赖关系也是自己注入。

使用spring之后,对象的创建以及依赖关系由spring完成创建以及注入。

反转控制就是反转了对象的创建方式,由我们自己创建反转给了程序(Spring)。

  • 传统方式

对象A如果想使用对象B的功能方法,在需要的时候创建对象B的实例,调用需要的方法,对对象B有主动的控制权。

  • IOC容器

当使用IOC容器之后,对象A和B之间失去了直接联系,对象A如果想使用对象B的功能方法,IOC容器会自动创建一个对象B实例注入到对象A需要的功能模块中,这样对象A失去了主动控制权,也就是控制反转了。

DI:依赖注入

实现IOC思想,需要DI做支持。

IOC给对象直接建立关系的动作,称为DI依赖注入(Dependency Injection);依赖:对象A需要使用对象B的功能,则称对象A依赖对象B。

注入:在对象A中实例化对象B,从而使用对象B的功能,该动作称为注入。

  • 注入方式:

    • set方法注入
    • 构造方法注入
    • 字段注入
  • 注入类型:

​ - 值类型注入(八大基本数据类型)

​ - 引用类型注入

Spring中的工厂(容器)

BeanFactory(过时)

Spring的原始接口,针对原始接口的实现类功能比较单一。

BeanFactory接口实现类的容器特点是:每次再获得对象时才会创建对象。

在硬件资源匮乏的年代使用

ApplicationContext

接口实现类的容器特点是:每次容器启动时,就会创建容器中配置的所有对象。

除此之外,还提供了更多丰富的功能。

ApplicatioContext 接口有两个实现类:

  • 从类路径下加载配置文件:ClassPathXmlApplicationContext

  • 从硬盘绝对路径下加载:FileSystemXmlApplicationContext

通俗地说:

  • BeanFactory :是在 getBean 的时候才会生成类的实例.

  • ApplicationContext :在加载 applicationContext.xml(容器启动)时候就会创建.

总结

使用场景:

  • 在web开发中,使用applicationContext

  • 在资源匮乏的环境可以使用Beanfactory(例如:手机端)

针对上面用到的几个核心API进行说明,后续持续总结。

1、BeanFactory

这是一个工厂,用于生成任意bean。采取延迟加载,第一次getBean时才会初始化Bean。

2、ApplicationContext

是BeanFactory的子接口,功能更强大。(国际化处理、事件传递、Bean自动装配、各种不同应用层的Context实现)。当配置文件被加载,就进行对象实例化。

3、ClassPathXmlApplicationContext

用于加载classpath(类路径、src)下的xml加载xml运行时位置:/WEB-INF/classes/…xml

4、FileSystemXmlApplicationContext

用于加载指定盘符下的xml加载xml运行时位置:/WEB-INF/…xml,通过ServletContext.getRealPath()获得具体盘符配置。

Bean元素

Bean元素: 使用该元素描述需要被spring容器管理的对象。

name属性:给被管理的对象起个名字,获得对象时根据该名称获得对象,名字可以重复,可以使用特殊字符。

class属性:被管理对象的完整类名。

id属性: 与name属性功能一致,但名称不可重复,不能使用特殊字符。

结论: 尽量使用name属性。

三种对象的创建方式:

  1. 空参构造创建:(重要)

    <bean name="user" class="pers.sfl.pojo.User"/>

  2. 静态工厂(了解)

    调用UserFactory类中的createUser方法创建名为user2的对象,放入容器

    1
    2
    3
    4
    <bean name="user2"
    class="pers.sfl.pojo.UserFactory"
    factory-method="createUser ">
    </bean>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package pers.sfl.pojo;

    import pers.sfl.pojo.User;

    public class UserFactory {

    public static User createUser (){
    System.out.println("静态工厂创建User");
    return new User();
    }
    }
    //测试
    public void test2() throws Exception{
    ApplicatiionContext ac = new ClassPathXmlApplicatiionContext("applicatiionContext.xml");
    User user = (User) ac.getBean("user2");
    System.outl.println(user);
    }
  3. 实例工厂

    会去走无参构造

    1
    2
    3
    4
    5
    6
    <bean name="user3"
    factory-bean="userFactory"
    factory-method="createUser2" ></bean>
    <bean name="userFactory"
    class="pers.sfl.pojo.UserFactory">
    </bean>
    1
    2
    3
    4
    5
    6
    public class UserFactory {
    public User createUser2 (){
    System.out.println("实例工厂创建User");
    return new User();
    }
    }

Scope 属性:

  • singleton:(默认值 )—— 单例对象,被标识为单例的对象在spring容器中只会存在一个实例。

    1
    <bean name="user" class="pers.sfl.pojo.User" scope="singleton"/>
    1
    2
    3
    4
    User user1 = ac.getBean("user");
    User user2 = ac.getBean("user");
    System.out.println(user1==user2); //结果返回true
    //这是返回true的原因是,因为是单例模式,只承认一个对象,所以返回true。
  • prototype:多例原型,被标识为多例的对象,每次再获得时才会创建,每次创建都是新的对象.

    1
    <bean name="user" class="pers.sfl.pojo.User" scope="prototype"/>
    1
    2
    3
    4
    5
    User user1 = ac.getBean("user");
    User user2 = ac.getBean("user");
    System.out.println(user1==user2); //结果返回false
    //现在返回的结果是false,而且打印了好几次的构造方法,
    //因为每次创建对象都会返回不同的对象,在内存中的地址也是不一样的,所以返回false
  • request:web环境下,对象与request生命周期一致

  • session:web环境下,对象与session生命周期一致

绝大多数,都使用默认值,单例对象。

整合struct2时,ActionBean必须配置为多例

生命周期属性

  • 可以配置一个方法作为生命周期初始化方法,spring会在对象创建之后立即调用。(init-method)

  • 也可以配置一个方法作为声明周期的销毁方法,spring容器在关闭并销毁所有容器中的对象之前调用(init-destory)

    1
    2
    3
    4
    5
    <bean name="user"
    class="pers.sfl.pojo.User"
    init-method="init"
    destroy-method="destory"/>
    //这两个名称:init/destory 必须和pojo类中定义的方法名保持一致。

分模块化配置:

​ 在实际开发中,对象pojo会很多,如果全放一个配置文件里,不好维护也比较繁琐

​ 在主配置文件中引用其他配置文件即可

1
<import resource="路径设置去掉src之前的部分即可">

spring属性注入

属性注入的四种方式:

set方法注入(最重要)

  • 注入值类型:

    1
    2
    3
    4
    <bean name="user" class="pers.sfl.pojo.User">
    <property name="username" value="Tom"></property>
    <property name="age" value="12"></property>
    </bean>
  • 引用类型: 引用类型注入,我们需要在创建一个pojo类型,以car为例:

    我们需要将Car类作为User类的属性并且在Car本类中,同时提供get()/set()方法以及toString()方法。

    1
    2
    3
    4
    5
    6
    7
    8
    public class User {
    private String username;
    private int age;
    //...
    private Car car;

    //get和set方法...
    }

    然后先将Car配置到核心配置文件中:

    1
    2
    3
    4
    5
    <!-- 将Car对象配置到容器中来 -->
    <bean name="car" class="pers.sfl.pojo.Car">
    <property name="name" value="布加迪威龙"></property>
    <property name="color" value="black"></property>
    </bean>

    然后对User对象的配置进行改造:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <bean name="user" class="pers.sfl.pojo.User">
    <property name="username" value="Tom"></property>
    <property name="age" value="12"></property>
    <!--为car属性注入上方配置的Car对象 -->
    <property name="car" ref="car"></property>
    </bean>

    说明:
    普通类型注入使用value
    引用类型注入使用ref

构造函数注入

​ 通过构造函数注入首先就要有构造函数

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

public User(String username,Car car){
this.username = username;
this.car = car;
System.out.println("这是User(String username,Car car)构造方法。。");
}

````

```xml
<bean name="user" class="pers.sfl.pojo.User">
<constructor-arg name="username" value="jerry"></constructor-arg>
<constructor-arg name="car" ref="car"></constructor-arg>
</bean>
````

- 如果有多个构造呢,把car放前面,username放后面会出现怎么样的结果?

> 有name type index 可以准确定位到某个具体的方法

```java
public User(String username,Car car){
this.username = username;
this.car = car;
System.out.println("这是User(String username,Car car)构造方法。。");
}

public User(Car car,String username){
this.username = username;
this.car = car;
System.out.println("这是User(Car car,String username)构造方法。。");
}

这里有一个index,可以指定某个属性的位置,从0开始

1
2
3
4
<bean name="user" class="pers.sfl.pojo.User">
<constructor-arg name="username" value="jerry" index="1"></constructor-arg>
<constructor-arg name="car" ref="car"></constructor-arg>
</bean>

我们还可以注意到有一个type,是指定属性的类型的,假如有属性名相同但数据类型不相同的属性时,就可以使用type属性。,

name属性:构造函数的参数名

index属性:构造函数的参数索引

type属性: 构造函数的参数类型

名称空间注入

p空间注入,使用的还是set无参构造方法

首先导入p命名空间,使用p:属性完成注入:

xmlns:p="http://www.springframework.org/schema/p"

  • 值类型: p:属性名="值"

  • 对象类型: p:属性名 ref="bean名称"

1
2
3
4
5
<bean name="user"
class="pers.sfl.pojo.User"
p:username="Jack"
p:car-ref="car">
</bean>

spel注入

Spring Expression Language——Spring表达式语言

1
2
3
4
5
<bean name="user4" class="pers.sfl.pojo.User">
<property name="username" value="#{user.username}"></property>
<property name="age" value="#{user.age}"></property>
<property name="car" ref="car"></property>(引用类型无法使用sperl注入)
</bean>

复杂类型注入

数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<bean name="cb" class="pers.sfl.pojo.CollectionBean">
<!-- 如果数组中只准备注入一个值(对象),直接使用value|ref即可 -->
<property name="arr" value="tom"></property>
</bean>

<!--多个属性注入 -->
<bean name="cb" class="pers.sfl.pojo.CollectionBean">
<property name="arr">
<array>
<value>tom</value>
<value>jerry</value>
<ref bean="user4"/>
</array>
</property>
</bean>

List:

1
2
3
4
5
6
7
8
9
<bean name="cb" class="pers.sfl.pojo.CollectionBean">
<property name="list">
<list>
<value>Jack</value>
<value>rose</value>
<ref bean="user3"/>
</list>
</property>
</bean>

数组和List集合大同小异,只是各用各的标签,数组用array,List集合用list

引用类型的注入都使用

Map:

1
2
3
4
5
6
7
8
9
<bean name="cb" class="pers.sfl.pojo.CollectionBean">
<property name="map">
<map>
<entry key="name" value="张山" ></entry>
<entry key-ref="user3" value-ref="user2"></entry>
<entry key="user" value-ref="user4"></entry>
</map>
</property>
</bean>

Prop:

1
2
3
4
5
6
7
8
9
<bean name="cb" class="pers.sfl.pojo.CollectionBean">
<property name="prop">
<props>
<prop key="username">zhangsan</prop>
<prop key="age">22</prop>
<prop key="address">Beijing</prop>
</props>
</property>
</bean>

Spring管理容器在项目中的生命周期

在使用junit单元测试时,每次都需要加载spring容器来获取被bean管理的对象。但是applicationContext容器一经加载就会创建容器中所有的对象,junit测试时每次都需要加载容器,这属于错误的示范,那么如何解决这个问题呢?

1
ApplicatiionContext ac = new ClassPathXmlApplicatiionContext("applicatiionContext.xml");

让spring容器的生命周期随着项目的启动而记载,项目的关闭而销毁,需要配置监听器:

  1. 导包 spring-web.XXX.jar

  2. 在web.xml里配置listener监听器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!-- 可以让spring容器随项目的启动而创建,随项目的关闭而销毁  -->
    <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- 指定配置spring配置文件的位置 -->
    <context-param>
    <!--名称固定,不能随便取,可以在源码里找到-->
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
  3. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //从application域中获取spring容器。
    public void test() throws Exception{
    //1.获得servletContext对象
    ServletContext sc = ServletActionContext.getServletContext();

    // 2.从sc中获得ac容器
    WebApplicationContext ac =WebApplicationContextUtils.getWebApplicationContext(ServletContext sc);

    // 3.从ac容器中获得bean
    User user = (User)ac.getbean("user2");
    System.outl.println(user);
    }

Spring注解开发

使用注解配置Spring,为主配置文件引入新的命名空间(约束)

spring-context-4.2.xsd

开启使用注解代替配置文件(如果报错,加一个aop包)

1
2
3
4
5
6
7
8
9
<!-- 指定扫描 pers.sfl.pojo包下的所有类中的注解
注意:扫描包时,会扫描指定包下的所有子包
-->

<context:component-scan
base-package="pers.sfl.pojo">
</context:component-scan>


在类中使用注解完成配置

对象的配置(*****)

@Component("user")

​ 相当于在xml中这样配:<bean name="user" class="pers.sfl.pojo.User">

  • @Component("")//pojo

    @Service("") //service

    @Controller("")//web

    @Repository("")//dao层

作用域的配置(*)

​ 对象使用注解配好了,那作用域怎么配呢,怎么指定是单例还是多例呢

@Scope(scopeName="singleton | prototype")//指定对象的作用域:单例/多例

属性的注入@Value(" ")

有两种方式:

  • 第一种:在属性上注入(通过反射的Field赋值)
  • 第二种:在set方法上注入(推荐使用)
1
2
3
4
5
6
7
8
9
10
public class User {
@Value("TOM")
private String name;
//...

@Value("TOM")
public void setName(String name){
this.username=name;
}
}

对象作为类的属性注入

  1. @AutoWired("")

    • 首先先将该类注入到bean中 @Component(" ")

    • 然后使用@AutoWired,假如匹配到多个类型一致的对象,将无法具体选择注入哪一个对象,可以用@Qualifier来指定具体注入的对象,比如你配置了两个userd对象,user1/user2,你想调用user2,就@Qualifier("user2")

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Component("user")
    public class User {
    @Value("TOM")
    private String name;
    //...

    @Autowired //自动装配
    private Car car;
    }
  2. @Resource(name="")推荐使用

    手动注入,指定需要注入哪个名称的bean对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Component("user")
    public class User {
    @Value("TOM")
    private String name;
    //...

    @Resource(name= "car") //自动装配
    private Car car;
    }

初始化和销毁方法

1
2
3
4
5
6
7
8
9
@PostConstruct //在对象被创建后调用
public void init(){
System.out.println("调用初始化方法。");
}

@PreDestroy //在对象被销毁之前调用
public void destroy(){
System.out.println("调用销毁方法。");
}

Spring与junit单元测试整合

  1. 导包 导一个test包

  2. 在类头添加注

    1
    2
    3
    4
    //创建容器
    @RunWith(SpringJunit4ClassRunner.class)
    //指定创建容器使使用哪个配置文件
    @ContextConfiguration("classpath:applicationContext.xml")
  3. 测试:

Spring——AOP

什么是AOP

​ 可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。AOP实际是GoF设计模式的延续,设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,提高代码的灵活性和可扩展性,AOP可以说也是这种目标的一种实现。

在Spring中提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。

主要功能

日志记录,性能统计,安全控制,事务处理,异常处理等等。

主要意图

将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

AOP的好处

对程序进行增强:不修改源码的情况下. * AOP 可以进行权限校验,日志记录,性能监控,事务控制.

底层实现

代理机制:Spring 的 AOP 的底层用到两种代理机制:

  • JDK 的动态代理 :针对实现了接口的类产生代理.

  • Cglib 的动态代理 :针对没有实现接口的类产生代理. 应用的是底层的字节码增强的技术 生成当前类 的子类对象.

相关术语

  • 目标对象:

  • Joinpoint(连接点):目标对象中,所有可以增强的代码。相当于sql中的一条记录

  • Pointcut(切入点):目标对象中,已经增强的方法,相当于sql中的查询条件

  • Advice(通知/增强):增强的代

    • method before advice 前置增强
    • after returning advice 后置增强
    • method interceptor 环绕增强(相当于方法拦截器)
    • throws advice 异常捕获增强
  • Target(目标对象):被代理对象

  • Weaving(织入):通知+切入点,将通知织入到切入点的过程叫织入。

  • Proxy(代理):将通知织入到目标对象之后,形成代理对象

  • aspect(切面):切入点+通知

Aspectj注解参数解释:

pointcut方法注解上的execution( * com.test.service.impl.. * . * (..))参数解释:

  • 第一个 * 代表返回值类型,
  • 第一个..代表当前包和子包,
  • 第二三个 * 分别代表所有类和所有方法,
  • 第二个..代表任意参数

AOP准备工作包:4+2+2

  • 4+2就是spring的基本包以及commons logging/log4j
  • 2:spring的aop包:spring-aop.jar + spring-aspects.jar
  • 第三方包:aopalliancce.jar / aspectj.weaver

准备目标对象

1
2
3
4
5
6
7
8
9
10
package pers.sfl.service;

public class UserServiceImpl implements UserService {
@Override
public void save(){
System.out.prinln("保存用户。");
}

//...
}

准备通知

1
2
3
4
5
6
public class Myadvice{
public void before(){
System.out.prinln("这是前置通知");
}
//....
}

配置进行织入

将通知织入目标对象中

  1. 在applicationContext.xml中导入新的约束,aop约束

  2. 配置目标对象

    1
    2
    3
    4
    <!-- 配置目标对象 -->
    <bean name="userService" class="pers.sfl.service.UserServiceImpl"></bean>
    <!-- 配置通知对象 -->
    <bean name="myAdvice" class="pers.sfl.aspect.Myadvice"></bean>
  3. 配置将通知织入到目标对象

    • 配置切入点:
      • public void pers.sfl.service.UserServiceImpl.save()
      • void pers.sfl.service.UserServiceImpl.save() ——public 忽略可以不用写
      • * pers.sfl.service.UserServiceImpl.save() ——对返回值不做要求,无论以什么类型返回都可以进行切入
      • * pers.sfl.service.UserServiceImpl.*()——不仅只对save()方法,对此类中所有的方法进行切入,不考虑返回值类型(必须是无参方法)
      • * pers.sfl.service.*ServiceImpl.*(..) ——不仅只对save()方法,对类中所有的方法进行切入,不考虑返回值类型(不考虑是否有参数),且不考虑是哪个class类
      • * cn.itcast.service..*ServiceImpl.*(..) ——一个.是找service包下的类,两个.还会找其子类,一般不常用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <aop:config>
    <!-- 配置切入点在spring中,切点是一个表达式。你想增强哪个方法你就切哪个方法 -->
    <aop:pointcut expression="execution(* pers.sfl.service.*ServiceImpl.*(..))" id="pc"/>

    <aop:aspect ref="myAdvice">
    <!-- 指定名为before()的方法作为前置通知 -->
    <aop:before method="before" pointcut-ref="pc"/>
    <!--后置通知 (出现异常则不执行)-->
    <aop:after-returning method="afterReturning" pointcut-ref="pc"/>
    <!-- 环绕通知 -->
    <aop:around method="around" pointcut-ref="pc"/>
    <!-- 异常拦截通知 -->
    <aop:after-throwing method="afterException" pointcut-ref="pc"/>
    <!-- 后置通知(无论有没有异常都执行) -->
    <aop:after method="after" pointcut-ref="pc"/>
    </aop:aspect>
    <!-- 切记:method里的内容不要加括号,会报错!!!!会报一个Failed to load ApplicationContext-->

注解式AOP配置

在配置文件中开启注解配置AOP

<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

在通知类上加入注解@Aspect(表示此类是一个通知类)

1
2
3
4
@Aspect
//表示一个通知类
public class Myadvice{
}

方法上加入注解

  • @Before

  • 加上注解不行,你还得告诉spring,你往哪个方法织入,就可以这样写,表达式+方法。

  • @Before("execution(* pers.sfl.service.*ServiceImpl.*(..))")

  • 因为每个方法都需要复制,不符合开发习惯,造成代码冗余,我们可以这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //定义一个公共方法,供其他方法调用,
    //方法头上需要配置@Pointcut注解来提示此方法为切入点
    //然后在改造一下其他的方法:
    @Pointcut("execution(* pers.sfl.service.*ServiceImpl.*(..))")
    public void controllerAspect(){

    }

    @Before("Myadvice.controllerAspect")
    @Before("controllerAspect()")

例子

  • around方法需要有返回值,不然会报错
    aournd方法不能try catch, 需要抛出异常,否则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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package pers.sfl.annotation;


/**
* @desc 切点类
*/

@Aspect
@Component
public class SystemLogAspect {

//注入Service用于把日志保存数据库
@Resource
private SystemLogService systemLogService;
private static final Logger logger = LoggerFactory.getLogger(SystemLogAspect. class);

//Controller层切点
@Pointcut("execution (* com.gcx.controller..*.*(..))")
public void controllerAspect() {
}

/**
* 前置通知 用于拦截Controller层记录用户的操作
*
* @param joinPoint 切点
*/
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint) {
System.out.println("==========执行controller前置通知===============");
if(logger.isInfoEnabled()){
logger.info("before " + joinPoint);
}
}

//配置controller环绕通知,使用在方法aspect()上注册的切入点
@Around("controllerAspect()")
public boolean around(JoinPoint joinPoint){
System.out.println("==========开始执行controller环绕通知===============");
long start = System.currentTimeMillis();
((ProceedingJoinPoint) joinPoint).proceed();
long end = System.currentTimeMillis();
if(logger.isInfoEnabled()){
logger.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms!");
}
System.out.println("==========结束执行controller环绕通知===============");
return true;
}

/**
* 后置通知 用于拦截Controller层记录用户的操作
*
* @param joinPoint 切点
*/
@After("controllerAspect()")
public void after(JoinPoint joinPoint) {

/* HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpSession session = request.getSession(); */
//读取session中的用户
// User user = (User) session.getAttribute("user");
//请求的IP
//String ip = request.getRemoteAddr();
User user = new User();
user.setId(1);
user.setName("张三");
String ip = "127.0.0.1";
try {

String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
Method[] methods = targetClass.getMethods();
String operationType = "";
String operationName = "";
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length) {
operationType = method.getAnnotation(Log.class).operationType();
operationName = method.getAnnotation(Log.class).operationName();
break;
}
}
}
//*========控制台输出=========*//
System.out.println("=====controller后置通知开始=====");
System.out.println("请求方法:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()")+"."+operationType);
System.out.println("方法描述:" + operationName);
System.out.println("请求人:" + user.getName());
System.out.println("请求IP:" + ip);
//*========数据库日志=========*//
SystemLog log = new SystemLog();
log.setId(UUID.randomUUID().toString());
log.setDescription(operationName);
log.setMethod((joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()")+"."+operationType);
log.setLogType((long)0);
log.setRequestIp(ip);
log.setExceptioncode( null);
log.setExceptionDetail( null);
log.setParams( null);
log.setCreateBy(user.getName());
log.setCreateDate(new Date());
//保存数据库
systemLogService.insert(log);
System.out.println("=====controller后置通知结束=====");
} catch (Exception e) {
//记录本地异常日志
logger.error("==后置通知异常==");
logger.error("异常信息:{}", e.getMessage());
}
}

//配置后置返回通知,使用在方法aspect()上注册的切入点
@AfterReturning("controllerAspect()")
public void afterReturn(JoinPoint joinPoint){
System.out.println("=====执行controller后置返回通知=====");
if(logger.isInfoEnabled()){
logger.info("afterReturn " + joinPoint);
}
}

/**
* 异常通知 用于拦截记录异常日志
*
* @param joinPoint
* @param e
*/
@AfterThrowing(pointcut = "controllerAspect()", throwing="e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
/*HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpSession session = request.getSession();
//读取session中的用户
User user = (User) session.getAttribute(WebConstants.CURRENT_USER);
//获取请求ip
String ip = request.getRemoteAddr(); */
//获取用户请求方法的参数并序列化为JSON格式字符串

User user = new User();
user.setId(1);
user.setName("张三");
String ip = "127.0.0.1";

String params = "";
if (joinPoint.getArgs() != null && joinPoint.getArgs().length > 0) {
for ( int i = 0; i < joinPoint.getArgs().length; i++) {
params += JsonUtil.getJsonStr(joinPoint.getArgs()[i]) + ";";
}
}
try {

String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
Method[] methods = targetClass.getMethods();
String operationType = "";
String operationName = "";
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length) {
operationType = method.getAnnotation(Log.class).operationType();
operationName = method.getAnnotation(Log.class).operationName();
break;
}
}
}
/*========控制台输出=========*/
System.out.println("=====异常通知开始=====");
System.out.println("异常代码:" + e.getClass().getName());
System.out.println("异常信息:" + e.getMessage());
System.out.println("异常方法:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()")+"."+operationType);
System.out.println("方法描述:" + operationName);
System.out.println("请求人:" + user.getName());
System.out.println("请求IP:" + ip);
System.out.println("请求参数:" + params);
/*==========数据库日志=========*/
SystemLog log = new SystemLog();
log.setId(UUID.randomUUID().toString());
log.setDescription(operationName);
log.setExceptioncode(e.getClass().getName());
log.setLogType((long)1);
log.setExceptionDetail(e.getMessage());
log.setMethod((joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"));
log.setParams(params);
log.setCreateBy(user.getName());
log.setCreateDate(new Date());
log.setRequestIp(ip);
//保存数据库
systemLogService.insert(log);
System.out.println("=====异常通知结束=====");
} catch (Exception ex) {
//记录本地异常日志
logger.error("==异常通知异常==");
logger.error("异常信息:{}", ex.getMessage());
}
/*==========记录本地异常日志==========*/
logger.error("异常方法:{}异常代码:{}异常信息:{}参数:{}", joinPoint.getTarget().getClass().getName() + joinPoint.getSignature().getName(), e.getClass().getName(), e.getMessage(), params);

}

}

事务

什么是事务:

​ 事务逻辑上的一组操作,组成这组操作的各个逻辑单元,要么一起成功,要么一起失败.

事务特性:ACID

  • 原子性 :强调事务的不可分割.

  • 一致性 :事务的执行的前后数据的完整性保持一致.

  • 隔离性 :一个事务执行的过程中,不应该受到其他事务的干扰

  • 持久性 :事务一旦结束,数据就持久到数据库

安全性

如果不考虑隔离性引发安全性问题:

  • 脏读 :一个事务读到了另一个事务的未提交的数据

  • 不可重复读 :一个事务读到了另一个事务已经提交的 update 的数据导致多次查询结果不一致.

  • 虚幻读 :一个事务读到了另一个事务已经提交的 insert 的数据导致多次查询结果不一致.

事务隔离级别

解决读问题:设置事务隔离级别

  • 未提交读 :脏读,不可重复读,虚读都有可能发生

  • 已提交读 :避免脏读。但是不可重复读和虚读有可能发生

  • 可重复读 :避免脏读和不可重复读.但是虚读有可能发生.

  • 串行化的 :避免以上所有读问题.

  • Mysql 默认:可重复读

  • Oracle 默认:读已提交

spring操作事务

因为在不同平台上,操纵事务的代码各不相同,spring提供了一个接口 PlatformTransactionManager接口

JDBC平台:DataSourceTransactionManager

Hibernate平台:HibernateTransactionManager

注意:在spring中玩事务管理最为核心的对象就是TransactionManager

spring管理事务的属性介绍

  • 事务的隔离级别:

    1. 读未提交
    2. 读已提交
    3. 可重复读
    4. 串行化
  • 是否只读:

​ true 只读

​ false 可操纵

  • 事务传播行为(propagation behavior):指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。 例如:methodA与methodB不在同一个类中,methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。

事务传播行为

PROPAGATION_REQUIRED

如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
可以把事务想像成一个胶囊,在这个场景下方法B用的是方法A产生的胶囊(事务)。

举例有两个方法:

1
2
3
4
5
6
7
8
9
10
11
@Transactional(propagation = Propagation.REQUIRED)

public void methodA() {
methodB();
// do something
}

@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
// do something
}

单独调用methodB方法时,因为当前上下文不存在事务,所以会开启一个新的事务。
调用methodA方法时,因为当前上下文不存在事务,所以会开启一个新的事务。当执行到methodB时,methodB发现当前上下文有事务,因此就加入到当前事务中来。

PROPAGATION_SUPPORTS

如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同。
举例有两个方法:

1
2
3
4
5
6
7
8
9
10
11
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}

// 事务属性为SUPPORTS
@Transactional(propagation = Propagation.SUPPORTS)
public void methodB() {
// do something
}

单纯的调用methodB时,methodB方法是非事务的执行的。

当调用methdA时,methodB则加入了methodA的事务中,事务地执行。

PROPAGATION_MANDATORY

如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}


// 事务属性为MANDATORY
@Transactional(propagation = Propagation.MANDATORY)
public void methodB() {
// do something

}

当单独调用methodB时,因为当前没有一个活动的事务,则会抛出异常throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”);

当调用methodA时,methodB则加入到methodA的事务中,事务地执行。

PROPAGATION_REQUIRES_NEW

使用PROPAGATION_REQUIRES_NEW,需要使用 JtaTransactionManager作为事务管理器。
它会开启一个新的事务。如果一个事务已经存在,则先将这个存在的事务挂起。

1
2
3
4
5
6
7
8
9
10
11
12
@Transactional(propagation = Propagation.REQUIRED)
doSomeThingA();
methodB();
doSomeThingB();
// do something else
}

// 事务属性为REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// do something
}

当调用

1
2
3
main{
methodA();
}

相当于调用

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
main(){
TransactionManager tm = null;
try{
//获得一个JTA事务管理器
tm = getTransactionManager();
tm.begin();//开启一个新的事务
Transaction ts1 = tm.getTransaction();
doSomeThing();
tm.suspend();//挂起当前事务
try{
tm.begin();//重新开启第二个事务
Transaction ts2 = tm.getTransaction();
methodB();
ts2.commit();//提交第二个事务
} Catch(RunTimeException ex) {
ts2.rollback();//回滚第二个事务
} finally {
//释放资源
}

//methodB执行完后,恢复第一个事务
tm.resume(ts1);
doSomeThingB();
ts1.commit();//提交第一个事务
} catch(RunTimeException ex) {
ts1.rollback();//回滚第一个事务
} finally {
//释放资源
}
}

在这里,我把ts1称为外层事务,ts2称为内层事务。从上面的代码可以看出,ts2与ts1是两个独立的事务,互不相干。Ts2是否成功并不依赖于 ts1。如果methodA方法在调用methodB方法后的doSomeThingB方法失败了,而methodB方法所做的结果依然被提交。而除了 methodB之外的其它代码导致的结果却被回滚了

PROPAGATION_NOT_SUPPORTED

PROPAGATION_NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务。使用PROPAGATION_NOT_SUPPORTED,也需要使用JtaTransactionManager作为事务管理器。

单独执行方法Bw无事务

执行方法A,运行到方法B的时候,会挂起事务A,然后以非事务的方式运行方法B.

PROPAGATION_NEVER

总是非事务地执行,如果存在一个活动事务,则抛出异常。

PROPAGATION_NESTED

如果一个活动的事务存在,则运行在一个嵌套的事务中。 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行。
这是一个嵌套事务,使用JDBC 3.0驱动时,仅仅支持DataSourceTransactionManager作为事务管理器。
需要JDBC 驱动的java.sql.Savepoint类。使用PROPAGATION_NESTED,还需要把PlatformTransactionManager的nestedTransactionAllowed属性设为true(属性值默认为false)。

这里关键是嵌套执行。

1
2
3
4
5
6
7
8
9
10
11
@Transactional(propagation = Propagation.REQUIRED)
methodA(){
doSomeThingA();
methodB();
doSomeThingB();
}

@Transactional(propagation = Propagation.NEWSTED)
methodB(){
//……
}

如果单独调用methodB方法,则按REQUIRED属性执行。如果调用methodA方法,相当于下面的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
main(){
Connection con = null;
Savepoint savepoint = null;
try{
con = getConnection();
con.setAutoCommit(false);
doSomeThingA();
savepoint = con2.setSavepoint();
try{
methodB();
} catch(RuntimeException ex) {
con.rollback(savepoint);
} finally {
//释放资源
}
doSomeThingB();
con.commit();
} catch(RuntimeException ex) {
con.rollback();
} finally {
//释放资源
}
}

当methodB方法调用之前,调用setSavepoint方法,保存当前的状态到savepoint。如果methodB方法调用失败,则恢复到之前保存的状态。但是需要注意的是,这时的事务并没有进行提交,如果后续的代码(doSomeThingB()方法)调用失败,则回滚包括methodB方法的所有操作。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。

PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别:

它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。
使用 PROPAGATION_REQUIRES_NEW时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA事务管理器的支持。

使用PROPAGATION_NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。DataSourceTransactionManager使用savepoint支持PROPAGATION_NESTED时,需要JDBC 3.0以上驱动及1.4以上的JDK版本支持。其它的JTATrasactionManager实现可能有不同的支持方式。

PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。

另一方面, PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。

由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于, PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back.

xml配置aop事务

  1. 导包+新约束

    加起来一共配置过四个命名空间:

    beans:最基本

    context: 读取properties配置

    aop:配置aop

    tx:配置事务通知

  2. 配置事务通知

    1
    2
    3
    4
    5
    <!-- 事务核心管理器,封装了所有事务操作,依赖于连接池 -->

    <bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"></property>
    </bean>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <tx:advice transaction-manager="transactionManager" id="txAdvice">
    <tx:attributes
    <!--以方法为单位指定方法应用什么事务属性
    isolation隔离级别
    propagation传播行为
    read-only是否只读
    -->

    <tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>

    <tx:method name="save*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>

    <tx:method name="persist*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
    <tx:method name="update*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
    <tx:method name="modify*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
    <tx:method name="delete*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
    <tx:method name="remove*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
    <tx:method name="get*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true"/>
    <tx:method name="find*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true"/>
    </tx:attributes>

    </tx:advice>


  3. 配置事务织入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!-- 配置事务织入 -->
    <aop:config>
    <!--配置切点表达式-->
    <aop:pointcut expression="execution(* cn.itcast.service.*ServiceImpl.*(..))" id="txpc"/>
    <!--配置切面:通知+切点
    advice-ref:通知的名称
    pointcut-ref:切点的名称
    -->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txpc"/>
    </aop:config>

Spring注解配置aop事务

之前的导包+tx约束是不变的~

  1. 开启注解事务

    1
    2
    <!-- 开启使用注解配置事务 -->
    <tx:annotation-driven/>
  2. 使用注解

    嫌麻烦,可以在类头上加入该注解,如果有的方法需要只读,可在添加在方法上

    1
    2
    3
    4
    @Transactional(isolation=Isolation.REPEATABLE_READ,propagation=Propagation.REQUIRED,readOnly=false)
    public void teansfer(Interger from, Integer to, Double money){
    //....
    }