dustland

dustball in dustland

Spring Core

Spring Core

Spring Ioc容器

容器配置文件

maven管理的Spring项目中,spring配置文件一半放在/src/main/java/resources目录下,比如

/src/main/java/resources/beans.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- 一个java bean-->
<bean id="dustball" class="top.dustball.pojo.User" >
<property name="name" value="dustball"/>
<property name="pwd" value="sjh123456"/>
<property name="id" value="114514"/>
</bean>

<!-- 别名-->


</beans>

配置Spring容器对象

总共有五种标签

标签 意义 属性 元素
beans bean组
bean 一个pojo对象
alias bean的别名
description 描述,相当于注释
import 导入其他xml配置

为什么要控制反转?

参考为什么要用IOC:inversion of controll反转控制(把创建对象的权利交给框架) - 周文豪 - 博客园 (cnblogs.com)

之前在学javaweb时,每次高层访问底层,比如Service访问Dao层,高层上都要持有底层对象的句柄,并且掌握合适释放底层对象

实际上很麻烦,也没必要

这就好比去饭店吃饭,客人非要管着厨师怎么做饭.而实际上只需要管好点菜就行了

于是考虑使用设计模式中的单例模式,而引入设计模式又会造成代码的复杂性

反正不就是要一个对象,并且好管理吗?让Spring框架干这个事情

这就是Spring容器干的事情

将所有beans扔进Spring容器,让他管理,对于程序来说,Spring容器在全局位置,程序员可以自由调用

然后用BeanFactory等等工厂,对程序员提供接口,这个BeanFactory干了个啥?意思意思:

img

类域里面有一个静态代码块,其用意是,当BeanFactory对象创建时仅执行一次,也就是单例模式.

静态代码块中读取了bean.properties,根据xml文件的配置,实例化bean放到Map<String,Object> beans容器中

这个容器实际上是一个Map字典,例子中使用HashMap实现之

此后要使用容器中的对象时,只需要调用工厂的getBean方法,传入的对象id作为键去查beans哈希表,查到就返回对象引用

所谓"依赖注入",就是指把java bean放到Spring容器中去

害tm注入,牛逼哄哄的,害什么控制反转,纯纯吓唬人

container magic

bean在何时创建

Spring Bean

从配置文件实例化Spring容器时创建

也就是说

1
ApplicationContext context=new ClassPathXmlApplicationContext("beans.xml");

这玩意执行之后,beans.xml中的所有Bean都会创建,并放置在Bean缓存池中

这一点可以下断点观察

但是,如果beans.xml中,一个bean如果带上了懒加载的属性,则啥时候用这个bean时,它才不得不加载

比如

1
2
3
4
5
<bean id="dustball" class="top.dustball.pojo.User" lazy-init="true">
<property name="name" value="dustball"/>
<property name="pwd" value="sjh123456"/>
<property name="id" value="114514"/>
</bean>

这个dustball就不会跟随context的实例化而同时创建,如果从来不调用dustball,它就不会被创建

bean属性

id&class

最基本的属性就是id和class

id是这个bean在容器中的键,值就是bean的对象引用

class是这个bean的类

1
2
3
4
5
<bean id="dustball" class="top.dustball.pojo.User">
<property name="name" value="dustball"/>
<property name="pwd" value="sjh123456"/>
<property name="id" value="114514"/>
</bean>

scope

规定bean的作用域,

singleton,单例模式,每次getBean调用,返回同一个实例.默认的作用域.

如果没有懒加载属性,则该bean会随容器一起创建,随容器一起销毁

prototype,原型模式,每次getBean调用,都会返回一个新的实例.

啥时候getBean,啥时候才会创建

init-method

不建议使用

不建议使用

不建议使用

初始化回调函数

创建bean使用无参构造函数,可以在其中进行初始化

也可以另外指定初始化函数,该函数必须无参数无返回值

1
2
3
4
5
<bean id="dustball" class="top.dustball.pojo.User" scope="prototype" init-method="init" destroy-method="destroy">
<property name="name" value="dustball"/>
<property name="pwd" value="sjh123456"/>
<property name="id" value="114514"/>
</bean>

指定User类的init函数为初始化函数

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
package top.dustball.pojo;


import lombok.*;

//@NoArgsConstructor
//@AllArgsConstructor
@Data
@ToString
public class User {
private int id;
private String name;
private String pwd;

public User() {
System.out.println("User ctor called");
}

public User(int id, String name, String pwd) {
System.out.println("User 3arg ctor called");
this.id = id;
this.name = name;
this.pwd = pwd;
}
public void init(){
System.out.println("init method called");
}
public void destroy(){
System.out.println("destroy method called");
}

}

Spring依赖注入

基于ctor的依赖注入

通过自定义的有参构造函数进行实例化就是基于ctor的依赖注入

1
2
3
4
5
<bean id="dustball" class="top.dustball.pojo.User" scope="prototype">
<constructor-arg name="id" value="1" type="int"/>
<constructor-arg name="name" value="dustball" type="java.lang.String"/>
<constructor-arg name="pwd" value="sjh" type="java.lang.String"/>
</bean>

这里使用键决定参数对应关系,也可以使用index下标对应ctor的参数

这个配置届时会调用User的三参数ctor

1
2
3
4
5
6
public User(int id, String name, String pwd) {
System.out.println("User 3arg ctor called");
this.id = id;
this.name = name;
this.pwd = pwd;
}

