Spring从原理到实战

Spring概述

Spring是分层的Java SE/EE应用 full-stack(全栈式)轻量级开源框架以IoC(Inverse Of Control:反转控制)和AOP(Aspect Oriented Programming:面向切面编程)为内核,提供了展现层Spring MVC和持久层Spring JDBC以及业务层事务管理等众多的企业级应用技术,还能整合开源世界众多著名的第三方框架和类库,逐渐成为使用最多的Java EE企业应用开源框架。

Spring的发展历程

1997年,IBM提出了EJB的思想
1998年,SUN制定开发标准规范EJB1.0
1999年,EJB1.1发布
2001年,EJB2.0发布
2003年,EJB2.1发布
2006年,EJB3.0发布

Rod Johnson(spring之父
Expert One-to-One J2EE Design and Development(2002)
阐述了J2EE使用EJB开发设计的优点及解决方案
Expert One-to-One J2EE Development without EJB(2004)
阐述了J2EE开发不使用EJB的解决方式(Spring雏形)

2017年9月份发布了spring的最新版本spring 5.0通用版(GA)

spring的优势

  1. 方便解耦,简化开发
    通过Spring提供的IoC容器,可以将对象间的依赖关系交由Spring进行控制,避免硬编码所造成的过度程序耦合。用户也不必再为单例模式类、属性文件解析等这些很底层的需求编写代码,可以更专注于上层的应用。
  2. AOP编程的支持
    通过Spring的AOP功能,方便进行面向切面的编程,许多不容易用传统OOP实现的功能可以通过AOP轻松应付。
  3. 声明式事务的支持
    可以将我们从单调烦闷的事务管理代码中解脱出来,通过声明式方式灵活的进行事务的管理,提高开发效率和质量。
  4. 方便程序的测试
    可以用非容器依赖的编程方式进行几乎所有的测试工作,测试不再是昂贵的操作,而是随手可做的事情。
  5. 方便集成各种优秀框架
    Spring可以降低各种框架的使用难度,提供了对各种优秀框架(Struts、Hibernate、Hessian、Quartz等)的直接支持。
  6. 降低JavaEE API的使用难度
    Spring对JavaEE API(如JDBC、JavaMail、远程调用等)进行了薄薄的封装层,使这些API的使用难度大为降低。
  7. Java源码是经典学习范例
    Spring的源代码设计精妙、结构清晰、匠心独用,处处体现着大师对Java设计模式灵活运用以及对Java技术的高深造诣。它的源代码无意是Java技术的最佳实践的范例。

Spring的体系结构

enter description here

核心容器

核心容器由spring-core,spring-beans,spring-context,spring-context-support和spring-expression(SpEL,Spring表达式语言,Spring Expression Language)等模块组成,它们的细节如下:

  • spring-core模块提供了框架的基本组成部分,包括 IoC 和依赖注入功能。
  • spring-beans 模块提供 BeanFactory,工厂模式的微妙实现,它移除了编码式单例的需要,并且可以把配置和依赖从实际编码逻辑中解耦。
  • context模块建立在由core和 beans 模块的基础上建立起来的,它以一种类似于JNDI注册的方式访问对象。Context模块继承自Bean模块,并且添加了国际化(比如,使用资源束)、事件传播、资源加载和透明地创建上下文(比如,通过Servelet容器)等功能。Context模块也支持Java EE的功能,比如EJB、JMX和远程调用等。ApplicationContext接口是Context模块的焦点。spring-context-support提供了对第三方库集成到Spring上下文的支持,比如缓存(EhCache, Guava, JCache)、邮件(JavaMail)、调度(CommonJ, Quartz)、模板引擎(FreeMarker, JasperReports, Velocity)等。
  • spring-expression模块提供了强大的表达式语言,用于在运行时查询和操作对象图。它是JSP2.1规范中定义的统一表达式语言的扩展,支持set和get属性值、属性赋值、方法调用、访问数组集合及索引的内容、逻辑算术运算、命名变量、通过名字从Spring IoC容器检索对象,还支持列表的投影、选择以及聚合等。

它们的完整依赖关系如下图所示:
enter description here

数据访问/集成

数据访问/集成层包括 JDBC,ORM,OXM,JMS 和事务处理模块,它们的细节如下:
(注:JDBC=Java Data Base Connectivity,ORM=Object Relational Mapping,OXM=Object XML Mapping,JMS=Java Message Service)

  • JDBC 模块提供了JDBC抽象层,它消除了冗长的JDBC编码和对数据库供应商特定错误代码的解析。
  • ORM 模块提供了对流行的对象关系映射API的集成,包括JPA、JDO和Hibernate等。通过此模块可以让这些ORM框架和spring的其它功能整合,比如前面提及的事务管理。
  • OXM 模块提供了对OXM实现的支持,比如JAXB、Castor、XML Beans、JiBX、XStream等。
  • JMS 模块包含生产(produce)和消费(consume)消息的功能。从Spring 4.1开始,集成了spring-messaging模块。
  • 事务模块为实现特殊接口类及所有的 POJO 支持编程式和声明式事务管理。(注:编程式事务需要自己写beginTransaction()、commit()、rollback()等事务管理方法,声明式事务是通过注解或配置由spring自动处理,编程式事务粒度更细)

Web

Web 层由 Web,Web-MVC,Web-Socket 和 Web-Portlet 组成,它们的细节如下:

扫描二维码关注公众号,回复: 11232769 查看本文章
  • Web 模块提供面向web的基本功能和面向web的应用上下文,比如多部分(multipart)文件上传功能、使用Servlet监听器初始化IoC容器等。它还包括HTTP客户端以及Spring远程调用中与web相关的部分。
  • Web-MVC 模块为web应用提供了模型视图控制(MVC)和REST Web服务的实现。Spring的MVC框架可以使领域模型代码和web表单完全地分离,且可以与Spring框架的其它所有功能进行集成。
  • Web-Socket 模块为 WebSocket-based 提供了支持,而且在 web 应用程序中提供了客户端和服务器端之间通信的两种方式。
  • Web-Portlet 模块提供了用于Portlet环境的MVC实现,并反映了spring-webmvc模块的功能。

其他

还有其他一些重要的模块,像 AOP,Aspects,Instrumentation,Web 和测试模块,它们的细节如下:

  • AOP 模块提供了面向方面的编程实现,允许你定义方法拦截器和切入点对代码进行干净地解耦,从而使实现功能的代码彻底的解耦出来。使用源码级的元数据,可以用类似于.Net属性的方式合并行为信息到代码中。

  • Aspects 模块提供了与 AspectJ 的集成,这是一个功能强大且成熟的面向切面编程(AOP)框架。

  • Instrumentation 模块在一定的应用服务器中提供了类 instrumentation 的支持和类加载器的实现。

  • Messaging 模块为 STOMP 提供了支持作为在应用程序中 WebSocket 子协议的使用。它也支持一个注解编程模型,它是为了选路和处理来自 WebSocket 客户端的 STOMP 信息。

  • 测试模块支持对具有 JUnit 或 TestNG 框架的 Spring 组件的测试。

程序的耦合

在软件工程中,耦合指的就是就是对象之间的依赖性。对象之间的耦合越高,维护成本越高。因此对象的设计应使类和构件之间的耦合最小。软件设计中通常用耦合度和内聚度作为衡量模块独立程度的标准。划分模块的一个准则就是高内聚低耦合。内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述的是模块内的功能联系。

解耦:降低程序间的依赖关系。在实际开发中应该做到:编译期不依赖,运行时才依赖

例如:早期我们的JDBC操作,注册驱动时,为什么不使用DriverManager的register方法,而是采用Class.forName的方式?

原因就是: 我们的类依赖了数据库的具体驱动类(MySQL),如果这时候更换了数据库品牌(比如Oracle),需要修改源码来重新数据库驱动。这显然是高耦合的。

public class JdbcDemo1 {
    public static void main(String[] args) throws  Exception{
        //1.注册驱动
        // DriverManager.registerDriver(new com.mysql.jdbc.Driver());
        Class.forName("com.mysql.jdbc.Driver");

        //2.获取连接
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/eesy","root","1234");
        //3.获取操作数据库的预处理对象
        PreparedStatement pstm = conn.prepareStatement("select * from account");
        //4.执行SQL,得到结果集
        ResultSet rs = pstm.executeQuery();
        //5.遍历结果集
        while(rs.next()){
            System.out.println(rs.getString("name"));
        }
        //6.释放资源
        rs.close();
        pstm.close();
        conn.close();
    }
}

解耦的思路:

  • 第一步:使用反射来创建对象,而避免使用new关键字;
  • 第二步:通过读取配置文件来获取要创建的对象全限定类名。

工厂模式解耦

我们在开发中,有些依赖关系是必须的,有些依赖关系可以通过优化代码来解除的。

public class AccountServiceImpl implements IAccountService { 
    private IAccountDao accountDao = new AccountDaoImpl(); 
}

上面的代码表示: 业务层调用持久层,并且此时业务层在依赖持久层的接口和实现类。如果此时没有持久层实现类,编译将不能通过。这种编译期依赖关系,应该在我们开发中杜绝。我们需要优化代码解决。

在实际开发中我们可以把三层的对象都使用配置文件配置起来,当启动服务器应用加载的时候,让一个类中的方法通过读取配置文件,把这些对象创建出来并存起来(单例)。在接下来的使用的时候,直接拿过来用就好了。 这个读取配置文件,创建和获取三层对象的类就是工厂。

step1:创建bean.properties文件,加入键值对,配置文件可以是xml也可以是properties,配置的内容:唯一标识=全限定类名(key=value)。

accountDao=io.github.tjtulong.dao.impl.AccountDaoImpl

step2:创建Bean工厂,使用单例模式:

public class BeanFactory {
    //定义一个Properties对象
    private static Properties props;

    //定义一个Map,用于存放我们要创建的对象。我们把它称之为容器
    private static Map<String,Object> beans;

    //使用静态代码块为Properties对象赋值
    static {
        try {
            //实例化对象
            props = new Properties();
            //获取properties文件的流对象
            InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
            props.load(in);
            //实例化容器
            beans = new HashMap<String,Object>();
            //取出配置文件中所有的Key
            Enumeration keys = props.keys();
            //遍历枚举
            while (keys.hasMoreElements()){
                //取出每个Key
                String key = keys.nextElement().toString();
                //根据key获取value
                String beanPath = props.getProperty(key);
                //反射创建对象
                Object value = Class.forName(beanPath).newInstance();
                //把key和value存入容器中
                beans.put(key,value);
            }
        }catch(Exception e){
            throw new ExceptionInInitializerError("初始化properties失败!");
        }
    }

    /**
     * 根据bean的名称获取对象
     * @param beanName
     * @return
     */
    public static Object getBean(String beanName){
        return beans.get(beanName);
    }
}

step3:在servise层中解耦dao对象。

public class AccountServiceImpl implements IAccountService {

    //private IAccountDao accountDao = new AccountDaoImpl();

    private IAccountDao accountDao = (IAccountDao)BeanFactory.getBean("accountDao");
    accountDao.saveAccount();
}

控制反转

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。

在使用工厂方法之前,我们在获取对象时,都是采用new的方式,是主动的
enter description here

现在我们获取对象时,同时跟工厂(容器)获取,有工厂为我们查找或者创建对象,是被动的
enter description here

这种被动接收的方式(由主动变被动)获取对象的思想就是控制反转,它是spring框架的核心之一。ioc的作用: 削减计算机程序的耦合(解除我们代码中的依赖关系)

使用spring的IOC解决程序耦合

环境准备----引入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
</dependencies>

enter description here
其中spring-jcl包含commons-logging的jar包

配置xml文件

在Spring提供的文档中有所需要的依赖,只需要在文档中搜索即可。

<?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">
    <!--把对象创建交给spring来管理,spring会自动解析-->
    <bean id="accountService" class="io.github.tjtulong.service.impl.AccountServiceImpl"></bean>

    <bean id="accountDao" class="io.github.tjtulong.dao.impl.AccountDaoImpl"></bean>
</beans>

获取对象

客户端代码:

/**
 * 模拟一个表现层,用于调用业务层
 */
public class Client {

    /**
     * 获取Spring的Ioc核心容器,根据id获取对象
     * @param args
     */
    public static void main(String[] args) {
        // 获取核心容器对象(相当于一个map)
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");

        // 根据id获取bean对象
        IAccountService as = (IAccountService) ac.getBean("accountService");
        IAccountDao adao = ac.getBean("accountDao",IAccountDao.class);

        System.out.println(as);
        System.out.println(adao);
        //as.saveAccount();
    }
}

基于XML的IOC

spring中工厂的类结构图:

enter description here

ApplicationContext 接口的实现类

  1. ClassPathXmlApplicationContext:它是从类的根路径下加载配置文件,不在的话加载不了,推荐使用这种方法。
  2. FileSystemXmlApplicationContext:它是从磁盘路径上加载配置文件,配置文件可以在磁盘的任意位置。
  3. AnnotationConfigApplicationContext:当我们使用注解配置容器对象时,需要使用此类来创建spring 容器。它用来读取注解。

核心容器的两个接口

  • ApplicationContext:它在创建核心容器时,创建对象采取的策略是采用立即加载的方式,也就是说,只要一读取完配置文件就马上创建配置文件中配置的对象
    • 单例对象适用
    • 开发中常采用此接口
  • BeanFactory:它在构建核心容器时,创建对象的策略是采用延迟加载的方式,什么时候获取id对象了,什么时候就创建对象。
    • 多例对象适用
--------BeanFactory----------
Resource resource = new ClassPathResource("bean.xml");
BeanFactory factory = new XmlBeanFactory(resource);
IAccountService as  = (IAccountService)factory.getBean("accountService");
System.out.println(as);

创建Bean的三种方式

1.使用默认构造函数创建

在spring的配置文件中,使用id和class属性之后,且没有其他属性和标签时,采用的就是默认构造函数创建bean对象,此时如果类中没有默认构造函数,则对象无法创建。

<bean id = "accountService" class ="io.github.tjtulong.service.impl.AccountServiceImpl">
</bean>

2.使用普通工厂中的方法创建对象(使用某个类中的方法创建对象,并存入spring容器)

/**
*模拟一个工厂类,该类可能存在于jar包中,无法通过修改源码的方式来提供默认构造函数
*/
public class InstanceFactory {
    public IAccountService getAccountService() {
        return new AccountServiceImpl();
    }
}

配置方式如下:

<bean id = "instanceFactory" class = "io.github.tjtulong.factory.InstanceFactory"></bean>
<bean id = "accountService" factory-bean="instanceFactory" factory-method="getAccountService"></bean>

3.使用工厂中的静态方法创建对象(使用某个类中的静态方法创建对象,并存入spring容器)

public class StaticFactory {
    public  static IAccountService getAccountService() {
        return new AccountServiceImpl();
    }
}

配置方式如下:

<bean id = "accountService" class = "io.github.tjtulong.factory.StaticFactory" factory-method="getAccountService"></bean>

bean的作用范围

bean标签的scope属性作用:用于指定bean的作用范围。

取值:常用的就是单例和多例

  • singleton : 单例的(default)
  • prototype : 多例的
  • request : WEB项目中,Spring创建一个Bean的对象,将对象存入到request域中
  • session :WEB项目中,Spring创建一个Bean的对象,将对象存入到session域中
  • global-session : 作用于集群的会话范围(全局会话范围),当不是集群范围时,它就是session

bean对象的生命周期

单例对象:

  • 出生:当容器创建时发生
  • 活着:只要容器还在对象就一直活着
  • 死亡:容器销毁,对象消亡
    总结:单例对象的声明周期和容器相同
public class AccountServiceImpl implements IAccountService {

    //private IAccountDao accountDao;


    public AccountServiceImpl() {
        System.out.println("创建了service对象");
    }

    public void saveAccount() {
        System.out.println("调用了service");
    }

    public void init(){
        System.out.println("初始化...");
    }

    public void destroy(){
        System.out.println("对象销毁...");
    }
}

通过bean.xml可以设置初始化和销毁方法

<bean id="accountService" class="io.github.tjtulong.service.impl.AccountServiceImpl"
    scope="singleton" init-method="init" destroy-method="destroy"></bean>

多例对象:
每次访问对象时,都会重新创建对象实例。

  • 出生:当我们使用对象时spring框架为我们创建
  • 活着:对象只要是在使用过程中就活着
  • 死亡:当对象长时间不用,且没有别的对象引用时,由Java的GC回收

依赖注入

Dependency Injection

IOC的作用:减低程序间的耦合(即依赖关系)

在当前类需要用到其他类的对象,由spring为我们提供,而我们在配置文件中说明依赖关系的维护,这种方式就称为依赖注入

能注入的数据包括三类:

  • 基本类型和String
  • 其他bean类型(在配置文件中或者注解中配置过的bean)
  • 复杂类型/集合类型

使用构造函数注入

使用的标签:constructor-arg
标签所在位置:bean标签的内部

标签中的属性:

  • type: 用于指定要注入的数据类型,该类型也是构造函数中某个或某些参数的类型
  • index: 用于指定要注入的数据给构造函数中指定索引位置的参数赋值,索引的位置时从0开始
  • name(常用): 用于指定给构造函数中指定名称的参数赋值
  • value: 用于提供基本类型和String类型的数据
  • ref: 用于指定其他的bean类型数据。它指的就是在spring的IOC核心容器出现过的bean对象
public class AccountServiceImpl implements IAccountService {

    // 如果是经常变化的数据,并不适用依赖注入
    private String name;
    private Integer age;
    private Date birthday;

    public AccountServiceImpl(String name, Integer age, Date birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }

    public void saveAccount() {
        System.out.println("调用了service"+name+","+age+","+birthday);
    }
}

配置文件:

<?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">

    <bean id="accountService" class="io.github.tjtulong.service.impl.AccountServiceImpl">
        <constructor-arg name="name" value="xiao"></constructor-arg>
        <constructor-arg name="age" value="22"></constructor-arg>
        <constructor-arg name="birthday" ref="now"></constructor-arg>
    </bean>

    <!--配置一个日期对象-->
    <bean id="now" class="java.util.Date"></bean>
</beans>

特点:在获取bean对象时,注入数据是必须的操作,否则无法操作成功
弊端:改变了bean对象的实例化方式,使我们在用不到这些数据的情况下也必须提供带参构造函数,因此开发中较少使用此方法,除非避无可避

set方法注入

使用的标签:property
出现的位置:bean标签的内部

标签的属性:

  • name: 用于指定注入时所使用的set方法;
  • value: 用于提供基本类型和String类型的数据;
  • ref: 用于指定其他的bean类型数据,它指的就是在Spring容器中出现过的bean对象。
public class AccountServiceImpl implements IAccountService {

    // 如果是经常变化的数据,并不适用依赖注入
    private String name;
    private Integer age;
    private Date birthday;

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public void saveAccount() {
        System.out.println("调用了service"+name+","+age+","+birthday);
    }
}

配置文件:

<!--set方法注入-->
<bean id="accountService" class="io.github.tjtulong.service.impl.AccountServiceImpl">
    <property name="name" value="xiao"></property>
    <property name="age" value="22"></property>
    <property name="birthday" ref="now"></property>
</bean>
<!--配置一个日期对象-->
<bean id="now" class="java.util.Date"></bean>

优势:创建对象时没有明确的限制,可以直接使用默认构造函数(更常用);
弊端:如果有某个成员必须有值,是有可能set方法没有执行。

集合类型的注入

用于给List结构集合注入的标签:

  • list
  • array
  • set

用于给map结构集合注入的标签:

  • map
  • properties

结构相同,标签可以互换,因此开发中只要记住两组标签即可。
类结构如下:

    public class AccountServiceImpl implements IAccountService {
    
        private String[] myStrs;
        private List<String> myList;
        private Set<String> mySet;
        private Map<String, String> myMap;
        private Properties myProps;
    
        public void setMyStrs(String[] myStrs) {
            this.myStrs = myStrs;
        }
    
        public void setMyList(List<String> myList) {
            this.myList = myList;
        }
    
        public void setMySet(Set<String> mySet) {
            this.mySet = mySet;
        }
    
        public void setMyMap(Map<String, String> myMap) {
            this.myMap = myMap;
        }
    
        public void setMyProps(Properties myProps) {
            this.myProps = myProps;
        }
    
        public void  saveAccount() {
            System.out.println(Arrays.toString(myStrs));
            System.out.println(myList);
            System.out.println(myMap);
            System.out.println(mySet);
            System.out.println(myProps);
        }    
    }

配置文件:

      <bean id = "accountService" class = "io.github.tjtulong.service.impl.AccountServiceImpl">
            <!--以下三个标签是等价的,set未列出-->
            <property name="myList">
                <list>
                    <value>aaa</value>
                    <value>bbb</value>
                </list>
            </property>
    
            <property name="myStrs">
                <array>
                    <value>aaa</value>
                    <value>bbbb</value>
                </array>
            </property>
    
            <property name="mySet">
                <array>
                    <value>aaa</value>
                    <value>bbbb</value>
                </array>
            </property>
    
            <!--以下两种方式等价-->
            <property name="myMap">
                <map>
                    <!--以下两种配置方式都可以-->
                    <entry key="testA" value="aaa"></entry>
                    <entry key="testA">
                        <value>bbb</value>
                    </entry>
                </map>
            </property>
    
            <property name="myProps">
                <props>
                    <prop key="testB">bbb</prop>
                </props>
            </property>
        </bean>

基于注解的IOC

添加配置文件,告知spring在创建容器时要扫描的包,配置所需的标签不是在bean约束中,而是一个名称为context的名称空间和约束中,完整配置如下:

<?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="io.github.tjtulong"></context:component-scan>
</beans>

用于创建对象

作用:等同于xml配置文件中编写一个<bean>标签

@Component
形式:@Component(value=" ")@Component(" ")
作用:用于把当前类对象存入spring容器中
属性value : 用于指定bean的id,当我们不写的时候,它的默认值是当前类名,且首字母改小写,当值只有一个的时候可以省略。

@Component
public class AccountServiceImpl implements IAccountService {

    public void saveAccount() {
        System.out.println("调用了service");
    }
}

以下三个注解的作用与@Component完全一样,它们是spring提供的更明确的划分,使三层对象更加清晰

  • @Controller 用于表现层
  • @Service 用于业务层
  • @Repository 用于持久层

用于注入数据

作用:等同于在<bean>标签中写一个<property>标签

@Autowired
作用:自动按照类型注入,只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功如果IOC容器中。如果没有任何bean的类型和要注入的变量类型匹配,则报错。如果有多个匹配,先按类型查找,再按变量名称匹配

出现位置:可以是变量上,也可以是方法上,
细节:在使用注解注入时,set方法就不是必须的了

@Qualifier
作用:在按照类型注入的基础上再按照名称注入,它在给类成员注入时不能单独使用,但是在给方法参数注入时可以。
属性value : 用于指定注入的bean的id

@Resource
作用:直接按照bean的id注入,可以直接使用
属性name : 用于指定bean的id
等同于**@Autowired+@Qualifier**

以上三个注入都只能注入其他bean类型的数据,而基本类型和String类型的数据无法使用上述注解实现。另外,集合类型的注入只能通过xml配置文件实现

@Value
作用:用于注入基本类型和String类型的数据
属性value : 用于指定数据的值,它可以使用spring中Spel(即spring的el表达式)
Spel的写法:${表达式}

实例示例代码,AccountDaoImpl1AccountDaoImpl2实现接口IAccountDao,两个类中分别实现了不同的saveAccount()方法,AccountServiceImpl实现接口IAccountService,其中调用了IAccountDao接口。AccountServiceImpl通过注解关键字@Autowired去spring容器中寻找accountDao,再根据Qualifier配置的value找到两个dao的实现类中与之相匹配的Repository的值。

    @Service("accountService")
    public class AccountServiceImpl implements IAccountService {
    
        @Autowired
        @Qualifier("accountDao2")
        private IAccountDao accountDao = null;

        public void  saveAccount() {
            accountDao.saveAccount();
        }    
    }
    
    @Repository("accountDao1")
    public class AccountDaoImpl implements IAccountDao {    
        public void  saveAccount() {
            System.out.println("对象创建了111");
        }
   
    }
    
    @Repository("accountDao2")
    public class IAccountDaoImpl2 implements IAccountDao{
    
        public void  saveAccount() {
            System.out.println("对象创建了222");
        }
    }

客户端:

    public static void main(String[] args) {
            //1.获取核心容器对象
            ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
            //2.根据id获取Bean对象
            IAccountService as  = (IAccountService)ac.getBean("accountService");
    
            as.saveAccount();
        }
    }

