参考资料:3y的文章
1. Spring AOP & IOC
1.1 是什么?有何特点?
IOC:依赖反转,解决的是对象管理和对象依赖的问题
AOP:面向切面编程,解决的是非业务代码抽取的问题。
1.2 怎么用?
-
IoC:我们将对象的依赖关系交给Spring,需要对象的时候就去Spring之中去取。
好处:
- 将对象统一管理,便于修改
- 降低耦合度(调用方无需自己在代码之中组装,也不需要关心对象如何实现,直接从【IOC容器】之中去取就可以。
一般什么时候用:
- 一般都是A对象之中有B对象的情况下使用
- 比如用
@Component
注解标志将对象放入【IoC容器】,用@Autowired
在代码之中将对象注入。(如果一个接口有多个实现类,可以配合@Qualifier
来进行注入。
-
AOP:我们将非业务的代码交给Spring,只专注于业务代码。运行的时候会在也业务方法上面“动态织入”切面类型的代码。
- 比如我们在搞一个数据库插入的代码, 那么需要连接数据库,插入,commit,将连接还给线程池等等。但是这些和我们的核心业务都没关系。那么如何简化呢?就是将这些代码直接抽取出来,需要的时候在“织进去”。
- 那么“动态代理”,会让我们可以将对象“增强”,将非业务代码写在要【增强】的逻辑里面。之后,我们使用【增强之后的最想】来调用方法,这种方式就可以屏蔽掉重复代码。
上面这个图就是Spring AOP+IoC的一个例子:
- 首先将UserDao标明
@Component
,告诉Spring:“我这个是要让你托管的类” - 然后从
applicationContext
里面去拿到这样的一个Bean,并且其将其作为IUser这个接口的实现。也可以看出,其对save()
方法上面加入了具体的逻辑。 - 我们对这个对象
getClass()
,看看其是什么class。 - 直接调用这个接口——>使用的是Spring自动生成放在applicationContext里面的对象,并且将维护代码(@Before和@After)之中的代码已经动态的“植入”到了save()这个过程之中。
这个就是结果。可以看到其class是$proxy
,为代理生成。
2. Spring 入门
2.1 什么是Spring?
用来简化网络应用的开发。
- 通过DI和面向接口实现松耦合
- 基于切面进行声明式编程,减少样板式代码。
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 之中使用的时候:
比如这个,那么就实现了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的好处?
-
不需要自己组装,拿来就用
-
单例,效率高,不浪费空间
-
便于单元测试和切换mock组件:可以直接使用自己的mock组件对其做接口实现类的替换
-
便于使用AOP操作,对于使用者透明
-
统一配置,便于修改
2.2.5 Spring IoC的初始化过程?
先读取XML获得XMLResource,之后解析出对应的BeanDefination,再注册到BeanFactory之中。由BeanFactory创建对应的Bean并且放到ApplicatioContext里面,随用随取。当然还有单例和多例的创建时间不同,懒加载用在单例上等等。
2.3 Spring 怎么用?
2.3.1 得到Spring容器对象【IoC容器】
Spring之中的IoC容器不止一个:
- BeanFactory,功能简单
- 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之中一般有以下几种创建对象的方式:
- 无参构造函数创建对象
- 带参数的构造函数创建对象
- 工厂创建对象
- 静态方法创建对象
- 非静态方法创建对象
带参数的构造函数创建对象:
<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.使用注解的两种步骤:
- 在XML文件之中使用注解扫描器
- 都用注解了,当然是做全套!直接在代码里面自定义扫描类
@ComponentScan
来扫描IoC容器的bean对象。这个注解一般情况下直接打在启动类上面。
2. 创建对象和处理对象依赖关系相关的注解:
- @ComponentScan:扫描器
- @Configuration:配置类
- @Component:最常用,指定将一个对象放入IoC容器
- @Repository, @Service,@controller这三个作用和@Component是一样的,只是程序员为了区分不同层次的代码,自己做的标记。
- @Resource:直接看下面
2.3.3 @Autowired 和 @Resource区别
我们常用的就是这两个,其区别是:
- @Autowired是Spring的注释,但是@Resource是JDK 1.6的注释
- @Autowired 默认是通过类型装配,如果一个接口有多个符合的实现,可以使用@Qualifier进一步指定名字。@Resource 是默认通过名字装配
2.3.4 装配配置
因为Spring的自动装配并不能将第三方库装配到应用之中,所以需要显式装配配置。显示装配有两种方式:通过Java代码和XML。一般都是使用Java代码配置。
如何通过Java代码配置Bean?
就是我们上面讲的,编写一个Java类,然后使用@Configuration修饰这个类
如何使用配置创建Bean?
- 使用@Bean 来修饰方法,该方法返回一个对象
- Spring是不管你对象如何创建的,只要能获取到对象就OK
- 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;
}
}
那么上面这段代码之中,我们能获得的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);
}
}
2.3.5 指定Spring创建对象的属性(单例/多例等)
之前的开篇,就有这么几个问题:
我们现在再看看,第一个问题迎刃而解:不需要自己写对象创建代码。
第二个Spring也是解决了的。
2.3.5.1 单例 多例
指定scope属性就可以。其只有单例 多例两个值可以选择。
- 当使用singleton 的时候,从IoC容器之中获取的对象都是同一个。
- 当使用prototype的时候,从IoC之中获取的对象每次都不同。
除了获取的对象是否是同一个,还有没有其他方面的不同呢?
有!创建时间不同:
- singleton的时候,对象在IoC容器之前就已经创建了
- prototype的时候,对象在使用时候才创建
为什么?singleton全局只需要一个对象,那么一开始就创建好比较省时间;但是prototype每次都要创建对象,我Spring又不知道你什么时候需要这个新对象,只能等你要求时候再创建咯~
2.3.5.2 lazy-init属性
之前讲了singleton的时候,对象会在IoC容器创建之前就被创建了,可是我们想要更多的掌控权,想要其在IoC容器创建之后再创建。那怎么做呢?
将lazy-init
设置成true。
2.3.5.3 init-method
和destroy-method
如果我们想在对象创建之后执行某个方法,指定为init-method就好
想在IoC容器销毁之后,执行某方法,指定为destroy-method。
注意哦,是对象创建之后和容器销毁之后,因为对象会一直在容器之中等待其他人调用。
2.4 Spring IoC容器之中BeanFactory和ApplicationContext的区别
一般而言我们推荐使用ApplicationContext,原因是:
- ApplicationContext是利用Java反射机制,自动识别出配置文件之中定义的 BeanpostProcessor,InstantiationAwareBeanPostProcessor和 BeanFactoryPostProcessor(这些相当于是在Bean创建前后对Bean做点操作,比如赋个值啥的),并且自动注册到应用上下文之中。而BeanFactory需要在代码之中手动调用addBeanPostProcessor()来进行注册。
- ApplicationContext在初始化应用上下文的时候就实例化所有单实例的Bean,但是BeanFactory在初始化容器的时候没有实例化Bean,直到第一次访问某个Bean的时候才实例化目标bean
2.5 有哪些不同类型的IoC方式?
- 构造器依赖注入:触发一个类的constructor实现的
- setter方法注入:调用无参构造器实现,再调用该Bean的setter方法,那么就实现了基于setter的依赖注入
3. AOP
3.1 如何实现?
有两种实现方式:
- jdk实现,缺点是只能对目标接口实现动态代理
- cglib实现,可以对类进行动态代理,但是类不可以是final
怎么选择?
单例的话最好使用CGLib代理,多例的话最好使用JDK代理。
原因是JDK在生成代理对象的时候性能高于CGLib,但是创建出来的对象运行速度却比不上CGLib。所以单例最好使用CGLib(创建的部分占比小),多例的时候使用JDK代理(创建的过程占比比较大)
3.2 Spring AOP和AspectJ AOP有和区别?
-
Spring AOP 是运行时增强—运行的时候产生新的功能增强过的类,AspectJ AOP使用的是编译时增强——在你编译的时候字节码就给你加好了。AspectJ具有专门的编译器来生成遵守Java字节码规范的Class文件。
-
切面多的情况下最好使用AspectJ,其性能比Spring AOP快很多。——编译就给你生成好了,当然比你后来运行时候生成要快
4. 杂项扩展
4.1 @Controller和@RestController区别?
@Controller是返回一个JSP页面,但是@RestController返回的是JSON或者XML格式的数据。
@RestController = @Controller + @ResponseBody
4.2 Spring之中Bean的作用域有哪些?
有五种,最后一种已经没了。
- singleton:默认,单例模式
- prototype: 每次请求都会生成一个新的bean实例
- request: 每次请求都会产生一个bean,其仅在当前的HTTP request之中有效
- session: 每次请求都会生成一个bean,仅在当前的HTTP session之中有效。
- global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话
4.3 Spring 之中单例 bean 的线程安全问题
多个线程操作同一个对象的时候,对这个对象的非静态成员的变量的写操作会存在线程问题。说白了,如果你的bean之中是有”状态“——比如有一个变量,那么就会存在线程安全问题。
两种方法:
- 在Bean对象之中尽可能避免可变的成员变量——Servlet之中所有参数全在方法上,自然没有多线程问题
- 在类之中定义一个ThreadLocal,将需要可变的变量存在ThreadLocal之中。
4.4 @Component 和 @Bean 区别在哪?
- 对象不同:@Component 注解在类上,@Bean 注解在方法上
- @Bean 注解比 @Component 的自定义性更强。比如引用第三方库的类需要装配到Spring容器之中时(比如@Configuration的),我们就只能使用@Bean
而且如果对于一个类,我们直接使用@Component, 那么可能内部有些不需要的类我们也生成了。@Bean的控制粒度更细
5 Spring事务
5.1 Spring 事务传播(Propagation)七种行为
- required:没有事务则新建事务,已经有的话就加入到外围事务之中。此处注意,会将一个标记了@Transaction的方法之中的所有事务都当做一个整体,其中任何一个有异常都会全部回滚。除此之外,即使是其中已经catch了相应的Exception也不算,也要回滚。
- supports:支持当前事务,当前没有事务则以非事务方式运行
- mandatory: 使用当前事务,当前没有事务则抛出异常
- requires_new:新建事务,如果当前存在事务,就将当前事务挂起——可以避免几个事务相互影响
- not_supported:以非事务方式运行,如果当前存在事务,就将当前事务挂起
- never:以非事务方式运行,当前存在事务,则抛出异常
- 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;
}
如图所示,事务仅仅存在于代理增强之后的proxy之中的addEmployee()
,那么这里使用的是我们原来的target,也就是上面没有标@Transactional
的这样一个类,当然就不会有事务啦。
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();
}
}
结果:
5.6 事务失效的几种原因?
参考:https://zhuanlan.zhihu.com/p/101396825
使用 Spring 的 @Transactional
注解控制事务,有哪些失效的场景?
主要是这八点:
- 数据库引擎是否支持事务(比如MyISAM就不支持事务)
- 注解所在的类是否被加载成Bean
- 注解所在的方法是否为public所修饰
- 是否发生了自调用机制——同一个类之中的方法相互调用
- 所用的数据源是否加载了事务管理器
@Transactional
的扩展配置propagation是否正确(七种传播行为)- 异常被吃掉了——自己try自己catch
- 异常抛出错误——抛的不是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)