百度面试官:来聊聊Spring吧

文章目录

Spring入门和IOC介绍

1. Spring介绍

Spring诞生:

  • 创建Spring的目的就是用来替代更加重量级的的企业级Java技术
  • 简化Java的开发
    • 基于POJO轻量级和最小侵入式开发
    • 通过依赖注入和面向接口实现松耦合
    • 基于切面和惯例进行声明式编程
    • 通过切面和模板**减少样板式代码 **

1.1侵入式概念

侵入式:对于EJB、Struts2等一些传统的框架,通常是要实现特定的接口,继承特定的类才能增强功能

  • 改变了java类的结构

非侵入式:对于Hibernate、Spring等框架,对现有的类结构没有影响,就能够增强JavaBean的功能

1.2 松耦合概念

前面我们在写程序的时候,都是面向接口编程,通过DaoFactroy等方法来实现松耦合

private CategoryDao categoryDao = DaoFactory.getInstance().createDao("zhongfucheng.dao.impl.CategoryDAOImpl", CategoryDao.class);

private BookDao bookDao = DaoFactory.getInstance().createDao("zhongfucheng.dao.impl.BookDaoImpl", BookDao.class);

private UserDao userDao = DaoFactory.getInstance().createDao("zhongfucheng.dao.impl.UserDaoImpl", UserDao.class);

private OrderDao orderDao = DaoFactory.getInstance().createDao("zhongfucheng.dao.impl.OrderDaoImpl", OrderDao.class);

代码如下:

DAO层和Service层通过DaoFactory来实现松耦合,如果Serivce层直接new DaoBook(),那么DAO和Service就紧耦合了【Service层依赖紧紧依赖于Dao】。

而Spring给我们更加合适的方法来实现松耦合,并且更加灵活、功能更加强大!---->IOC控制反转(这个后面会说)

1.3 切面编程

切面编程也就是AOP编程,其实我们在之前也接触过…动态代理就是一种切面编程了

当时我们使用动态代理+注解的方式给Service层的方法添加权限.

    @Override
    @permission("添加分类")
    /*添加分类*/
    public void addCategory(Category category) {
        categoryDao.addCategory(category);
    }


    /*查找分类*/
    @Override
    public void findCategory(String id) {
        categoryDao.findCategory(id);
    }

    @Override
    @permission("查找分类")
    /*查看分类*/
    public List<Category> getAllCategory() {
        return categoryDao.getAllCategory();
    }

    /*添加图书*/
    @Override
    public void addBook(Book book) {
        bookDao.addBook(book);

    }

Controller调用Service的时候,Service返回的是一个代理对象,代理对象得到Controller想要调用的方法,通过反射来看看该方法上有没有注解

如果有注解的话,那么就判断该用户是否有权限来调用 此方法,如果没有权限,就抛出异常给Controller,Controller接收到异常,就可以提示用户没有权限了。

AOP编程可以简单理解成:在执行某些代码前,执行另外的代码(Struts2的拦截器也是面向切面编程【在执行Action业务方法之前执行拦截器】)

Spring也为我们提供更好地方式来实现面向切面编程

2. 引出Spring

我们试着回顾一下没学Spring的时候,是怎么开发Web项目的

  • 1. 实体类—>class User{ }
  • 2. daoclass–> UserDao{ … 访问db}
  • 3. service—>class UserService{ UserDao userDao = new UserDao();}
  • 4. actionclass UserAction{UserService userService = new UserService();}

用户访问:Tomcat->servlet->service->dao

我们来思考几个问题:

  • ①:对象创建创建能否写死?
  • ②:对象创建细节
    • 对象数量
      •         action  多个   【维护成员变量】
        
      •         service 一个   【不需要维护公共变量】
        
      •         dao     一个   【不需要维护公共变量】
        
    • 创建时间
      •         action    访问时候创建
        
      •         service   启动时候创建
        
      •         dao       启动时候创建
        
  • ③:对象的依赖关系
    • action 依赖 service
    • service依赖 dao

对于第一个问题和第三个问题,我们可以通过DaoFactory解决掉(虽然不是比较好的解决方法)

对于第二个问题,我们要控制对象的数量和创建时间就有点麻烦了

Spring框架通过IOC就很好地可以解决上面的问题

2.1 IOC控制反转

Spring的核心思想之一:Inversion of Control , 控制反转 IOC

那么控制反转是什么意思呢???对象的创建交给外部容器完成,这个就做控制反转。

  • Spring使用控制反转来实现对象不用在程序中写死
  • 控制反转解决对象处理问题【把对象交给别人创建】

那么对象的对象之间的依赖关系Spring是怎么做的呢??依赖注入:dependency injection.

  • Spring使用依赖注入来实现对象之间的依赖关系
  • 在创建完对象之后,对象的关系处理就是依赖注入

上面已经说了,控制反转是通过外部容器完成的,而Spring又为我们提供了这么一个容器,我们一般将这个容器叫做:IOC容器.

无论是创建对象、处理对象之间的依赖关系、对象创建的时间还是对象的数量,我们都是在Spring为我们提供的IOC容器上配置对象的信息就好了。

那么使用IOC控制反转这一思想有什么作用呢???我们来看看一些优秀的回答…

来自知乎:https://www.zhihu.com/question/23277575/answer/24259844

我摘取一下核心的部分:

ioc的思想最核心的地方在于,资源不由使用资源的双方管理,而由不使用资源的第三方管理,这可以带来很多好处。第一,资源集中管理,实现资源的可配置和易管理第二,降低了使用资源双方的依赖程度,也就是我们说的耦合度

也就是说,甲方要达成某种目的不需要直接依赖乙方,它只需要达到的目的告诉第三方机构就可以了,比如甲方需要一双袜子,而乙方它卖一双袜子,它要把袜子卖出去,并不需要自己去直接找到一个卖家来完成袜子的卖出。它也只需要找第三方,告诉别人我要卖一双袜子。这下好了,甲乙双方进行交易活动,都不需要自己直接去找卖家,相当于程序内部开放接口,卖家由第三方作为参数传入。甲乙互相不依赖,而且只有在进行交易活动的时候,甲才和乙产生联系。反之亦然。这样做什么好处么呢,甲乙可以在对方不真实存在的情况下独立存在,而且保证不交易时候无联系,想交易的时候可以很容易的产生联系。甲乙交易活动不需要双方见面,避免了双方的互不信任造成交易失败的问题。因为交易由第三方来负责联系,而且甲乙都认为第三方可靠。那么交易就能很可靠很灵活的产生和进行了。这就是ioc的核心思想。生活中这种例子比比皆是,支付宝在整个淘宝体系里就是庞大的ioc容器,交易双方之外的第三方,提供可靠性可依赖可灵活变更交易方的资源管理中心。另外人事代理也是,雇佣机构和个人之外的第三方。
update=

在以上的描述中,诞生了两个专业词汇,依赖注入和控制反转所谓的依赖注入,则是,甲方开放接口,在它需要的时候,能够讲乙方传递进来(注入)所谓的控制反转,甲乙双方不相互依赖,交易活动的进行不依赖于甲乙任何一方,整个活动的进行由第三方负责管理。

参考优秀的博文②:这里写链接内容

知乎@Intopass的回答:

  1. 不用自己组装,拿来就用。
  2. 享受单例的好处,效率高,不浪费空间。
  3. 便于单元测试,方便切换mock组件。
  4. 便于进行AOP操作,对于使用者是透明的。
  5. 统一配置,便于修改。

3.Spring模块

Spring可以分为6大模块:

  • Spring Core spring的核心功能: IOC容器, 解决对象创建及依赖关系
  • Spring Web Spring对web模块的支持。
    • 					可以与struts整合,让struts的action创建交给spring
      
    • 					spring mvc模式
      
  • Spring DAO Spring 对jdbc操作的支持 【JdbcTemplate模板工具类】
  • Spring ORM spring对orm的支持:
    • 既可以与hibernate整合,【session】
      
    • 也可以使用spring的对hibernate操作的封装
      
  • Spring AOP 切面编程
  • SpringEE spring 对javaEE其他模块的支持

这里写图片描述

上面文主要引出了为啥我们需要使用Spring框架,以及大致了解了Spring是分为六大模块的…下面主要讲解Spring的core模块!

4. Core模块快速入门

4.1搭建配置环境

更新:如果使用maven的同学,引入pom文件就好了

本博文主要是core模块的内容,涉及到Spring core的开发jar包有五个

  • commons-logging-1.1.3.jar 日志
  • spring-beans-3.2.5.RELEASE.jar bean节点
  • spring-context-3.2.5.RELEASE.jar spring上下文节点
  • spring-core-3.2.5.RELEASE.jar spring核心功能
  • spring-expression-3.2.5.RELEASE.jar spring表达式相关表

这次使用的是Spring3.2版本

**编写配置文件:**Spring核心的配置文件applicationContext.xml或者叫bean.xml

那这个配置文件怎么写呢??一般地,我们都知道框架的配置文件都是有约束的…我们可以在spring-framework-3.2.5.RELEASE\docs\spring-framework-reference\htmlsingle\index.html找到XML配置文件的约束

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    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">
	
</beans>   

我是使用Intellij Idea集成开发工具的,可以选择自带的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">

</beans>

前面在介绍Spring模块的时候已经说了,Core模块是:IOC容器,解决对象创建和之间的依赖关系

因此Core模块主要是学习如何得到IOC容器,通过IOC容器来创建对象、解决对象之间的依赖关系、IOC细节。

4.2 得到Spring容器对象【IOC容器】

Spring容器不单单只有一个,可以归为两种类型

  • **Bean工厂,BeanFactory【功能简单】 **
  • 应用上下文,ApplicationContext【功能强大,一般我们使用这个】

4.2.1通过Resource获取BeanFactory

步骤

  • 加载Spring配置文件
  • 通过XmlBeanFactory+配置文件来创建IOC容器
        //加载Spring的资源文件
        Resource resource = new ClassPathResource("applicationContext.xml");

        //创建IOC容器对象【IOC容器=工厂类+applicationContext.xml】
        BeanFactory beanFactory = new XmlBeanFactory(resource);
        

4.2.2类路径下XML获取ApplicationContext

直接通过ClassPathXmlApplicationContext对象来获取

	    // 得到IOC容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
        
        System.out.println(ac);

		

在Spring中总体来看可以通过四种方式来配置对象:

  • 使用XML文件配置
  • 使用注解来配置
  • 使用JavaConfig来配置
  • groovy脚本 DSL

4.3XML配置方式

在上面我们已经可以得到IOC容器对象了。接下来就是在applicationContext.xml文件中配置信息【让IOC容器根据applicationContext.xml文件来创建对象】

首先我们先有个JavaBean的类

/**
 * Created by ozc on 2017/5/10.
 */
public class User {

    private String id;
    private String username;


    public String getId() {
        return id;
    }

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

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

以前我们是通过new User的方法创建对象的…

	User user = new User();

现在我们有了IOC容器,可以让IOC容器帮我们创建对象了。在applicationContext.xml文件中配置对应的信息就行了

       <!--
        使用bean节点来创建对象
            id属性标识着对象
            name属性代表着要创建对象的类全名
    	-->
    <bean id="user" class="User"/>

通过IOC容器对象获取对象:在外界通过IOC容器对象得到User对象

// 得到IOC容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");

User user = (User) ac.getBean("user");

System.out.println(user);

这里写图片描述

上面我们使用的是IOC通过无参构造函数来创建对象,我们来回顾一下一般有几种创建对象的方式:

  • 无参构造函数创建对象
  • 带参数的构造函数创建对象
  • 工厂创建对象
    • 静态方法创建对象
    • 非静态方法创建对象

使用无参的构造函数创建对象我们已经会了,接下来我们看看使用剩下的IOC容器是怎么创建对象的。

4.3.1带参数的构造函数创建对象

首先,JavaBean就要提供带参数的构造函数:

    public User(String id, String username) {
        this.id = id;
        this.username = username;
    }

接下来,关键是怎么配置applicationContext.xml文件了。

    <bean id="user" class="User">

        <!--通过constructor这个节点来指定构造函数的参数类型、名称、第几个-->
        <constructor-arg index="0" name="id" type="java.lang.String" value="1"></constructor-arg>
        <constructor-arg index="1" name="username" type="java.lang.String" value="zhongfucheng"></constructor-arg>
    </bean>

这里写图片描述

在constructor上如果构造函数的值是一个对象,而不是一个普通类型的值,我们就需要用到ref属性了,而不是value属性

比如说:我在User对象上维护了Person对象的值,想要在构造函数中初始化它。因此,就需要用到ref属性了

    <bean id="person" class="Person"></bean> 

    <bean id="user" class="User" >

        <!--通过constructor这个节点来指定构造函数的参数类型、名称、第几个-->
        <constructor-arg index="0" name="id" type="java.lang.String" value="1"></constructor-arg>
        <constructor-arg index="1" name="username" type="java.lang.String" ref="person"></constructor-arg>
    </bean>

4.3.2工厂静态方法创建对象

首先,使用一个工厂的静态方法返回一个对象

public class Factory {

    public static User getBean() {

        return new User();
    }
      
}

配置文件中使用工厂的静态方法返回对象

    <!--工厂静态方法创建对象,直接使用class指向静态类,指定静态方法就行了-->
    <bean id="user" class="Factory" factory-method="getBean" >

    </bean>

这里写图片描述

4.3.3工厂非静态方法创建对象

首先,也是通过工厂的非非静态方法来得到一个对象

public class Factory {

    
    public User getBean() {

        return new User();
    }


}

配置文件中使用工厂的非静态方法返回对象

    <!--首先创建工厂对象-->
    <bean id="factory" class="Factory"/>

    <!--指定工厂对象和工厂方法-->
    <bean id="user" class="User" factory-bean="factory" factory-method="getBean"/>

这里写图片描述

4.3.4 c名称空间

我们在使用XML配置创建Bean的时候,如果该Bean有构造器,那么我们使用<constructor-arg>这个节点来对构造器的参数进行赋值…

<constructor-arg>未免有点太长了,为了简化配置,Spring来提供了c名称空间…

要想c名称空间是需要导入xmlns:c="http://www.springframework.org/schema/c"