基于setter的依赖注入

之前用property元素配置bean时,之所以能够成功,是因为lombok的@Data注解,自动帮我们加上了setter方法

1
2
3
4
5
<bean id="deutschball" class="top.dustball.pojo.User" scope="prototype">
<property name="id" value="2"/>
<property name="name" value="deutschball" />
<property name="pwd" value="sjh"/>
</bean>

<property name="id" value="2"/>这句要求User类要有setID方法,对其传递参数2

注入成员对象

之前的User类有三个成员变量,一个基本数据类型int,两个String类型

现在UserProxy{User user;String seviceID};类型包括了一个User成员对象和一个String成员变量

如何注入这么一个UserProxy类型的bean呢?

1
2
3
4
5
6
7
8
9
<bean id="vader" class="top.dustball.pojo.User" >
<property name="id" value="11"/>
<property name="name" value="vader"/>
<property name="pwd" value="sjh"/>
</bean>
<bean id="vader_proxy" class="top.dustball.pojo.UserProxy">
<property name="user" ref="vader"/>
<property name="serviceID" value="0x00001"/>
</bean>

<property name="user" ref="vader"/>这里的ref应为一个bean的id标识

注入内部bean

bean里面可以包含一个bean作为成员对象

1
2
3
4
5
6
7
8
9
10
11
12
<bean id="proxy" class="top.dustball.pojo.UserProxy">
<property name="user" >
<bean class="top.dustball.pojo.User" id="proxyUser">
<property name="id" value="3"/>
<property name="name" value="puppet" />
<property name="pwd" value="sjh"/>
</bean>
</property>

<property value="0x00001" name="serviceID"/>

</bean>

其中UserProxy类长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package top.dustball.pojo;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class UserProxy {
User user;
String serviceID;
}

注入集合

啥时候用到啥时候回来学

自动装配

自动装配是为了简化注入成员对象的情形

不使用自动装配:

1
2
3
4
5
6
7
8
9
<bean id="vader" class="top.dustball.pojo.User" >
<property name="id" value="11"/>
<property name="name" value="vader"/>
<property name="pwd" value="sjh"/>
</bean>
<bean id="vader_proxy" class="top.dustball.pojo.UserProxy">
<property name="user" ref="vader"/>
<property name="serviceID" value="0x00001"/>
</bean>

关键在于<property name="user" ref="vader"/>这一句,表明了本bean引用了哪一个bean

使用自动装配之后可以省去这句,由Spring(具体是谁我也不知道)自动决定引用哪一个bean

默认情况下是不会自动装配的,需要手动设置装配哪一个bean.

感觉上显示写明装配哪一个bean,代码更加清晰,并且也不会多复杂.自动装配反而降低可读性

byName

1
2
3
4
5
6
7
8
9
    <bean id="user" class="top.dustball.pojo.User" >
<property name="id" value="11"/>
<property name="name" value="vader"/>
<property name="pwd" value="sjh"/>
</bean>
<bean id="vader_proxy" class="top.dustball.pojo.UserProxy" autowire="byName">
<!-- <property name="user" ref="vader"/>-->
<property name="serviceID" value="0x00001"/>
</bean>

UserProxy中不需要显式指定user成员引用哪一个bean,因为其bean属性中有autowire="byName",根据名称自动装配

啥叫"根据名称自动装配?"

1
2
3
4
public class UserProxy {
User user;
String serviceID;
}

这里成员对象user,其键名就是"user",因此Spring会根据这个"user"去找id="user"的bean,也就是

1
<bean id="user" class="top.dustball.pojo.User" >

如果找到了,则使用该bean,否则使用null值

byType

1
2
3
4
5
6
7
8
9
    <bean id="user" class="top.dustball.pojo.User">
<property name="id" value="11"/>
<property name="name" value="vader"/>
<property name="pwd" value="sjh"/>
</bean>
<bean id="vader_proxy" class="top.dustball.pojo.UserProxy" autowire="byType">
<!-- <property name="user" ref="vader"/>-->
<property name="serviceID" value="0x00001"/>
</bean>

byType,根据类型自动专配,user成员是一个User类型,beans.xml中的user类型的bean自动作为成员进行装配

如果有两个以上的user bean,则会报错expected single matching bean but found 2: dustball,user

constructor

构造函数参数匹配

比较类似于byType,

区别就是constructor-arg和property的区别

1
2
3
4
5
6
7
8
9
10
    <bean id="user" class="top.dustball.pojo.User">
<property name="id" value="11"/>
<property name="name" value="vader"/>
<property name="pwd" value="sjh"/>
</bean>
<bean id="vader_proxy" class="top.dustball.pojo.UserProxy" autowire="constructor">
<!-- <property name="user" ref="vader"/>-->
<constructor-arg name="serviceID" value="0x0001"/>
<!-- <property name="serviceID" value="0x00001"/>-->
</bean>

使用注解开发

在beans.xml中加上注解配置和自动扫描

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

<context:annotation-config/>
<context:component-scan base-package="top.dustball.pojo"/>

</beans>

注意此时自动扫描主机范围是top.dustball.pojo这个包下,可以扩大范围

1
<context:component-scan base-package="top.dustball"/>

@Component

作用于类上

自动给类创建一个pojo

比如top.dustball.pojo.User类这样写

1
2
3
4
5
6
7
8
@Data
@ToString
@Component("dustball")
public class User {
public int id;
private String name;
private String pwd;
}