用于改变范围

作用:等同于在<bean>标签中使用scope属性

@Scope
作用:用于指定bean的作用范围
属性value : 指定范围的取值,同xml中值,常用为singleton, prototype

@Component
@Scope("prototype")

用于生命周期

作用:等同于在<bean>标签中使用init-method和destroy-method

@PreDestory
作用:用于指定销毁方法,放在方法上。
@PostContrust
作用:用于指定初始化方法,放在方法上。

Spring新注解

待改进问题

我们发现,之所以我们现在离不开xml配置文件,是因为我们有一句很关键的配置:

<!-- 告知spring框架在,读取配置文件,创建容器时,扫描注解,依据注解创建对象,并存入容器中 --> 
<context:component-scan base-package="io.github.tjtulong"></context:component-scan>

如果这句也能用注解配置,那么我们就离脱离xml文件又进了一步。 另外,数据源和JdbcTemplate的配置也需要靠注解来实现,因为无法在jar包内部的类中加入注解进行依赖注入。

<!--配置QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <!--注入数据源-->
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </bean>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--连接数据库的必备信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property>
        <property name="user" value="root"></property>
        <property name="password" value="1234"></property>
    </bean>

@Configuration

作用: 用于指定当前类是一个spring配置类,当创建容器时会从该类上加载注解。获取容器时需要使用AnnotationApplicationContext(有@Configuration注解的类.class)。
属性: value:用于指定配置类的字节码。

