Spring相关基础知识

Spring

  • 解决的是编译时的依赖

  • Class.forname()方法的作用,就是初始化给定的类。而我们给定的 MySQL 的 Driver 类中,它在静态代码块中通过 JDBC 的 DriverManager 注册了一下驱动。我们也可以直接使用 JDBC 的驱动管理器注册 mysql 驱动,从而代替使用 Class.forName

Reference

《Spring揭秘》 《Spring IN ACTION》

约定大于配置

约定大于配置,也可以叫做约定优于配置(convention over configuration),也称作按约定编程,是一种软件设计范式,指在减少软件开发人员需做决定的数量,获得简单的好处,而又不失灵活性。

循环依赖

If you use predominantly constructor injection, it is possible to create an unresolvable circular dependency scenario.

For example: Class A requires an instance of class B through constructor injection, and class B requires an instance of class A through constructor injection. If you configure beans for classes A and B to be injected into each other, the Spring IoC container detects this circular reference at runtime, and throws a BeanCurrentlyInCreationException.

One possible solution is to edit the source code of some classes to be configured by setters rather than constructors. Alternatively, avoid constructor injection and use setter injection only. In other words, although it is not recommended, you can configure circular dependencies with setter injection.

Unlike the typical case (with no circular dependencies), a circular dependency between bean A and bean B forces one of the beans to be injected into the other prior to being fully initialized itself (a classic chicken-and-egg scenario).

如果您主要使用构造函数注入,则可能会创建无法解决的循环依赖场景。

例如:A类通过构造函数注入需要B类的实例,B类通过构造函数注入需要A类的实例。如果为类 A 和 B 配置 bean 以相互注入,Spring IoC 容器会在运行时检测到此循环引用,并抛出 BeanCurrentlyInCreationException。

一种可能的解决方案是编辑某些类的源代码以由设置器而不是构造器配置。或者,避免构造函数注入并仅使用 setter 注入。也就是说,虽然不推荐,但是可以通过setter注入来配置循环依赖。

与典型情况(没有循环依赖关系)不同,bean A 和 bean B 之间的循环依赖关系强制其中一个 bean 在完全初始化之前注入另一个 bean(典型的先有鸡还是先有蛋的场景)。

延迟加载lazy-init

By default, ApplicationContext implementations eagerly create and configure all singleton beans as part of the initialization process.

通过lazy-init属性,可以将部分单例bean延迟加载。

Method Inject方法注入

在大多数应用场景中,容器中的大多数 bean 都是单例的。 当一个单例 bean 需要与另一个单例 bean 协作或非单例 bean 需要与另一个非单例 bean 协作时,您通常通过将一个 bean 定义为另一个 bean 的属性来处理依赖关系。 当 bean 生命周期不同时,就会出现问题。 假设单例 bean A 需要使用非单例(原型)bean B,可能在 A 上的每个方法调用上。容器只创建一次单例 bean A,因此只有一次设置属性的机会。 容器无法在每次需要时为 bean A 提供一个新的 bean B 实例。 一个解决方案是放弃一些控制反转。 您可以通过实现 ApplicationContextAware 接口让 bean A 知道容器,并在每次 bean A 需要时对容器进行 getBean("B") 调用来请求(通常是新的)bean B 实例。

//TODO

Java中的getResourceAsStream

首先,Java中的getResourceAsStream有以下几种:

  1. Class.getResourceAsStream(String path) : path 不以’/'开头时默认是从此类所在的包下取资源,以’/'开头则是从ClassPath根下获取。其只是通过path构造一个绝对路径,最终还是由ClassLoader获取资源。

  2. Class.getClassLoader.getResourceAsStream(String path) :默认则是从ClassPath根下获取,path不能以’/'开头,最终是由ClassLoader获取资源。

  3. ServletContext. getResourceAsStream(String path):默认从WebAPP根目录下取资源,Tomcat下path是否以’/'开头无所谓,当然这和具体的容器实现有关。

  4. Jsp下的application内置对象就是上面的ServletContext的一种实现。

其次,getResourceAsStream 用法大致有以下几种:

第一: 要加载的文件和.class文件在同一目录下,例如:com.x.y 下有类me.class ,同时有资源文件myfile.xml

那么,应该有如下代码:

1
me.class.getResourceAsStream("myfile.xml"); 

第二:在me.class目录的子目录下,例如:com.x.y 下有类me.class ,同时在 com.x.y.file 目录下有资源文件myfile.xml

那么,应该有如下代码:

1
me.class.getResourceAsStream("file/myfile.xml"); 

第三:不在me.class目录下,也不在子目录下,例如:com.x.y 下有类me.class ,同时在 com.x.file 目录下有资源文件myfile.xml

那么,应该有如下代码:

1
me.class.getResourceAsStream("/com/x/file/myfile.xml"); 

总结一下,可能只是两种写法

第一:前面有 “ / ”

“ / ”代表了工程的根目录,例如工程名叫做myproject,“ / ”代表了myproject

1
me.class.getResourceAsStream("/com/x/file/myfile.xml"); 

第二:前面没有 “ / ”

代表当前类的目录

1
2
me.class.getResourceAsStream("myfile.xml"); 
me.class.getResourceAsStream("file/myfile.xml");

Spring中的IOC

通过工厂模式和单例模式可以实现解耦的作用,但是Spring框架简化了我们的开发过程。通过使用Spring的IOC可以解决程序耦合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.shijieq.ui;

import com.shijieq.dao.IAccountDao;
import com.shijieq.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

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

/**
* 获取spring的Ioc核心容器,并根据id获取对象
*
* ApplicationContext的三个常用实现类:
* ClassPathXmlApplicationContext:它可以加载类路径下的配置文件,要求配置文件必须在类路径下。不在的话,加载不了。(更常用)
* FileSystemXmlApplicationContext:它可以加载磁盘任意路径下的配置文件(必须有访问权限)
*
* AnnotationConfigApplicationContext:它是用于读取注解创建容器的,是明天的内容。
*
* 核心容器的两个接口引发出的问题:
* ApplicationContext: 单例对象适用 采用此接口
* 它在构建核心容器时,创建对象采取的策略是采用立即加载的方式。也就是说,只要一读取完配置文件马上就创建配置文件中配置的对象。
*
* BeanFactory: 多例对象使用
* 它在构建核心容器时,创建对象采取的策略是采用延迟加载的方式。也就是说,什么时候根据id获取对象了,什么时候才真正的创建对象。
* @param args
*/
public static void main(String[] args) {
//1.获取核心容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// ApplicationContext ac = new FileSystemXmlApplicationContext("C:\\Users\\zhy\\Desktop\\bean.xml");
//2.根据id获取Bean对象
IAccountService as = (IAccountService)ac.getBean("accountService");
IAccountDao adao = ac.getBean("accountDao",IAccountDao.class);

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


//--------BeanFactory----------
// Resource resource = new ClassPathResource("bean.xml");
// BeanFactory factory = new XmlBeanFactory(resource);
// IAccountService as = (IAccountService)factory.getBean("accountService");
// System.out.println(as);
}
}

基于XML的IOC配置

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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 标签:用于配置让 spring 创建对象,并且存入 ioc 容器之中
id 属性:对象的唯一标识。
class 属性:指定要创建对象的全限定类名
-->
<!-- 配置 service -->
<bean id="accountService" class="com.shijieq.service.impl.AccountServiceImpl"></bean>
<!-- 配置 dao -->
<bean id="accountDao" class="com.shijieq.dao.impl.AccountDaoImpl"></bean>
</beans>

