Spring家族详解

Posted by Haiming on April 27, 2020

参考资料:3y的文章

1. Spring AOP & IOC

1.1 是什么?有何特点?

IOC:依赖反转,解决的是对象管理和对象依赖的问题

AOP:面向切面编程,解决的是非业务代码抽取的问题。

1.2 怎么用?

  1. IoC:我们将对象的依赖关系交给Spring,需要对象的时候就去Spring之中去取。

    好处:

    • 将对象统一管理,便于修改
    • 降低耦合度(调用方无需自己在代码之中组装,也不需要关心对象如何实现,直接从【IOC容器】之中去取就可以。

    一般什么时候用:

    • 一般都是A对象之中有B对象的情况下使用
    • 比如用@Component注解标志将对象放入【IoC容器】,用@Autowired在代码之中将对象注入。(如果一个接口有多个实现类,可以配合@Qualifier来进行注入。
  2. AOP:我们将非业务的代码交给Spring,只专注于业务代码。运行的时候会在也业务方法上面“动态织入”切面类型的代码。

    • 比如我们在搞一个数据库插入的代码, 那么需要连接数据库,插入,commit,将连接还给线程池等等。但是这些和我们的核心业务都没关系。那么如何简化呢?就是将这些代码直接抽取出来,需要的时候在“织进去”。
    • 那么“动态代理”,会让我们可以将对象“增强”,将非业务代码写在要【增强】的逻辑里面。之后,我们使用【增强之后的最想】来调用方法,这种方式就可以屏蔽掉重复代码。

img

上面这个图就是Spring AOP+IoC的一个例子:

  1. 首先将UserDao标明@Component,告诉Spring:“我这个是要让你托管的类”
  2. 然后从 applicationContext 里面去拿到这样的一个Bean,并且其将其作为IUser这个接口的实现。也可以看出,其对save()方法上面加入了具体的逻辑。
  3. 我们对这个对象getClass(),看看其是什么class。
  4. 直接调用这个接口——>使用的是Spring自动生成放在applicationContext里面的对象,并且将维护代码(@Before和@After)之中的代码已经动态的“植入”到了save()这个过程之中。

img

这个就是结果。可以看到其class是$proxy,为代理生成。

2. Spring 入门

2.1 什么是Spring?

用来简化网络应用的开发。

  1. 通过DI和面向接口实现松耦合
  2. 基于切面进行声明式编程,减少样板式代码。

2.2 为什么要有Spring?

2.2.1 “前Spring”时代如何进行松耦合

可以使用面向接口编程,通过DaoFactory等实现松耦合。

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不是直接生成,而是通过Factory去对应的类找对应的DAOImpl来完成。那么如果需要在Service 之中使用的时候:

img

比如这个,那么就实现了Service 和 DAO本身的解耦(中间使用Factory进行松耦合,修改的话只需要修改相应的DAOImpl,而不是需要在所有用到这个DAO的地方全部修改。

2.2.2 Spring如何实现松耦合

说白了,Factory不就是让创建对象的过程和使用对象的过程分开么?那我直接框架全给你生成,你需要时候自己过来取就好——Spring。

2.2.3 切面编程AOP

在第一部分已经讲了AOP是干嘛的,怎么使用。下面是一个Spring提供AOP的例子:

    @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);

    }

这里面的@Permission就是AOP,我们并不想每次都去写和业务无关的“判断权限”代码,那么就会动态织入这部分代码,然后将其生成之后的这个类传进来。

2.2.4 Spring IoC的好处?

  1. 不需要自己组装,拿来就用

  2. 单例,效率高,不浪费空间

  3. 便于单元测试和切换mock组件:可以直接使用自己的mock组件对其做接口实现类的替换

  4. 便于使用AOP操作,对于使用者透明

  5. 统一配置,便于修改

2.2.5 Spring IoC的初始化过程?

Spring IoC的初始化过程

先读取XML获得XMLResource,之后解析出对应的BeanDefination,再注册到BeanFactory之中。由BeanFactory创建对应的Bean并且放到ApplicatioContext里面,随用随取。当然还有单例和多例的创建时间不同,懒加载用在单例上等等。

2.3 Spring 怎么用?

2.3.1 得到Spring容器对象【IoC容器】

Spring之中的IoC容器不止一个:

  1. BeanFactory,功能简单
  2. ApplicationContext,功能强大【推荐使用】

BeanFactory的获取:

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

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

类路径下XML获取ApplicationContext

直接使用ClassPathXmlApplicationContext来获取

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

        System.out.println(ac);

IoC之中一般有以下几种创建对象的方式:

  1. 无参构造函数创建对象
  2. 带参数的构造函数创建对象
  3. 工厂创建对象
    1. 静态方法创建对象
    2. 非静态方法创建对象

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

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

2.3.2 使用注解方式得到对象

1.使用注解的两种步骤:

  1. 在XML文件之中使用注解扫描器
  2. 都用注解了,当然是做全套!直接在代码里面自定义扫描类@ComponentScan来扫描IoC容器的bean对象。这个注解一般情况下直接打在启动类上面。

2. 创建对象和处理对象依赖关系相关的注解:

  1. @ComponentScan:扫描器
  2. @Configuration:配置类
  3. @Component:最常用,指定将一个对象放入IoC容器
  4. @Repository, @Service,@controller这三个作用和@Component是一样的,只是程序员为了区分不同层次的代码,自己做的标记。
  5. @Resource:直接看下面

2.3.3 @Autowired 和 @Resource区别

我们常用的就是这两个,其区别是:

  1. @Autowired是Spring的注释,但是@Resource是JDK 1.6的注释
  2. @Autowired 默认是通过类型装配,如果一个接口有多个符合的实现,可以使用@Qualifier进一步指定名字。@Resource 是默认通过名字装配

2.3.4 装配配置

因为Spring的自动装配并不能将第三方库装配到应用之中,所以需要显式装配配置。显示装配有两种方式:通过Java代码和XML。一般都是使用Java代码配置。

如何通过Java代码配置Bean?

就是我们上面讲的,编写一个Java类,然后使用@Configuration修饰这个类

如何使用配置创建Bean?

  1. 使用@Bean 来修饰方法,该方法返回一个对象
  2. Spring是不管你对象如何创建的,只要能获取到对象就OK
  3. Spring会将这个对象加入Spring容器之中
  4. 容器之中的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;
    }

}