细节:当配置类作为AnnotationConfigApplicationContext对象创建的参数时,该注解可以不写。

@Configuration
public class SpringConfiguration {
}

@ComponentScan

作用:用于通过注解指定spring在创建容器时要扫描的包;
属性:value:它和basePackages的作用是一样的,都是用于指定创建容器时要扫描的包。我们使用此注解就等同于在xml中配置了:
<context:component-scan base-package="com.tjtulong"></context:component-scan>

@Configuration
@ComponentScan("com.tjtulong")
public class SpringConfiguration {
}

@Bean

作用:用于把当前方法的返回值作为bean对象存入spring的ioc容器中
属性: name:用于指定bean的id。当不写时,默认值是当前方法的名称

细节:当我们使用注解配置方法时,如果方法有参数,spring框架会去容器中查找有没有可用的bean对象,查找的方式和Autowired注解的作用是一样的。

     /**
     * 用于创建一个QueryRunner对象
     * @param dataSource
     * @return
     */
    @Bean(name="runner")
    @Scope("prototype")
    public QueryRunner createQueryRunner(DataSource dataSource){
        return new QueryRunner(dataSource);
    }

@Import

作用:用于导入其他的配置类,在引入其他配置类时,可以不用再写@Configuration注解;
属性value[]:用于指定其他配置类的字节码;
当我们使用Import的注解之后,有Import注解的类就父配置类,而导入的都是子配置类。