bean标签

  • 作用:

    用于配置对象让 spring 来创建的。 默认情况下它调用的是类中的无参构造函数。如果没有无参构造函数则不能创建成功。

  • 属性:

    • id:给对象在容器中提供一个唯一标识。用于获取对象。
    • class:指定类的全限定类名。用于反射创建对象。默认情况下调用无参构造函数。
    • scope:指定对象的作用范围。
      • singleton: 默认值,单例的.
        • 一个应用只有一个对象的实例。它的作用范围就是整个引用。
        • 生命周期:
          • 对象出生:当应用加载,创建容器时,对象就被创建了。
          • 对象活着:只要容器在,对象一直活着。
          • 对象死亡:当应用卸载,销毁容器时,对象就被销毁了。
      • prototype: 多例的.
        • 每次访问对象时,都会重新创建对象实例。
        • 生命周期:
          • 对象出生:当使用对象时,创建新的对象实例。
          • 对象活着:只要对象在使用中,就一直活着。
          • 对象死亡:当对象长时间不用时,被 java 的垃圾回收器回收了。
      • request: WEB 项目中, Spring 创建一个 Bean 的对象,将对象存入到 request 域中.
      • session: WEB 项目中, Spring 创建一个 Bean 的对象,将对象存入到 session 域中.
      • global session: WEB 项目中, 应用在 Portlet 环境. 如果没有 Portlet 环境那么 globalSession 相当于 session.
    • init-method:指定类中的初始化方法名称。
    • destroy-method:指定类中销毁方法名称。
    • ...

实例化bean的三种方式

默认无参构造函数
1
2
3
4
<!--在默认情况下:
它会根据默认无参构造函数来创建类对象。如果 bean 中没有默认无参构造函数,将会创建失败。
-->
<bean id="accountService" class="com.shijieq.service.impl.AccountServiceImpl"/>
静态工厂创建对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 模拟一个静态工厂,创建业务层实现类
*/
public class StaticFactory {
public static IAccountService createAccountService(){
return new AccountServiceImpl();
}
}
<!-- 此种方式是:
使用 StaticFactory 类中的静态方法 createAccountService 创建对象,并存入 spring 容器
id 属性:指定 bean 的 id,用于从容器中获取
class 属性:指定静态工厂的全限定类名
factory-method 属性:指定生产对象的静态方法
-->
<bean id="accountService"
class="com.shijieq.factory.StaticFactory"
factory-method="createAccountService"></bean>
实例工厂创建对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 模拟一个实例工厂,创建业务层实现类
* 此工厂创建对象,必须现有工厂实例对象,再调用方法
*/
public class InstanceFactory {
public IAccountService createAccountService(){
return new AccountServiceImpl();
}
}
<!-- 此种方式是:
先把工厂的创建交给 spring 来管理。
然后在使用工厂的 bean 来调用里面的方法
factory-bean 属性:用于指定实例工厂 bean 的 id。
factory-method 属性:用于指定实例工厂中创建对象的方法。
-->
<bean id="instancFactory" class="com.shijieq.factory.InstanceFactory"></bean>
<bean id="accountService"
factory-bean="instancFactory"
factory-method="createAccountService"></bean>

依赖注入

构造方法注入