    <bean id="userService" class="bb.UserService" c:userDao-ref="">

    </bean>

c名称空间有个**缺点:不能装配集合,**当我们要装配集合的时候还是需要<constructor-arg>这个节点

4.3.5装载集合

如果对象上的属性或者构造函数拥有集合的时候,而我们又需要为集合赋值,那么怎么办?

在构造函数上,普通类型

    <bean id="userService" class="bb.UserService" >
        <constructor-arg >
            <list>
				//普通类型
                <value></value>
            </list>
        </constructor-arg>
    </bean>

在属性上,引用类型

     <property name="userDao">
         
         <list>
             <ref></ref>
         </list>
     </property>

4.4注解方式

自从jdk5有了注解这个新特性,我们可以看到Struts2框架、Hibernate框架都支持使用注解来配置信息…

通过注解来配置信息就是为了简化IOC容器的配置,注解可以把对象添加到IOC容器中、处理对象依赖关系,我们来看看怎么用吧:

使用注解步骤:

  • 1)先引入context名称空间
  •    xmlns:context="http://www.springframework.org/schema/context"
    
  • 2)开启注解扫描器
  •    `<context:component-scan base-package=""></context:component-scan>`
    
  •    第二种方法:也可以通过自定义扫描类以@CompoentScan修饰来扫描IOC容器的bean对象。如下代码:
    
//表明该类是配置类
@Configuration

//启动扫描器,扫描bb包下的
	//也可以指定多个基础包
	//也可以指定类型
@ComponentScan("bb")
public class AnnotationScan {

}

在使用@ComponentScan()这个注解的时候,在测试类上需要@ContextConfiguration这个注解来加载配置类…

  • @ContextConfiguration这个注解又在Spring的test包下…

创建对象以及处理对象依赖关系,相关的注解:

  • @ComponentScan扫描器
  • @Configuration表明该类是配置类
  • @Component 指定把一个对象加入IOC容器—>@Name也可以实现相同的效果【一般少用】
  • @Repository 作用同@Component; 在持久层使用
  • @Service 作用同@Component; 在业务逻辑层使用
  • @Controller 作用同@Component; 在控制层使用
  • @Resource 依赖关系
    • 如果@Resource不指定值,那么就根据类型来找,相同的类型在IOC容器中不能有两个
    • 如果@Resource指定了值,那么就根据名字来找

测试代码:UserDao

package aa;

import org.springframework.stereotype.Repository;

/**
 * Created by ozc on 2017/5/10.
 */

//把对象添加到容器中,首字母会小写
@Repository
public class UserDao {

    public void save() {
        System.out.println("DB:保存用户");
    }


}

userService

package aa;


import org.springframework.stereotype.Service;

import javax.annotation.Resource;


//把UserService对象添加到IOC容器中,首字母会小写
@Service
public class UserService {

    //如果@Resource不指定值,那么就根据类型来找--->UserDao....当然了,IOC容器不能有两个UserDao类型的对象
    //@Resource

    //如果指定了值,那么Spring就在IOC容器找有没有id为userDao的对象。
    @Resource(name = "userDao")
    private UserDao userDao;

    public void save() {
        userDao.save();
    }
}

userAction

package aa;

import org.springframework.stereotype.Controller;

import javax.annotation.Resource;

/**
 * Created by ozc on 2017/5/10.
 */

//把对象添加到IOC容器中,首字母会小写
@Controller
public class UserAction {

    @Resource(name = "userService")
    private UserService userService;

    public String execute() {
        userService.save();
        return null;
    }
}

测试

package aa;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * Created by ozc on 2017/5/10.
 */
public class App {

    public static void main(String[] args) {

        // 创建容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("aa/applicationContext.xml");

        UserAction userAction = (UserAction) ac.getBean("userAction");

        userAction.execute();
    }
}

这里写图片描述


4.5通过JavaConfig方式

怎么通过java代码来配置Bean呢??

  • 编写一个java类,使用@Configuration修饰该类
  • 被@Configuration修饰的类就是配置类

编写配置类:

	@org.springframework.context.annotation.Configuration
	public class Configuration {
	  
	}

使用配置类创建bean:

  • 使用@Bean来修饰方法,该方法返回一个对象。
  • 不管方法体内的对象是怎么创建的,Spring可以获取得到对象就行了。
  • Spring内部会将该对象加入到Spring容器中
  • 容器中bean的ID默认为方法名
@org.springframework.context.annotation.Configuration
public class Configuration {

    @Bean
    public UserDao userDao() {

        UserDao userDao = new UserDao();
        System.out.println("我是在configuration中的"+userDao);
        return userDao;
    }

}
  • 测试代码:要使用@ContextConfiguration加载配置类的信息【引入test包】
package bb;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;

/**
 * Created by ozc on 2017/5/11.
 */
//加载配置类的信息
@ContextConfiguration(classes = Configuration.class)
public class Test2 {

    @Test
    public void test33() {

        ApplicationContext ac =
                new ClassPathXmlApplicationContext("bb/bean.xml");

        UserDao userDao = (UserDao) ac.getBean("userDao");

        System.out.println(userDao);
    }
}

结果如下:

4.6三种方式混合使用?

注解和XML配置是可以混合使用的,JavaConfig和XML也是可以混合使用的…

如果JavaConfig的配置类是分散的,我们一般再创建一个更高级的配置类(root),然后使用**@Import来将配置类进行组合**
如果XML的配置文件是分散的,我们也是创建一个更高级的配置文件(root),然后使用<import>来将配置文件组合

在JavaConfig引用XML

  • 使用@ImportResource()

在XML引用JavaConfig

  • 使用<bean>节点就行了

在公司的项目中,一般我们是XML+注解

5. bean对象创建细节

既然我们现在已经初步了解IOC容器了,那么这些问题我们都是可以解决的。并且是十分简单【对象写死问题已经解决了,IOC容器就是控制反转创建对象】

5.1 scope属性

指定scope属性,IOC容器就知道创建对象的时候是单例还是多例的了。

属性的值就只有两个:单例/多例

这里写图片描述

当我们使用singleton【单例】的时候,从IOC容器获取的对象都是同一个:

这里写图片描述

当我们使用prototype【多例】的时候,从IOC容器获取的对象都是不同的:

这里写图片描述

scope属性除了控制对象是单例还是多例的,还控制着对象创建的时间

我们在User的构造函数中打印出一句话,就知道User对象是什么时候创建了。

    public User() {

        System.out.println("我是User,我被创建了");
    }

当使用singleton的时候,对象在IOC容器之前就已经创建了

这里写图片描述

当使用prototype的时候,对象在使用的时候才创建

5.2lazy-init属性

lazy-init属性只对singleton【单例】的对象有效…lazy-init默认为false…

有的时候,可能我们想要对象在使用的时候才创建,那么将lazy-init设置为ture就行了

这里写图片描述


5.3 init-method和destroy-method

如果我们想要对象在创建后,执行某个方法,我们指定为init-method属性就行了。。

如果我们想要IOC容器销毁后,执行某个方法,我们指定destroy-method属性就行了。

    <bean id="user" class="User" scope="singleton" lazy-init="true" init-method="" destroy-method=""/>

5.4 Bean创建细节总结

	/**
	 * 1) 对象创建: 单例/多例
	 * 	scope="singleton", 默认值, 即 默认是单例	【service/dao/工具类】
	 *  scope="prototype", 多例; 				【Action对象】
	 * 
	 * 2) 什么时候创建?
	 * 	  scope="prototype"  在用到对象的时候,才创建对象。
	 *    scope="singleton"  在启动(容器初始化之前), 就已经创建了bean,且整个应用只有一个。
	 * 3)是否延迟创建
	 * 	  lazy-init="false"  默认为false,  不延迟创建,即在启动时候就创建对象
	 * 	  lazy-init="true"   延迟初始化, 在用到对象的时候才创建对象
	 *    (只对单例有效)
	 * 4) 创建对象之后,初始化/销毁
	 * 	  init-method="init_user"       【对应对象的init_user方法,在对象创建之后执行 】
	 *    destroy-method="destroy_user"  【在调用容器对象的destroy方法时候执行,(容器用实现类)】
	 */

对象依赖

1. 回顾以前对象依赖

我们来看一下我们以前关于对象依赖,是怎么的历程

1.1 直接new对象

在最开始,我们是直接new对象给serice的userDao属性赋值…

class  UserService{
	UserDao userDao = new UserDao();
}

1.2 写DaoFactory,用字符串来维护依赖关系

后来,我们发现service层紧紧耦合了dao层。我们就写了DaoFactory,在service层只要通过字符串就能够创建对应的dao层的对象了。

DaoFactory

public class DaoFactory {

    private static final DaoFactory factory = new DaoFactory();
    private DaoFactory(){}

    public static DaoFactory getInstance(){
        return factory;
    }

    public <T> T createDao(String className,Class<T> clazz){
        try{
            T t = (T) Class.forName(className).newInstance();
            return t;
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

serivce

    private CategoryDao categoryDao = DaoFactory.getInstance().createDao("zhongfucheng.dao.impl.CategoryDAOImpl", CategoryDao.class);

    private BookDao bookDao = DaoFactory.getInstance().createDao("zhongfucheng.dao.impl.BookDaoImpl", BookDao.class);

    private UserDao userDao = DaoFactory.getInstance().createDao("zhongfucheng.dao.impl.UserDaoImpl", UserDao.class);

    private OrderDao orderDao = DaoFactory.getInstance().createDao("zhongfucheng.dao.impl.OrderDaoImpl", OrderDao.class);

1.3 DaoFactory读取配置文件

再后来,我们发现要修改Dao的实现类,还是得修改service层的源代码呀…于是我们就在DaoFactory中读取关于daoImpl的配置文件,根据配置文件来创建对象,这样一来,创建的是哪个daoImpl对service层就是透明的

DaoFactory

public class DaoFactory {
	
	private  UserDao userdao = null;
	
	private DaoFactory(){
		try{
			InputStream in = DaoFactory.class.getClassLoader().getResourceAsStream("dao.properties");
			Properties prop = new Properties();
			prop.load(in);
			
			String daoClassName = prop.getProperty("userdao");
			userdao = (UserDao)Class.forName(daoClassName).newInstance();
			
		}catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	private static final DaoFactory instance = new DaoFactory();
	
	public static DaoFactory getInstance(){
		return instance;
	}
	
	
	public UserDao createUserDao(){
		return userdao;
	}
	
}

service

	UserDao dao = DaoFactory.getInstance().createUserDao();

2. Spring依赖注入

通过上面的历程,我们可以清晰地发现:对象之间的依赖关系,其实就是给对象上的属性赋值!因为对象上有其他对象的变量,因此存在了依赖

Spring提供了好几种的方式来给属性赋值

  • 1) 通过构造函数
  • 2) 通过set方法给属性注入值
    1. p名称空间
  • 4)自动装配(了解)
  • 5) 注解

2.1 搭建测试环境

UserService中使用userDao变量来维护与Dao层之间的依赖关系,UserAction中使用userService变量来维护与Service层之间的依赖关系。

userDao

public class UserDao {

	public void save() {
		System.out.println("DB:保存用户");
	}
}

userService

public class UserService {
	
	private UserDao userDao; 

	public void save() {
		userDao.save();
	}
}

userAnction

public class UserAction {

	private UserService userService;

	public String execute() {
		userService.save();
		return null;
	}
}

2.2构造函数给属性赋值

其实我们在讲解创建带参数的构造函数的时候已经讲过了…我们还是来回顾一下呗…

我们测试service和dao的依赖关系就好了…在serice中加入一个构造函数,参数就是userDao

public UserService(UserDao userDao) {
  this.userDao = userDao;

  //看看有没有拿到userDao
  System.out.println(userDao);
}

applicationContext.xml配置文件

<!--创建userDao对象-->
<bean id="userDao" class="UserDao"/>

<!--创建userService对象-->
<bean id="userService" class="UserService">
  <!--要想在userService层中能够引用到userDao,就必须先创建userDao对象-->
  <constructor-arg index="0" name="userDao" type="UserDao" ref="userDao"></constructor-arg>
</bean>

测试:可以成功获取到userDao对象

// 创建容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");

//得到service对象
UserService userService = (UserService) ac.getBean("userService");

这里写图片描述

2.3通过set方法给属性注入值

我们这里也是测试service和dao层的依赖关系就好了…在service层通过set方法来把userDao注入到UserService中

为UserService添加set方法

public class UserService {

    private UserDao userDao;


    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;

        //看看有没有拿到userDao
        System.out.println(userDao);
    }

    public void save() {
        userDao.save();
    }
}

applicationContext.xml配置文件:通过property节点来给属性赋值

  • 引用类型使用ref属性
  • 基本类型使用value属性
<!--创建userDao对象-->
<bean id="userDao" class="UserDao"/>

<!--创建userService对象-->
<bean id="userService" class="UserService">
  <property name="userDao" ref="userDao"/>
</bean>

测试:

这里写图片描述

2.4 内部Bean

我们刚才是先创建userDao对象,再由userService对userDao对象进行引用…我们还有另一种思维:先创建userService,发现userService需要userDao的属性,再创建userDao…我们来看看这种思维方式是怎么配置的:

applicationContext.xml配置文件:property节点内置bean节点

    <!--
        1.创建userService,看到有userDao这个属性
        2.而userDao这个属性又是一个对象
        3.在property属性下又内置了一个bean
        4.创建userDao
    -->
    <bean id="userService" class="UserService">
        <property name="userDao">
            <bean id="userDao" class="UserDao"/>
        </property>
    </bean>


测试

这里写图片描述

我们发现这种思维方式和服务器访问的执行顺序是一样的,但是如果userDao要多次被其他service使用的话,就要多次配置了

2.5 p 名称空间注入属性值

p名称控件这种方式其实就是set方法的一种优化,优化了配置而已…p名称空间这个内容需要在Spring3版本以上才能使用…我们来看看:

applicationContext.xml配置文件:使用p名称空间

    <bean id="userDao" class="UserDao"/>
    
    <!--不用写property节点了,直接使用p名称空间-->
    <bean id="userService" class="UserService" p:userDao-ref="userDao"/>