父配置类:

@Configuration
@ComponentScan("com.tjtulong")
@Import(JdbcConfig.class)
public class SpringConfiguration {
}

子配置类:

@Configuration
public class JdbcConfig{ }

@PropertySource

作用:用于加载.properties文件中的配置。例如我们配置数据源时,可以把连接数据库的信息写到properties配置文件中,就可以使用此注解指定properties配置文件的位置。
属性value[]:用于指定properties文件位置。如果是在类路径下,需要写上classpath

public class JdbcConfig {

    @Value("${jdbc.driver}")
    private String driver;

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;
	
	@Bean(name="datasourse")
    public DataSource createDataSource(){
        try {
            ComboPooledDataSource ds = new ComboPooledDataSource();
            ds.setDriverClass(driver);
            ds.setJdbcUrl(url);
            ds.setUser(username);
            ds.setPassword(password);
            return ds;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }
}

通过注解获取容器

AnnotationConfigApplicationContext代替ClassPathXmlApplicationContext。

ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);

Spring操作持久层

持久层总图

enter description here

JdbcTemplate是spring框架中提供的一个对象,是对原始JDBC API对象的简单封装。

基本操作

设置坐标

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.44</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>

实体类

public class Account implements Serializable {
    private Integer id;
    private String name;
    private Float money;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Float getMoney() {
        return money;
    }