那么上面这段代码之中,我们能获得的bean 名字应该是 userDao。

如何测试配置之中的Bean?

要使用@ContextConfiguration加载配置类的信息,然后在相应的 applicationContext之中拿取。

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);
    }
}

img

2.3.5 指定Spring创建对象的属性(单例/多例等)

之前的开篇,就有这么几个问题:

img

我们现在再看看,第一个问题迎刃而解:不需要自己写对象创建代码。

第二个Spring也是解决了的。

2.3.5.1 单例 多例

指定scope属性就可以。其只有单例 多例两个值可以选择。

  1. 当使用singleton 的时候,从IoC容器之中获取的对象都是同一个。

img

  1. 当使用prototype的时候,从IoC之中获取的对象每次都不同。

img

除了获取的对象是否是同一个,还有没有其他方面的不同呢?

有!创建时间不同:

  1. singleton的时候,对象在IoC容器之前就已经创建了

img

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

img

为什么?singleton全局只需要一个对象,那么一开始就创建好比较省时间;但是prototype每次都要创建对象,我Spring又不知道你什么时候需要这个新对象,只能等你要求时候再创建咯~

2.3.5.2 lazy-init属性

之前讲了singleton的时候,对象会在IoC容器创建之前就被创建了,可是我们想要更多的掌控权,想要其在IoC容器创建之后再创建。那怎么做呢?

lazy-init设置成true。

img

2.3.5.3 init-methoddestroy-method

如果我们想在对象创建之后执行某个方法,指定为init-method就好

想在IoC容器销毁之后,执行某方法,指定为destroy-method。

注意哦,是对象创建之后和容器销毁之后,因为对象会一直在容器之中等待其他人调用。

2.4 Spring IoC容器之中BeanFactory和ApplicationContext的区别

一般而言我们推荐使用ApplicationContext,原因是:

  1. ApplicationContext是利用Java反射机制,自动识别出配置文件之中定义的 BeanpostProcessor,InstantiationAwareBeanPostProcessor和 BeanFactoryPostProcessor(这些相当于是在Bean创建前后对Bean做点操作,比如赋个值啥的),并且自动注册到应用上下文之中。而BeanFactory需要在代码之中手动调用addBeanPostProcessor()来进行注册。
  2. ApplicationContext在初始化应用上下文的时候就实例化所有单实例的Bean,但是BeanFactory在初始化容器的时候没有实例化Bean,直到第一次访问某个Bean的时候才实例化目标bean

2.5 有哪些不同类型的IoC方式?

  1. 构造器依赖注入:触发一个类的constructor实现的
  2. setter方法注入:调用无参构造器实现,再调用该Bean的setter方法,那么就实现了基于setter的依赖注入