测试

这里写图片描述


2.6 自动装配

Spring还提供了自动装配的功能,能够非常简化我们的配置

自动装载默认是不打开的,自动装配常用的可分为两种:

  • 根据名字来装配
  • 根据类型类装配

2.6.1XML配置根据名字

applicationContext.xml配置文件:使用自动装配,根据名字

    <bean id="userDao" class="UserDao"/>

    <!--
        1.通过名字来自动装配
        2.发现userService中有个叫userDao的属性
        3.看看IOC容器中没有叫userDao的对象
        4.如果有,就装配进去
    -->
    <bean id="userService" class="UserService" autowire="byName"/>

测试

这里写图片描述


2.6.2 XML配置根据类型

applicationContext.xml配置文件:使用自动装配,根据类型

值得注意的是:如果使用了根据类型来自动装配,那么在IOC容器中只能有一个这样的类型,否则就会报错!

    <bean id="userDao" class="UserDao"/>

    <!--
        1.通过名字来自动装配
        2.发现userService中有个叫userDao的属性
        3.看看IOC容器UserDao类型的对象
        4.如果有,就装配进去
    -->
    <bean id="userService" class="UserService" autowire="byType"/>

测试:

这里写图片描述

我们这只是测试两个对象之间的依赖关系,如果我们有很多对象,我们也可以使用默认自动分配

这里写图片描述

2.7 使用注解来实现自动装配

@Autowired注解来实现自动装配:

  • 可以在构造器上修饰
  • 也可以在setter方法上修饰
  • 来自java的@Inject的和@AutoWired有相同的功能

如果没有匹配到bean,又为了避免异常的出现,我们可以使用required属性上设置为false。【谨慎对待】

测试代码

@Component
public class UserService {

    private UserDao userDao ;


    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

顺利拿到userDao的引用

这里写图片描述

AOP入门

在讲解AOP模块之前,首先我们来讲解一下cglib代理、以及怎么手动实现AOP编程

1. cglib代理

在讲解cglib之前,首先我们来回顾一下静态代理和动态代理

由于静态代理需要实现目标对象的相同接口,那么可能会导致代理类会非常非常多…不好维护---->因此出现了动态代理

动态代理也有个约束:目标对象一定是要有接口的,没有接口就不能实现动态代理…----->因此出现了cglib代理

cglib代理也叫子类代理,从内存中构建出一个子类来扩展目标对象的功能!

  • CGLIB是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口。它广泛的被许多AOP的框架使用,例如Spring AOP和dynaop,为他们提供方法的interception(拦截)。

1.1 编写cglib代理

接下来我们就讲讲怎么写cglib代理:

  • 需要引入cglib – jar文件, 但是spring的核心包中已经包括了cglib功能,所以直接引入spring-core-3.2.5.jar即可。(如果用maven的同学,引入pom依赖就好了)
  • 引入功能包后,就可以在内存中动态构建子类
  • 代理的类不能为final,否则报错【在内存中构建子类来做扩展,当然不能为final,有final就不能继承了】
  • 目标对象的方法如果为final/static, 那么就不会被拦截,即不会执行目标对象额外的业务方法。
//需要实现MethodInterceptor接口
public class ProxyFactory implements MethodInterceptor{
	
	// 维护目标对象
	private Object target;
	public ProxyFactory(Object target){
		this.target = target;
	}
	
	// 给目标对象创建代理对象
	public Object getProxyInstance(){
		//1. 工具类
		Enhancer en = new Enhancer();
		//2. 设置父类
		en.setSuperclass(target.getClass());
		//3. 设置回调函数
		en.setCallback(this);
		//4. 创建子类(代理对象)
		return en.create();
	}
	

	@Override
	public Object intercept(Object obj, Method method, Object[] args,
			MethodProxy proxy) throws Throwable {
		
		System.out.println("开始事务.....");
		
		// 执行目标对象的方法
		//Object returnValue = method.invoke(target, args);
		 proxy.invokeSuper(object, args); 
		System.out.println("提交事务.....");
		
		return returnValue;
	}

}

测试:

public class App {

    public static void main(String[] args) {

        UserDao userDao = new UserDao();

        UserDao factory = (UserDao) new ProxyFactory(userDao).getProxyInstance();

        factory.save();
    }
}

结果如下:

这里写图片描述

这里写图片描述

使用cglib就是为了弥补动态代理的不足【动态代理的目标对象一定要实现接口】

2. 手动实现AOP编程

AOP 面向切面的编程:AOP可以实现“业务代码”与“关注点代码”分离

下面我们来看一段代码:

// 保存一个用户
public void add(User user) { 
		Session session = null; 
		Transaction trans = null; 
		try { 
			session = HibernateSessionFactoryUtils.getSession();   // 【关注点代码】
			trans = session.beginTransaction();    // 【关注点代码】
			 
			session.save(user);     // 核心业务代码
			 
			trans.commit();     //…【关注点代码】

		} catch (Exception e) {     
			e.printStackTrace(); 
			if(trans != null){ 
				trans.rollback();   //..【关注点代码】

			} 
		} finally{ 
			HibernateSessionFactoryUtils.closeSession(session);   ////..【关注点代码】

		} 
   } 

关注点代码,就是指重复执行的代码。

业务代码与关注点代码分离,好处?

  • 关注点代码写一次即可
  • 开发者只需要关注核心业务
  • 运行时期,执行核心业务代码时候动态植入关注点代码; 【代理】

2.1案例分析:

IUser接口

public interface IUser {

    void save();
}

我们一步一步来分析,首先我们的UserDao有一个save()方法,每次都要开启事务和关闭事务

//@Component  -->任何地方都能用这个
@Repository  //-->这个在Dao层中使用
    public class UserDao {

    public void save() {

        System.out.println("开始事务");
        System.out.println("DB:保存用户");
        System.out.println("关闭事务");

    }


}

在刚学习java基础的时候,我们知道:如果某些功能经常需要用到就封装成方法:

//@Component  -->任何地方都能用这个
@Repository  //-->这个在Dao层中使用
    public class UserDao {

    public void save() {

        begin();
        System.out.println("DB:保存用户");
        close();
        
    }

    public void begin() {
        System.out.println("开始事务");
    }
    public void close() {
        System.out.println("关闭事务");
    }
}

现在呢,我们可能有多个Dao,都需要有开启事务和关闭事务的功能,现在只有UserDao中有这两个方法,重用性还是不够高。因此我们抽取出一个类出来

public class AOP {
    
    public void begin() {
        System.out.println("开始事务");
    }
    public void close() {
        System.out.println("关闭事务");
    }
}

在UserDao维护这个变量,要用的时候,调用方法就行了。

@Repository  //-->这个在Dao层中使用
public class UserDao {


    AOP aop;

    public void save() {

        aop.begin();
        System.out.println("DB:保存用户");
        aop.close();

    }
    
}

现在的开启事务、关闭事务还是需要我在userDao中手动调用。还是不够优雅。。我想要的效果:当我在调用userDao的save()方法时,动态地开启事务、关闭事务。因此,我们就用到了代理。当然了,真正执行方法的都是userDao、要干事的是AOP,因此在代理中需要维护他们的引用

public class ProxyFactory {
    //维护目标对象
    private static Object target;

    //维护关键点代码的类
    private static AOP aop;
    public static Object getProxyInstance(Object target_, AOP aop_) {

        //目标对象和关键点代码的类都是通过外界传递进来
        target = target_;
        aop = aop_;

        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        aop.begin();
                        Object returnValue = method.invoke(target, args);
                        aop.close();

                        return returnValue;
                    }
                }
        );
    }
}

2.2工厂静态方法:

把AOP加入IOC容器中

//把该对象加入到容器中
@Component
public class AOP {

    public void begin() {
        System.out.println("开始事务");
    }
    public void close() {
        System.out.println("关闭事务");
    }
}

把UserDao放入容器中

@Component
public class UserDao {

    public void save() {

        System.out.println("DB:保存用户");

    }

}

在配置文件中开启注解扫描,使用工厂静态方法创建代理对象

<?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:p="http://www.springframework.org/schema/p"
       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">


    <bean id="proxy" class="aa.ProxyFactory" factory-method="getProxyInstance">
        <constructor-arg index="0" ref="userDao"/>
        <constructor-arg index="1" ref="AOP"/>
    </bean>

    <context:component-scan base-package="aa"/>


</beans>

测试,得到UserDao对象,调用方法

public class App {

    public static void main(String[] args) {

        ApplicationContext ac =
                new ClassPathXmlApplicationContext("aa/applicationContext.xml");


        IUser iUser = (IUser) ac.getBean("proxy");

        iUser.save();



    }
}

这里写图片描述

2.3 工厂非静态方法

上面使用的是工厂静态方法来创建代理类对象。我们也使用一下非静态的工厂方法创建对象

package aa;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * Created by ozc on 2017/5/11.
 */

public class ProxyFactory {

    public Object getProxyInstance(final Object target_, final AOP aop_) {

        //目标对象和关键点代码的类都是通过外界传递进来

        return Proxy.newProxyInstance(
                target_.getClass().getClassLoader(),
                target_.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        aop_.begin();
                        Object returnValue = method.invoke(target_, args);
                        aop_.close();

                        return returnValue;
                    }
                }
        );
    }
}

配置文件:先创建工厂,再创建代理类对象

<?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:p="http://www.springframework.org/schema/p"
       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">



    <!--创建工厂-->
    <bean id="factory" class="aa.ProxyFactory"/>


    <!--通过工厂创建代理-->
    <bean id="IUser" class="aa.IUser" factory-bean="factory" factory-method="getProxyInstance">
        <constructor-arg index="0" ref="userDao"/>
        <constructor-arg index="1" ref="AOP"/>
    </bean>

    <context:component-scan base-package="aa"/>
</beans>

效果如下:

这里写图片描述

3. AOP的概述

Aop: aspect object programming 面向切面编程

  • 功能: 让关注点代码与业务代码分离!
  • 面向切面编程就是指: 对很多功能都有的重复的代码抽取,再在运行的时候往业务方法上动态植入“切面类代码”。

关注点:重复代码就叫做关注点。

// 保存一个用户
public void add(User user) { 
		Session session = null; 
		Transaction trans = null; 
		try { 
			session = HibernateSessionFactoryUtils.getSession();   // 【关注点代码】
			trans = session.beginTransaction();    // 【关注点代码】
			 
			session.save(user);     // 核心业务代码
			 
			trans.commit();     //…【关注点代码】

		} catch (Exception e) {     
			e.printStackTrace(); 
			if(trans != null){ 
				trans.rollback();   //..【关注点代码】

			} 
		} finally{ 
			HibernateSessionFactoryUtils.closeSession(session);   ////..【关注点代码】

		} 
   } 

切面:关注点形成的类,就叫切面(类)!

public class AOP {

    public void begin() {
        System.out.println("开始事务");
    }
    public void close() {
        System.out.println("关闭事务");
    }
}

切入点:

  • 执行目标对象方法,动态植入切面代码。
  • 可以通过切入点表达式指定拦截哪些类的哪些方法; 给指定的类在运行的时候植入切面类代码

切入点表达式:

  • 指定哪些类的哪些方法被拦截

4. 使用Spring AOP开发步骤

更新:如果用maven的同学,引入pom依赖就好了

1) 先引入aop相关jar文件 (aspectj aop优秀组件)

  • spring-aop-3.2.5.RELEASE.jar 【spring3.2源码】
  • aopalliance.jar 【spring2.5源码/lib/aopalliance】
  • aspectjweaver.jar 【spring2.5源码/lib/aspectj】或【aspectj-1.8.2\lib】
  • aspectjrt.jar 【spring2.5源码/lib/aspectj】或【aspectj-1.8.2\lib】

注意: 用到spring2.5版本的jar文件,如果用jdk1.7可能会有问题

  • 需要升级aspectj组件,即使用aspectj-1.8.2版本中提供jar文件提供。

2) bean.xml中引入aop名称空间

  • xmlns:context="http://www.springframework.org/schema/context"
  • http://www.springframework.org/schema/context
  • http://www.springframework.org/schema/context/spring-context.xsd

引入4个jar包:

这里写图片描述

4.1 引入名称空间

<?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:p="http://www.springframework.org/schema/p"
       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">
    
</beans>

4.2注解方式实现AOP编程

我们之前手动的实现AOP编程是需要自己来编写代理工厂的**,现在有了Spring,就不需要我们自己写代理工厂了。Spring内部会帮我们创建代理工厂**。也就是说,不用我们自己写代理对象了。

因此,我们只要关心切面类、切入点、编写切入表达式指定拦截什么方法就可以了!

还是以上一个例子为案例,使用Spring的注解方式来实现AOP编程

4.2.1在配置文件中开启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:p="http://www.springframework.org/schema/p"
       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 http://www.springframework.org/schema/aop/spring-aop.xsd">


    <context:component-scan base-package="aa"/>

    <!-- 开启aop注解方式 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

</beans>

4.2.2代码:

切面类

@Component
@Aspect//指定为切面类
public class AOP {


	//里面的值为切入点表达式
    @Before("execution(* aa.*.*(..))")
    public void begin() {
        System.out.println("开始事务");
    }


    @After("execution(* aa.*.*(..))")
    public void close() {
        System.out.println("关闭事务");
    }
}

UserDao实现了IUser接口

@Component
public class UserDao implements IUser {

    @Override
    public void save() {
        System.out.println("DB:保存用户");
    }

}

IUser接口

public interface IUser {
    void save();
}

测试代码:

public class App {

    public static void main(String[] args) {

        ApplicationContext ac =
                new ClassPathXmlApplicationContext("aa/applicationContext.xml");

		//这里得到的是代理对象....
        IUser iUser = (IUser) ac.getBean("userDao");

        System.out.println(iUser.getClass());

        iUser.save();
        
    }
}

效果:

这里写图片描述


4.3目标对象没有接口

上面我们测试的是UserDao有IUser接口,内部使用的是动态代理…那么我们这次测试的是目标对象没有接口

OrderDao没有实现接口