    public void setMoney(Float money) {
        this.money = money;
    }

    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", money=" + money +
                '}';
    }
}

JdbcTemplate操作数据库

public class JdbcTemplateDemo1 {
    public static void main(String[] args) {
        //准备数据源:spring的内置数据源
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/mydb1?characterEncoding=utf8");
        ds.setUsername("root");
        ds.setPassword("xxxxxx");

        //1.创建JdbcTemplate对象
        JdbcTemplate jt = new JdbcTemplate();
        //给jt设置数据源
        jt.setDataSource(ds);
        //2.执行操作
        jt.execute("insert into account(name,money)values('ddd',1000)");
    }
}

IOC配置

bean.xml

<?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">
    <!--配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置数据源-->
    <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/mydb1?characterEncoding=utf8"></property>
        <property name="username" value="root"></property>
        <property name="password" value="xxxxxx"></property>
    </bean>
</beans>

ioc获取对象:

public class JdbcTemplateDemo1 {
    public static void main(String[] args) {
        //1. 获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("JdbcTemplateBean.xml");
        //2. 获取对象
        JdbcTemplate jt = ac.getBean("jdbcTemplate", JdbcTemplate.class);
        //3. 执行操作
        jt.execute("insert into account(name,money) values('eee',1000)");
    }
}

CRUD操作

/**
 * JdbcTemplate的CRUD操作
 */
public class JdbcTemplateDemo3 {

    public static void main(String[] args) {
        //1.获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.获取对象
        JdbcTemplate jt = ac.getBean("jdbcTemplate",JdbcTemplate.class);
        //3.执行操作
        //保存
        jt.update("insert into account(name,money)values(?,?)","eee",3333f);
        //更新
        jt.update("update account set name=?,money=? where id=?","test",4567,7);
        //删除
        jt.update("delete from account where id=?",8);
        //查询所有
        List<Account> accounts = jt.query("select * from account where money > ?",new AccountRowMapper(),1000f);
        List<Account> accounts = jt.query("select * from account where money > ?",new BeanPropertyRowMapper<Account>(Account.class),1000f);
        for(Account account : accounts){
            System.out.println(account);
        }
        //查询一个
        List<Account> accounts = jt.query("select * from account where id = ?",new BeanPropertyRowMapper<Account>(Account.class),1);
        System.out.println(accounts.isEmpty()?"没有内容":accounts.get(0));

        //查询返回一行一列(使用聚合函数,但不加group by子句)
        Long count = jt.queryForObject("select count(*) from account where money > ?",Long.class,1000f);
        System.out.println(count);
    }
}

转账及事务问题

在项目中有一个转账事件

    @Override
    public void transfer(String sourceName, String targetName, Float money) {
        //2.1根据名称查询转出账户
        Account source = accountDao.findAccountByName(sourceName);
        //2.2根据名称查询转入账户
        Account target = accountDao.findAccountByName(targetName);
        //2.3转出账户减钱
        source.setMoney(source.getMoney() - money);
        //2.4转入账户加钱
        target.setMoney(target.getMoney() + money);
        //2.5更新转出账户
        accountDao.updateAccount(source);
		int i = 1/0;
        //2.6更新转入账户
        accountDao.updateAccount(target);
    }

当调用该方法时,会产生异常(int i = 1/0;),出现事务问题。

    @Test
    public void testTransfer(){
        as.transfer("aaa","bbb",100f);
    }

应使用数据库中的事务进行控制,前提是在同一个线程中使用的是同一个连接,需要利用ThreadLocal类进行连接和线程的绑定。

/**
 * 连接的工具类,实现连接与线程的绑定
 */
public class ConnectionUtils {
    private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * 获取当前线程上的连接
     * @return
     */
    public Connection getThreadConnection(){
        Connection conn = tl.get();

        if(conn == null){
            try {
                conn = dataSource.getConnection();
                tl.set(conn);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        return conn;
    }

    public void removeConnection() {
        tl.remove();
    }
}

事务的工具方法:

/**
 * 和事务管理相关的工具类
 */
public class TransactionManager {
    private ConnectionUtils connectionUtils;

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    /**
     * 开启事务
     */
    public void beginTransaction() {
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 提交事务
     */
    public void commit() {
        try {
            connectionUtils.getThreadConnection().commit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 回滚事务
     */
    public void rollback() {
        try {
            connectionUtils.getThreadConnection().rollback();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 释放连接
     */
    public void release() {
        try {
            connectionUtils.getThreadConnection().close();//还回连接池中
            connectionUtils.removeConnection();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Runner中不再配置连接池,而是提供连接:

List<Account> accounts = runner.query(connectionUtils.getThreadConnection(), "select * from account where name = ? ", new BeanListHandler<Account>(Account.class), accountName);

Spring的IOC配置:

<?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">
    <!-- 配置Service -->
    <bean id="accountService" class="io.github.tjtulong.service.impl.AccountServiceImpl">
        <!-- 注入dao -->
        <property name="accountDao" ref="accountDao"></property>
        <!-- 注入事务管理器 -->
        <property name="txManager" ref="txManager"></property>
    </bean>

    <!--配置Dao对象-->
    <bean id="accountDao" class="io.github.tjtulong.dao.impl.AccountDaoImpl">
        <!-- 注入QueryRunner -->
        <property name="runner" ref="runner"></property>
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

    <!--配置QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
    </bean>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--连接数据库的必备信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mydb1?characterEncoding=utf8"></property>
        <property name="user" value="root"></property>
        <property name="password" value="xxxxxx"></property>
    </bean>

    <!--配置Connection的工具类ConnectionUtils-->
    <bean id="connectionUtils" class="io.github.tjtulong.utils.ConnectionUtils">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置事务管理器-->
    <bean id="txManager" class="io.github.tjtulong.utils.TransactionManager">
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>
</beans>

转账操作的改写:

    @Override
    public void transfer(String sourceName, String targetName, Float money) {
        try {
            //1 开启事务
            txManager.beginTransaction();
            //2 执行操作
            //2.1根据名称查询转出账户
            Account source = accountDao.findAccountByName(sourceName);
            //2.2根据名称查询转入账户
            Account target = accountDao.findAccountByName(targetName);
            //2.3转出账户减钱
            source.setMoney(source.getMoney() - money);
            //2.4转入账户加钱
            target.setMoney(target.getMoney() + money);
            //2.5更新转出账户
            accountDao.updateAccount(source);
            int i = 1/0;
            //2.6更新转入账户
            accountDao.updateAccount(target);
            //3 提交事务
            txManager.commit();
        } catch (Exception e) {
            System.out.println("出错了");
            //5 回滚操作
            txManager.rollback();
        } finally {
            //6 释放连接
            txManager.release();
        }
    }

动态代理

详细可参考本人另一篇博文:https://blog.csdn.net/TJtulong/article/details/89813735

特点:字节码随用随创建,随用随加载
作用:不修改源码的基础上对方法增强

基于接口的动态代理

涉及的类:Proxy
提供者:JDK官方
创建代理对象:使用Proxy类中的newProxyInstance方法

创建代理对象的要求:被代理类最少实现一个接口,如果没有则不能使用。

newProxyInstance方法的参数:
ClassLoader:类加载器 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。
Class[]:字节码数组。它是用于让代理对象和被代理对象有相同方法。
InvocationHandler:用于提供增强的代码它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。此接口的实现类都是谁用谁写。

public class Client {

    public static void main(String[] args) {
        final Producer producer = new Producer();
       IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
                producer.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 作用:执行被代理对象的任何接口方法都会经过该方法
                     * 方法参数的含义
                     * @param proxy   代理对象的引用
                     * @param method  当前执行的方法
                     * @param args    当前执行方法所需的参数
                     * @return        和被代理对象方法有相同的返回值
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //提供增强的代码
                        Object returnValue = null;

                        //1.获取方法执行的参数
                        Float money = (Float)args[0];
                        //2.判断当前方法是不是销售
                        if("saleProduct".equals(method.getName())) {
                            returnValue = method.invoke(producer, money*0.8f);
                        }
                        return returnValue;
                    }
                });
        proxyProducer.saleProduct(10000f);
    }
}

基于子类的动态代理

需要使用cglib类库

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2.2</version>
</dependency>

案例:

/**
 * 一个生产者
 */
public class Producer{

    /**
     * 销售
     * @param money
     */
    public void saleProduct(float money){
        System.out.println("销售产品,并拿到钱:"+money);
    }

    /**
     * 售后
     * @param money
     */
    public void afterService(float money){
        System.out.println("提供售后服务,并拿到钱:"+money);
    }
}

使用子类的动态代理:

public class Client {
    public static void main(String[] args) {
        final Producer producer = new Producer();

        Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
            /**
             * 执行被代理对象的任何方法都会经过该方法
             * @param proxy
             * @param method
             * @param args
             * @param methodProxy
             * @return
             * @throws Throwable
             */
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                Object returnValue = null;

                Float money = (Float)args[0];
                if("saleProduct".equals(method.getName())) {
                    returnValue = method.invoke(producer, money*0.8f);
                }
                return returnValue;
            }
        });

        cglibProducer.saleProduct(10000);
    }
}

使用动态代理处理事务

创建一个动态代理工厂,使用动态代理对原有的Service方法进行加强,增加事务。

/**
 * 用于创建Service的代理对象的工厂
 */
public class BeanFactory {
    private IAccountService accountService;

    public void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }

    private TransactionManager txManager;

    public final void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }

    public IAccountService getAccountService() {
        return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
                accountService.getClass().getInterfaces(),
                new InvocationHandler() {
                    // 添加事务的支持
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object rtValue = null;
                        try {
                            //1 开启事务
                            txManager.beginTransaction();
                            //2 执行操作
                            rtValue = method.invoke(accountService, args);
                            //3 提交事务
                            txManager.commit();
                        } catch (Exception e) {
                            System.out.println("出错了");
                            //5 回滚操作
                            txManager.rollback();
                        } finally {
                            //6 释放连接
                            txManager.release();
                        }
                        return rtValue;
                    }
                });
    }
}