@Component("dustball")将会自动创建一个bean,其id就是dustball

在测试类中就可以通过context.getBean("dustball")调用之

Component注解只能创建对象,但是无法设置对象的属性值,可以通过@Value注解设置属性值

如果只写@Component,不显示指定id,则默认bean使用类名的首字母小写作为id,即User类对应user bean

衍生注解

衍生注解 作用于
@Respository UserDao
@Service UserService
@Controller UserController

需要注意的是,beans.xml中指定要扫描的位置,

原来的位置是pojo包下的所有Dao层的类

1
<context:component-scan base-package="top.dustball.pojo"/>

现在要拓展范围,扫描所有Dao,Service,Controller层的类

1
<context:component-scan base-package="top.dustball"/>

@Value

作用于属性或者setter上,配合@Component使用,初始化bean的各个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package top.dustball.pojo;

import org.springframework.beans.factory.annotation.*;
import lombok.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

//@NoArgsConstructor
//@AllArgsConstructor
//@Data
@Data
@ToString
@Component("dustball")
public class User {
@Value("1")
public int id;
@Value("dustball")
private String name;
@Value("sjh")
private String pwd;
}

此时Spring容器中自动创建的id=dustball的bean,三个属性均已初始化

@Autowired

作用于成员对象或者其setter方法上

自动装配成员对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package top.dustball.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;

@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
@Repository
public class UserProxy {
@Autowired
User user;//此处由Spring容器自动寻找user类型的bean,找到则自动装配
@Value("0x10000")
String serviceID;//此处默认初始化为0x10000
}

实际上是byType实现的

因此beans.xml中应该有一个id为user的bean,或者User类上带有Component注解或者其衍生注解

Autowired和Required的关系:

1
2
3
public @interface Autowired {
boolean required() default true;
}

默认情况下required=true,这意味着user这个成员对象必须被一个bean装配,如果SpringIoc容器中找不到合适的bean则报错

1
No qualifying bean of type 'top.dustball.pojo.User' available

如果required=false,则该成员对象可以为null,SpringIoc容器中找不到合适的bean就直接摆烂不找了,赋值为null

@Qualifier

配合@Autowired一起使用

1
2
3
4
5
6
7
8
public class UserProxy {

@Autowired(required = true)
@Qualifier(value = "dustball")
User user;
@Value("0x10000")
String serviceID;
}

本来只用@Autowired相当于byType自动装配bean,现在希望装配一个指定id的bean

1
@Qualifier(value = "dustball")

这就限定了必须使用id=dustball的这个bean

1
2
3
4
5
<bean id="dustball" class="top.dustball.pojo.User">
<property name="id" value="11"/>
<property name="name" value="vader"/>
<property name="pwd" value="sjh"/>
</bean>

@Scope

作用于类上,配合@Component以及其衍生注解一起使用

用于指定该bean的作用域,要么是singleton要么是prototype

1
2
@Scope("singleton")
@Scope("prototype")
1
2
3
4
5
6
7
8
9
10
11
...
@Repository
@Scope("prototype")
public class UserProxy {

@Autowired(required = true)
@Qualifier(value = "dustball")
User user;
@Value("0x10000")
String serviceID;
}

xml给👴爬

之前不管是纯xml配置还是使用注解简化的xml配置,都离不开xml

即使是注解开发,也需要一个这种的xml文件:

1
2
3
4
5
6
7
8
9
10
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<context:component-scan base-package="top.dustball"/>
</beans>

实际上注解贡献的bean和xml中注册的bean地位是完全相同的

"基于java的配置",就是指,使用一个java类作为bean的注册来源,而不再使用任何xml文件

比如top.dustball.pojo.UserConfig这个类,作为bean的来源,可以这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package top.dustball.pojo;

import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

@Configuration
public class UserConfig {
@Bean(initMethod = "init")
@Scope("singleton")
public User deutschball(){
return new User(100,"deutschball","sjh");
}


@Bean
public UserProxy deutschproxy(){
return new UserProxy(deutschball(),"0x1000");
}
}

@Configuration注解作用于类上,表明本类作为bean的注册来源

被@Bean注解修饰的成员函数将会生成一个id为函数名的bean

可以结合@Scope修饰该bean的作用域

如果有成员对象依赖,可以使用其他函数的返回值,只要将对应函数修饰为单例模式,就可以保证每次返回同一个对象

需要注意的是,基于java类的配置,会与直接在User,UserProxy类上写的@Component注解打架,并且有可能创建两个bean

因此要么 使用java类的配置,要么使用xml+注解,不要混用

基于java类的配置 基于xml文件的配置
方法名 bean id
方法返回值 bean class
@Bean修饰方法 注册一个bean

还要注意的是,实例化容器时有变动

从xml构建容器要这样写:

1
ApplicationContext applicationContext = new ApplicationContext("beans.xml");

从java类构建容器要这样写:

1
ApplicationContext context=new AnnotationConfigApplicationContext(UserConfig.class);

代理模式

为啥要学代理模式呢?

因为Spring AOP由动态代理实现,学动态代理是为了知道其原理

为啥要先学静态代理呢?因为动态代理和静态代理的目的相同

在不改变已有代码的基础上,增加新功能

至于为啥不能改变原有代码,是主人的变态任务罢了

比如第三方jar包提供的类或者函数,要增强其功能,就可以通过代理模式实现

十五斤,三十块,满意了吧?

静态代理

静态代理通过组合实现