@Component
public class OrderDao {

    public void save() {

        System.out.println("我已经进货了!!!");
        
    }
}

测试代码:

public class App {

    public static void main(String[] args) {

        ApplicationContext ac =
                new ClassPathXmlApplicationContext("aa/applicationContext.xml");

        OrderDao orderDao = (OrderDao) ac.getBean("orderDao");

        System.out.println(orderDao.getClass());

        orderDao.save();

    }
}

效果:

这里写图片描述


4.4 AOP注解API

api:

  • @Aspect 指定一个类为切面类
  • @Pointcut("execution( cn.itcast.e_aop_anno..(…))") 指定切入点表达式*
  • @Before(“pointCut_()”) 前置通知: 目标方法之前执行
  • @After(“pointCut_()”) 后置通知:目标方法之后执行(始终执行)
  • @AfterReturning(“pointCut_()”) 返回后通知: 执行方法结束前执行(异常不执行)
  • @AfterThrowing(“pointCut_()”) 异常通知: 出现异常时候执行
  • @Around(“pointCut_()”) 环绕通知: 环绕目标方法执行
	
	// 前置通知 : 在执行目标方法之前执行
	@Before("pointCut_()")
	public void begin(){
		System.out.println("开始事务/异常");
	}
	
	// 后置/最终通知:在执行目标方法之后执行  【无论是否出现异常最终都会执行】
	@After("pointCut_()")
	public void after(){
		System.out.println("提交事务/关闭");
	}
	
	// 返回后通知: 在调用目标方法结束后执行 【出现异常不执行】
	@AfterReturning("pointCut_()")
	public void afterReturning() {
		System.out.println("afterReturning()");
	}
	
	// 异常通知: 当目标方法执行异常时候执行此关注点代码
	@AfterThrowing("pointCut_()")
	public void afterThrowing(){
		System.out.println("afterThrowing()");
	}
	
	// 环绕通知:环绕目标方式执行
	@Around("pointCut_()")
	public void around(ProceedingJoinPoint pjp) throws Throwable{
		System.out.println("环绕前....");
		pjp.proceed();  // 执行目标方法
		System.out.println("环绕后....");
	}

4.5表达式优化

我们的代码是这样的:每次写Before、After等,都要重写一次切入点表达式,这样就不优雅了。

    @Before("execution(* aa.*.*(..))")
    public void begin() {
        System.out.println("开始事务");
    }


    @After("execution(* aa.*.*(..))")
    public void close() {
        System.out.println("关闭事务");
    }

于是乎,我们要使用@Pointcut这个注解,来指定切入点表达式,在用到的地方中,直接引用就行了!

那么我们的代码就可以改造成这样了:

@Component
@Aspect//指定为切面类
public class AOP {


    // 指定切入点表达式,拦截哪个类的哪些方法
    @Pointcut("execution(* aa.*.*(..))")
    public void pt() {

    }

    @Before("pt()")
    public void begin() {
        System.out.println("开始事务");
    }


    @After("pt()")
    public void close() {
        System.out.println("关闭事务");
    }
}



4.6 XML方式实现AOP编程

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


    <!--对象实例-->
    <bean id="userDao" class="aa.UserDao"/>
    <bean id="orderDao" class="aa.OrderDao"/>

    <!--切面类-->
    <bean id="aop" class="aa.AOP"/>

    <!--AOP配置-->
    <aop:config >

        <!--定义切入表达式,拦截哪些方法-->
        <aop:pointcut id="pointCut" expression="execution(* aa.*.*(..))"/>

        <!--指定切面类是哪个-->
        <aop:aspect ref="aop">

            <!--指定来拦截的时候执行切面类的哪些方法-->
            <aop:before method="begin" pointcut-ref="pointCut"/>
            <aop:after method="close" pointcut-ref="pointCut"/>

        </aop:aspect>
    </aop:config>

    
</beans>

测试:

public class App {

    @Test
    public  void test1() {

        ApplicationContext ac =
                new ClassPathXmlApplicationContext("aa/applicationContext.xml");

        OrderDao orderDao = (OrderDao) ac.getBean("orderDao");

        System.out.println(orderDao.getClass());

        orderDao.save();

    }

    @Test
    public  void test2() {

        ApplicationContext ac =
                new ClassPathXmlApplicationContext("aa/applicationContext.xml");

        IUser userDao = (IUser) ac.getBean("userDao");

        System.out.println(userDao.getClass());

        userDao.save();

    }
}

测试OrderDao

这里写图片描述

测试UserDao

这里写图片描述


5. 切入点表达式

切入点表达式主要就是来配置拦截哪些类的哪些方法

5.1 查官方文档

我们去文档中找找它的语法…

这里写图片描述