3. AOP

3.1 如何实现?

有两种实现方式:

  1. jdk实现,缺点是只能对目标接口实现动态代理
  2. cglib实现,可以对类进行动态代理,但是类不可以是final

怎么选择?

单例的话最好使用CGLib代理,多例的话最好使用JDK代理。

原因是JDK在生成代理对象的时候性能高于CGLib,但是创建出来的对象运行速度却比不上CGLib。所以单例最好使用CGLib(创建的部分占比小),多例的时候使用JDK代理(创建的过程占比比较大)

3.2 Spring AOP和AspectJ AOP有和区别?

  1. Spring AOP 是运行时增强—运行的时候产生新的功能增强过的类,AspectJ AOP使用的是编译时增强——在你编译的时候字节码就给你加好了。AspectJ具有专门的编译器来生成遵守Java字节码规范的Class文件。

  2. 切面多的情况下最好使用AspectJ,其性能比Spring AOP快很多。——编译就给你生成好了,当然比你后来运行时候生成要快

4. 杂项扩展

4.1 @Controller和@RestController区别?

@Controller是返回一个JSP页面,但是@RestController返回的是JSON或者XML格式的数据。

@RestController = @Controller + @ResponseBody

4.2 Spring之中Bean的作用域有哪些?

有五种,最后一种已经没了。

  1. singleton:默认,单例模式
  2. prototype: 每次请求都会生成一个新的bean实例
  3. request: 每次请求都会产生一个bean,其仅在当前的HTTP request之中有效
  4. session: 每次请求都会生成一个bean,仅在当前的HTTP session之中有效。
  5. global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话

4.3 Spring 之中单例 bean 的线程安全问题

多个线程操作同一个对象的时候,对这个对象的非静态成员的变量的写操作会存在线程问题。说白了,如果你的bean之中是有”状态“——比如有一个变量,那么就会存在线程安全问题。

两种方法:

  1. 在Bean对象之中尽可能避免可变的成员变量——Servlet之中所有参数全在方法上,自然没有多线程问题
  2. 在类之中定义一个ThreadLocal,将需要可变的变量存在ThreadLocal之中。

4.4 @Component 和 @Bean 区别在哪?

  1. 对象不同:@Component 注解在类上,@Bean 注解在方法上
  2. @Bean 注解比 @Component 的自定义性更强。比如引用第三方库的类需要装配到Spring容器之中时(比如@Configuration的),我们就只能使用@Bean

而且如果对于一个类,我们直接使用@Component, 那么可能内部有些不需要的类我们也生成了。@Bean的控制粒度更细

5 Spring事务

5.1 Spring 事务传播(Propagation)七种行为

  1. required:没有事务则新建事务,已经有的话就加入到外围事务之中。此处注意,会将一个标记了@Transaction的方法之中的所有事务都当做一个整体,其中任何一个有异常都会全部回滚。除此之外,即使是其中已经catch了相应的Exception也不算,也要回滚。
  2. supports:支持当前事务,当前没有事务则以非事务方式运行
  3. mandatory: 使用当前事务,当前没有事务则抛出异常
  4. requires_new:新建事务,如果当前存在事务,就将当前事务挂起——可以避免几个事务相互影响
  5. not_supported:以非事务方式运行,如果当前存在事务,就将当前事务挂起
  6. never:以非事务方式运行,当前存在事务,则抛出异常
  7. nested:当前存在事务,就在嵌套事务之内运行。当前没有事务,就和required一样。

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

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

5.2 Spring 事务的五种隔离级别

TransactionDefinition 接口中定义了五个表示隔离级别的常量:

  • TransactionDefinition.ISOLATION_DEFAULT: 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别.
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
  • TransactionDefinition.ISOLATION_READ_COMMITTED: 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
  • TransactionDefinition.ISOLATION_REPEATABLE_READ: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • TransactionDefinition.ISOLATION_SERIALIZABLE: 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

5.3 @Transactional(rollback=Exception.class)作用是?

如果不加这个,那么只有遇到RuntimeException才会回滚。加了的话,在非运行异常时候也会回滚。

5.4 使用同一个类之中一个没有事务的方法去调用另一个有事务的方法,是否会有事务?

答案是不会。而且和事务的传播机制没关系。

// 没有事务的方法去调用有事务的方法
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;
}

img

如图所示,事务仅仅存在于代理增强之后的proxy之中的addEmployee(),那么这里使用的是我们原来的target,也就是上面没有标@Transactional的这样一个类,当然就不会有事务啦。

img