也就是被代理对象作为代理类的成员对象.

类图表示为:

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
classDiagram 
class NotePad{
+void print();
+void insert(row,col,text)
+void delete(row,col,length)
}
class RealNotePad{
-filename:string
+void print();
+void insert(int index,string text)
+void delete(int begin,int end)
+void open(string filename)
}
class ProxyNotePad{
-realnotepad:RealNotePad
-filename:string
-buffer:string
+void print();
+void insert(row,col,text)
+void delete(row,col,length)
}
NotePad<|..RealNotePad
NotePad<|..ProxyNotePad
class Main{
void main();
}
ProxyNotePad<..Main
RealNotePad <..ProxyNotePad
ProxyNotePad o..RealNotePad

动态代理

动态体现在:没有实际存在的代理类,在运行时用反射创建临时代理类,实例化代理对象之后返回这个代理对象,交由被代理的接口管理句柄

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
package top.dustball.dao;

import lombok.AllArgsConstructor;
import lombok.Data;

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

@Data
@AllArgsConstructor
public class ProxyInvocationHandler implements InvocationHandler {
private Object target;

public Object getProxy(){
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName()+" method called");
return method.invoke(target,args);
}
}

使用代理:

1
2
3
4
UserDaoImpl userDao=new UserDaoImpl();
ProxyInvocationHandler handler = new ProxyInvocationHandler(userDao);
UserDao proxy=(UserDao)handler.getProxy();
proxy.delete(10);

1.UserDaoImpl userDao=new UserDaoImpl();

实例化了一个普通的UserDao实现类对象userDao,这跟之前没有用代理的情形没有区别

2.ProxyInvocationHandler handler = new ProxyInvocationHandler(userDao);

userDao作为被代理对象,其引用传递给handler.target保管,便于handler成员方法调用

ProxyInvocationHandler类中实现了两个方法,getProxy和invoke,(忽略lombok自动实现的方法),

其中InvocationHandler接口实际上只要求必须实现invoke方法

1
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable

其中proxy是被代理的对象,method是proxy对象要被增强的方法,args是本来应该传递给该方法的参数

getProxy纯粹是我们为了省事才写到ProxyInvocationHandler类中的方法

到此为止只调用过ProxyInvocationHandler的全参数构造函数(lombok注解@AllArgsConstructor),也就是说,只做了target引用赋值这么一件事

大的要来了

3.UserDao proxy=(UserDao)handler.getProxy();

获取了一个代理对象,怎么说获取就获取了?为什么从handler那里获取?关键在于handler.getProxy函数

这个函数干了啥?

1
2
3
4
5
6
7
public Object getProxy(){
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}

java.lang.reflect.Proxy这个类用于动态生成代理类,只需传入目标接口、目标接口的类加载器以及InvocationHandler便可为目标接口生成代理类及代理对象。具体函数干了啥,需要反射的知识,现在不会

三个参数,

第一个是被代理类的类加载器,也就是target.getClass().getClassLoader(),也就是UserDaoImpl类的类加载器,

然而视频教程里这里写的是this.getClass().getClassLoader(),也就是ProxyInvocationHandler类的类加载器

弹幕的说法是,这两个类都是我们自己写的自定义类,所以类加载器是一个

打印观察发现确实如此

1
2
3
4
5
UserDaoImpl userDao=new UserDaoImpl();
System.out.println(userDao.getClass().getClassLoader());

ProxyInvocationHandler handler = new ProxyInvocationHandler(userDao);
System.out.println(handler.getClass().getClassLoader());

执行结果

1
2
jdk.internal.loader.ClassLoaders$AppClassLoader@78308db1
jdk.internal.loader.ClassLoaders$AppClassLoader@78308db1

都是使用应用类加载器(实际上一共就三个类加载器)

在这里插入图片描述

也就是说,这三个参数传递给Proxy.newProxyInstance之后,该函数并不知道要代理哪一个对象,只知道需要代理哪些接口,增强方法在this.invoke,那么代理对象执行的业务,是如何作用到原对象上的呢?

通过invoke函数作用到原对象userDao上

第二个是需要代理的接口,因为被代理类可以implements多个接口,因此这里可以有选择地代理其接口

第三个是本对象(一个实现InvocationHanler接口的对象,目的是绑定本对象的invoke函数,增强代理接口的功能)

三个参数传入Proxy.newProxyInstance之后,返回一个代理对象,交给左值UserDao proxy保管

这个代理对象具有哪些函数呢?这个由第二个参数决定,传入的接口中有啥函数,这个代理对象就有啥函数

并且代理对象每次调用这些函数,(不管哪一个)都会首先调用invoke方法,可以在invoke函数中增强函数功能

4.proxy.delete(10);

代理对象调用接口中的delete函数,然而是否真的能够调用到UserDaoImpl.delete()函数,得看ProxyInvocationHandler.invoke函数的脸色

1
2
3
4
5
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName()+" method called");
return method.invoke(target,args);
}

invoke拦截proxy.delete这次调用,然后打印delete method called

然后才会将调用转发给target.delete(args)

具体底层怎么实现,学了反射再说

Spring AOP

之前学了动态代理,并不是让我们直接用动态代理写代码,SpringAOP已经帮我们实现了,我们只需要调用其接口

官方の废话