在文档中搜索:execution(

这里写图片描述

5.2语法解析

那么它的语法是这样子的:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

符号讲解:

  • ?号代表0或1,可以不写
  • “*”号代表任意类型,0或多
  • 方法参数为…表示为可变参数

参数讲解:

  • modifiers-pattern?【修饰的类型,可以不写】
  • ret-type-pattern【方法返回值类型,必写】
  • declaring-type-pattern?【方法声明的类型,可以不写】
  • name-pattern(param-pattern)【要匹配的名称,括号里面是方法的参数】
  • throws-pattern?【方法抛出的异常类型,可以不写】

官方也有给出一些例子给我们理解:

这里写图片描述

5.3 测试代码

		<!-- 【拦截所有public方法】 -->
		<!--<aop:pointcut expression="execution(public * *(..))" id="pt"/>-->
		
		<!-- 【拦截所有save开头的方法 】 -->
		<!--<aop:pointcut expression="execution(* save*(..))" id="pt"/>-->
		
		<!-- 【拦截指定类的指定方法, 拦截时候一定要定位到方法】 -->
		<!--<aop:pointcut expression="execution(public * cn.itcast.g_pointcut.OrderDao.save(..))" id="pt"/>-->
		
		<!-- 【拦截指定类的所有方法】 -->
		<!--<aop:pointcut expression="execution(* cn.itcast.g_pointcut.UserDao.*(..))" id="pt"/>-->
		
		<!-- 【拦截指定包,以及其自包下所有类的所有方法】 -->
		<!--<aop:pointcut expression="execution(* cn..*.*(..))" id="pt"/>-->
		
		<!-- 【多个表达式】 -->
		<!--<aop:pointcut expression="execution(* cn.itcast.g_pointcut.UserDao.save()) || execution(* cn.itcast.g_pointcut.OrderDao.save())" id="pt"/>-->
		<!--<aop:pointcut expression="execution(* cn.itcast.g_pointcut.UserDao.save()) or execution(* cn.itcast.g_pointcut.OrderDao.save())" id="pt"/>-->
		<!-- 下面2个且关系的,没有意义 -->
		<!--<aop:pointcut expression="execution(* cn.itcast.g_pointcut.UserDao.save()) &amp;&amp; execution(* cn.itcast.g_pointcut.OrderDao.save())" id="pt"/>-->
		<!--<aop:pointcut expression="execution(* cn.itcast.g_pointcut.UserDao.save()) and execution(* cn.itcast.g_pointcut.OrderDao.save())" id="pt"/>-->
		
		<!-- 【取非值】 -->
		<!--<aop:pointcut expression="!execution(* cn.itcast.g_pointcut.OrderDao.save())" id="pt"/>-->

JDBCTemplate和Spring事务

上一篇Spring博文主要讲解了如何使用Spring来实现AOP编程,本博文主要讲解Spring的DAO模块对JDBC的支持,以及Spring对事务的控制

对于JDBC而言,我们肯定不会陌生,我们在初学的时候肯定写过非常非常多的JDBC模板代码

1. 回顾对模版代码优化过程

我们来回忆一下我们怎么对模板代码进行优化的!

首先来看一下我们原生的JDBC:需要手动去数据库的驱动从而拿到对应的连接

		try {
			String sql = "insert into t_dept(deptName) values('test');";
			Connection con = null;
			Statement stmt = null;
			Class.forName("com.mysql.jdbc.Driver");
			// 连接对象
			con = DriverManager.getConnection("jdbc:mysql:///hib_demo", "root", "root");
			// 执行命令对象
			stmt =  con.createStatement();
			// 执行
			stmt.execute(sql);
			
			// 关闭
			stmt.close();
			con.close();
		} catch (Exception e) {
			e.printStackTrace();
		}

因为JDBC是面向接口编程的,因此数据库的驱动都是由数据库的厂商给做到好了,我们只要加载对应的数据库驱动,便可以获取对应的数据库连接…因此,我们写了一个工具类,专门来获取与数据库的连接(Connection),当然啦,为了更加灵活,我们的工具类是读取配置文件的方式来做的

    /*
    * 连接数据库的driver,url,username,password通过配置文件来配置,可以增加灵活性
    * 当我们需要切换数据库的时候,只需要在配置文件中改以上的信息即可
    *
    * */

    private static String  driver = null;
    private static String  url = null;
    private static String  username = null;
    private static String password = null;

    static {
        try {

            //获取配置文件的读入流
            InputStream inputStream = UtilsDemo.class.getClassLoader().getResourceAsStream("db.properties");

            Properties properties = new Properties();
            properties.load(inputStream);

            //获取配置文件的信息
            driver = properties.getProperty("driver");
            url = properties.getProperty("url");
            username = properties.getProperty("username");
            password = properties.getProperty("password");

            //加载驱动类
            Class.forName(driver);


        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(url,username,password);
    }
    public static void release(Connection connection, Statement statement, ResultSet resultSet) {

        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

经过上面一层的封装,我们可以**在使用的地方直接使用工具类来得到与数据库的连接…那么比原来就方便很多了!**但是呢,每次还是需要使用Connection去创建一个Statement对象。并且无论是什么方法,其实就是SQL语句和传递进来的参数不同!

于是我们可以使用DBUtils这样的组件来解决上面的问题

2. 使用Spring的JDBC

上面已经回顾了一下以前我们的JDBC开发了,那么看看Spring对JDBC又是怎么优化的

首先,想要使用Spring的JDBC模块,就必须引入两个jar文件:

  • 引入jar文件(如果用maven的同学,直接使用pom文件依赖导入就好了)
    • spring-jdbc-3.2.5.RELEASE.jar
    • spring-tx-3.2.5.RELEASE.jar

首先还是看一下我们原生的JDBC代码:获取Connection是可以抽取出来的,直接使用dataSource来得到Connection就行了

	public void save() {
		try {
			String sql = "insert into t_dept(deptName) values('test');";
			Connection con = null;
			Statement stmt = null;
			Class.forName("com.mysql.jdbc.Driver");
			// 连接对象
			con = DriverManager.getConnection("jdbc:mysql:///hib_demo", "root", "root");
			// 执行命令对象
			stmt =  con.createStatement();
			// 执行
			stmt.execute(sql);
			
			// 关闭
			stmt.close();
			con.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

值得注意的是,JDBC对C3P0数据库连接池是有很好的支持的。因此我们直接可以使用Spring的依赖注入,在配置文件中配置dataSource就行了

	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
		<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
		<property name="jdbcUrl" value="jdbc:mysql:///hib_demo"></property>
		<property name="user" value="root"></property>
		<property name="password" value="root"></property>
		<property name="initialPoolSize" value="3"></property>
		<property name="maxPoolSize" value="10"></property>
		<property name="maxStatements" value="100"></property>
		<property name="acquireIncrement" value="2"></property>
	</bean>
	// IOC容器注入
	private DataSource dataSource;
	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	
	public void save() {
		try {
			String sql = "insert into t_dept(deptName) values('test');";
			Connection con = null;
			Statement stmt = null;
			// 连接对象
			con = dataSource.getConnection();
			// 执行命令对象
			stmt =  con.createStatement();
			// 执行
			stmt.execute(sql);
			
			// 关闭
			stmt.close();
			con.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

Spring来提供了JdbcTemplate这么一个类给我们使用!它封装了DataSource,也就是说我们可以在Dao中使用JdbcTemplate就行了。

创建dataSource,创建jdbcTemplate对象

<?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:c="http://www.springframework.org/schema/c"
       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">

    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql:///zhongfucheng"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
        <property name="initialPoolSize" value="3"></property>
        <property name="maxPoolSize" value="10"></property>
        <property name="maxStatements" value="100"></property>
        <property name="acquireIncrement" value="2"></property>
    </bean>

    <!--扫描注解-->
    <context:component-scan base-package="bb"/>

    <!-- 2. 创建JdbcTemplate对象 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    
</beans>

userDao

package bb;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

/**
 * Created by ozc on 2017/5/10.
 */


@Component
public class UserDao implements IUser {

    //使用Spring的自动装配
    @Autowired
    private JdbcTemplate template;

    @Override
    public void save() {
        String sql = "insert into user(name,password) values('zhoggucheng','123')";
        template.update(sql);
    }

}

测试:

@Test
public void test33() {
  ApplicationContext ac = new ClassPathXmlApplicationContext("bb/bean.xml");

  UserDao userDao = (UserDao) ac.getBean("userDao");
  userDao.save();
}

这里写图片描述


2.1 JdbcTemplate查询

我们要是使用JdbcTemplate查询会发现有很多重载了query()方法

这里写图片描述

一般地,如果我们使用queryForMap(),那么只能封装一行的数据,如果封装多行的数据、那么就会报错!并且,Spring是不知道我们想把一行数据封装成是什么样的,因此返回值是Map集合…我们得到Map集合的话还需要我们自己去转换成自己需要的类型。

我们一般使用下面这个方法:

这里写图片描述

我们可以实现RowMapper,告诉Spriing我们将每行记录封装成怎么样的

    public void query(String id) {
        String sql = "select * from USER where password=?";

        List<User> query = template.query(sql, new RowMapper<User>() {


            //将每行记录封装成User对象
            @Override
            public User mapRow(ResultSet resultSet, int i) throws SQLException {
                User user = new User();
                user.setName(resultSet.getString("name"));
                user.setPassword(resultSet.getString("password"));

                return user;
            }

        },id);


        System.out.println(query);
    }

这里写图片描述


当然了,一般我们都是将每行记录封装成一个JavaBean对象的,因此直接实现RowMapper,在使用的时候创建就好了

	class MyResult implements RowMapper<Dept>{

		// 如何封装一行记录
		@Override
		public Dept mapRow(ResultSet rs, int index) throws SQLException {
			Dept dept = new Dept();
			dept.setDeptId(rs.getInt("deptId"));
			dept.setDeptName(rs.getString("deptName"));
			return dept;
		}
		
	}

3. 事务控制概述

下面主要讲解Spring的事务控制,如何使用Spring来对程序进行事务控制…

  • Spring的事务控制是属于Spring Dao模块的

一般地,我们事务控制都是在service层做的。。为什么是在service层而不是在dao层呢??有没有这样的疑问…

service层是业务逻辑层,service的方法一旦执行成功,那么说明该功能没有出错

一个service方法可能要调用dao层的多个方法…如果在dao层做事务控制的话,一个dao方法出错了,仅仅把事务回滚到当前dao的功能,这样是不合适的[因为我们的业务由多个dao方法组成]。如果没有出错,调用完dao方法就commit了事务,这也是不合适的[导致太多的commit操作]。

事务控制分为两种:

  • 编程式事务控制
  • 声明式事务控制

3.1 编程式事务控制

自己手动控制事务,就叫做编程式事务控制。

  • Jdbc代码:Conn.setAutoCommite(false); // 设置手动控制事务
  • Hibernate代码:Session.beginTransaction(); // 开启一个事务
  • 特点:细粒度的事务控制: 可以对指定的方法、指定的方法的某几行添加事务控制(比较灵活,但开发起来比较繁琐: 每次都要开启、提交、回滚.)

3.2声明式事务控制

Spring提供对事务的控制管理就叫做声明式事务控制

Spring提供了对事务控制的实现。

  • 如果用户想要使用Spring的事务控制,只需要配置就行了
  • 当不用Spring事务的时候,直接移除就行了。
  • 特点:Spring的事务控制是基于Spring AOP实现的。因此它的耦合度是非常低的。【粗粒度的事务控制: 只能给整个方法应用事务,不可以对方法的某几行应用事务。】(因为aop拦截的是方法。)

Spring给我们提供了事务的管理器类,事务管理器类又分为两种,因为JDBC的事务和Hibernate的事务是不一样的

  • Spring声明式事务管理器类:
  •    Jdbc技术:DataSourceTransactionManager
    
  •    Hibernate技术:HibernateTransactionManager
    

3.3 声明式事务控制教程

我们基于Spring的JDBC来做例子吧

引入相关jar包(如果用maven,那引入pom依赖就好了)

  • AOP相关的jar包【因为Spring的声明式事务控制是基于AOP的,那么就需要引入AOP的jar包。】
  • 引入tx名称空间
  • 引入AOP名称空间
  • 引入jdbcjar包【jdbc.jar包和tx.jar包】

3.3.1搭建配置环境

编写一个接口

public interface IUser {
    void save();
}

UserDao实现类,使用JdbcTemplate对数据库进行操作!

@Repository
public class UserDao implements IUser {

    //使用Spring的自动装配
    @Autowired
    private JdbcTemplate template;

    @Override
    public void save() {
        String sql = "insert into user(name,password) values('zhong','222')";
        template.update(sql);
    }

}

userService

@Service
public class UserService {

    @Autowired
    private UserDao userDao;
    public void save() {

        userDao.save();
    }
}

bean.xml配置:配置数据库连接池、jdbcTemplate对象、扫描注解

<?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:c="http://www.springframework.org/schema/c"
       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">


    <!--数据连接池配置-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql:///zhongfucheng"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
        <property name="initialPoolSize" value="3"></property>
        <property name="maxPoolSize" value="10"></property>
        <property name="maxStatements" value="100"></property>
        <property name="acquireIncrement" value="2"></property>
    </bean>

    <!--扫描注解-->
    <context:component-scan base-package="bb"/>

    <!-- 2. 创建JdbcTemplate对象 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

</beans>

前面搭建环境的的时候,是没有任何的事务控制的。也就是说,当我在service中调用两次userDao.save(),即时在中途中有异常抛出,还是可以在数据库插入一条记录的

Service代码:

@Service
public class UserService {

    @Autowired
    private UserDao userDao;
    public void save() {

        userDao.save();

        int i = 1 / 0;
        userDao.save();
    }
}

测试代码:

public class Test2 {

    @Test
    public void test33() {
        ApplicationContext ac = new ClassPathXmlApplicationContext("bb/bean.xml");

        UserService userService = (UserService) ac.getBean("userService");
        userService.save();
    }
}

这里写图片描述


3.3.2XML方式实现声明式事务控制

首先,我们要配置事务的管理器类:因为JDBC和Hibernate的事务控制是不同的。

    <!--1.配置事务的管理器类:JDBC-->
    <bean id="txManage" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <!--引用数据库连接池-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

再而,配置事务管理器类如何管理事务

    <!--2.配置如何管理事务-->
    <tx:advice id="txAdvice" transaction-manager="txManage">
        
        <!--配置事务的属性-->
        <tx:attributes>
            <!--所有的方法,并不是只读-->
            <tx:method name="*" read-only="false"/>
        </tx:attributes>
    </tx:advice>
    

最后,配置拦截哪些方法,

    <!--3.配置拦截哪些方法+事务的属性-->
    <aop:config>
        <aop:pointcut id="pt" expression="execution(* bb.UserService.*(..) )"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"></aop:advisor>
    </aop:config>

配置完成之后,service中的方法都应该被Spring的声明式事务控制了。因此我们再次测试一下:

@Test
public void test33() {
  ApplicationContext ac = new ClassPathXmlApplicationContext("bb/bean.xml");

  UserService userService = (UserService) ac.getBean("userService");
  userService.save();
}

这里写图片描述


3.3.3 使用注解的方法实现事务控制

当然了,有的人可能觉得到XML文件上配置太多东西了。Spring也提供了使用注解的方式来实现对事务控制

第一步和XML的是一样的,必须配置事务管理器类:

    <!--1.配置事务的管理器类:JDBC-->
    <bean id="txManage" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <!--引用数据库连接池-->
        <property name="dataSource" ref="dataSource"/>
    </bean>
    

第二步:开启以注解的方式来实现事务控制

    <!--开启以注解的方式实现事务控制-->
    <tx:annotation-driven transaction-manager="txManage"/>
    

最后,**想要控制哪个方法事务,在其前面添加@Transactional这个注解就行了!**如果想要控制整个类的事务,那么在类上面添加就行了。

    @Transactional
    public void save() {

        userDao.save();

        int i = 1 / 0;
        userDao.save();
    }

这里写图片描述

4.事务属性

其实我们**在XML配置管理器类如何管理事务,就是在指定事务的属性!**我们来看一下事务的属性有什么:

这里写图片描述

4.1事务传播行为:

看了上面的事务属性,没有接触过的属性其实就这么一个:propagation = Propagation.REQUIRED事务的传播行为。

事务传播行为的属性有以下这么多个,常用的就只有两个:

  • Propagation.REQUIRED【如果当前方法已经有事务了,加入当前方法事务
  • Propagation.REQUIRED_NEW【如果当前方法有事务了,当前方法事务会挂起。始终开启一个新的事务,直到新的事务执行完、当前方法的事务才开始】

这里写图片描述

4.2 当事务传播行为是Propagation.REQUIRED

现在有一个日志类,它的事务传播行为是Propagation.REQUIRED

	Class Log{
			Propagation.REQUIRED  
			insertLog();  
	}

现在,我要在保存之前记录日志

	Propagation.REQUIRED
	Void  saveDept(){
		insertLog();   
		saveDept();
	}

saveDept()本身就存在着一个事务,当调用insertLog()的时候,insertLog()的事务会加入到saveDept()事务中

也就是说,saveDept()方法内始终是一个事务,如果在途中出现了异常,那么insertLog()的数据是会被回滚的【因为在同一事务内】

	Void  saveDept(){
		insertLog();    // 加入当前事务
		.. 异常, 会回滚
		saveDept();
	}

4.3当事务传播行为是Propagation.REQUIRED_NEW

现在有一个日志类,它的事务传播行为是Propagation.REQUIRED_NEW

	Class Log{
			Propagation.REQUIRED  
			insertLog();  
	}

现在,我要在保存之前记录日志

	Propagation.REQUIRED
	Void  saveDept(){
		insertLog();   
		saveDept();
	}

当执行到saveDept()中的insertLog()方法时,insertLog()方法发现 saveDept()已经存在事务了,insertLog()会独自新开一个事务,直到事务关闭之后,再执行下面的方法

如果在中途中抛出了异常,insertLog()是不会回滚的,因为它的事务是自己的,已经提交了

	Void  saveDept(){
		insertLog();    // 始终开启事务
		.. 异常, 日志不会回滚
		saveDept();
	}

Spring事务原理

Spring事务管理我相信大家都用得很多,但可能仅仅局限于一个@Transactional注解或者在XML中配置事务相关的东西。不管怎么说,日常可能足够我们去用了。但作为程序员,无论是为了面试还是说更好把控自己写的代码,还是应该得多多了解一下Spring事务的一些细节。

这里我抛出几个问题,看大家能不能瞬间答得上:

  • 如果嵌套调用含有事务的方法,在Spring事务管理中,这属于哪个知识点?
  • 我们使用的框架可能是Hibernate/JPA或者是Mybatis,都知道的底层是需要一个session/connection对象来帮我们执行操作的。要保证事务的完整性,我们需要多组数据库操作要使用同一个session/connection对象,而我们又知道Spring IOC所管理的对象默认都是单例的,这为啥我们在使用的时候不会引发线程安全问题呢?内部Spring到底干了什么?
  • 人家所说的BPP又是啥东西?
  • Spring事务管理重要接口有哪几个?

一、阅读本文需要的基础知识

阅读这篇文章的同学我默认大家都对Spring事务相关知识有一定的了解了。(ps:如果不了解点解具体的文章去阅读再回到这里来哦)

我们都知道,Spring事务是Spring AOP的最佳实践之一,所以说AOP入门基础知识(简单配置,使用)是需要先知道的。如果想更加全面了解AOP可以看这篇文章:AOP重要知识点(术语介绍、全面使用)。说到AOP就不能不说AOP底层原理:动态代理设计模式。到这里,对AOP已经有一个基础的认识了。于是我们就可以使用XML/注解方式来配置Spring事务管理

在IOC学习中,可以知道的是Spring中Bean的生命周期(引出BPP对象)并且IOC所管理的对象默认都是单例的:单例设计模式,单例对象如果有"状态"(有成员变量),那么多线程访问这个单例对象,可能就造成线程不安全。那么何为线程安全?,解决线程安全有很多方式,但其中有一种:让每一个线程都拥有自己的一个变量:ThreadLocal

如果对我以上说的知识点不太了解的话,建议点击蓝字进去学习一番。

二、两个不靠谱直觉的例子

2.1第一个例子

之前朋友问了我一个例子:

在Service层抛出Exception,在Controller层捕获,那如果在Service中有异常,那会事务回滚吗?

// Service方法
	
@Transactional
public Employee addEmployee() throws Exception {

    Employee employee = new Employee("3y", 23);
    employeeRepository.save(employee);
	// 假设这里出了Exception
    int i = 1 / 0;

    return employee;
}

// Controller调用
@RequestMapping("/add")
public Employee addEmployee() {
    Employee employee = null;
    try {
        employee = employeeService.addEmployee();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return employee;

}

第一反应:不会回滚吧。

  • 我当时是这样想的:因为Service层已经抛出了异常,由Controller捕获。那是否回滚应该由Controller的catch代码块中逻辑来决定,如果catch代码块没有回滚,那应该是不会回滚。

但朋友经过测试说,可以回滚阿。(pappapa打脸)

发生了运行时Exception,Spring事务管理自动回滚

看了一下文档,原来文档有说明:

By default checked exceptions do not result in the transactional interceptor marking the transaction for rollback and instances of RuntimeException and its subclasses do

结论:如果是编译时异常不会自动回滚,如果是运行时异常,那会自动回滚

2.2第二个例子

第二个例子来源于知乎@柳树文章,文末会给出相应的URL

我们都知道,带有@Transactional注解所包围的方法就能被Spring事务管理起来,那如果我在当前类下使用一个没有事务的方法去调用一个有事务的方法,那我们这次调用会怎么样?是否会有事务呢?

用代码来描述一下:

// 没有事务的方法去调用有事务的方法
public Employee addEmployee2Controller() throws Exception {

    return this.addEmployee();
}

@Transactional
public Employee addEmployee() throws Exception {

    employeeRepository.deleteAll();
    Employee employee = new Employee("3y", 23);

    // 模拟异常
    int i = 1 / 0;

    return employee;
}

我第一直觉是:这跟Spring事务的传播机制有关吧。

其实这跟Spring事务的传播机制没有关系,下面我讲述一下:

  • Spring事务管理用的是AOP,AOP底层用的是动态代理。所以如果我们在类或者方法上标注注解@Transactional,那么会生成一个代理对象

接下来我用图来说明一下:

Spring会自动生成代理对象

显然地,我们拿到的是代理(Proxy)对象,调用addEmployee2Controller()方法,而addEmployee2Controller()方法的逻辑是target.addEmployee(),调用回原始对象(target)的addEmployee()。所以这次的调用压根就没有事务存在,更谈不上说Spring事务传播机制了。

原有的数据:

原有的数据

测试结果:压根就没有事务的存在

没有事务的存在

2.2.1再延伸一下

从上面的测试我们可以发现:如果是在本类中没有事务的方法来调用标注注解@Transactional方法,最后的结论是没有事务的。那如果我将这个标注注解的方法移到别的Service对象上,有没有事务?

@Service
public class TestService {

    @Autowired
    private EmployeeRepository employeeRepository;
    
    @Transactional
    public Employee addEmployee() throws Exception {

        employeeRepository.deleteAll();

        Employee employee = new Employee("3y", 23);

        // 模拟异常
        int i = 1 / 0;

        return employee;
    }

}


@Service
public class EmployeeService {

    @Autowired
    private TestService testService;
    // 没有事务的方法去调用别的类有事务的方法
    public Employee addEmployee2Controller() throws Exception {
        return testService.addEmployee();
    }
}


测试结果:

抛出了运行时异常,但我们的数据还是存在的!

因为我们用的是代理对象(Proxy)去调用addEmployee()方法,那就当然有事务了。

看完这两个例子,有没有觉得3y的直觉是真的水

三、Spring事务传播机制

如果嵌套调用含有事务的方法,在Spring事务管理中,这属于哪个知识点?

在当前含有事务方法内部调用其他的方法(无论该方法是否含有事务),这就属于Spring事务传播机制的知识点范畴了。

Spring事务基于Spring AOP,Spring AOP底层用的动态代理,动态代理有两种方式:

  • 基于接口代理(JDK代理)
    • 基于接口代理,凡是类的方法非public修饰,或者用了static关键字修饰,那这些方法都不能被Spring AOP增强
  • 基于CGLib代理(子类代理)
    • 基于子类代理,凡是类的方法使用了private、static、final修饰,那这些方法都不能被Spring AOP增强

至于为啥以上的情况不能增强,用你们的脑瓜子想一下就知道了。

值得说明的是:那些不能被Spring AOP增强的方法并不是不能在事务环境下工作了。只要它们被外层的事务方法调用了,由于Spring事务管理的传播级别,内部方法也可以工作在外部方法所启动的事务上下文中

至于Spring事务传播机制的几个级别,我在这里就不贴出来了。这里只是再次解释“啥情况才是属于Spring事务传播机制的范畴”。

四、多线程问题

我们使用的框架可能是Hibernate/JPA或者是Mybatis,都知道的底层是需要一个session/connection对象来帮我们执行操作的。要保证事务的完整性,我们需要多组数据库操作要使用同一个session/connection对象,而我们又知道Spring IOC所管理的对象默认都是单例的,这为啥我们在使用的时候不会引发线程安全问题呢?内部Spring到底干了什么?

回想一下当年我们学Mybaits的时候,是怎么编写Session工具类

Mybatis工具类部分代码截图

没错,用的就是ThreadLocal,同样地,Spring也是用的ThreadLocal。

以下内容来源《精通 Spring4.x》

我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态的“状态性对象”采用ThreadLocal封装,让它们也成为线程安全的“状态性对象”,因此,有状态的Bean就能够以singleton的方式在多线程中工作。

我们可以试着点一下进去TransactionSynchronizationManager中看一下:

全都是ThreadLocal

五、啥是BPP?

BBP的全称叫做:BeanPostProcessor,一般我们俗称对象后处理器

  • 简单来说,通过BeanPostProcessor可以对我们的对象进行“加工处理”。

Spring管理Bean(或者说Bean的生命周期)也是一个常考的知识点,我在秋招也重新整理了一下步骤,因为比较重要,所以还是在这里贴一下吧:

  1. ResouceLoader加载配置信息
  2. BeanDefintionReader解析配置信息,生成一个一个的BeanDefintion
  3. BeanDefintion由BeanDefintionRegistry管理起来
  4. BeanFactoryPostProcessor对配置信息进行加工(也就是处理配置的信息,一般通过PropertyPlaceholderConfigurer来实现)
  5. 实例化Bean
  6. 如果该Bean配置/实现了InstantiationAwareBean,则调用对应的方法
  7. 使用BeanWarpper来完成对象之间的属性配置(依赖)
  8. 如果该Bean配置/实现了Aware接口,则调用对应的方法
  9. 如果该Bean配置了BeanPostProcessor的before方法,则调用
  10. 如果该Bean配置了init-method或者实现InstantiationBean,则调用对应的方法
  11. 如果该Bean配置了BeanPostProcessor的after方法,则调用
  12. 将对象放入到HashMap中
  13. 最后如果配置了destroy或者DisposableBean的方法,则执行销毁操作

Application中Bean的声明周期

其中也有关于BPP图片:

BBP所在的位置

5.1为什么特意讲BPP?

Spring AOP编程底层通过的是动态代理技术,在调用的时候肯定用的是代理对象。那么Spring是怎么做的呢?

我只需要写一个BPP,在postProcessBeforeInitialization或者postProcessAfterInitialization方法中,对对象进行判断,看他需不需要织入切面逻辑,如果需要,那我就根据这个对象,生成一个代理对象,然后返回这个代理对象,那么最终注入容器的,自然就是代理对象了。

Spring提供了BeanPostProcessor,就是让我们可以对有需要的对象进行“加工处理”啊!

六、认识Spring事务几个重要的接口

Spring事务可以分为两种:

  • 编程式事务(通过代码的方式来实现事务)
  • 声明式事务(通过配置的方式来实现事务)

编程式事务在Spring实现相对简单一些,而声明式事务因为封装了大量的东西(一般我们使用简单,里头都非常复杂),所以声明式事务实现要难得多。

在编程式事务中有以下几个重要的了接口:

  • TransactionDefinition:定义了Spring兼容的事务属性(比如事务隔离级别、事务传播、事务超时、是否只读状态)
  • TransactionStatus:代表了事务的具体运行状态(获取事务运行状态的信息,也可以通过该接口间接回滚事务等操作)
  • PlatformTransactionManager:事务管理器接口(定义了一组行为,具体实现交由不同的持久化框架来完成—类比JDBC)

PlatformTransactionManager解析

在声明式事务中,除了TransactionStatus和PlatformTransactionManager接口,还有几个重要的接口:

  • TransactionProxyFactoryBean:生成代理对象
  • TransactionInterceptor:实现对象的拦截
  • TransactionAttrubute:事务配置的数据

Spring事务的一个线程安全问题

只有光头才能变强。

文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y

大年初二,朋友问了我一个技术的问题(朋友实在是好学,佩服!)

该问题来源知乎(synchronized锁问题):

开启10000个线程,每个线程给员工表的money字段【初始值是0】加1,没有使用悲观锁和乐观锁,但是在业务层方法上加了synchronized关键字,问题是代码执行完毕后数据库中的money 字段不是10000,而是小于10000 问题出在哪里?

Service层代码:

代码

SQL代码(没有加悲观/乐观锁):

SQL代码(没有加悲观/乐观锁)

用1000个线程跑代码:

用1000个线程跑代码:

简单来说:多线程跑一个使用synchronized关键字修饰的方法,方法内操作的是数据库,按正常逻辑应该最终的值是1000,但经过多次测试,结果是低于1000。这是为什么呢?

一、我的思考

既然测试出来的结果是低于1000,那说明这段代码不是线程安全的。不是线程安全的,那问题出现在哪呢?众所周知,synchronized方法能够保证所修饰的代码块、方法保证有序性、原子性、可见性

讲道理,以上的代码跑起来,问题中Service层的increaseMoney()有序的、原子的、可见的,所以断定跟synchronized应该没关系。

(参考我之前写过的synchronize锁笔记:Java锁机制了解一下)

既然Java层面上找不到原因,那分析一下数据库层面的吧(因为方法内操作的是数据库)。在increaseMoney()方法前加了@Transcational注解,说明这个方法是带有事务的。事务能保证同组的SQL要么同时成功,要么同时失败。讲道理,如果没有报错的话,应该每个线程都对money值进行+1。从理论上来说,结果应该是1000的才对。

(参考我之前写过的Spring事务:一文带你看懂Spring事务!)

根据上面的分析,我怀疑是提问者没测试好(hhhh,逃),于是我也跑去测试了一下,发现是以提问者的方式来使用是真的有问题

首先贴一下我的测试代码:

@RestController
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    @RequestMapping("/add")
    public void addEmployee() {
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> employeeService.addEmployee()).start();
        }
    }


}

@Service
public class EmployeeService {

    @Autowired
    private EmployeeRepository employeeRepository;


    @Transactional
    public synchronized void addEmployee() {

        // 查出ID为8的记录,然后每次将年龄增加一
        Employee employee = employeeRepository.getOne(8);
        System.out.println(employee);
        Integer age = employee.getAge();
        employee.setAge(age + 1);

        employeeRepository.save(employee);
    }

}

简单地打印了每次拿到的employee值,并且拿到了SQL执行的顺序,如下(贴出小部分):

SQL执行的顺序

从打印的情况我们可以得出:多线程情况下并没有串行执行addEmployee()方法。这就导致对同一个值做重复的修改,所以最终的数值比1000要少。

二、图解出现的原因

发现并不是同步执行的,于是我就怀疑synchronized关键字和Spring肯定有点冲突。于是根据这两个关键字搜了一下,找到了问题所在。

我们知道Spring事务的底层是Spring AOP,而Spring AOP的底层是动态代理技术。跟大家一起回顾一下动态代理:

    public static void main(String[] args) {

        // 目标对象
        Object target ;

        Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), Main.class, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                // 但凡带有@Transcational注解的方法都会被拦截

                // 1... 开启事务

                method.invoke(target);

                // 2... 提交事务

                return null;
            }
            
        });
    }

(详细请参考我之前写过的动态代理:给女朋友讲解什么是代理模式)

实际上Spring做的处理跟以上的思路是一样的,我们可以看一下TransactionAspectSupport类中invokeWithinTransaction()

Spring事务管理是如何实现的

调用方法开启事务,调用方法提交事务

Spring事务和synchronized锁互斥问题

在多线程环境下,就可能会出现:方法执行完了(synchronized代码块执行完了),事务还没提交,别的线程可以进入被synchronized修饰的方法,再读取的时候,读到的是还没提交事务的数据,这个数据不是最新的,所以就出现了这个问题。

事务未提交,别的线程读取到旧数据

三、解决问题

从上面我们可以发现,问题所在是因为@Transcational注解和synchronized一起使用了,加锁的范围没有包括到整个事务。所以我们可以这样做:

新建一个名叫SynchronizedService类,让其去调用addEmployee()方法,整个代码如下:

@RestController
public class EmployeeController {

    @Autowired
    private SynchronizedService synchronizedService ;

    @RequestMapping("/add")
    public void addEmployee() {
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> synchronizedService.synchronizedAddEmployee()).start();
        }
    }
}