修改配置文件:

<!--配置代理的Service-->
<bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>
<bean id="beanFactory" class="io.github.tjtulong.factory.BeanFactory">
    <property name="accountService" ref="accountService"></property>
    <property name="txManager" ref="txManager"></property>
</bean>

在客户端调用时,注入的Service应是动态代理获得的:

@Autowired
@Qualifier("proxyAccountService")
private  IAccountService as;

AOP

AOP概述

AOP:全称是Aspect Oriented Programming,即面向切面编程
enter description here
简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强。

作用:在程序运行期间,不修改源码对已有方法进行增强。
优势:减少重复代码、提高开发效率、维护方便。

AOP术语

  • Joinpoint(连接点):类里面可以被增强的方法,这些方法称为连接点;
  • Pointcut(切入点):所谓切入点是指我们要对哪些Joinpoint进行拦截的定义;
  • Advice(通知/增强):所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能);
    enter description here
  • Aspect(切面):是切入点和通知(引介)的结合;
  • Introduction(引介):引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field;
  • Target(目标对象):代理的目标对象(要增强的类);
  • Weaving(织入):是把增强应用到目标的过程;
  • Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类。

基于XML的AOP

配置步骤

目标对象为:

public class AccountServiceImpl implements IAccountService {

    @Override
    public void saveAccount() {
        System.out.println("执行了保存");
    }

    @Override
    public void updateAccount(int i) {
        System.out.println("执行了更新" + i);
    }

    @Override
    public int deleteAccount() {
        System.out.println("执行了删除");
        return 0;
    }
}

想要为Target方法执行之前添加日志:

/**
 * 用于记录日志的工具类,提供了公共的代码
 */
public class Logger {
    //用于向控制台打印日志,在切入点方法执行之前执行
    public void printLog(){
        System.out.println("Logger类中的printLog方法开始记录日志...");
    }
}

spring中基于XML的AOP配置步骤:

  1. 把通知Bean也交给Spring来管理;
  2. 使用aop:config标签表明开始AOP的配置;
  3. 使用aop:aspect标签表明配置切面
    id属性:是给切面提供一个唯一标识
    ref属性:是指定通知类bean的Id
  4. aop:aspect标签的内部使用对应标签来配置通知的类型;
  • aop:before:表示配置前置通知
  • method属性:用于指定Logger类中哪个方法是前置通知
  • pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强

下面的配置对saveAccount()方法进行了增强

<?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: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/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--配置IOC,配置Service对象-->
    <bean id="accountService" class="io.github.tjtulong.impl.AccountServiceImpl"></bean>

    <!--配置Logger类-->
    <bean id="logger" class="io.github.tjtulong.utils.Logger"></bean>

    <!--配置AOP-->
    <aop:config>
        <!--配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <aop:before method="printLog" pointcut="execution(public void io.github.tjtulong.impl.AccountServiceImpl.saveAccount())"></aop:before>
        </aop:aspect>
    </aop:config>

</beans>

切入点表达式

enter description here

四种通知(增强)类型

四种通知位置:

public class Logger {
    /**
     * 前置通知
     */
    public void beforePrintLog() {
        System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
    }

    /**
     * 后置通知
     */
    public void afterReturningPrintLog() {
        System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
    }

    /**
     * 异常通知
     */
    public void afterThrowingPrintLog() {
        System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
    }

    /**
     * 最终通知
     */
    public void afterPrintLog() {
        System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");
    }
}

配置文件:

    <!--配置AOP-->
    <aop:config>
        <!--配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置前置通知-->
            <aop:before method="beforePrintLog" pointcut="execution(public void io.github.tjtulong.impl.AccountServiceImpl.saveAccount())"></aop:before>

            <!--配置后置通知-->
            <aop:after-returning method="afterReturningPrintLog" pointcut="execution(public void io.github.tjtulong.impl.AccountServiceImpl.saveAccount())"></aop:after-returning>

            <!--配置异常通知-->
            <aop:after-throwing method="afterThrowingPrintLog" pointcut="execution(public void io.github.tjtulong.impl.AccountServiceImpl.saveAccount())"></aop:after-throwing>

            <!--配置最终通知-->
            <aop:after method="afterPrintLog" pointcut="execution(public void io.github.tjtulong.impl.AccountServiceImpl.saveAccount())"></aop:after>
        </aop:aspect>
    </aop:config>

执行结果:

前置通知Logger类中的beforePrintLog方法开始记录日志了。。。
执行了保存
后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。
最终通知Logger类中的afterPrintLog方法开始记录日志了。。。

注:异常通知和最终通知只能出现一个。

可以把切入点表达式提取出来,简化配置:

    <!--配置AOP-->
    <aop:config>
        <!-- 配置切入点表达式 id属性用于指定表达式的唯一标识。expression属性用于指定表达式内容
              此标签写在aop:aspect标签内部只能当前切面使用。
              它还可以写在aop:aspect外面,此时就变成了所有切面可用
          -->
        <aop:pointcut id="pt1" expression="execution(* io.github.tjtulong.service.impl.*.*(..))"></aop:pointcut>
        <!--配置切面 -->
        <aop:aspect id="logAdvice" ref="logger">
            <aop:before method="beforePrintLog" pointcut-ref="pt1" ></aop:before>
            <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>
            <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>
        </aop:aspect>
    </aop:config>

配置环绕通知

配置环绕通知:它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。

Spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。该方法与自己写Handler类的方法类似,较为灵活

<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>

环绕通知方法:

    /**
     * 环绕通知
     */
    public Object aroundPrintLog(ProceedingJoinPoint pjp){
        Object rtValue = null;
        try {
            Object[] args = pjp.getArgs();//得到方法执行所需的参数
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。");
            rtValue = pjp.proceed(args);//切入点方法
            return rtValue;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }finally {
            return rtValue;
        }
    }

基于注解的AOP

若使用注解实现AOP,则不需要再在配置文件中加入过多代码,只需要加入包扫描的注释即可:

<?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:aop="http://www.springframework.org/schema/aop"
       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/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 配置spring创建容器时要扫描的包-->
    <context:component-scan base-package="io.github.tjtulong"></context:component-scan>

    <!-- 配置spring开启注解AOP的支持 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

在切面类上加入注解@Aspect
在方法上加上标注以确定通知类型:

  • 前置通知:@Before()
  • 后置通知:@AfterReturning()
  • 异常通知:@AfterThrowing()
  • 最终通知:@After()
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {

    @Pointcut("execution(* io.github.tjtulong.service.impl.*.*(..))")
    private void pt1(){}

    /**
     * 前置通知
     */
    @Before("pt1()")
    public  void beforePrintLog(){
        System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
    }

    /**
     * 后置通知
     */
   @AfterReturning("pt1()")
    public  void afterReturningPrintLog(){
        System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
    }
    /**
     * 异常通知
     */
    @AfterThrowing("pt1()")
    public  void afterThrowingPrintLog(){
        System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
    }

    /**
     * 最终通知
     */
    @After("pt1()")
    public  void afterPrintLog(){
        System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");
    }
}

事务

事务是指数据库中的一组逻辑操作,这个操作的特点就是在该组逻辑中,所有的操作要么全部成功,要么全部失败。在各个数据具有特别紧密的联系时,最好是使用数据库的事务来完成逻辑处理。例如转账业务就是一个事务。

MySQL数据库默认事务是自动提交的,也就是发一条SQL数据库就执行一条。如果想将多条SQL放置在一个事务中执行,就必须使用如下语句:

start  transaction
sql1
sql2
…
commit

当开启事务后(start transaction),无论数据库是否对其中的多条SQL语句是否执行成功,只要没有提交事务(commit),都会将之前执行的SQL进行回滚。使数据回到开启事务之前的值。

事务的四大特性

ACID

  1. 原子性(Atomicity)
    原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚;事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
  2. 一致性(Consistency)
    一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。拿转账来说,假设用户A和用户B两者钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户钱相加起来应该还得是5000,这就是事务的一致性。
  3. 隔离性(Isolation)
    隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。 关于事务的隔离性数据库提供了多种隔离级别。
    即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
  4. 持久性(Durability)
    持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

事务的隔离性

当多个线程都开启事务操作数据库中的数据时,数据库系统要能进行隔离操作,以保证各个线程获取数据的准确性。如果不考虑事务的隔离性,会发生的几种问题:

  1. 脏读
    脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。
  2. 不可重复读
    不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
    例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发送生了不可重复读。
  3. 虚读(幻读)
    幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。

不可重复读和幻读的区别:不可重复读是读取到了另一事务的更新;而幻读是读取到了另一事务的插入(MySQL中无法测试到幻读);

四大隔离级别