概念 意义
横切关注点 跨越应用程序多个模块的方法或者功能,与业务逻辑无关但是需要关注,比如日志,安全,缓存,事务
切面 横切关注点被模块化的特殊对象,切面是一个类
通知 切面要完成的工作,通知是切面类的一个方法
目标 被通知的对象(目标对象)
代理 代理对象
切入点 可以被通知的函数,每个成员函数都可以作为切入点
连接点 实际被通知的函数,只有感兴趣的切入点才会被作为连接点

AOP/过滤器/钩子

比较类似的几个东西

AOP用于拦截函数调用

过滤器用于拦截用户请求

钩子是回调性质的,用钩子也可以改变程序控制流

AOP和钩子的区别在于,钩子必须给每个函数分别设置,但是AOP可以直接给多个函数上钩子

Spring AOP

Spring AOP是AOP的实现

Spring AOP可以劫持一个方法,在方法执行之前/之后,或者抛出异常时添加额外功能

image-20230129091802544

当代理对象执行一个业务,比如add时:

1
2
3
4
5
6
代理对象.add(){
验证参数();
前置日志();
目标对象.add();
后置日志();
}

这样看来目标对象的所有业务逻辑都被劫持了,并且在代理对象中加上了相同的前置和后置业务

能否有选择的劫持,不同的函数调用劫持后不同对待呢?

如果是我们自己实现

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
package top.dustball.dao;

import lombok.AllArgsConstructor;
import lombok.Data;

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

@Data
@AllArgsConstructor
public class ProxyInvocationHandler implements InvocationHandler {
private Object target;

public Object getProxy(){
return Proxy.newProxyInstance(
this.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().equals("add")){
System.out.println("add hijacked");
return 0;//劫持add方法不予执行
}
else if(method.getName().equals("select")){
return method.invoke(target,args);//直接放行
}
else{
System.out.println(method.getName()+" method called");//其他方法打印前置日志后放行
return method.invoke(target,args);
}
}

}

代理对象的所有方法调用都会首先被劫持到invoke方法,invoke方法决定下一步如何

在SpringAOP中,不需要自己写动态代理的逻辑,只需要写好配置文件

方法1:XML配置

首先在Service层有一个UserServiceImpl下面用SpringAOP代理之

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package top.dustball.service.User;

public class UserServiceImpl implements UserService{
@Override
public void add() {
System.out.println("UserService add calleda");
}
@Override
public void select() {
}
@Override
public void update() {
}
@Override
public void delete() {
}
}

在log包下有一个日志类LogBefore实现了MethodBeforeAdvice接口,这个类将会被作为切面,其before函数将会作为通知作用于UserServiceImpl的成员函数调用之前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package top.dustball.log;

import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

public class LogBefore implements MethodBeforeAdvice {

@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("log before "+method.getName()+" is called");
method.invoke(target,args);//此处存在错误,留作伏笔

}
}

下面就是xml文件的配置了

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
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<context:annotation-config/>
<context:component-scan base-package="top.dustball"/>
<bean id="userService" class="top.dustball.service.User.UserServiceImpl"/>
<bean id="logBefore" class="top.dustball.log.LogBefore"/>
<bean id="logAfter" class="top.dustball.log.LogAfter"/>

<aop:config>
<!-- 定义切入点-->
<aop:pointcut id="cutBefore" expression="execution(* top.dustball.service.User.UserServiceImpl.* (..))"/>
<aop:pointcut id="cutAfterAdd" expression="execution(* top.dustball.service.User.UserServiceImpl.add())"/>

<!-- 切入点和哪个切面通知挂钩-->
<aop:advisor advice-ref="logBefore" pointcut-ref="cutBefore"/>
<aop:advisor advice-ref="logAfter" pointcut-ref="cutAfterAdd"/>
</aop:config>
</beans>

首先注册三个bean,其中userService是目标对象

另外两个bean都是日志类,作为切面

然后是aop配置,其中expression表达式是关键,它决定本切入点作用于被代理对象的哪一个函数

1
<aop:pointcut id="cutBefore" expression="execution(* top.dustball.service.User.UserServiceImpl.* (..))"/>

这句话干了个什么事呢?给UserServieImpl类的任何函数都带上cutBefore切入点

1
2
3
4
5
6
7
execution(
modifiers-pattern
ret-type-pattern
declaring-type-pattern
name-pattern(param-pattern)
throws-pattern
)

returning type pattern,name pattern, and parameters pattern是必须的.

ret-type-pattern:可以为*表示任何返回值,全路径的类名等.

name-pattern:指定方法名, 代表所有

set代表以set开头的所有方法.

parameters pattern:指定方法参数(声明的类型),

(..)代表所有参数,(*)代表一个参数

(*,String)代表第一个参数为任何值,第二个为String类型.

到现在位置是只定义了切入点,并没有往里切入东西,此时调用userService对象,看不出被代理的作用

下面给切入点插入通知就体现代理的作用了

1
<aop:advisor advice-ref="logBefore" pointcut-ref="cutBefore"/>

给cutBefore切入点加上logBefore通知

测试类这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package top.dustball;

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

import top.dustball.service.User.UserService;

public class TestUser {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
}

运行结果:

1
2
3
4
log before add is called
UserService add called
UserService add called
log after add is called with return value = null

表明userService已经被代理了

然而奇怪的是,我们并没有显式调用代理对象,而是从容器context中拿出目标对象userService直接使用

程序逻辑却没有根据userService.add本来的样子走,而是执行了logBefore.before切面通知之后,然后才执行userService.add