// 新建的Service类
@Service
public class SynchronizedService {

    @Autowired
    private EmployeeService employeeService ;
	
    // 同步
    public synchronized void synchronizedAddEmployee() {
        employeeService.addEmployee();

    }
}

@Service
public class EmployeeService {

    @Autowired
    private EmployeeRepository employeeRepository;

    
    @Transactional
    public void addEmployee() {

        // 查出ID为8的记录,然后每次将年龄增加一
        Employee employee = employeeRepository.getOne(8);
        System.out.println(Thread.currentThread().getName() + employee);
        Integer age = employee.getAge();
        employee.setAge(age + 1);

        employeeRepository.save(employee);

    }
}

我们将synchronized锁的范围包含到整个Spring事务上,这就不会出现线程安全的问题了。在测试的时候,我们可以发现1000个线程跑起来比之前要慢得多,当然我们的数据是正确的:

正确的数据

最后

可以发现的是,虽然说Spring事务用起来我们是非常方便的,但如果不了解一些Spring事务的细节,很多时候出现Bug了就百思不得其解。还是得继续加油努力呀~~~

IOC再回顾和面试题

本来想的是刷完《Spring 实战 (第4版)》和《精通Spring4.x 企业应用开发实战》的IOC章节后来重新编写一篇IOC的文章的,看了一下之前已经写过的入门系列Spring入门这一篇就够了Spring【依赖注入】就是这么简单。最主要的知识点都已经讲过了,所以感觉就没必要重新来编写这些知识点了…

这篇文章主要是补充和强化一些比较重要的知识点,并会把上面的两本书关于IOC的知识点整理出来。

那么接下来就开始吧,如果有错的地方希望能多多包涵,并不吝在评论区指正!

一、Spring IOC全面认知

结合《Spring 实战 (第4版)》和《精通Spring4.x 企业应用开发实战》两本书的IOC章节将其知识点整理起来~

1.1IOC和DI概述

在《精通Spring4.x 企业应用开发实战》中对IOC的定义是这样的:

IoC(Inversion of Control)控制反转,包含了两个方面:一、控制。二、反转

我们可以简单认为:

  • 控制指的是:当前对象对内部成员的控制权
  • 反转指的是:这种控制权不由当前对象管理了,由其他(类,第三方容器)来管理。

IOC不够开门见山,于是Martin Fowler提出了DI(dependency injection)来替代IoC,即让调用类对某一接口实现类的依赖关系由第三方(容器或协作类)注入,以移除调用类对某一接口实现类的依赖。

在《Spring 实战 (第4版)》中并没有提及到IOC,而是直接来说DI的:

通过DI,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定,对象无需自行创建或管理它们的依赖关系,依赖关系将被自动注入到需要它们的对象当中去

从书上我们也可以发现:IoC和DI的定义(区别)并不是如此容易就可以说得清楚的了。这里我就简单摘抄一下:

  • IoC(思想,设计模式)主要的实现方式有两种:依赖查找,依赖注入
  • 依赖注入是一种更可取的方式(实现的方式)

对我们而言,其实也没必要分得那么清,混合一谈也不影响我们的理解…

再通过昨天写过的工厂模式理解了没有?,我们现在就可以很清楚的发现,其实所谓的IOC容器就是一个大工厂【第三方容器】(Spring实现的功能很强大!比我们自己手写的工厂要好很多)。

使用IOC的好处(知乎@Intopass的回答):

  1. 不用自己组装,拿来就用。
  2. 享受单例的好处,效率高,不浪费空间。
  3. 便于单元测试,方便切换mock组件。
  4. 便于进行AOP操作,对于使用者是透明的。
  5. 统一配置,便于修改。

参考资料:

1.2IOC容器的原理

从上面就已经说了:IOC容器其实就是一个大工厂,它用来管理我们所有的对象以及依赖关系。

  • 原理就是通过Java的反射技术来实现的!通过反射我们可以获取类的所有信息(成员变量、类名等等等)!
  • 再通过配置文件(xml)或者注解来描述类与类之间的关系
  • 我们就可以通过这些配置信息和反射技术来构建出对应的对象和依赖关系了!

