1. 初识Spring
1.1 Spring起源
- Spring : 春天 —->给软件行业带来了春天
- 2002年,Rod Jahnson 首次推出了 Spring 框架雏形 interface21 框架。
- 2004年3月24日,Spring 框架以 interface21 框架为基础,经过重新设计,发布了1.0正式版。
- 很难想象 Rod Johnson 的学历 , 他是悉尼大学的博士,然而他的专业不是计算机,而是音乐学。
- Spring理念 : 使现有技术更加实用 . 本身就是一个大杂烩 , 整合现有的框架技术。
- 官网 : http://spring.io/
- 官方下载地址 : https://repo.spring.io/libs-release-local/org/springframework/spring/
- GitHub : https://github.com/spring-projects
1.2 Spring是什么
Spring 是一个轻量级的基于 IOC 和 AOP的容器框架,并且它是开源的,为了简化企业级开发而生。通过 Spring,可以使 JavaEE 开发更容易使用,并且通过使基于 POJO 的编程模型,促进良好的编程习惯。SpringMVC 是一个 MVC 的开源框架,相当于是 Struts2 加上 Spring 的整合,用于替代 servlet(处理响应请求,获取表单参数等),属于 SpringFrameWork 的后续产品。
SpringBoot 是一个微服务框架,延续了 Spring 框架的核心思想 IOC 和 AOP,并简化了应用的开发和部署,使用约定大于配置的思想,提供了一堆依赖打包,无需关注XML文件配置。
1.3 Spring组成结构
- spring core:spring 核心,是框架最基础的部分,提供spring IOC和依赖注入的功能
- spring context:spring 上下文容器,是对BeanFactory功能增强的一个子接口
- spring web:spring 的web模块,提供了对web应用开发的支持
- spring mvc:针对web应用mvc思想的实现
- spring orm:支持对流行ORM框架的整合,mybatis、hibernate
- spring dao:提供对JDBC的抽象,简化JDBC编码
- spring aop:面向切面编程,提供了与AOP兼容的编程实现
1.4 依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
2. IOC
2.1 IOC是什么
IoC(Inverse of Control:控制反转)是⼀种设计思想,就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。也就是说,你只需要在 spring 配置文件中配置对应的 bean 以及设置相关的属性,让 spring 容器来生成类的实例对象以及管理对象。在spring容器启动的时候,spring 会把你在配置文件中配置的 bean 都初始化好,然后在你需要调用的时候,就把它已经初始化好的那些 bean 分配给你需要调用这些 bean 的类。
DI—(Dependency Injection),即“依赖注入”:指 Spring 创建对象的过程中,将对象依赖属性通过配置进行注入。
2.2 IOC容器实现
在创建 Bean 之前,首先需要创建 IOC 容器,Spring 提供了 IOC 容器的两种实现方式:
- BeanFactory:IOC 容器的基本实现,是 Spring 内部的使用接口,是面向 Spring 本身的。
- ApplicationContext:BeanFactory 的子接口,它可以进行国际化处理、事件传递和 bean 自动装配以及各种不同应用层的Context实现 ,开发中基本都在使用 ApplicationContext,很少用到 BeanFactory。
- ClassPathXmlApplicationContext:它是从类的根路径下加载配置文件,推荐使用。
- FileSystemXmlApplicationContext:是从磁盘路径上加载配置文件,配置文件可以在磁盘的任意位置,且在初始化时就创建单例的bean,也可以通过配置的方式指定创建的 Bean 是多实例的。
- WebApplicationContext:专门为WEB应用而准备的,它允许从相对于 WEB 根目录的路径中完成初始化工作。
- ConfigurableApplicationContext:是 ApplicationContext 的子接口,包含一些扩展方法refresh()和close(),让ApplicationContext 具有启动、关闭和刷新上下文的能力。
2.3 Bean实例化
1、通过全类名(反射):从 IOC 容器中获取 bean 时,除了通过 id 值来获取,还可以通过 bean 的类型来获取,但是如果同一个类型的 bean 在 XML 配置文件中配置了多个,则获取时会抛出异常,所以同一个类型的 bean 在容器中必须是唯一的。
2、工厂方法获取 Bean:直接调用某一个类的静态方法就可以返回 Bean 的实例。 ```javajavaPerson person = ac.getBean("personTwo", Person.class);
public class BeanFactory2{ public static Object getBean() { Person p = new Person(); p.setAge(21); p.setName(“威少”); return p; } }
3、通过 FactoryBean 来获取:Spring中有两种类型的 bean,一种是普通 bean,另一种是工厂 bean,即 FactoryBean。工厂 bean 跟普通 bean 不同,其返回的对象不是指定类的一个实例,其返回的是该工厂 bean 的 getObject 方法所返回的对象。工厂 bean 必须实现 org.springframework.beans.factory.FactoryBean 接口。
```java
<bean id="factory" class="com.xuwei.test.MyFactory"/>
public class MyFactory implements FactoryBean {
@Override
public boolean isSingleton() {
return false;
}
//将创建好的 Bean 返回给 IOC 容器
@Override
public Object getObject() throws Exception {
Person p = new Person();
p.setName("威少666");
return p;
}
//返回 bean 的类型
@Override
public Class<?> getObjectType() {
return Person.class;
}
}
2.4 bean属性赋值
- setter 注入:在 bean 标签中通过 property 标签设置 bean 的属性值。
- 构造器注入:在 bean 标签中通过 constructor-arg 标签设置 bean 的属性值。
- 特殊值处理。
- 字面量:基本数据类型及其包装类型、String 等类型都可以采取字面值注入的方式。
- null 值:通过
标签给 bean 的属性赋 null 值。 - 特殊字符:若字面值中包含特殊字符,可以使用 <![CDATA[ ]]> 把字面值包裹起来。
- p 空间:使用p名称空间需要引入对应的约束,在 Idea 中根据提示引入即可。
- 引用外部已声明的 bean:通过 ref 属性或 ref 子标签引用 IOC 容器中配置好的该类型的 bean。
- 内部 bean:内部 bean 声明直接包含在
或 元素里,不需要设置 id。 - 级联属性赋值:当 bean 的属性是一个对象,我们可以通过配置当前 bean 的方式给属性中对象的属性赋值,即给属性的属性赋值,这种方式我们称为给级联属性赋值。
- 集合属性赋值。
:配置数组类型。 - :配置 Map 类型。
举例:引用外部属性文件。
druid.properties
#key=value
jdbc.username=root
jdbc.password=123
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.initialSize=5
jdbc.maxActive=10
application-context.xml
<!-- 引入外部属性文件 -->
<context:property-placeholder location="classpath:druid.properties"/>
<!-- 配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="driverClassName" value="${jdbc.driverClassName}"></property>
<property name="initialSize" value="${jdbc.initialSize}"></property>
<property name="maxActive" value="${jdbc.maxActive}"></property>
</bean>
2.5 bean作用域
可以通过 bean 定义中的 scope 属性来定义。
singleton
:spring ioc 容器中只存在一个 bean 实例,bean 以单例模式存在,是系统默认值。prototype
:每次调用 Bean 时,都返回一个新的实例。request
:每次 HTTP 请求都会创建一个新的 Bean,该作用域仅适用于 WebApplicationContext 环境。session
:同一个 HTTP Session 共享一个 Bean,不同 Session 使用不同 Bean,仅适用于 WebApplicationContext 环境。global-session
:用于 portlet 容器,因为每个 portlet 有单独的 session,globalsession 提供一个全局性的 http session。
Spring 中的单例 bean 的线程安全问题了解吗?有什么解决办法?
的确是存在安全问题的。因为,当多个线程操作同一个对象的时候,对这个对象的成员变量的写操作会存在线程安全问题。
但是,一般情况下,我们常用的 Controller、Service、Dao 这些 Bean 是无状态的。无状态的 Bean 不能保存数据,因此是线程安全的。
2 种解决办法:
- 在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。
- 改变 Bean 的作用域为 “prototype”:每次请求都会创建一个新的 bean 实例,自然不会存在线程安全问题。
2.6 bean生命周期
- Bean 容器找到配置文件中 Spring Bean 的定义。
- Bean 容器利用 Java Reflection API 实例化 bean 对象(通过构造方法或者工厂方法)。
- 如果涉及到一些属性值 利用 set() 方法设置一些属性值。
- 如果 Bean 实现了 *.Aware 接口,比如 BeanNameAware,就调用对应的方法。
- 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行前置方法
- 如果 Bean 在配置⽂件中的定义包含 init-method 属性,执行指定的方法。
- 如果有和加载这个 Bean的 Spring 容器相关的 BeanPostProcessor 对象,执行后置方法,此处会进行AOP。
- 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执⾏指定的⽅法。注意:必须手动关闭 ClassPathXmlApplicationContext 对象才会被调用。
举例:测试 Bean 的生命周期
1、创建 Person 类,在 Person 类创建 init 方法和 destroy 方法。
public class Person {
private String name;
private Integer age;
public Person() {
System.out.println("1:创建对象");
}
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
System.out.println("2:依赖注入");
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public void init() {
System.out.println("3:初始化方法");
}
public void destroy(){
System.out.println("5:销毁");
}
@Override
public String toString() {
return "4:使用Bean---" + "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
2、创建 bean 的后置处理器,需要实现 BeanPostProcessor 接口。
//bean的后置处理器,允许在初始化方法前后对bean进行额外的处理
public class RearProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("init之前,检查bean");
Person person = (Person) bean;
if ("威少".equals(person.getName())) {
person.setAge(100);
}
return person;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("init之后");
return bean;
}
}
3、在 applicationContext.xml 中配置 bean。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="person" class="com.xuwei.pojo.Person" init-method="init" destroy-method="destroy">
<property name="name" value="威少"/>
<property name="age" value="22"/>
</bean>
<bean class="com.xuwei.pojo.RearProcessor" />
</beans>
4、测试
public class PersonTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
Person person = ac.getBean("person", Person.class);
System.out.println(person);
ac.close();
}
}
5、结果
3. 自动装配
3.1 认识自动装配
自动装配,简单来说,就是将一个Bean 注入到其他 Bean 的 Property 中。Spring 框架默认不支持自动装配的,要想使用自动装配需要修改 spring 配置文件中
@Autowired 和 @Resource 的区别?
- @Autowired 默认按 byType 自动装配,而 @Resource 默认 byName 自动装配。
- @Autowired 只包含一个参数:required,表示是否开启自动装配,默认是 true。而 @Resource 包含七个参数,其中最重要的两个参数是:name 和 type。
- @Autowired 如果要使用 byName,需要使用 @Qualifier一起配合。而 @Resource 如果指定了 name,则用 byName 自动装配,如果指定了 type,则用 byType 自动装配。
- @Autowired 能够用在:构造器、方法、参数、成员变量和注解上,而 @Resource 能用在:类、成员变量和方法上。
- @Autowired 是 spring 定义的注解,而 @Resource 是 JSR-250 定义的注解。
3.2 开启自动装配
若要实现自动装配,光是使用 @Autowired 及 @Service 是不行的,需要进行组件扫描。可以在配置文件中配置哪些类需要被扫描(即需要自动装配的类)。
<context:component-scan base-package="com.maple.learn" />
SpringBoot 中 使用注解 @ComponentScan,用于类或接口上主要是指定扫描路径,spring 会把指定路径下带有指定注解的类自动装配到 bean 容器里。会被自动装配的注解包括 @Controller、@Service、@Component、@Repository 等等。其作用等同于配置。
推荐阅读:https://www.zhihu.com/question/39356740
4. AOP
4.1 AOP是什么
AOP:面向切面编程,其基本思想是在保证不修改源代码的前提下,去为系统中的业务组件添加某种通用功能。在实际开发中,比如商品查询、促销查询等业务,都需要记录日志、异常处理等操作,AOP 把所有共用代码都剥离出来,单独放置到某个类中进行集中管理,在具体运行时,由容器进行动态织入这些公共代码。
Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib ,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理。
AOP主要一般应用于签名验签、参数校验、日志记录、事务控制、权限控制、性能统计、异常处理等。
4.2 Spring AOP 和 AspectJ AOP
Spring AOP 属于运行时增强,⽽ AspectJ AOP 是编译时增强。 Spring AOP 基于代理,而 AspectJ 基于字节码操作。
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ,它比 Spring AOP 快很多。
4.3 AOP术语
- 横切关注点:与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志 , 安全 , 缓存 , 事务等等。
- 通知(Advice): AOP 框架中的增强处理,通知描述了切面何时执行以及如何执行增强处理。
- 连接点(join point):连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用。
- 切入点(pointcut):定位连接点的方式。每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物。如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP可以通过切入点定位到特定的连接点。切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
- 切点(PointCut): 可以插入增强处理的连接点。
- 切面(Aspect): 切面是通知和切点的结合。
- 引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。
- 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,这个过程就是织入。
4.4 配置AspectJ AOP
在 Spring2.0 以上版本中,可以使用基于 AspectJ 注解或基于 XML 配置的 AOP。AspectJ 支持5种类型的通知注解:
- @Before:前置通知,在方法执行之前执行。
- @After:后置通知,在方法执行之后执行。
- @AfterRunning:返回通知,在方法返回结果之后执行。
- @AfterThrowing:异常通知,在方法抛出异常之后执行。
- @Around:环绕通知,围绕着方法执行,相当于动态代理的全过程。
推荐阅读:https://www.cnblogs.com/joy99/p/10941543.html
举例说明:在方法执行之前进行一项操作。
实体类
@Component
public class CalculatorImpl implements Calculator {
@Override
public int add(int a, int b) {
int result = a + b;
return result;
}
@Override
public int sub(int a, int b) {
int result = a - b;
return result;
}
@Override
public int mul(int a, int b) {
int result = a * b;
return result;
}
@Override
public int div(int a, int b) {
int result = a / b;
return result;
}
}
代理类
@Aspect
@Component
public class LoggingAspect {
//前置通知,在方法执行之前执行
@Before(value = "execution(public int com.xuwei.aop.Calculator.*(int , int ))")
public void beforeAdvice() {
System.out.println("在方法执行之前执行!");
}
}
测试类
public void testAspects() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
Calculator calculator = ioc.getBean(Calculator.class);
calculator.add(10,2);
calculator.sub(10,2);
calculator.mul(10,2);
calculator.div(10,2);
}
配置文件
<!-- 配置要扫描的包 -->
<conntext:component-scan base-package="com.xuwei.aop"/>
<!-- 开启AspectJ注解支持 -->
<aop:aspectj-autoproxy/>
查看结果
5. Spring事务
5.1 声明式事务
Spring 中的事务有两种:编程式事务 和 声明式事务。
- 编程式事务管理需要将事务管理代码嵌入到业务方法中来控制事务的提交和回滚。相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余。
- 声明式事务将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。事务管理代码的固定模式作为一种横切关注点,可以通过AOP方法模块化,进而借助Spring AOP框架实现声明式事务管理。
5.2 事务管理器
Spring 的核心事务管理抽象是 PlatformTransactionManager 接口,它为事务管理封装了一组独立于技术的方法。无论使用 Spring 的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。事务管理器可以以普通的 bean 的形式声明在 Spring IOC 容器中。5.3 配置事务
基于 XML 配置 ```xml
**基于注解配置**
```xml
<!-- 配置要扫描的包 -->
<conntext:component-scan base-package="com.xuwei.aop"/>
<!-- 开启AspectJ注解支持 -->
<aop:aspectj-autoproxy/>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 开启事务注解支持 -->
<tx:annotation-driven/>
<!-- 引入外部属性文件 -->
<context:property-placeholder location="classpath:druid.properties"/>
<!--配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="driverClassName" value="${jdbc.driverClassName}"></property>
<property name="initialSize" value="${jdbc.initialSize}"></property>
<property name="maxActive" value="${jdbc.maxActive}"></property>
</bean>
5.4 事务传播行为
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。事务传播属性可以在 @Transactional 注解的 propagation 属性中定义。
支持当前事务的情况:
- TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
不支持当前事务的情况:
- TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。
其他情况:
- TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
注意:可以使用 read-only=”true” 不使用事务
5.5 事务隔离级别
TransactionDefinition 接口中定义了五个表示隔离级别的常量,我们在 @Transactiona l的 isolation 属性中设置隔离级别。
- 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.6 @Transactional(rollbackFor = Exception.class)注解了解吗?
我们知道:Exception 分为运行时异常 RuntimeException 和非运行时异常。事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。
当 @Transactional 注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。
- 在 @Transactional 注解中如果不配置 rollbackFor 属性,那么事务只会在遇到 RuntimeException 的时候才会回滚,加上 rollbackFor=Exception.class ,可以让事务在遇到非运行时异常时也回滚。
5.7 事务失效情况
- 方法访问权限问题,只支持 public。
- 方法用 final 修饰,动态代理不能代理 final 方法。
- 多线程调用,事务管理内部使用 threadLocal,不同线程间不在同一事务。
- 嵌套事务回滚多了,需要局部回滚的地方未做异常控制