也就是说,在Test类看来,他不知道userService.add到底怎么实现的,他也看不到存在AOP,他只管调用就可以了

奇怪的是,测试类中只调用了一次userService.add,结果中却打印了两次UserService add called

这是因为在LogBefore.before切面通知函数中,我们画蛇添足了

1
2
3
4
5
6
7
8
9
public class LogBefore implements MethodBeforeAdvice {

@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("log before "+method.getName()+" is called");
method.invoke(target,args);//画蛇添足
}
}

method.invoke(target,args);这句会在before函数执行完毕之后,被自动执行,不需要我们手动调用

手动调用了就会再执行一次,去掉即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package top.dustball.log;

import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

public class LogBefore implements MethodBeforeAdvice {

@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("log before "+method.getName()+" is called");
// method.invoke(target,args);
}
}

遗留问题

既然Test中调用的仍然是userService这个bean,那么把他作为UserServiceImpl可以不

也就是说

1
2
UserServiceImpl userServiceimp = (UserServiceImpl) context.getBean("userService");
UserService userService = (UserService) context.getBean("userService");

这两种写法哪个对?

结果证明UserService userService = (UserService) context.getBean("userService");这个是对的

一定要注意左值是UserService接口类型,不是UserServiceImpl类型

那么为啥userService这个bean一开始是一个UserServiceImpl的对象,后来就不是了?

如果不加aop:config这一段,两种写法是都可以的

1
2
3
4
5
6
7
8
9
10
    <aop:config>
<!-- 定义切入点-->
<aop:pointcut id="cutBefore" expression="execution(* top.dustball.service.User.UserServiceImpl.* (..))"/>
<aop:pointcut id="cutAfterAdd" expression="execution(* top.dustball.service.User.UserServiceImpl.add())"/>

<!-- 切入点和哪个切面通知挂钩-->
<aop:advisor advice-ref="logBefore" pointcut-ref="cutBefore"/>
<aop:advisor advice-ref="logAfter" pointcut-ref="cutAfterAdd"/>
</aop:config>

加上aop:config之前,userService.getClass()="class top.dustball.service.User.UserServiceImpl"

加上aop:config之后,userService.getClass()="class jdk.proxy2.$Proxy7",已经是代理对象了

至于到底发生了什么让userService被狸猫换太子,现在不想管

方法2:使用自定义类实现AOP

基本都使用方法1,啥时候用到这个法再来学吧

方法3:使用注解开发

首先要有一个切面类

@Aspect注解作用到的类就作为切面类,

其方法中被@Before或者@After或者@Around注解的,成为通知

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
package top.dustball.AOP;


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;


@Aspect
public class AnnotationPointCut {

//before和after注解的方法,在执行之后,控制流会自动执行目标对象的函数,不需要手动执行,因此没有参数
// @Before("execution(* top.dustball.service.User.UserServiceImpl.* (..))")
// public void before(){
// System.out.println("log before method is called");
// }
//
// @After("execution(* top.dustball.service.User.UserServiceImpl.* (..))")
// public void after(){
// System.out.println("log after method is called");
// }
@Around("execution(* top.dustball.service.User.UserServiceImpl.* (..))")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("before");
Signature signature = proceedingJoinPoint.getSignature();//获取函数签名
System.out.println(signature);
proceedingJoinPoint.proceed();//真正调用目标对象的add方法
System.out.println("after");
}
}

然后将该切面类注册到Spring容器中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<context:annotation-config/>
<context:component-scan base-package="top.dustball"/>
<bean id="annotationPointCut" class="top.dustball.AOP.AnnotationPointCut"/>
<aop:aspectj-autoproxy/>

</beans>

实现的功能和方法1相同

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package top.dustball;

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

import top.dustball.service.User.UserService;
import top.dustball.service.User.UserServiceImpl;

public class TestUser {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
}

执行结果

1
2
3
4
before
void top.dustball.service.User.UserService.add()
UserService add called
after

Spring+MyBatis

maven依赖

一定要注意spring的各个组件版本号一致

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>6.0.4</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>6.0.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->


<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.19</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.2</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.32</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.0.4</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.6</version>
</dependency>

回顾javaweb中使用mybatis

之前javaweb中如何使用mybatis的?

mybatis-config.xml配置

在resources目录下有一个mybatis-config.xml文件,其作用主要是配置数据源和映射mapper映射关系

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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>



<typeAliases>
<!-- top.dustball.pojo下面的类在本文件中均可以只是用非全限制类名-->
<package name="top.dustball.pojo"/>
</typeAliases>


<!-- environments目录下面可以配置多个environment环境,在environments标签的default属性中设置使用哪一个即可-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="username" value="root"/>
<property name="password" value="sjh123456"/>
</dataSource>
</environment>

</environments>


<!-- 注册映射关系-->
<mappers>
<mapper resource="top/dustball/mapper/UserMapper/UserMapper.xml"/>
</mappers>
</configuration>

top.dustball.mapper.UserMapper包

一个UserMapper接口,一个UserMapper.xml映射配置文件

其中UserMapper.xml必须绑定UserMapper接口,

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.dustball.mapper.UserMapper.UserMapper"> <!--绑定接口-->
<!-- 在UserMapper接口中使用注解开发-->
</mapper>

然后可以在UserMapper.xml中写CRUD标签,比如<select>

也可以在UserMapper接口中直接使用注解开发

1
2
3
4
5
6
7
8
9
10
11
package top.dustball.mapper.UserMapper;