采用构造函数的方式,给bean属性传值,要求类中存在一个对应参数列表的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AccountServiceImpl implements IAccountService {
private String name;
private Integer age;
private Date birthday;
public AccountServiceImpl(String name, Integer age, Date birthday) {
this.name = name;
this.age = age;
this.birthday = birthday;
}
@Override
public void saveAccount() {
System.out.println(name+","+age+","+birthday);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 使用构造函数的方式,给 service 中的属性传值
要求:
类中需要提供一个对应参数列表的构造函数。
涉及的标签:
constructor-arg
属性:
index:指定参数在构造函数参数列表的索引位置
type:指定参数在构造函数中的数据类型
name:指定参数在构造函数中的名称 用这个找给谁赋值
=======上面三个都是找给谁赋值,下面两个指的是赋什么值的==============
value:它能赋的值是基本数据类型和 String 类型
ref:它能赋的值是其他 bean 类型,也就是说,必须得是在配置文件中配置过的 bean
-->
<bean id="accountService" class="com.shijieq.service.impl.AccountServiceImpl">
<constructor-arg name="name" value="张三"></constructor-arg>
<constructor-arg name="age" value="18"></constructor-arg>
<constructor-arg name="birthday" ref="now"></constructor-arg>
</bean>
<bean id="now" class="java.util.Date"></bean>

set方法注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AccountServiceImpl implements IAccountService {
private String name;
private Integer age;
private Date birthday;
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
@Override
public void saveAccount() {
System.out.println(name+","+age+","+birthday);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 通过配置文件给 bean 中的属性传值:使用 set 方法的方式
涉及的标签:
property
属性:
name:找的是类中 set 方法后面的部分
ref:给属性赋值是其他 bean 类型的
value:给属性赋值是基本数据类型和 string 类型的
实际开发中,此种方式用的较多。
-->
<bean id="accountService" class="com.shijieq.service.impl.AccountServiceImpl">
<property name="name" value="test"></property>
<property name="age" value="21"></property>
<property name="birthday" ref="now"></property>
</bean>
<bean id="now" class="java.util.Date"></bean>

p 名称空间注入数据

p 名称空间注入本质上还是调用set方法

1
2
3
4
5
6
7
8
9
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="accountService"
class="com.shijieq.service.impl.AccountServiceImpl4"
p:name="test" p:age="21" p:birthday-ref="now"/>
</beans>

注入集合属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!-- 注入集合数据
List 结构的: array,list,set
Map 结构的: map,entry,props,prop
-->
<bean id="accountService" class="com.shijieq.service.impl.AccountServiceImpl">
<!-- 在注入集合数据时,只要结构相同,标签可以互换 -->
<!-- 给数组注入数据 -->
<property name="myStrs">
<set>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</set>
</property>
<!-- 注入 list 集合数据 -->
<property name="myList">
<array>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</array>
</property>
<!-- 注入 set 集合数据 -->
<property name="mySet">
<list>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</list>
</property>
<!-- 注入 Map 数据 -->
<property name="myMap">
<props>
<prop key="testA">aaa</prop>
<prop key="testB">bbb</prop>
</props>
</property>
<!-- 注入 properties 数据 -->
<property name="myProps">
<map>
<entry key="testA" value="aaa"></entry>
<entry key="testB">
<value>bbb</value>
</entry>
</map>
</property>
</bean>

基于注解的IOC配置

模板

相较于”基于XML的IOC配置“模板,导入约束时需要多导入一个 context 名称空间下的约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
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
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<!-- 告知 spring 创建容器时要扫描的包 -->
<context:component-scan base-package="com.shijieq"></context:component-scan>

<!-- 配置 dbAssit -->
<bean id="dbAssit" class="com.shijieq.dbassit.DBAssit">
<property name="dataSource" ref="dataSource"></property>
</bean>

<!-- 配置数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql:///spring_day02"></property>
<property name="user" value="root"></property>
<property name="password" value="1234"></property>
</bean>
</beans>

常用注解

细节:如果注解中有且只有一个属性要赋值时,且名称是 value,value 在赋值是可以不写

创建对象

相当于:<bean id="" class="">

  • @Component
    • 把资源让 spring 来管理。相当于在 xml 中配置一个 bean
    • 属性:
      • value:指定 bean 的 id。如果不指定 value 属性,默认 bean 的 id 是当前类的类名。首字母小写
  • @Controller, @Service, @Repository
    • @Controller:一般用于表现层的注解
    • @Service:一般用于业务层的注解
    • @Repository:一般用于持久层的注解

注入数据

相当于: <property name="" ref=""><property name="" value="">

  • @Autowired
    • 自动按照类型注入。当使用注解注入属性时,set 方法可以省略。它只能注入其他 bean 类型。当有多个类型匹配时,使用要注入的对象变量名称作为 bean 的 id,在 spring 容器查找,找到了也可以注入成功。找不到就报错。
  • @Qualifier
    • 在自动按照类型注入的基础之上,再按照 Bean 的 id 注入。它在给字段注入时不能独立使用,必须和 @Autowire 一起使用;但是给方法参数注入时,可以独立使用。
    • 属性:
      • value:指定 bean 的 id
  • @Resource
    • 直接按照 Bean 的 id 注入。它也只能注入其他 bean 类型。
    • 属性:
      • name:指定 bean 的 id
  • @Value
    • 注入基本数据类型和 String 类型数据
    • 属性:
      • value:用于指定值

改变作用范围

相当于: <bean id="" class="" scope="">

  • @Scope
    • 指定 bean 的作用范围
    • 属性:
      • value:指定范围的值,取值为:singleton, prototype, request, session, global session

生命周期相关*

相当于: <bean id="" class="" init-method="" destroy-method="" />

  • @PostConstruct:用于指定初始化方法
  • @PreDestroy:用于指定销毁方法

基于注解和XML的IOC对比

image-20220318235631736

相关替代

替代bean.xml

如何用注解完全替代bean.xml

  • @Configuration
    • 用于指定当前类是一个 spring 配置类,当创建容器时会从该类上加载注解。
    • 获取容器时需要使用 AnnotationApplicationContext(有@Configuration 注解的类.class)
    • 属性:
      • value:用于指定配置类的字节码
  • @ComponentScan
    • 用于指定 spring 在初始化容器时要扫描的包
    • 作用和在 spring 的 xml 配置文件中的:<context:component-scan base-package="com.shijieq"/> 是一样的
    • 属性:
      • basePackages:用于指定要扫描的包。和该注解中的 value 属性作用一样
  • @Bean
    • 该注解只能写在方法上,表明使用此方法创建一个对象,并且放入 spring 容器
    • 属性:
      • name:给当前@Bean 注解方法创建的对象指定一个名称(即 bean 的 id)。

提取配置

此时bean.xml文件已经可以删除,对于一些数据源或者其他如何将他们配置出来?

  • @PropertySource

    • 用于加载.properties文件中的配置
    • 例如我们配置数据源时,可以把连接数据库的信息写到 properties 配置文件中,就可以使用此注解指定 properties 配置文件的位置。
    • 属性:
      • value[]:用于指定 properties 文件位置,如果是在类路径下,需要写上classpath:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    /**
    * 连接数据库的配置类
    */
    @Configuration
    @PropertySource("classpath:jdbc.properties")
    public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;

    /**
    * 创建一个数据源,并存入 spring 容器中
    * @return
    */
    @Bean(name="dataSource")
    public DataSource createDataSource() {
    try {
    ComboPooledDataSource ds = new ComboPooledDataSource();
    ds.setDriverClass(driver);
    ds.setJdbcUrl(url);
    ds.setUser(username);
    ds.setPassword(password);
    return ds;
    } catch (Exception e) {
    throw new RuntimeException(e);
    }
    }
    }
    /*
    jdbc.properties 文件:
    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/day44_ee247_spring
    jdbc.username=root
    jdbc.password=1234
    */

导入其他配置

  • @Import

    • 用于导入其他配置类,在引入其他配置类时,可以不用再写@Configuration 注解(写上也没事)。
    • 属性:
    • value[]:用于指定其他配置类的字节码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Configuration
    @ComponentScan(basePackages = "com.shijieq.spring")
    @Import({ JdbcConfig.class})
    public class SpringConfiguration {
    }

    @Configuration
    @PropertySource("classpath:jdbc.properties")
    public class JdbcConfig{
    }

获取容器

当配置文件被完全替代后,如何通过注解获取容器?

1
ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);

Spring整合Junit

以下主要介绍在Spring中如何整合单元测试,这部分在Springboot中有更好更为简洁的方式。

配置步骤

  1. 拷贝整合 junit 的必备 jar 包到 lib 目录,或通过maven导入依赖
  2. 使用@RunWith注解替换原有运行器
  3. 使用@ContextConfiguration指定 spring 配置文件的位置
  4. 使用@Autowired给测试类中的变量注入数据

举个例子

1
2
3
4
5
6
7
8
9
10
/**
* 测试类
* @Version 1.0
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations= {"classpath:bean.xml"})
public class AccountServiceTest {
@Autowired
private IAccountService as ;
}

Spring中的AOP

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

例如转账系统中对持久层的操作都是独立事务,导致无法实现事务控制,不符合事务的一致性。我们通过让业务层来控制事务的提交和回滚,而这个过程中存在一些重复的代码,此时就可以采用动态代理技术进行增强。

动态代理与静态代理

动态代理的特点是字节码随用随创建,随用随加载。它与静态代理的区别也在于此。因为静态代理是字节码一上来就创建好,并完成加载。

装饰者模式就是静态代理的一种体现。

动态代理常用的有两种方式:基于接口的动态代理基于子类的动态代理

基于接口的动态代理

使用JDK官方提供的Proxy类,要求被代理类最少实现一个接口

基于子类的动态代理

使用第三方的CGLib,要求被代理类不能用final修饰,即不能为最终类

相关术语

  • Joinpoint(连接点): 所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。
  • Pointcut(切入点): 所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。
  • Advice(通知/增强):
    • 所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
    • 通知类型
      • 前置通知
      • 后置通知
      • 异常通知
      • 最终通知
      • 环绕通知
  • Introduction(引介): 引介是一种特殊的通知在不修改类代码的前提下,Introduction 可以在运行期为类动态地添加一些方法或 Field。
  • Target(目标对象): 代理的目标对象。
  • Weaving(织入):
    • 是指把增强应用到目标对象来创建新的代理对象的过程。
    • spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。
  • Proxy(代理): 一个类被 AOP 织入增强后,就产生一个结果代理类。
  • Aspect(切面): 是切入点和通知(引介)的结合。

要明确的事

开发阶段

这部分工作由开发人员完成:

  1. 编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。
  2. 把公用代码抽取出来,制作成通知。(开发阶段最后再做):AOP 编程人员来做。
  3. 在配置文件中,声明切入点与通知间的关系,即切面。:AOP 编程人员来做。

运行阶段

这部分工作由Spring框架完成:

Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对 象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

在 spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。

基于XML的AOP配置

模板

在IOC模板的基础上还需要导入aop的约束

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>

配置步骤

  1. 把通知类用 bean 标签配置起来

  2. 使用 aop:config 声明 aop 配置

  3. 使用 aop:aspect 配置切面

  4. 使用 aop:pointcut 配置切入点表达式expression

  5. 使用 aop:xxx 配置对应的通知类型

    • aop:xxx都拥有的共同属性:
      • method: 用于指定通知类中的增强方法名称
      • pointcut: 用于指定切入点表达式
      • pointcut-ref: 用于指定切入点的表达式的引用
    • aop:before
      • 用于配置前置通知。指定增强的方法在切入点方法之前执行。
    • aop:after-returning
      • 用于配置后置通知
      • 执行时间点:切入点方法正常执行之后。它和异常通知只能有一个执行。
    • aop:after-throwing
      • 用于配置异常通知
      • 执行时间点:切入点方法执行产生异常后执行。它和后置通知只能执行一个。
    • aop:after
      • 用于配置最终通知
      • 执行时间点:无论切入点方法执行时是否有异常,它都会在其后面执行。
    • aop:around
      • 用于配置环绕通知
      • 它是spring框架为我们提供的一种可以在代码中手动控制增强代码什么时候执行的方式。
      • 通常情况下环绕通知都是单独使用的。

切入点表达式expression

execution(表达式)

语法

1
execution([修饰符] 返回值类型 包名.类名.方法名(参数))

写法

全匹配模式
1
execution(public void com.shijieq.service.impl.AccountServiceImpl.saveAccount(com.shijieq.domain.Account))
访问修饰符省略
1
execution(void com.shijieq.service.impl.AccountServiceImpl.saveAccount(com.shijieq.domain.Account))
返回值用*表示

返回值*表示任意返回值

1
execution(* com.shijieq.service.impl.AccountServiceImpl.saveAccount(com.shijieq.domain.Account))
包名用*表示

包名*表示任意包名,但是有几级包,就要写几个*

1
execution(* *.*.*.*.AccountServiceImpl.saveAccount(com.shijieq.domain.Account))
用..表示当前包
1
execution(* com..AccountServiceImpl.saveAccount(com.shijieq.domain.Account))
类名使用*号

类名*表示任意类

1
execution(* com..*.saveAccount(com.shijieq.domain.Account))
方法名使用*号

方法名*表示任意方法

1
execution(* com..*.*(com.shijieq.domain.Account))
参数列表使用*号

参数列表*表示可以是任意数据类型,但是必须有参数

1
execution(* com..*.*(*))
参数列表使用..号

参数列表..表示有无参数均可,如有参数可为任意数据类型

1
execution(* com..*.*(..))
全通配模式
1
execution(* *..*.*(..))

举个例子

假设我们要对业务层代码切入事务控制,首先定义一个事务控制类,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 事务控制类
*/
public class TransactionManager {
//定义一个 DBAssit
private DBAssit dbAssit ;

public void setDbAssit(DBAssit dbAssit) {
this.dbAssit = dbAssit;
}
//开启事务
public void beginTransaction() {
try {
dbAssit.getCurrentConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}

//提交事务
public void commit() {
try {
dbAssit.getCurrentConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}

//回滚事务
public void rollback() {
try {
dbAssit.getCurrentConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}

//释放资源
public void release() {
try {
dbAssit.releaseConnection();
} catch (Exception e) {
e.printStackTrace();
}
}
}

对应的配置文件,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 配置通知 -->
<bean id="txManager" class="com.shijieq.utils.TransactionManager">
<property name="dbAssit" ref="dbAssit"></property>
</bean>

<!--配置AOP-->
<aop:config>
<!--配置切面-->
<aop:aspect id="txAdvice" ref="txManager">
<!--声明一个切入点pt1-->
<aop:pointcut id="pt1" expression="execution(* com.shijieq.service.impl.*.*(..))"/>
<!--配置前置通知-->
<aop:before method="beginTransaction" pointcut-ref="pt1"/>
<!--配置后置通知-->
<aop:after-returning method="commit" pointcut-ref="pt1"/>
<!--配置异常通知-->
<aop:after-throwing method="rollback" pointcut-ref="pt1"/>
<!--配置最终通知-->
<aop:after method="release" pointcut-ref="pt1"/>
</aop:aspect>
</aop:config>
</beans>

环绕通知

//TODO

基于注解的AOP配置

模板

相较于”基于XML的AOP配置“模板,导入约束时需要多导入一个 context 名称空间下的约束

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
</beans>

配置步骤

  1. 把通知类也使用注解配置

  2. 在通知类上使用@Aspect 注解声明为切面

  3. 在增强的方法上使用注解配置通知

  4. 在 spring 配置文件中开启 spring 对注解 AOP 的支持

    1
    2
    <!-- 开启 spring 对注解 AOP 的支持 -->
    <aop:aspectj-autoproxy/>

    或者通过注解开启

    1
    2
    3
    4
    5
    @Configuration
    @ComponentScan(basePackages="com.shijie")
    @EnableAspectJAutoProxy
    public class SpringConfiguration {
    }

相关注解

  • @Aspect: 表明当前类是一个切面类

  • @Before: 把当前方法看成是前置通知

  • @AfterReturning: 把当前方法看成是后置通知

  • @AfterThrowing: 把当前方法看成是异常通知

  • @After: 把当前方法看成是最终通知

  • @Around: 把当前方法看成是环绕通知

  • @Pointcut: 指定切入点表达式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Pointcut("execution(* com.shijieq.service.impl.*.*(..))")
    private void pt1() {}

    @After("pt1()")
    public void release() {
    try {
    dbAssit.releaseConnection();
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 事务控制类
*/
@Component("txManager")
@Aspect//表明当前类是一个切面类
public class TransactionManager {
//定义一个 DBAssit
@Autowired
private DBAssit dbAssit;

//开启事务
@Before("execution(* com.shijieq.service.impl.*.*(..))")
public void beginTransaction() {
try {
dbAssit.getCurrentConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}

//提交事务
@AfterReturning("execution(* com.shijieq.service.impl.*.*(..))")
public void commit() {
try {
dbAssit.getCurrentConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}

//回滚事务
@AfterThrowing("execution(* com.shijieq.service.impl.*.*(..))")
public void rollback() {
try {
dbAssit.getCurrentConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}

//释放资源
@After("execution(* com.shijieq.service.impl.*.*(..))")
public void release() {
try {
dbAssit.releaseConnection();
} catch (Exception e) {
e.printStackTrace();
}
}
}

Spring中的事务控制

//TODO


Spring相关基础知识
http://shijieq.github.io/2022/11/10/Java/Spring/Spring基础知识/
Author
ShijieQ
Posted on
November 10, 2022
Licensed under