5.5 使用另一个类之中一个没有事务的方法去调用另一个有事务的方法,是否会有事务?

有的。因为另一个类调用的是proxy代理对象之中的这个方法,是增强之后的。

@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();
    }
}

结果:

img

5.6 事务失效的几种原因?

参考:https://zhuanlan.zhihu.com/p/101396825

使用 Spring 的 @Transactional 注解控制事务,有哪些失效的场景?

主要是这八点:

  1. 数据库引擎是否支持事务(比如MyISAM就不支持事务)
  2. 注解所在的类是否被加载成Bean
  3. 注解所在的方法是否为public所修饰
  4. 是否发生了自调用机制——同一个类之中的方法相互调用
  5. 所用的数据源是否加载了事务管理器
  6. @Transactional的扩展配置propagation是否正确(七种传播行为)
  7. 异常被吃掉了——自己try自己catch
  8. 异常抛出错误——抛的不是RuntimeException

5.6.1 数据库引擎不支持事务

底层基础决定上层建筑,如果数据库的底层引擎不支持事务,那么怎么搞都白搭啊。

5.6.2 没有被Spring管理

// @Service
public class OrderServiceImpl implements OrderService {

    @Transactional
    public void updateOrder(Order order) {
        // update order
    }

}

上面这个例子里面将@Service注释掉,那么这个类就不会被Spring进行管理,自然就没法进行AOP,那么自然就不会由Proxy生成新的带有注解的类,自然就没法使用了。

5.6.3 方法不是public的

包括下面的”自己类之中调用自己的方法导致失效“,Spring的@Transaction本质上是要别的类来调用这个方法,那么方法不是public的话,自然就没法使用别的类进行调用,不进行生成Transaction的相关部分也就情有可原了。

以下来自 Spring 官方文档:

When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.

大概意思就是 @Transactional 只能用于 public 的方法上。想要用在非public 方法上,请使用AspectJ模式——在编译阶段直接编制进去

5.6.4 自身调用问题

来了,他来了——

先两个栗子:

@Service
public class OrderServiceImpl implements OrderService {

    public void update(Order order) {
        updateOrder(order);
    }

    @Transactional
    public void updateOrder(Order order) {
        // update order
    }

}

可见我们上面代码之中的update()这个方法是没有@Transactional注解的,那么调用带有注解的updateOrder(Order order) ,updateOrder方法上面的事务会有作用吗?

或者是这种Propagation不当的”小天才“:

@Service
public class OrderServiceImpl implements OrderService {

    @Transactional
    public void update(Order order) {
        updateOrder(order);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateOrder(Order order) {
        // update order
    }

}

可见上面的代码之中,update(Order order)上面加入了 @Transactional,但是下面的updateOrder(Order order)之中使用REQUIRES_NEW新加入一个事务,那么新开的事务起作用么?

两个答案都是否定的。

类本身调用自己本身的方法,那么就是直接调用本身方法,而不是经过Spring的代理类。默认的情况下面,只有外部类调用其方法才会生效。你传播的方式打的再好也是白搭。

5.6.5 数据源没有配置事务管理器

数据源你都没按上@Transactional,其他的地方你在玩什么??

比如:

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

下面这个示例是是事务管理器的xml配置方法:

<!-- 设定transactionManager -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
   <property name="dataSource" ref="dataSource" />
</bean>
<!-- 支持 @Transactional 标记 -->
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" />

5.6.6 传播机制之中自己定义不支持事务

@Service
public class OrderServiceImpl implements OrderService {

    @Transactional
    public void update(Order order) {
        updateOrder(order);
    }

    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void updateOrder(Order order) {
        // update order
    }

}

Propagation.NOT_SUPPORTED: 表示不以事务运行,当前若存在事务则挂起。

那么这种情况下,生效和不生效有区别么?

5.6.7 异常被主动吃掉了,但是没抛出来

@Service
public class OrderServiceImpl implements OrderService {

    @Transactional
    public void updateOrder(Order order) {
        try {
            // update order
        } catch {

        }
    }

}

里面的try…catch会将异常吃掉,那么外围就感知不到异常,自然无法回滚。

5.6.8 异常抛的不对

@Service
public class OrderServiceImpl implements OrderService {

    @Transactional
    public void updateOrder(Order order) {
        try {
            // update order
        } catch {
            throw new Exception("更新错误");
        }
    }

}

这里面抛的是Exception,但是默认回滚的部分是RuntimeException。当然,可以通过额外的配置来支持触发其他异常的回滚:

@Transactional(rollbackFor = Exception.class)