import org.apache.ibatis.annotations.Select;
import top.dustball.pojo.User;

public interface UserMapper {
// 使用注解之后,就不需要在UserMapper.xml中写<select>这种增删改查的标签了
@Select("SELECT * FROM user WHERE id= #{userID}")
public User getUser(int userID);

}

top.dustball.utils下创建MyBatisUtil工具类

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
package top.dustball.utils;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;

public class MyBatisUtil {

private static SqlSessionFactory sqlSessionFactory;//类变量,只会被静态代码块初始化一次
static {//静态代码块,只会在类加载时执行一次
try {
String resource = "mybatis-config.xml";//配置文件位置
InputStream inputStream = Resources.getResourceAsStream(resource);//xml文件转化为文件输入流
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);//根据文件创建sqlSessionFactory工厂
} catch (Exception e) {
e.printStackTrace();
}
}

public static SqlSession getSqlSession(){//获取一个数据库会话连接
return sqlSessionFactory.openSession();
}
}

测试

此后就可以在测试类中MyBatisUtil.getSqlSession了

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
package top.dustball;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.InputStreamResource;
import top.dustball.mapper.UserMapper.UserMapper;
import top.dustball.pojo.User;
import top.dustball.utils.MyBatisUtil;

import java.io.IOException;
import java.io.InputStream;

public class TestUser {
public static void main(String[] args) throws IOException {

SqlSession sqlSession = MyBatisUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);//获取UserMapper.class的映射器
User user = mapper.getUser(2);
System.out.println(user);

}
}

Spring中使用mybatis

配置spring-dao.xml

(mybatis-config.xml可以保留也可以直接扬了,spring完全可以覆盖mybatis的配置,这里选择保留)

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
<?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:aop="http://www.springframework.org/schema/aop" xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">


<!-- 注册数据源,使用第三方类DriverManagerDataSource-->
<bean id="datasource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="sjh123456"/>
</bean>


<!--注册工厂类,此处可以导入mybatis-config.xml-->
<!--也可以直接在本sqlSessionFactory中配置,不使用mybatis-config.xml-->
<!--教程的做法是,mybatis-config.xml只保留typeAilas作用-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="datasource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!-- 可以在此处注册映射器,也可以在mybatis-config.xml中注册映射器,由于mybatis-config.xml已经注册过,这里不再重复-->
<!-- <property name="mapperLocations" value="top/dustball/mapper/UserMapper/UserMapper.xml"/>-->
<!-- <property name="typeAliases" value="top.dustball.pojo"/>-->
</bean>

<!-- 创建数据库会话实例,本bean的作用与之前的MyBatisUtil.getSqlSession相同-->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<!-- 本bean只可以只用构造函数注入,因其不含setter方法-->
<constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>

<!-- 实例化映射器,关于user表的业务可以直接从userMapper上进行操作,相当于sqlSession.getMapper(UserMapper.class)-->
<bean id="userMapper" class="top.dustball.mapper.UserMapper.UserMapperImpl">
<property name="sqlSession" ref="sqlSession"/>
</bean>
</beans>

一是注意sqlSession的类型是SqlSessionTemplate,不再是SelSession(实际上是相同的作用)

二是注意最后创建的映射器实例,是UserMapperImpl类的实例,(显然UserMapper接口不能实例化)

这是区别于javaweb中使用mybatis的地方,之前只需要UserMapper接口和UserMapper.xml即可

现在还需要加一个UserMapperImpl,因为Spring bean是实例,只有类才可以实例化

总结一下就是

实例化数据源

实例化工厂

实例化会话

实例化映射器

top.dustball.mapper.UserMapper包下再创建UserMapperImpl类

现在UserMapper包下有三个文件

1
2
3
4
5
6
7
8
9
PS C:\Users\86135\Desktop\sprint-01\Demo2\src\main\java\top\dustball\mapper\UserMapper> ls

Directory: C:\Users\86135\Desktop\sprint-01\Demo2\src\main\java\top\dustball\mapper\UserMapper

Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2023/1/30 11:22 352 UserMapper.java
-a--- 2023/1/30 10:47 316 UserMapper.xml
-a--- 2023/1/30 11:24 668 UserMapperImpl.java

其中UserMapperImpl是新增的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package top.dustball.mapper.UserMapper;

import org.mybatis.spring.SqlSessionTemplate;
import top.dustball.pojo.User;

public class UserMapperImpl implements UserMapper{
private SqlSessionTemplate sqlSession;

public void setSqlSession(SqlSessionTemplate sqlSession) {
//设置setter方法,方便注入
this.sqlSession = sqlSession;
}

@Override
public User getUser(int userID) {
// System.out.println("getUser called");
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
return mapper.getUser(userID);//实际上还是调用了UserMapper接口中被@Select注解的getUser方法
}
}

原本获取mapper映射器是用户(程序员)的任务,需要在测试类中完成

现在mapper直接被封装到UserMapperImpl中

程序员只需要在测试类中调用userMapper.getUser(2);

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package top.dustball;

import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.dustball.mapper.UserMapper.UserMapper;
import top.dustball.pojo.User;
import top.dustball.utils.MyBatisUtil;

import java.io.IOException;

public class TestUser {
public static void main(String[] args) throws IOException {
//创建SpringIoc容器
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-dao.xml");

//获取userMapper映射器bean
UserMapper userMapper = (UserMapper) context.getBean("userMapper");

//执行dao业务
User user = userMapper.getUser(2);

System.out.println(user);
}
}