上面描述的技术只要学过点Java的都能说出来,这一下子可能就会被面试官问倒了,我们简单来看看实际Spring IOC容器是怎么实现对象的创建和依赖的:

  1. 根据Bean配置信息在容器内部创建Bean定义注册表
  2. 根据注册表加载、实例化bean、建立Bean与Bean之间的依赖关系
  3. 将这些准备就绪的Bean放到Map缓存池中,等待应用程序调用

Spring容器(Bean工厂)可简单分成两种:

  • BeanFactory
    • 这是最基础、面向Spring的
  • ApplicationContext
    • 这是在BeanFactory基础之上,面向使用Spring框架的开发者。提供了一系列的功能!

几乎所有的应用场合都是使用ApplicationContext!

BeanFactory的继承体系:

ApplicationContext的继承体系:

其中在ApplicationContext子类中又有一个比较重要的:WebApplicationContext

专门为Web应用准备的

Web应用与Spring融合:

我们看看BeanFactory的生命周期:

接下来我们再看看ApplicationContext的生命周期:

初始化的过程都是比较长,我们可以分类来对其进行解析:

  • Bean自身的方法:如调用 Bean 构造函数实例化 Bean,调用 Setter 设置 Bean 的属性值以及通过的 init-method 和 destroy-method 所指定的方法;
  • Bean级生命周期接口方法:如 BeanNameAware、 BeanFactoryAware、 InitializingBean 和 DisposableBean,这些接口方法由 Bean 类直接实现;
  • 容器级生命周期接口方法:在上图中带“★” 的步骤是由 InstantiationAwareBean PostProcessor 和 BeanPostProcessor 这两个接口实现,一般称它们的实现类为“ 后处理器” 。 后处理器接口一般不由 Bean 本身实现,它们独立于 Bean,实现类以容器附加装置的形式注册到Spring容器中并通过接口反射为Spring容器预先识别。当Spring 容器创建任何 Bean 的时候,这些后处理器都会发生作用,所以这些后处理器的影响是全局性的。当然,用户可以通过合理地编写后处理器,让其仅对感兴趣Bean 进行加工处理

ApplicationContext和BeanFactory不同之处在于:

  • ApplicationContext会利用Java反射机制自动识别出配置文件中定义的BeanPostProcessor、 InstantiationAwareBeanPostProcesso 和BeanFactoryPostProcessor后置器,并自动将它们注册到应用上下文中。而BeanFactory需要在代码中通过手工调用addBeanPostProcessor()方法进行注册
  • ApplicationContext在初始化应用上下文的时候就实例化所有单实例的Bean。而BeanFactory在初始化容器的时候并未实例化Bean,直到第一次访问某个Bean时实例化目标Bean。

有了上面的知识点了,我们再来详细地看看Bean的初始化过程:

简要总结:

  • BeanDefinitionReader读取Resource所指向的配置文件资源,然后解析配置文件。配置文件中每一个<bean>解析成一个BeanDefinition对象,并保存到BeanDefinitionRegistry中;
  • 容器扫描BeanDefinitionRegistry中的BeanDefinition;调用InstantiationStrategy进行Bean实例化的工作;使用BeanWrapper完成Bean属性的设置工作;
  • 单例Bean缓存池:Spring 在DefaultSingletonBeanRegistry类中提供了一个用于缓存单实例 Bean 的缓存器,它是一个用HashMap实现的缓存器,单实例的Bean以beanName为键保存在这个HashMap中。

1.3IOC容器装配Bean

1.3.1装配Bean方式

Spring4.x开始IOC容器装配Bean有4种方式:

  • XML配置
  • 注解
  • JavaConfig
  • 基于Groovy DSL配置(这种很少见)

总的来说:我们以XML配置+注解来装配Bean得多,其中注解这种方式占大部分

1.3.2依赖注入方式

依赖注入的方式有3种方式:

  • 属性注入–>通过setter()方法注入
  • 构造函数注入
  • 工厂方法注入

总的来说使用属性注入是比较灵活和方便的,这是大多数人的选择!

1.3.3对象之间关系

<bean>对象之间有三种关系:

  • 依赖–>挺少用的(使用depends-on就是依赖关系了–>前置依赖【依赖的Bean需要初始化之后,当前Bean才会初始化】)
  • 继承–>可能会用到(指定abstract和parent来实现继承关系)
  • 引用–>最常见(使用ref就是引用关系了)

1.3.4Bean的作用域

Bean的作用域:

  • 单例Singleton
  • 多例prototype
  • 与Web应用环境相关的Bean作用域
    • reqeust
    • session

使用到了Web应用环境相关的Bean作用域的话,是需要我们手动配置代理的~

原因也很简单:因为我们默认的Bean是单例的,为了适配Web应用环境相关的Bean作用域—>每个request都需要一个对象,此时我们返回一个代理对象出去就可以完成我们的需求了!


将Bean配置单例的时候还有一个问题:

  • 如果我们的Bean配置的是单例,而Bean对象里边的成员对象我们希望是多例的话。那怎么办呢??
  • 默认的情况下我们的Bean单例,返回的成员对象也默认是单例的(因为对象就只有那么一个)!

此时我们需要用到了lookup方法注入,使用也很简单,看看例子就明白了:

1.3.6处理自动装配的歧义性

昨天在刷书的时候刚好看到了有人在知乎邀请我回答这个问题:

结合两本书的知识点,可以归纳成两种解决方案:

  • 使用@Primary注解设置为首选的注入Bean
  • 使用@Qualifier注解设置特定名称的Bean来限定注入!
    • 也可以使用自定义的注解来标识

1.3.7引用属性文件以及Bean属性

之前在写配置文件的时候都是直接将我们的数据库配置信息在里面写死的了:

其实我们有更优雅的做法:将这些配置信息写到配置文件上(因为这些配置信息很可能是会变的,而且有可能被多个配置文件引用).

  • 如此一来,我们改的时候就十分方便了。

引用配置文件的数据使用的是${}

除了引用配置文件上的数据,我们还可以引用Bean的属性

引用Bean的属性使用的是#{}

在这种技术在《Spring 实战 第四版》称之为Spring EL,跟我们之前学过的EL表达式是类似的。主要的功能就是上面的那种,想要更深入了解可参考下面的链接:

1.3.8组合配置文件

xml文件之间组合:

xml和javaconfig互相组合的方式:

	public static void main(String[] args) {
		
        //1.通过构造函数加载配置类
         ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConf.class);

        //2.通过编码方式注册配置类
		 AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
		 ctx.register(DaoConfig.class);
		 ctx.register(ServiceConfig.class);
		 ctx.refresh();

        //3.通过XML组装@Configuration配置类所提供的配置信息
		 ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/conf/beans2.xml");

        //4.通过@Configuration组装XML配置所提供的配置信息
		 ApplicationContext ctx = new AnnotationConfigApplicationContext(LogonAppConfig.class);

		 //5.@Configuration的配置类相互引用
		 ApplicationContext ctx = new AnnotationConfigApplicationContext(DaoConfig.class,ServiceConfig.class);
         LogonService logonService = ctx.getBean(LogonService.class);
         System.out.println((logonService.getLogDao() !=null));
         logonService.printHelllo();   
	}

第一种的例子:

第二种的例子:

第三种的例子:

第四种的例子:

1.3.9装配Bean总结

总的来说,Spring IOC容器就是在创建Bean的时候有很多的方式给了我们实现,其中也包括了很多关于Bean的配置~

对于Bean相关的注入教程代码和简化配置(p和c名称空间)我就不一一说明啦,你们去看Spring入门这一篇就够了Spring【依赖注入】就是这么简单就行了。

总的对比图:


分别的应用场景:

至于一些小的知识点:

  • 方法替换
    • 使用某个Bean的方法替换成另一个Bean的方法
  • 属性编辑器
    • Spring可以对基本类型做转换就归结于属性编辑器的功劳!
  • 国际化
    • 使用不同语言(英语、中文)的操作系统去显式不同的语言
  • profile与条件化的Bean
    • 满足了某个条件才初始化Bean,这可以方便切换生产环境和开发环境~
  • 容器事件
    • 类似于我们的Servlet的监听器,只不过它是在Spring中实现了~

上面这些小知识点比较少情况会用到,这也不去讲解啦。知道有这么一回事,到时候查查就会用啦~~~

二、Spring IOC相关面试题

将SpringIOC相关知识点整理了一遍,要想知道哪些知识点是比较重要的。很简单,我们去找找相关的面试题就知道了,如果该面试题是常见的,那么说明这个知识点还是相对比较重要的啦!

以下的面试题从各种博客上摘抄下来,摘抄量较大的会注明出处的~

2.1什么是spring?

什么是spring?

Spring 是个java企业级应用的开源开发框架。Spring主要用来开发Java应用,但是有些扩展是针对构建J2EE平台的web应用。Spring框架目标是简化Java企业级应用开发,并通过POJO为基础的编程模型促进良好的编程习惯。

2.2使用Spring框架的好处是什么?

使用Spring框架的好处是什么?

  • 轻量:Spring 是轻量的,基本的版本大约2MB。
  • 控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们。
  • 面向切面的编程(AOP):Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。
  • 容器:Spring 包含并管理应用中对象的生命周期和配置。
  • MVC框架:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。
  • 事务管理:Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)。
  • 异常处理:Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转化为一致的unchecked 异常。

2.3Spring由哪些模块组成?

Spring由哪些模块组成?

简单可以分成6大模块:

  • Core
  • AOP
  • ORM
  • DAO
  • Web
  • Spring EE

2.4BeanFactory 实现举例

BeanFactory 实现举例

Bean工厂是工厂模式的一个实现,提供了控制反转功能,用来把应用的配置和依赖从正真的应用代码中分离

在spring3.2之前最常用的是XmlBeanFactory的,但现在被废弃了,取而代之的是:XmlBeanDefinitionReader和DefaultListableBeanFactory

2.5什么是Spring的依赖注入?

什么是Spring的依赖注入?

依赖注入,是IOC的一个方面,是个通常的概念,它有多种解释。这概念是说你不用创建对象,而只需要描述它如何被创建。你不在代码里直接组装你的组件和服务,但是要在配置文件里描述哪些组件需要哪些服务,之后一个容器(IOC容器)负责把他们组装起来。

2.6有哪些不同类型的IOC(依赖注入)方式?

有哪些不同类型的IOC(依赖注入)方式?

  • 构造器依赖注入:构造器依赖注入通过容器触发一个类的构造器来实现的,该类有一系列参数,每个参数代表一个对其他类的依赖。
  • Setter方法注入:Setter方法注入是容器通过调用无参构造器或无参static工厂 方法实例化bean之后,调用该bean的setter方法,即实现了基于setter的依赖注入。
  • 工厂注入:这个是遗留下来的,很少用的了!

2.7哪种依赖注入方式你建议使用,构造器注入,还是 Setter方法注入?

哪种依赖注入方式你建议使用,构造器注入,还是 Setter方法注入?

你两种依赖方式都可以使用,构造器注入和Setter方法注入。最好的解决方案是用构造器参数实现强制依赖,setter方法实现可选依赖

2.8什么是Spring beans?

什么是Spring beans?

Spring beans 是那些形成Spring应用的主干的java对象。它们被Spring IOC容器初始化,装配,和管理。这些beans通过容器中配置的元数据创建。比如,以XML文件中<bean/>的形式定义。

这里有四种重要的方法给Spring容器提供配置元数据

  • XML配置文件。
  • 基于注解的配置。
  • 基于java的配置。
  • Groovy DSL配置

2.9解释Spring框架中bean的生命周期

解释Spring框架中bean的生命周期

  • Spring容器 从XML 文件中读取bean的定义,并实例化bean。
  • Spring根据bean的定义填充所有的属性。
  • 如果bean实现了BeanNameAware 接口,Spring 传递bean 的ID 到 setBeanName方法。
  • 如果Bean 实现了 BeanFactoryAware 接口, Spring传递beanfactory 给setBeanFactory 方法。
  • 如果有任何与bean相关联的BeanPostProcessors,Spring会在postProcesserBeforeInitialization()方法内调用它们。
  • 如果bean实现IntializingBean了,调用它的afterPropertySet方法,如果bean声明了初始化方法,调用此初始化方法。
  • 如果有BeanPostProcessors 和bean 关联,这些bean的postProcessAfterInitialization() 方法将被调用。
  • 如果bean实现了 DisposableBean,它将调用destroy()方法。

2.10解释不同方式的自动装配

解释不同方式的自动装配

  • no:默认的方式是不进行自动装配,通过显式设置ref 属性来进行装配。
  • byName:通过参数名 自动装配,Spring容器在配置文件中发现bean的autowire属性被设置成byname,之后容器试图匹配、装配和该bean的属性具有相同名字的bean。
  • byType::通过参数类型自动装配,Spring容器在配置文件中发现bean的autowire属性被设置成byType,之后容器试图匹配、装配和该bean的属性具有相同类型的bean。如果有多个bean符合条件,则抛出错误。
  • constructor:这个方式类似于byType, 但是要提供给构造器参数,如果没有确定的带参数的构造器参数类型,将会抛出异常。
  • autodetect:首先尝试使用constructor来自动装配,如果无法工作,则使用byType方式。

只用注解的方式时,注解默认是使用byType的

2.11IOC的优点是什么?

IOC的优点是什么?

IOC 或 依赖注入把应用的代码量降到最低。它使应用容易测试,单元测试不再需要单例和JNDI查找机制。最小的代价和最小的侵入性使松散耦合得以实现。IOC容器支持加载服务时的饿汉式初始化和懒加载

2.12哪些是重要的bean生命周期方法? 你能重载它们吗?

哪些是重要的bean生命周期方法? 你能重载它们吗?

有两个重要的bean 生命周期方法,第一个是setup, 它是在容器加载bean的时候被调用。第二个方法是 teardown 它是在容器卸载类的时候被调用。

The bean 标签有两个重要的属性(init-methoddestroy-method)。用它们你可以自己定制初始化和注销方法。它们也有相应的注解(@PostConstruct@PreDestroy)。

2.13怎么回答面试官:你对Spring的理解?

怎么回答面试官:你对Spring的理解?

来源:

下面我就截几个答案:

一、

二、

2.14Spring框架中的单例Beans是线程安全的么?

Spring框架中的单例Beans是线程安全的么?

Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。但实际上,大部分的Spring bean并没有可变的状态(比如Serview类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。如果你的bean有多种状态的话(比如 View Model 对象),就需要自行保证线程安全

最浅显的解决办法就是将多态bean的作用域由“singleton”变更为“prototype”

2.15FileSystemResource和ClassPathResource有何区别?

FileSystemResource和ClassPathResource有何区别?

在FileSystemResource 中需要给出spring-config.xml文件在你项目中的相对路径或者绝对路径。在ClassPathResource中spring会在ClassPath中自动搜寻配置文件,所以要把ClassPathResource文件放在ClassPath下。

如果将spring-config.xml保存在了src文件夹下的话,只需给出配置文件的名称即可,因为src文件夹是默认。

简而言之,ClassPathResource在环境变量中读取配置文件,FileSystemResource在配置文件中读取配置文件

AOP再回顾

这篇文章主要是补充和强化一些比较重要的知识点

一、Spring AOP全面认知

结合《Spring 实战 (第4版)》和《精通Spring4.x 企业应用开发实战》两本书的AOP章节将其知识点整理起来~

1.1AOP概述

AOP称为面向切面编程,那我们怎么理解面向切面编程??

我们可以先看看下面这段代码:

我们学Java面向对象的时候,如果代码重复了怎么办啊??可以分成下面几个步骤:

  • 1:抽取成方法
  • 2:抽取类

抽取成类的方式我们称之为:纵向抽取

  • 通过继承的方式实现纵向抽取

但是,我们现在的办法不行:即使抽取成类还是会出现重复的代码,因为这些逻辑(开始、结束、提交事务)依附在我们业务类的方法逻辑中

现在纵向抽取的方式不行了,AOP的理念:就是将分散在各个业务逻辑代码中相同的代码通过横向切割的方式抽取到一个独立的模块中!

上面的图也很清晰了,将重复性的逻辑代码横切出来其实很容易(我们简单可认为就是封装成一个类就好了),但我们要将这些被我们横切出来的逻辑代码融合到业务逻辑中,来完成和之前(没抽取前)一样的功能!这就是AOP首要解决的问题了!

1.2Spring AOP原理

被我们横切出来的逻辑代码融合到业务逻辑中,来完成和之前(没抽取前)一样的功能

没有学Spring AOP之前,我们就可以使用代理来完成。

  • 如果看过我写的给女朋友讲解什么是代理模式这篇文章的话,一定就不难理解上面我说的那句话了
  • 代理能干嘛?代理可以帮我们增强对象的行为!使用动态代理实质上就是调用时拦截对象方法,对方法进行改造、增强

其实Spring AOP的底层原理就是动态代理

来源《精通Spring4.x 企业应用开发实战》一段话:

Spring AOP使用纯Java实现,它不需要专门的编译过程,也不需要特殊的类装载器,它在运行期通过代理方式向目标类织入增强代码。在Spring中可以无缝地将Spring AOP、IoC和AspectJ整合在一起。

来源《Spring 实战 (第4版)》一句话:

Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截

在Java中动态代理有两种方式:

  • JDK动态代理
  • CGLib动态代理

JDK动态代理是需要实现某个接口了,而我们类未必全部会有接口,于是CGLib代理就有了~~

  • CGLib代理其生成的动态代理对象是目标类的子类
  • Spring AOP默认是使用JDK动态代理,如果代理的类没有接口则会使用CGLib代理

那么JDK代理和CGLib代理我们该用哪个呢??在《精通Spring4.x 企业应用开发实战》给出了建议:

  • 如果是单例的我们最好使用CGLib代理,如果是多例的我们最好使用JDK代理

原因:

  • JDK在创建代理对象时的性能要高于CGLib代理,而生成代理对象的运行性能却比CGLib的低。
  • 如果是单例的代理,推荐使用CGLib

看到这里我们就应该知道什么是Spring AOP(面向切面编程)了:将相同逻辑的重复代码横向抽取出来,使用动态代理技术将这些重复代码织入到目标对象方法中,实现和原来一样的功能

  • 这样一来,我们就在写业务时只关心业务代码,而不用关心与业务无关的代码

1.3AOP的实现者

AOP除了有Spring AOP实现外,还有著名的AOP实现者:AspectJ,也有可能大家没听说过的实现者:JBoss AOP~~

我们下面来说说AspectJ扩展一下知识面:

AspectJ是语言级别的AOP实现,扩展了Java语言,定义了AOP语法,能够在编译期提供横切代码的织入,所以它有专门的编译器用来生成遵守Java字节码规范的Class文件。

而Spring借鉴了AspectJ很多非常有用的做法,融合了AspectJ实现AOP的功能。但Spring AOP本质上底层还是动态代理,所以Spring AOP是不需要有专门的编辑器的~

1.4AOP的术语

嗯,AOP搞了好几个术语出来~~两本书都有讲解这些术语,我会尽量让大家看得明白的:

连接点(Join point):

  • 能够被拦截的地方:Spring AOP是基于动态代理的,所以是方法拦截的。每个成员方法都可以称之为连接点~

切点(Poincut):

  • 具体定位的连接点:上面也说了,每个方法都可以称之为连接点,我们具体定位到某一个方法就成为切点

增强/通知(Advice):

  • 表示添加到切点的一段逻辑代码,并定位连接点的方位信息
    • 简单来说就定义了是干什么的,具体是在哪干
    • Spring AOP提供了5种Advice类型给我们:前置、后置、返回、异常、环绕给我们使用!

织入(Weaving):

  • 增强/通知添加到目标类的具体连接点上的过程。

引入/引介(Introduction):

  • 引入/引介允许我们向现有的类添加新方法或属性。是一种特殊的增强!

切面(Aspect):

  • 切面由切点和增强/通知组成,它既包括了横切逻辑的定义、也包括了连接点的定义。

在《Spring 实战 (第4版)》给出的总结是这样子的:

通知/增强包含了需要用于多个应用对象的横切行为;连接点是程序执行过程中能够应用通知的所有点;切点定义了通知/增强被应用的具体位置。其中关键的是切点定义了哪些连接点会得到通知/增强。

总的来说:

  • 这些术语可能翻译过来不太好理解,但对我们正常使用AOP的话影响并没有那么大~~看多了就知道它是什么意思了。

1.5Spring对AOP的支持

Spring提供了3种类型的AOP支持:

  • 基于代理的经典SpringAOP
    • 需要实现接口,手动创建代理
  • 纯POJO切面
    • 使用XML配置,aop命名空间
  • @AspectJ注解驱动的切面
    • 使用注解的方式,这是最简洁和最方便的!

二、基于代理的经典SpringAOP

这部分配置比较麻烦,用起来也很麻烦,这里我就主要整理一下书上的内容,大家看看了解一下吧,我们实际上使用Spring AOP基本不用这种方式了!

首先,我们来看一下增强接口的继承关系图:

可以分成五类增强的方式:

Spring提供了六种的切点类型

切面类型主要分成了三种

  • 一般切面
  • 切点切面
  • 引介/引入切面

一般切面,切点切面,引介/引入切面介绍:


对于切点切面我们一般都是直接用就好了,我们来看看引介/引入切面是怎么一回事:

  • 引介/引入切面是引介/引入增强的封装器,通过引介/引入切面,可以更容易地为现有对象添加任何接口的实现

继承关系图:

引介/引入切面有两个实现类:

  • DefaultIntroductionAdvisor:常用的实现类
  • DeclareParentsAdvisor:用于实现AspectJ语言的DeclareParent注解表示的引介/引入切面

实际上,我们使用AOP往往是Spring内部使用BeanPostProcessor帮我们创建代理

这些代理的创建器可以分成三类:

  • 基于Bean配置名规则的自动代理创建器:BeanNameAutoProxyCreator
  • 基于Advisor匹配机制的自动代理创建器:它会对容器所有的Advisor进行扫描,实现类为DefaultAdvisorAutoProxyCreator
  • 基于Bean中的AspectJ注解标签的自动代理创建器:AnnotationAwareAspectJAutoProxyCreator

对应的类继承图:

嗯,基于代理的经典SpringAOP就讲到这里吧,其实我是不太愿意去写这个的,因为已经几乎不用了,在《Spring 实战 第4版》也没有这部分的知识点了。

  • 但是通过这部分的知识点可以更加全面地认识Spring AOP的各种接口吧~

三、拥抱基于注解和命名空的AOP编程

Spring在新版本中对AOP功能进行了增强,体现在这么几个方面:

  • 在XML配置文件中为AOP提供了aop命名空间
  • 增加了AspectJ切点表达式语言的支持
  • 可以无缝地集成AspectJ

那我们使用@AspectJ来玩AOP的话,学什么??其实也就是上面的内容,学如何设置切点、创建切面、增强的内容是什么…

具体的切点表达式使用还是前往:Spring【AOP模块】就这么简单看吧~~

对应的增强注解:

3.1使用引介/引入功能实现为Bean引入新方法

其实前置啊、后置啊这些很容易就理解了,整篇文章看下来就只有这个引介/引入切面有点搞头。于是我们就来玩玩吧~

我们来看一下具体的用法吧,现在我有个服务员的接口:

public interface Waiter {

    // 向客人打招呼
    void greetTo(String clientName);

    // 服务
    void serveTo(String clientName);
}

一位年轻服务员实现类:

public class NaiveWaiter implements Waiter {
    public void greetTo(String clientName) {
        System.out.println("NaiveWaiter:greet to " + clientName + "...");
    }

    @NeedTest
    public void serveTo(String clientName) {
        System.out.println("NaiveWaiter:serving " + clientName + "...");
    }

}

现在我想做的就是:想这个服务员可以充当售货员的角色,可以卖东西!当然了,我肯定不会加一个卖东西的方法到Waiter接口上啦,因为这个是暂时的~

所以,我搞了一个售货员接口:

public interface Seller {

  // 卖东西
  int sell(String goods, String clientName);
}

一个售货员实现类:

public class SmartSeller implements Seller {

	// 卖东西
	public int sell(String goods,String clientName) {
		System.out.println("SmartSeller: sell "+goods +" to "+clientName+"...");
		return 100;
	}
	
}

此时,我们的类图是这样子的:

现在我想干的就是:借助AOP的引入/引介切面,来让我们的服务员也可以卖东西

我们的引入/引介切面具体是这样干的:

@Aspect
public class EnableSellerAspect {
    
    @DeclareParents(value = "com.smart.NaiveWaiter",  // 指定服务员具体的实现
            defaultImpl = SmartSeller.class) // 售货员具体的实现
    public Seller seller; // 要实现的目标接口
    
}

写了这个切面类会发生什么??

  • 切面技术将SmartSeller融合到NaiveWaiter中,这样NaiveWaiter就实现了Seller接口!!!!

是不是很神奇??我也觉得很神奇啊,我们来测试一下:

我们的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"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
    <aop:aspectj-autoproxy/>
	<bean id="waiter" class="com.smart.NaiveWaiter"/>
	<bean class="com.smart.aspectj.basic.EnableSellerAspect"/>
</beans>

测试一下:

public class Test {
    public static void main(String[] args) {


        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/aspectj/basic/beans.xml");
        Waiter waiter = (Waiter) ctx.getBean("waiter");

        // 调用服务员原有的方法
        waiter.greetTo("Java3y");
        waiter.serveTo("Java3y");

        // 通过引介/引入切面已经将waiter服务员实现了Seller接口,所以可以强制转换
        Seller seller = (Seller) waiter;
        seller.sell("水军", "Java3y");

    }
}


具体的调用过程是这样子的:

当引入接口方法被调用时,代理对象会把此调用委托给实现了新接口的某个其他对象。实际上,一个Bean的实现被拆分到多个类中

3.2在XML中声明切面

我们知道注解很方便,但是,要想使用注解的方式使用Spring AOP就必须要有源码(因为我们要在切面类上添加注解)。如果没有源码的话,我们就得使用XML来声明切面了~

其实就跟注解差不多的功能:

我们就直接来个例子终结掉它吧:

首先我们来测试一下与传统的SpringAOP结合的advisor是怎么用的:

实现类:

xml配置文件:

一个一个来讲解还是太花时间了,我就一次性用图的方式来讲啦:

最后还有一个切面类型总结图,看完就几乎懂啦:

四、总结

看起来AOP有很多很多的知识点,其实我们只要记住AOP的核心概念就行啦。

下面是我的简要总结AOP:

  • AOP的底层实际上是动态代理,动态代理分成了JDK动态代理和CGLib动态代理。如果被代理对象没有接口,那么就使用的是CGLIB代理(也可以直接配置使用CBLib代理)
  • 如果是单例的话,那我们最好使用CGLib代理,因为CGLib代理对象运行速度要比JDK的代理对象要快
  • AOP既然是基于动态代理的,那么它只能对方法进行拦截,它的层面上是方法级别的
  • 无论经典的方式、注解方式还是XML配置方式使用Spring AOP的原理都是一样的,只不过形式变了而已。一般我们使用注解的方式使用AOP就好了。
  • 注解的方式使用Spring AOP就了解几个切点表达式,几个增强/通知的注解就完事了,是不是贼简单…使用XML的方式和注解其实没有很大的区别,很快就可以上手啦。
  • 引介/引入切面也算是一个比较亮的地方,可以用代理的方式为某个对象实现接口,从而能够使用借口下的方法。这种方式是非侵入式的~
  • 要增强的方法还可以接收与被代理方法一样的参数、绑定被代理方法的返回值这些功能…

各类知识点总结

下面的文章都有对应的原创精美PDF,在持续更新中,可以来找我催更~

涵盖Java后端所有知识点的开源项目(已有8K+ star):

如果大家想要实时关注我更新的文章以及分享的干货的话,微信搜索Java3y

PDF文档的内容均为手打,有任何的不懂都可以直接来问我(公众号有我的联系方式)。

我是三歪,一个想要变强的男人,感谢大家的点赞收藏和转发,下期见。

猜你喜欢

转载自blog.csdn.net/Java_3y/article/details/106563551