4个等级的事务隔离级别,在相同数据环境下,使用相同的输入,执行相同的工作,根据不同的隔离级别,可以导致不同的结果。不同事务隔离级别能够解决的数据并发问题的能力是不同的。

  1. SERIALIZABLE(串行化
    不会出现任何并发问题,因为它是对同一数据的访问是串行的,非并发访问的;
    性能最差;
  2. REPEATABLE READ(可重复读)(MySQL)
    防止脏读和不可重复读,不能处理幻读问题;
    性能比SERIALIZABLE好
  3. READ COMMITTED(读已提交数据)(Oracle)
    防止脏读,没有处理不可重复读,也没有处理幻读;
    性能比REPEATABLE READ好;
  4. READ UNCOMMITTED(读未提交数据
    可能出现任何事务并发问题;
    性能最好;

MySQL的默认隔离级别为REPEATABLE READ

[参考] https://www.cnblogs.com/fjdingsd/p/5273008.html

事务控制

通过手写的事务管理器txManager可以利用XML配置AOP来控制事务:

<?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: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/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

     <!-- 配置Service -->
    <bean id="accountService" class="io.github.tjtulong.service.impl.AccountServiceImpl">
        <!-- 注入dao -->
        <property name="accountDao" ref="accountDao"></property>
    </bean>

    <!--配置Dao对象-->
    <bean id="accountDao" class="io.github.tjtulong.dao.impl.AccountDaoImpl">
        <!-- 注入QueryRunner -->
        <property name="runner" ref="runner"></property>
        <!-- 注入ConnectionUtils -->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

    <!--配置QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--连接数据库的必备信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property>
        <property name="user" value="root"></property>
        <property name="password" value="xxxxxx"></property>
    </bean>

    <!-- 配置Connection的工具类 ConnectionUtils -->
    <bean id="connectionUtils" class="io.github.tjtulong.utils.ConnectionUtils">
        <!-- 注入数据源-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 配置事务管理器-->
    <bean id="txManager" class="io.github.tjtulong.utils.TransactionManager">
        <!-- 注入ConnectionUtils -->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

    <!--配置aop-->
    <aop:config>
        <!--配置通用切入点表达式-->
        <aop:pointcut id="pt1" expression="execution(* io.github.tjtulong.service.impl.*.*(..))"></aop:pointcut>
        <aop:aspect id="txAdvice" ref="txManager">
            <!--配置前置通知:开启事务-->
            <aop:before method="beginTransaction" pointcut-ref="pt1"></aop:before>
            <!--配置后置通知:提交事务-->
            <aop:after-returning method="commit" pointcut-ref="pt1"></aop:after-returning>
            <!--配置异常通知:回滚事务-->
            <aop:after-throwing method="rollback" pointcut-ref="pt1"></aop:after-throwing>
            <!--配置最终通知:释放连接-->
            <aop:after method="release" pointcut-ref="pt1"></aop:after>
        </aop:aspect>
    </aop:config>
</beans>

Spring中事务控制的API

spring框架也为我们提供了一组事务控制的接口。,这组接口是在spring-tx-5.0.2.RELEASE.jar中。spring的事务控制都是基于AOP的,它既可以使用编程的方式实现,也可以使用配置的方式实现。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>

Spring 事务抽象的关键是由 org.springframework.transaction.PlatformTransactionManager接口定义,如下所示:
enter description here

真正管理事务的对象:

  • org.springframework.jdbc.datasource.DataSourceTransactionManager 使用Spring JDBC或iBatis进行持久化数据时使用
  • org.springframework.orm.hibernate5.HibernateTransactionManager 使用Hibernate版本进行持久化数据时使用

TransactionDefinition是在 Spring 中事务支持的核心接口,它的定义如下:
enter description here

TransactionStatus接口为事务代码提供了一个简单的方法来控制事务的执行和查询事务状态:
enter description here

Spring的事务控制

基于XML

spring中基于XML的声明式事务控制配置步骤:

  1. 配置事务管理器;
  2. 配置事务的通知;
    此时我们需要导入事务的约束tx名称空间和约束,同时也需要aop的
    使用tx:advice标签配置事务通知
    属性:id:给事务通知起一个唯一标识;transaction-manager:给事务通知提供一个事务管理器引用
  3. 配置AOP中的通用切入点表达式
  4. 建立事务通知和切入点表达式的对应关系
  5. 配置事务的属性:是在事务的通知tx:advice标签的内部

配置事务的属性

  • isolation:用于指定事务的隔离级别,默认值是DEFAULT,表示使用数据库的默认隔离级别。
  • propagation:用于指定事务的传播行为。默认值是REQUIRED,表示一定会有事务,增删改的选择;查询方法可以选择SUPPORTS。
  • read-only:用于指定事务是否只读,只有查询方法才能设置为true;默认值是false,表示读写。
  • timeout:用于指定事务的超时时间,默认值是-1,表示永不超时;如果指定了数值,以秒为单位。
  • rollback-for:用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,事务不回滚;没有默认值,表示任何异常都回滚。
  • no-rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,产生其他异常时事务回滚;没有默认值,表示任何异常都回滚。
<?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:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置业务层-->
    <bean id="accountService" class="tx.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao"></property>
    </bean>

    <!-- 配置账户的持久层-->
    <bean id="accountDao" class="tx.dao.impl.AccountDaoImpl">
        <property name="dataSource" ref="dataSource"></property>
    </bean>


    <!-- 配置数据源-->
    <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/mydb1?characterEncoding=utf8"></property>
        <property name="username" value="root"></property>
        <property name="password" value="xxxxxx"></property>
    </bean>

    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 配置事务的通知-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="*" propagation="REQUIRED" read-only="false"/>
            <tx:method name="find*" propagation="SUPPORTS" read-only="true"></tx:method>
        </tx:attributes>
    </tx:advice>

    <!-- 配置aop-->
    <aop:config>
        <!-- 配置切入点表达式-->
        <aop:pointcut id="pt1" expression="execution(* tx.service.impl.*.*(..))"></aop:pointcut>
        <!--建立切入点表达式和事务通知的对应关系 -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
    </aop:config>
</beans>

注解方式

spring中基于注解 的声明式事务控制配置步骤:

  1. 配置事务管理器
  2. 开启spring对注解事务的支持
  3. 在需要事务支持的地方使用@Transactional注解
<!-- 开启spring对注解事务的支持-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>

需要事务支持的方法:

    //需要的是读写型事务配置
    @Transactional(propagation= Propagation.REQUIRED,readOnly=false)
    @Override
    public void transfer(String sourceName, String targetName, Float money) {}

全部注解式事务

已转账为例,持久层(JdbcTemplate):

/**
 * 账户的持久层实现类
 */

@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public Account findAccountById(Integer accountId) {
        List<Account> accounts = jdbcTemplate.query("select * from account where id = ?",new BeanPropertyRowMapper<Account>(Account.class),accountId);
        return accounts.isEmpty()?null:accounts.get(0);
    }

    @Override
    public Account findAccountByName(String accountName) {
        List<Account> accounts = jdbcTemplate.query("select * from account where name = ?",new BeanPropertyRowMapper<Account>(Account.class),accountName);
        if(accounts.isEmpty()){
            return null;
        }
        if(accounts.size()>1){
            throw new RuntimeException("结果集不唯一");
        }
        return accounts.get(0);
    }

    @Override
    public void updateAccount(Account account) {
        jdbcTemplate.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
    }
}

Dao依赖JdbcTemplate,通过配置类JdbcConfig.Class进行依赖注入:

/**
 * 和连接数据库相关的配置类
 */
public class JdbcConfig {

    @Value("${jdbc.driver}")
    private String driver;

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    /**
     * 创建JdbcTemplate对象
     *
     * @param dataSource
     * @return
     */
    @Bean(name = "jdbcTemplate")
    public JdbcTemplate createJdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    /**
     * 创建数据源对象
     *
     * @return
     */
    @Bean(name = "dataSourse")
    public DataSource createDataSourse() {
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(username);
        ds.setPassword(password);
        return ds;
    }
}

配置文件内容为:

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb1?characterEncoding=utf8
jdbc.username=root
jdbc.password=916521

Service层,在需要事务的方法上加入@Transactional注解:

/**
 * 账户的业务层实现类
 *
 * 事务控制应该都是在业务层
 */
@Service("accountService")
public class AccountServiceImpl implements IAccountService {

    @Autowired
    private IAccountDao accountDao;

    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    @Override
    public Account findAccountById(Integer accountId) {
        return accountDao.findAccountById(accountId);

    }

    @Override
    @Transactional(propagation= Propagation.REQUIRED,readOnly=false)
    public void transfer(String sourceName, String targetName, Float money) {
        System.out.println("transfer....");
            //2.1根据名称查询转出账户
            Account source = accountDao.findAccountByName(sourceName);
            //2.2根据名称查询转入账户
            Account target = accountDao.findAccountByName(targetName);
            //2.3转出账户减钱
            source.setMoney(source.getMoney()-money);
            //2.4转入账户加钱
            target.setMoney(target.getMoney()+money);
            //2.5更新转出账户
            accountDao.updateAccount(source);

            //int i=1/0;

            //2.6更新转入账户
            accountDao.updateAccount(target);
    }
}

事务的配置类:

/**
 * 和事务相关的配置类
 */
public class TransactionConfig {
    /**
     * 用于创建事务管理器对象
     *
     * @param dataSource
     * @return
     */
    @Bean(name = "transactionManager")
    public PlatformTransactionManager createPlatformTransactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

Spring的总体配置类:

/**
 * Spring的配置类,相当于bean.xml
 */

@Configuration
@ComponentScan("tx")
@Import({JdbcConfig.class,TransactionConfig.class})
@PropertySource("jdbcConfig.properties")
@EnableTransactionManagement
public class SpringConfiguration {
}

测试类:

/**
 * 使用Junit单元测试:测试我们的配置
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class AccountServiceTest2 {

    @Autowired
    private IAccountService as;

    @Test
    public void testTransfer(){
        as.transfer("bbb","aaa",100f);
    }

}

猜你喜欢

转载自blog.csdn.net/TJtulong/article/details/105171675