spring+mybatis简化用法

之前在UserMapperImpl中我们需要维护一个成员对象sqlSession,如果让UserMapperImpl继承SqlSessionDaoSupport类,则不再需要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package top.dustball.mapper.UserMapper;

import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import top.dustball.pojo.User;

public class UserMapperImpl extends SqlSessionDaoSupport implements UserMapper{
// private SqlSessionTemplate sqlSession;
// public void setSqlSession(SqlSessionTemplate sqlSession) {
////设置setter方法,方便注入
// this.sqlSession = sqlSession;
// }

@Override
public User getUser(int userID) {
return getSqlSession().getMapper(UserMapper.class).getUser(userID);
//实际上还是调用了UserMapper接口中被@Select注解的getUser方法
}
}

这个类已经帮我们实现了getSqlSession方法

需要注意的是,在spring-dao.xml中稍有变化

1
2
3
4
5
6
7
8
9
   之前:
<bean id="userMapper" class="top.dustball.mapper.UserMapper.UserMapperImpl">
<property name="sqlSession" ref="sqlSession"/>
</bean>

之后:
<bean id="userMapper" class="top.dustball.mapper.UserMapper.UserMapperImpl">
<property name="sqlSessionTemplate" ref="sqlSession"/>
</bean>

之前我们手动维护的成员对象叫做sqlSession,而之后继承自SqlSessionDaoSupport的是sqlSessionTemplate对象,实际上两个作用相同,就是property中的键名要改一下而已

使用事务

AOP实现事务织入

只需要在spring-dao.xml中增加配置,不需要修改任何源代码

spring-dao.xml这样写:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/util
https://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">






<!-- 注册数据源,使用第三方类DriverManagerDataSource-->
<bean id="datasource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="sjh123456"/>
</bean>


<!--注册工厂类,此处可以导入mybatis-config.xml-->
<!--也可以直接在本sqlSessionFactory中配置,不使用mybatis-config.xml-->
<!--教程的做法是,mybatis-config.xml只保留typeAilas作用-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="datasource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!-- 可以在此处注册映射器,也可以在mybatis-config.xml中注册映射器,由于mybatis-config.xml已经注册过,这里不再重复-->
<!-- <property name="mapperLocations" value="top/dustball/mapper/UserMapper/UserMapper.xml"/>-->
<!-- <property name="typeAliases" value="top.dustball.pojo"/>-->
</bean>

<!-- 创建数据库会话实例,本bean的作用与之前的MyBatisUtil.getSqlSession相同-->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<!-- 本bean只可以只用构造函数注入,因其不含setter方法-->
<constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>

<!-- 实例化映射器,关于user表的业务可以直接从userMapper上进行操作,相当于sqlSession.getMapper(UserMapper.class)-->
<bean id="userMapper" class="top.dustball.mapper.UserMapper.UserMapperImpl">
<property name="sqlSessionTemplate" ref="sqlSession"/>
</bean>

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg ref="datasource"/>
</bean>


<!-- tx是事务标签-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 给哪些方法上事务-->
<!-- 配置事务的传播特性-->
<!-- 一般设计数据库的业务都需要事务,查询除外,因此可以简单粗暴全上事务-->
<tx:attributes>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="update*"/>
<tx:method name="select" read-only="true"/>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>


<aop:config>
<!-- mapper下面的所有类的所有方法都作为切点-->
<aop:pointcut id="pointcut" expression="execution(* top.dustball.mapper.*.*.* (..) )"/>

<!--txAdivce作为通知应用于pointcut切点-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"/>
</aop:config>

</beans>

增加了一个transactionManager,一个txAdvice,一个aop

其中事务管理器可以有多种,JDBC,Druid等等

关键在于<tx:advice>标签,即事务通知,该标签用于说明给哪些方法上事务,只是针对方法名,此时还不会针对类名

1
<tx:method name="add*" propagation="REQUIRED"/>

这里propagation属性用于设置事务的传播属性,一般都是用REQUIRED,其他的有需要再查

事务的七种传播特性

默认值是required

默认值是required

默认值是required

传播特性 意义
required 如果存在当前事务,那么加入该事务,
如果不存在事务,就创建一个事务。这是propagation的默认值
supports 如果当前已经存在事务,那么加入该事务,
否则创建一个所谓的空事务。
mandatory 当前必须存在一个事务,否则抛出异常
requires-new 如果当前存在事务,先把当前事务相关内容封装到一个实体,
然后重新创建一个新事务,并接受这个实体作为参数,用于事务恢复。
not-supported 如果当前存在事务,挂起当前事务,然后新的方法在没有事务的环境中执行。
没有spring事务的环境下,sql的提交完全依赖于defaultAutoCommit属性值
never 如果当前存在事务,则抛出异常。
否则在无事务的环境上执行代码
nested 如果当前存在事务,则使用savepoint技术将当前事务状态进行保存,<br /然后底层公用一个链接,
当nested内部出现错误的时候,自行回滚到save point的状态。
只要外部捕获到了异常,就可以继续进行外部事务的提交,而不会受到内嵌事务的干扰。
但是,如果外部事物抛出了异常,整个大事务都会回滚。

最后aop:config标签的作用是,将tx:advice通知作用与mapper包下的所有类的所有方法

之后所有的Dao层操作都是事务操作.

此前和此后在测试类中调用没有区别.