Loading

Spring学习笔记

1 简介

1.1 历史

  • Spring框架以interface21框架为基础,经过重新设计,并不断丰富其内涵,与2004年3月24日发布了1.0正式版

  • 作者为Rod Johnson

1.2 理念

  • 使现有的技术更加容易使用
  • 本身包括很多内容,并整合了现有的技术框架

1.3 优点

  • Spring是一个开源的免费的框架(容器)
  • Spring是一个轻量级的、非入侵式的框架
  • 控制反转(IOC)
  • 面向切面编程(AOP)
  • 支持事务的处理

1.4 组成

1.5 其他

  • Spring Boot
    • 一个快速开发的框架
    • 基于Spring Boot可以快速开发单个微服务
  • Spring Cloud
    • 基于Spring Boot实现

2 IoC

以前的JavaWeb项目开发流程,比如

  1. 写UserDao接口
public interface UserDao {
    //方法
}
  1. 写UserDaoImpl实现类
public class UserDaoImpl implements UserDao {
    //方法实现
}
  1. 写UserService业务接口
public interface UserService {
    //方法
}
  1. 写UserServiceImpl业务实现类
public class UserServiceImpl implements UserService {
    private UserDao userDao = new UserDaoImpl();
    
    //方法实现
}
  1. 测试
@Test
public void test() {
    UserService userService = new UserServiceImpl();
    //调用userService的方法
}

这种方法的弊端在于用户的需求可能会影响代码,需要根据用户需求去修改代码,代价很大

那么,如果在UserServiceImpl中使用set接口,程序变为

public class UserServiceImpl implements UserService {
    private UserDao userDao;
    //利用set进行动态注入
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
    
    //方法实现
}

测试的时候代码变为

@Test
public void test() {
    UserService userService = new UserServiceImpl();
    (UserServiceImpl) userService.setUserDao(new UserDaoImpl());
    //调用userService的方法
}

使用set注入后,程序不再具有主动性,变成了被动的接收对象。主动权转到了用户,用户选择实现类对象

这种思想从本质上解决了问题,程序员不用管理对象的创建,降低了系统的耦合,从而专注业务的实现,这就是IoC的原型

2.1 IoC本质

控制反转(IoC, Inversion of Control)是一种设计思想。在没有IoC的程序中,使用面向对象编程,对象的创建和对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制。而控制反转则是将对象的创建转移给第三方

控制反转是一种通过描述(XML或注解)并通过第三方去生产或获取特定对象的方式,在Spring中实现控制反转的是IoC容器,其实现方法是依赖注入(DI, Dependency Injection)

3 HelloSpring

  1. 编写实体类
public class Hello {
    private String str;
    
    public void setStr(String str) {
        this.str = str;
    }
    
    public String getStr() {
        return str;
    }
}
  1. 配置xml文件
<bean id="hello" class="com.hjc.pojo.Hello">
    <property name="str" value="Hello, world"/>
</bean>
  1. 测试
@Test
public void test() {
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    Hello hello = (Hello) context.getBean("hello");
    System.out.println(hello);
}

要实现不同的操作,我们只需要在xml文件中修改。对象由Spring来创建,管理,装配

3.1 BeanFactory和ApplicationContext

ApplicationContext是BeanFactory的子接口

  • BeanFactory是bean的工厂,负责创建bean的实例,放进容器内,容器其实是一个map。BeanFactory是最底层的接口
  • ApplicationContext是容器接口,更多地负责容器功能的实现(基于BeanFactory创建好的对象之上完成强大的容器功能),容器可以从map中获取bean,并且AOP,DI的实现是在ApplicationContext内。ApplicationContext是留给我们使用的

4 IoC创建对象的方式

4.1 无参构造函数创建对象

要用无参构造函数创建对象,那么类中就要保留无参构造函数,要么显示写出无参构造函数,要么就不写构造函数

public class User {
    private int id;
    private String name;
    
    public void setId(int id) {
        this.id = id;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    //其他函数
}

此时,在配置xml时就如上文那样,数据注入是由set注入的,那么在类中就要写出属性的set方法

<bean id="user" class="com.hjc.pojo.User">
    <property name="id" value="1"/>
    <property name="name" value="admin"/>
</bean>

用这种方式配置,那么对象是先由类的无参构造函数创建,再由类的set方法进行数据的注入,要注意这个先后顺序

4.2 有参构造函数创建对象

如果要使用有参构造函数创建对象,那么在类中就要写出有参构造函数。此时,数据是由有参构造函数注入

public class User {
    private int id;
    private String name;
    
    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

在配置xml文件时就和上文不太一样

<bean id="user" class="com.hjc.pojo.User">
    <constructor-arg index="0" value="1"/>
    <constructor-arg index="1" value="admin"/>
</bean>
<bean id="user" class="com.hjc.pojo.User">
    <constructor-arg type="int" value="1"/>
    <constructor-arg type="java.lang.String" value="admin"/>
</bean>
<bean id="User" class="com.hjc.pojo.User">
    <constructor-arg name="id" value="1"/>
    <constructor-arg name="name" value="admin"/>
</bean>

以上三种方法都可以实现相同的效果。以这种方式配置,对象就由类的有参构造函数创建,而数据也是由有参构造函数注入

不管对象是由无参构造函数还是有参构造函数创建的,在配置文件加载的时候,容器中的对象就已经被初始化了(默认是单例模式)

4.3 通过静态工厂方法创建bean

静态工厂:工厂本身不用被创建,可以直接通过静态方法调用

// 静态工厂类
public class UserStaticFactory {
    public static User getUser(int id, String name) {
        User user = new User();
        user.setId(id);
        user.setName(name);
        return user;
    }
}

在xml中配置bean由静态工厂方法创建
class中指定静态工厂全类名,factory-method指定工厂方法,constructor-arg传入参数

<bean id="user" class="com.hjc.factory.UserStaticFactory" factory-method="getUser">
    <constructor-arg name="id" value="1"/>
    <constructor-arg name="name" value="admin"/>
</bean>

4.4 通过实例工厂方法创建bean

实例工厂:工厂本身需要被创建

// 实例工厂类
public class UserInstanceFactory {
    public User getUser(int id, String name) {
        User user = new User();
        user.setId(id);
        user.setName(name);
        return user;
    }
}

在xml中配置bean由实例工厂方法创建
先创建实例工厂对象,再配置要创建的bean,factory-bean指定使用的工厂,factory-method指定使用的工厂方法

<bean id="userInstanceFactory" class="com.hjc.factory.UserInstanceFactory"></bean>

<bean id="user" class="com.hjc.pojo.User" factory-bean="userInstanceFactory" factory-method="getUser">
    <constructor-arg name="id" value="1"/>
    <constructor-arg name="name" value="admin"/>
</bean>

4.5 通过FactoryBean创建bean

FactoryBean是Spring规定的一个接口,只要是这个接口的实现类,Spring都认为是一个工厂,Spring会自动调用工厂方法创建实例

public class MyFactoryBeanImpl implements FactoryBean<User> {

    // 工厂方法,返回创建的对象
    @Override
    public User getObject() throws Exception {
        User user = new User();
        user.setName("admin");
        return user;
    }

    // 返回创建的对象的类型,Spring自动调用这个方法来确认创建的对象是什么类型
    @Override
    public Class<?> getObjectType() {
        return User.class
    }

    // 是否是单例
    @Override
    public boolean isSingleton() {
        return true;
    }
}

在xml文件中配置由FactoryBean创建的bean

<bean id="myFactoryBeanImpl " class="com.hjc.factory.MyFactoryBeanImpl"></bean>

只需要配置FactoryBean接口的实现类,Spring会自动创建在工厂实现类中定义好的bean实例,并且创建的bean不管是singleton还是prototype,容器启动时都不会立即创建bean实例,只会在需要的时候创建bean

5 Spring配置

5.1 别名

Spring中别名的使用和MyBatis中类似,使用别名之后可以省略类的包路径

5.2 bean的配置

<!--
id: bean的唯一标识符
class: bean对象对应的全限定名
name: 别名,可以取多个别名
scope: bean为单例还是多例
-->
<bean id="user" class="com.hjc.pojo.User" name="user2" scope="singleton">
    <property name="id" value="1"/>
    <property name="name" value="admin"/>
</bean>

5.3 使用import

将多个配置文件导入合并为一个配置文件。比如,在applicationContext.xml文件中导入service.xml和dao.xml配置文件,将这两个配置文件合并为一个applicationContext.xml配置文件

<import resource="service.xml"/>
<import resource="dao.xml"/>

5.4 引用外部属性文件

在使用数据库连接池的时候,可以让Spring帮我们创建连接池对象,管理连接池
在配置连接池的时候,需要输入连接池的多个属性,比如username,password,url,driverClass等,我们可以直接在value属性中输入相应的值,也可以使用外部配置文件(properties文件)

  1. 定义外部属性文件config.properties
jdbc.username=root
jdbc.password=123456
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.driverClass=com.mysql.jdbc.Driver
  1. 在xml文件中加载外部属性文件(依赖context名称空间)
<context:property-placeholder location="classpath:config.properties"/>
  1. 使用外部属性文件的属性,使用${}
<bean id="dataSource" class="...">
    <property name="username" value="${jdbc.username}"></property>
    <property name="password" value="${jdbc.password}"></property>
    <property name="url" value="${jdbc.url}"></property>
    <property name="driverClass" value="${jdbc.driverClass}"></property>
</bean>

6 依赖注入

6.1 构造器注入

参考4.2

6.2 set方式注入

  • 依赖注入
    • 依赖:bean对象的创建依赖于容器
    • 注入:bean对象的所有属性由容器注入

实例

  1. 编写实体类
public class Student {
    private String name;
    private Address address;
    private String[] books;
    private List<String> hobbies;
    private Map<String, String> scores;
    private Set<String> games;
    private String nullPoint;
    private Properties info;
    
    //一定要写各属性的set方法,在此省略
}
public class Address {
    private String address;
    
    public setAddress(String address) {
        this.address = address;
    }
}
  1. 配置xml文件,使用set方法注入
<bean id="address" class="com.hjc.pojo.Address">
    <property name="address" value="testAddress"/>
</bean>

<bean id="student" class="com.hjc.pojo.Student">
    <!--基本类型(包括String)注入,value-->
    <property name="name" value="test"/>
    <!--bean注入,ref-->
    <property name="address" ref="address"/>
    <!--数组注入,-->
    <property name="books">
        <array>
            <value>testBook1</value>
            <value>testBook2</value>
            <value>testBook3</value>
        </array>
    </property>
    <!--list注入-->
    <property name="hobbies">
        <list>
            <value>testHobby1</value>
            <value>testHobby2</value>
        </list>
    </property>
    <!--map注入-->
    <property name="scores">
        <map>
            <entry key="Math" value="100"/>
            <entry key="Physics" value="100"/>
        </map>
    </property>
    <!--set注入-->
    <property name="games">
        <set>
            <value>testGame1</value>
            <value>testGame2</value>
        </set>
    </property>
    <!--null注入-->
    <property name="nullPoint">
        <null/>
    </property>
    <!--properties注入-->
    <property name="info">
        <props>
            <prop key="testInfo1">test1</prop>
            <prop key="testInfo2">test2</prop>
        </props>
    </property>
</bean>
  1. 测试
@Test
public void test() {
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    Student student = (Student) context.getBean("student");
    System.out.println(student);
}

6.3 其他方式注入

p命名空间,对应set注入,参考官方文档

c命名空间,对象构造器注入,参考官方文档

7 bean的作用域

7.1 singleton

单例模式,也是Spring的默认模式,不显示写出就表示单例

单例模式表示在容器中对象只会创建一个,且在容器启动完成之前就已经创建好对象,保存在容器中了

7.2 prototype

原型模式,表示每次从容器中获得对象的时候,容器会创建一个新的对象返回,且在容器启动时默认不会去创建bean,只会在获取对象的时候才创建bean

7.3 request、session、application

这几个在web开发中使用

8 bean的生命周期

8.1 生命周期函数

对于单例模式,bean的生命周期为

  • (容器启动)构造器 --> 初始化方法 --> (容器关闭)销毁方法
    这里的初始化方法和销毁方法由类定义,并在xml配置文件中进行配置(init-method和destroy-method)

对于多实例多模式,bean的生命周为

  • (容器启动)--> 获取bean --> 构造器 --> 初始化方法 --> 容器关闭不会调用销毁方法

8.2 bean的后置处理器

Spring提供了一个后置处理器的接口BeanPostProcessor,可以在bean的初始化前后调用相应的方法

使用方法

  1. 定义后置处理器实现类实现BeanPostProcessor
  2. 重写函数postProcessBeforeInitialization和postProcessAfterInitialization,表示bean初始化前后会调用的方法,函数的参数为bean的对象和bean的id,函数返回值为bean对象
  3. 在xml文件中注册定义好的后置处理器实现类

生命周期过程:(容器启动)构造器 --> 后置处理器before方法 --> 初始化方法 --> 后置处理器after方法 --> bean初始化完成

无论bean是否定义了初始化方法,只要定义且配置了后置处理器,后置处理器都会工作

9 bean的自动装配

之前的xml配置都是手动配置的,那么自动装配是Spring会在上下文中自动寻找,自动装配属性

在Spring中有三种装配的方式

  • 在xml中显示配置
  • 在java类中显示配置
  • 隐式自动装配

9.1 byName自动装配

public class Student {
    private String name;
    private Computer computer;
    private KeyBoard keyBoard;
    
    //set方法
}
public class Computer {
}

public class KeyBoard {
}

有三个实体类,用byName自动装配配置xml

<bean id="computer" class="com.hjc.pojo.Computer"/>
<bean id="keyBoard" class="com.hjc.pojo.KeyBoard"/>
<!--
byName: 自动在容器上下文中查找和自己对象set方法后面的值对应的bean id
如:类中setComputer方法,Computer对应bean id为computer
-->
<bean id="student" class="com.hjc.pojo.Student" autowire="byName">
    <property name="name" value="test"/>
</bean>

这种方法要求bean的id不能随便选择,要和自动注入的属性的set方法的值一致,除了首字母大写变成小写

9.2 byType自动装配

还是上面三个实体类,用byType实现自动装配

<bean id="computer" class="com.hjc.pojo.Computer"/>
<bean id="keyBoard" class="com.hjc.pojo.KeyBoard"/>
<!--
byType: 会自动在容器上下文中查找和自己对象属性类型相同的bean
-->
<bean id="student" class="com.hjc.pojo.Student" autowire="byType">
    <property name="name" value="test"/>
</bean>

这种方法要求对象中属性类型要唯一,如果有几个相同的属性类型,就不能自动装配

9.3 constructor自动装配

通过有参构造器为bean属性自动装配,在执行过程中,会先调用有参构造器,按照有参构造器中参数的类型进行装配,如果按照类型找到了多个,则按照参数的名字进行装配,找不到则为null

<bean id="computer" class="com.hjc.pojo.Computer"/>
<bean id="keyBoard" class="com.hjc.pojo.KeyBoard"/>
<!--
在类中要存在有参构造器
-->
<bean id="student" class="com.hjc.pojo.Student" autowire="constructor">
    <property name="name" value="test"/>
</bean>

9.4 注解自动装配

jdk1.5支持注解,spring2.5支持注解

要使用注解

  1. 导入约束,context约束
  2. 配置注解的支持
<context:annotation-config/>

使用@Autowired注解,直接在属性上使用,或者在set方法上使用。另外,使用@Autowired注解可以不写set方法,前提是属性符合byType的要求

实体类

public class Student {
    private String name;
    @Autowired
    private Computer computer;
    @Autowired
    private KeyBoard keyBoard;
    
    //set方法
}

xml文件

<bean id="computer" class="com.hjc.pojo.Computer"/>
<bean id="keyBoard" class="com.hjc.pojo.KeyBoard"/>
<bean id="student" class="com.hjc.pojo.Student"/>

如果使用@Autowired实现自动装配的环境比较复杂,无法通过一个注解完成的时候,可以配合使用@Qualifier(value="xxx")来指定唯一的一个bean

10 使用注解开发

通过在类中添加某些注解,可以快速地将bean添加到ioc容器中

在Spring4之后,要使用注解开发,必须要导入aop的包。另外,需要在xml文件中导入context约束,配置注解的支持

<context:annotation-config/>

首先,要配置扫描的包,也就是说,在类中配置了注解还要被扫描到才有效

<context:component-scan base-package="com.hjc"/>

另外,还可以使用context:exclude-filtercontext:include-filter指定扫描包时不包含的类和扫描包时只要包含的类

10.1 @Component

这个注解放在类上,表示这个类被Spring管理了,比如

@Component
public class User {
    //省略
}

相当于在xml文件中配置bean

<bean id="user" class="com.hjc.pojo.User"/>

这样配置相当于默认bean的id为类名首字母小写,如果要指定bean的id,需要在注解中手动添加
比如@Component("user")

10.2 @Value

这个注解放在类中的属性上,表示注入数据,比如

@Component
public class User {
    @Value("test")
    private String name;
}

相当于在xml文件中配置bean,并进行依赖注入

<bean id="user" class="com.hjc.pojo.User">
    <property name="name" value="test"/>
</bean>

@Value注解也可以放在属性的对应set方法上,但这个注解只能进行简单的配置,复杂的配置还是使用xml文件

10.3 @Component的衍生注解

web项目一般有三层,不同层中的注解名称不同,但作用相同

  • Dao层:@Repository
  • Service层:@Service
  • Controller层:@Controller

这几个注解和@Component作用是一样的,都表示将类注册到容器中,装配bean

10.4 @Autowired

这个注解实现自动装配,参考9.4

实体类

@Component("student")
public class Student {
    private String name;
    @Autowired
    @Qualifier("computer")
    private Computer computer;
    @Autowired
    @Qualifier("keyBoard")
    private KeyBoard keyBoard;
    
    //set方法
}
@Component("computer")
public class Computer {
    //...
}
@Component("keyBoard")
public class KeyBoard {
    //...
}

这样在xml文件中就不需要再手动配置bean了

@Autowired原理

  1. 先按照类型去容器中找对应的组件
  2. 如果容器中有多个同类型的bean,会按照变量名作为id继续匹配
  3. 如果按照变量名作为id没有匹配到,会报错,这时可以更改变量名为目标bean的id。一般,我们可以使用@Qualifier注解指定一个id,不使用变量名为id

在@Autowired中有一个required属性,默认为true,意思是一定要装配,如果装配不上则会报错。
如果required属性改为false,如@Autowired(required=false),意思是不一定要装配上,如果装配不上,则该变量为null

10.5 @Qualifier

根据@Autowired注解的原理,如果只使用@Autowired注解找不到bean时,需要使用@Qualifier注解手动指定一个bean,比如@Qualifier("computer")

@Qualifier注解既可以写在类中的属性上,也可以写在set方法内的参数前面,作用相同

10.6 @Resource

@Resource注解和@Autowired注解作用相同,此外还有@Inject注解,都用来自动装配

但是,相比其他两个注解,@Autowired注解是Spring规定的注解,功能最为强大,而@Resource是j2ee规定的注解,没有required属性。

由于@Resource注解是java自己的标准,@Resource注解扩展性更强,如果切换为另外一个容器框架,@Resource注解还可以被识别,而@Autowired注解脱离了Spring就没法运行

10.7 @Scope

这个注解配置类的作用域,是单例模式还是原型模式,比如

@Component
@Scope("singleton")
public class User {
    @Value("test")
    private String name;
}

总结:

  • xml文件配置更加万能,适用于任何场合,维护简单
  • 注解适用于简单的配置,维护相对复杂
  • 项目中一般使用xml管理bean,使用注解完成属性注入

11 使用JavaConfig配置

不使用xml文件配置,都由Java代码配置

JavaConfig是Spring的一个子项目,在Spring4之后,称为一个核心功能

11.1 @Configuration、@Bean

要用Java代码进行配置,就要用到@Configuration和@Bean这两个注解,比如

@Configuration
public class AppConfig {
    @Bean
    public User getUser() {
        return new User();
    }
}

这就相当于在xml中配置

<bean id="getUser" class="com.hjc.pojo.User"/>

测试

@Test
public void test() {
    ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
    User user = context.getBean("getUser");
}

注意和xml文件配置实例化对象的不同

11.2 @ComponentScan

在配置类中使用这个注解表示设置扫描类包

@Configuration
@ComponentScan("com.hjc")
public class AppConfig {
    @Bean
    public User getUser() {
        return new User();
    }
}

12 代理模式

代理模式是SpringAOP的底层实现

代理模式有两种

  • 静态代理
  • 动态代理

12.1 静态代理

角色分析

  • 抽象角色:一般会使用接口或者抽象类来解决
  • 真实角色:被代理角色
  • 代理角色:代理真实角色,代理真实角色后,一般会做附加操作
  • 客户:访问代理对象的人

使用静态代理的例子

  1. 编写接口(抽象角色)
//租房接口
public interface Rent {
    void rent();
}
  1. 编写被代理类(真实角色)
//房东
public class Host implements Rent {
    @Override
    public void rent() {
        System.out.println("房东出租房子");
    }
}
  1. 编写代理类(代理角色)
public class Proxy implements Rent {
    private Host host;
    
    public Proxy() {
    }
    
    public Proxy(Host host) {
        this.host = host;
    }
    
    @Override
    public void rent() {
        findHouse();
        host.rent();
        signContract();
    }
    
    public void findHouse() {
        System.out.println("找到房子");
    }
    
    public void signContract() {
        System.out.println("签合同");
    }
}
  1. 编写客户访问代理角色
public class Client {
    public static void main(String[] args) {
        Proxy proxy = new Proxy(new Host());
        proxy.rent();
    }
}

静态代理的好处:

  • 可以使真实角色的操作更加纯粹,不用关注一些公共的业务
  • 公共业务交给代理角色,实现了业务的分工
  • 公共业务发生扩展的时候,方便集中管理

缺点:

  • 一个真实角色就会产生一个代理角色,增加代码量,开发效率低

在web开发中,如果要对类中的方法进行增强,直接改类中对应方法的代码比较繁琐,而且实际开发不允许直接改代码,我们可以使用静态代理实现,举个例子加深理解

  1. 编写service接口
public interface UserService {
    void add();
    void delete();
    void update();
    void query();
}
  1. 编写service实现类
public class UserServiceImpl implements UserService {
    //实现对应方法
}
  1. 编写代理类
public class UserServiceProxy implements UserService {
    private UserService userService;
    
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
    
    public void add() {
        log("add");
        userService.add();
    }
    
    public void delete() {
        log("delete");
        userService.delete();
    }
    
    public void update() {
        log("update");
        userService.update();
    }
    public void query() {
        log("query");
        userService.find();
    }
    
    public void log(String msg) {
        System.out.println("[debug] 使用了" + msg + "方法");
    }
}
  1. 客户访问
public class Client {
    public static void main(String[] args) {
        UserServiceProxy proxy = new UserServiceProxy();
        proxy.setUserService(new UserServiceImpl);
        proxy.add();
        proxy.delete();
        proxy.update();
        proxy.query();
    }
}

这样子就避免修改了原有的service代码,而是通过代理类对service类的方法进行增强

12.2 动态代理

  • 动态代理和静态代理的角色一样
  • 动态代理的代理类是动态生成的,不是直接写好的
  • 静态代理的代理关系在编译时确定,而动态代理的代理关系在运行时确定
  • 动态代理分为两大类
    • 基于接口的动态代理:jdk动态代理
    • 基于类的动态代理:cglib
    • 还有基于java字节码实现:Javassist

需要了解两个类:Proxy,InvocationHandler

动态代理的好处

  • 动态代理有静态代理的优点
  • 另外,一个动态代理类代理的是一个接口,一般对应一类业务,解决了静态代理的缺点

12.3 基于jdk的动态代理

我们使用上面静态代理的例子变为使用jdk的动态代理来实现

同样的,我们有Service接口及其实现类

public interface UserService {
    void add();
    void delete();
    void update();
    void query();
}

public class UserServiceImpl implements UserService {
    //实现对应方法
}

不同于上文使用静态代理的方法手动编写一个代理类,我们使用jdk动态代理

  1. 实现一个InvocationHandler,方法调用会被转发到该类的invoke方法上
  2. 在需要使用Service的时候,通过jdk动态代理获取Service的代理对象
// 1. 实现一个InvocationHandler
class UserServiceInvocationHandler implements InvocationHandler {
    private UserService userService;

    public UserServiceInvocationHandler(UserService userService) {
        this.userService = userService;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if ("add".equals(method.getName())) {
            log("add start");
        } else if ("delete".equals(method.getName())) {
            log("delete start");
        } else if ("update".equals(method.getName())) {
            log("update start");
        } else if ("query".equals(method.getName()d)) {
            log("query start");
        }
        Object result = method.invoke(userService, args);
        log("finish");
        return result;
    }
}

// 2. 获取UserService的代理对象
UserService userServiceProxy = (UserService) Proxy.getProxyInstance(
                                getClass().getClassLoader(),    // 1. 类加载器
                                new Class<?>[] {UserService.class},    // 2. 代理需要实现的接口,数组
                                new UserServiceInvocationHandler(new UserService()));    // 3. 方法调用的实际处理者
userServiceProxy.add();
userServiceProxy.delete();
userServiceProxy.update();
userServiceProxy.query();

jdk动态代理的关键是Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler)方法,该方法会根据指定的参数动态地创建代理对象。
三个参数的含义为:

  1. loader,指定代理对象的类加载器
  2. interfaces,代理对象需要实现的接口,这也是被代理对象实现的接口
  3. handler,方法调用的实际处理者,代理对象的方法调用都会转发到此处

此方法会返回一个实现了指定接口的代理对象,该对象的所有方法调用都会转发到InvocationHandler.invoke()方法,在invoke方法内,我们可以加入各种逻辑,对原来的方法进行增强,再通过调用Method.invoke来执行真正的被代理方法体

jdk动态代理是Java原生支持的,不需要任何外部依赖,但是只能基于接口进行动态代理。如果一个类没有实现任何接口,那么就不能使用jdk动态代理,这时就要使用CGLIB的动态代理

12.4 基于cglib的动态代理

CGLIB是一个基于ASM的字节码生成库,允许我们在运行期间对字节码进行修改和动态生成。CGLIB通过继承方式实现动态代理,不需要实现接口

假设我们有一个Service的类,没有实现任何接口

public class UserService {
    public void add() {
        //...
    }

    public void delete() {
        //...
    }

    public void update() {
        //...
    }

    public void query() {
        //...
    }
}

因为UserService没有实现接口,无法使用jdk动态代理,则通过cglib动态代理的方式如下

  1. 实现一个MethodInterceptor,方法调用会被转发到该类的intercept方法
  2. 在需要使用Service的时候,通过cglib动态代理获取代理对象
// 1. 实现一个MethodInterceptor
class UserServiceMethodInterceptor implements MethodInterceptor {
    
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        log("start");
        Object result = proxy.invokeSuper(obj, args);
        log("finish");
        return result;
    }
}

// 2. 获取UserService的代理对象
Enhancer enhancer = new Enhancer();
enhancer.setSuperClass(UserService.class);
enhancer.setCallback(new UserServiceMethodInterceptor());

UserService userService = (UserService) enhancer.create();
userService.add();
userService.delete();
userService.update();
userService.query();

CGLIB通过Enhancer来指定要代理的目标对象和实际处理代理逻辑的对象,通过create方法得到代理对象,这个对象的所有非final方法的调用都会被转发到MethodInterceptor.intercept方法上,在intercept方法内我们可以加入任何逻辑,
通过调用MethodProxy.invokeSuper方法来执行真正的被代理方法体

因为CGLIB是通过继承实现动态代理,无论被代理对象有没有实现接口都可以进行动态代理。但是如果被代理对象是一个final类,就不能被继承,也就不能使用CGLIB动态代理,会抛出异常。
同样,final方法是不能被覆盖的,也不能通过CGLIB动态代理,遇到这种情况不会抛异常,会跳过final方法只代理其他方法

13 AOP

13.1 AOP定义

AOP(Aspect Oriented Programming),面向切面编程,通过预编译方式和运行期动态代理实现程序功能统一维护的技术。AOP是OOP的延续,是函数式编程的一种衍生范型

13.2 AOP在Spring中的作用

提供声明式事务,允许用户自定义切面

  • 横切关注点:跨越应用程序多个模块的方法或功能,如日志,安全,缓存,事务等等
  • 切面(Aspect):横切关注点被模块化的特殊对象,即是一个类
  • 通知(Advice):切面必须要完成的工作,即是类中的一个方法
  • 目标(Target):被通知对象
  • 代理(Proxy):向目标对象应用通知之后创建的对象
  • 切入点(PointCut):切面通知执行的“地点”
  • 连接点(JointPoint):与切入点匹配的执行点
  • 切入点表达式:在众多连接点中选出相应的切入点

SpringAOP中,通过Advice定义横切逻辑,Spring支持5种类型的Advice

  • 前置通知
    • 连接点:方法前
    • 实现接口:org.springframework.aop.MethodBeforeAdvice
  • 后置通知
    • 连接点:方法后
    • 实现接口:org.springframework.aop.AfterReturningAdvice
  • 环绕通知
    • 连接点:方法前后
    • 实现接口:org.aopalliance.intercept.MethodInterceptor
  • 异常抛出通知
    • 连接点:方法抛出异常
    • 实现接口:org.springframework.aop.ThrowsAdvice
  • 引介通知
    • 连接点:类中增加新的方法属性
    • 实现接口:org.springframework.aop.IntroductionInterceptor

AOP在不改变原有代码的情况下, 增加新的功能

13.3 使用Spring实现AOP

要使用AOP织入,需要导入包

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

假设有service接口和实现类

public interface UserService {
    void add();
    void delete();
    void update();
    void query();
}
public class UserServiceImpl implements UserService {
    //实现对应方法
}

那么,要对这几个方法使用AOP进行方法增强

方式一:使用Spring的API实现

  1. 首先要定义切面和通知,定义类实现对应接口
public class BeforeLog implements MethodBeforeAdvice {
    //Method: 被代理的目标对象的方法
    //args: 方法参数
    //target: 被代理的目标对象
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("执行" + target.getClass().getName() + "的" + method.getName());
    }
}
public class AfterLog implements AfterReturningAdvice {
    //returnValue: 被代理的方法的返回值
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("执行" + method.getName() + "返回结果" + returnValue);
    }
}
  1. 在xml中配置,把这些类注册到Spring中,注册bean的过程可以使用注解代替
<!--注册bean-->
<bean id="userService" class="com.hjc.service.UserServiceImpl"/>
<bean id="beforeLog" class="com.hjc.log.BeforeLog"/>
<bean id="afterLog" class="com.hjc.log.AfterLog"/>

<!--配置aop,导入aop约束-->
<aop:config>
    <!--切入点,expression表达式-->
    <aop:pointcut id="pointcut" expression="execution(* com.hjc.service.UserServiceImpl.*(..))"/>
    <!--执行增强-->
    <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
    <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
</aop:config>
  1. 测试
@Test
public void test() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    //动态代理的是接口
    UserService userService = (UserService) context.getBean("userService");
    userService.add();
    userService.delete();
}

方式二:xml配置实现AOP

不通过实现Spring的接口来实现AOP,而是自己定义切面类和通知

  1. 将目标类和切面类注册到IOC容器中
  2. 配置切面及切入点来定义通知方法在何时何地运行
public class MyLog {
    public void beforeLog(Object result) {  // 通知方法中需要用到被代理方法的返回值
        //具体实现
    }
    
    public void afterLog(JoinPoint joinPoint) { // 通知方法中需要用到被代理方法的信息(方法名等信息)  
        //具体实现
    }

    public void afterThrowingLog(Exception exception) {  // 通知方法中需要用到被代理方法的异常信息
        //...
    }
}

同样,在xml中配置,注册bean的过程可以使用注解代替

在expression属性中写切入点表达式

<bean id="myLog" class="com.hjc.log.MyLog"/>

<aop:config>
    <!--自定义切面,order属性用来定义在多个切面下,多个切面的执行顺序,数字越小越先执行-->
    <aop:aspect ref="myLog" order="1">
        <!--切入点-->
        <aop:pointcut id="pointcut" expression="execution(* com.hjc.service.UserServiceImpl.*(..))"/>
        <!--通知,result属性和throwing是指当在通知方法中需要获取被代理方法的返回值和异常信息时,需要绑定这两个参数,而JoinPoint不需要绑定-->
        <aop:before method="beforeLog" pointcut-ref="pointcut" result="result"/>
        <aop:after method="afterLog" pointcut-ref="pointcut"/>
        <aop:after-throwing method="afterThrowingLog" pointcut-ref="pointcut" throwing="exception"/>
    </aop:aspect>
</aop:config>

方式三:注解实现AOP

使用注解实现AOP,主要是在定义切面类的时候使用注解@Aspect

在具体方法上使用相应的通知类型注解,在通知类型注解内部写切入点表达式

一般使用这种方式进行AOP

  1. 将目标类和切面类都注册到IOC容器中
  2. 告知Spring哪个是切面类(@Aspect)
  3. 在切面类中使用五种通知注解来配置切面中的通知方法都何时何地运行
  4. 开启基于注解的AOP支持
@Aspect
public class MyLog {
    @Before("execution(* com.hjc.service.UserServiceImpl.*(..))")
    public void before() {
        //具体实现
    }
    
    @After("execution(* com.hjc.service.UserServiceImpl.*(..))")
    public void after() {
        //具体实现
    }
}

如果需要获取被代理方法的详细信息,我们只需要在切面类的通知方法上添加一个参数JoinPoint

@Aspect
public class MyLog {
    @Before("execution(* com.hjc.service.UserServiceImpl.*(..))")
    public void before(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();  // 获取目标方法运行时的参数
        Signature signature = joinPoint.getSignature();  // 获取目标方法签名
        String name = signature.getName();  // 获取目标方法名
        //具体实现
    }
}

如果需要获取被代理方法的返回值,需要在切面类的通知方法上再添加一个参数result,另外再注解中指定返回参数returning="result"

@Aspect
public class MyLog {
    @After(value="execution(* com.hjc.service.UserServiceImpl.*(..))", returning="result")
    public void after(JoinPoint joinPoint, Object result) {
        //具体实现
    }
}

如果需要获取被代理方法的异常信息,需要再切面类的通知方法上再添加一个参数exception,另外在注解中指定异常参数throwing="exception"

@Aspect
public class MyLog {
    @AfterThrowing(value="execution(* com.hjc.service.UserServiceImpl.*(..))", throwing="exception")
    public void logException(JoinPoint joinPoint, Exception exception) {
        //具体实现
    }
}

在xml中只需注册bean,当然也可以使用注解标识。

还要开启注解支持,开启注解支持的方法有两种

一种是xml配置

<bean id="myLog" class="com.hjc.log.MyLog"/>
<!-- 开启基于注解的aop支持 -->
<aop:aspectj-autoproxy/>

另一种是在配置类上使用@EnableAspectJAutoProxy注解

Spring对通知方法的要求不严格,也就是说,不管定义的通知方法是public还是private,是否是static,是否有返回值,只要配置好了都可以正常运行。
唯一有要求的是通知方法的参数列表,因为通知方法是Spring利用反射调用的,每次方法调用必须确定这个方法的参数表的值,
所以参数列表上的每个参数,Spring必须都要认识,不认识的参数需要通过注解指定相应属性从而告知Spring

  • 如果需要获取被代理方法的详细信息,就加上JoinPoint joinPoint参数
  • 如果需要获取被代理方法的返回值,就加上Object result参数,并在注解中指定returning="result"
  • 如果需要获取被代理方法的异常信息,就加上Exception exception参数,并在注解中指定throwing="exception"
  • 使用xml配置时也是如此

13.4 切入点表达式

对于切入点表达式expression,有固定格式:execution(访问权限符 返回值类型 方法全类名(参数表)),访问权限符可以省略

还有通配符

  • \*
    1. 匹配一个或多个字符
    2. 匹配任意一个参数
  • ..
    1. 匹配任意多个参数,任意类型参数
    2. 匹配任意多层路径

13.5 通知方法的执行顺序

各种通知方法的执行顺序可以用如下代码来表示

try {
    @Before
    method.invoke(obj, args);
    @AfterReturning
} catch(e) {
    @AfterThrowing
} finally {
    @After
}

但是,如果是正常执行,通知方法的顺序为:@Before(前置通知) --> @After(后置通知) --> @AfterReturning

如果是执行出现异常,通知方法的顺序为:@Before --> @After --> @AfterThrowing

13.6 环绕通知

环绕通知是Spring中最大强的通知,环绕通知中有一个ProceedingJoinPoint参数,可以使用ProceedingJoinPoint.proceed显示执行被代理方法,相当于动态代理中的method.invoke

@Aspect
@Component
public class MyLog {
    @Around(value="execution(* com.hjc.service.UserServiceImpl.*(..))")
    public Object logAround(ProceedingJoinPoint pjp) {
        Object[] args = pjp.getArgs();  // 获取被代理方法参数
        Object proceed = null;
        try {
            // 前置通知
            proceed = pjp.proceed(args);  // 执行被代理方法
            // 返回通知
        } catch (e) {
            // 异常通知
        } finally {
            // 后置通知
        }
        return proceed
    }
}

环绕通知相当于一个通知包含了四个其他的通知,可以在代理方法内自行定义通知
环绕通知中可以决定被代理方法的什么时候执行,可以影响被代理方法,而其他四种通知是不能影响被代理方法的执行

13.7 多切面的执行顺序

当多个切面作用在同一个被代理方法上时,前置通知先执行的切面,相应的后置通知和返回通知会后执行。
比如有切面类A和B,当作用在同一个方法上时,A的前置通知比B的前置通知先执行,那么A的后置和返回通知会比B的后置和返回通知后执行。
而切面类A和B谁先执行是根据比较类名的大小,小的先执行。也可以通过注解@Order来定义切面的执行先后,@Order内需要指定一个数字,数字越小,相应的切面就先执行

13.8 AOP的应用

AOP使用场景

  1. AOP加日志保存到数据库
  2. AOP做权限验证
  3. AOP做安全检查
  4. AOP做事务控制

14 整合MyBatis

步骤

  1. 导入相关jar包
    • junit
    • mybatis
    • mysql
    • spring
    • aop织入
    • mybatis-spring
  2. 编写配置文件

14.1 MyBatis

14.2 MyBatis-Spring

参考官方文档

  1. 编写数据源配置
  2. sqlSessionFactory
  3. sqlSessionTemplate
  4. 给Dao接口编写实现类
  5. 注册实现类到Spring中
  6. 测试

15 声明式事务

15.1 事务

  • 事务涉及数据的完整性和一致性问题
  • 事务ACID原则
    • 原子性
    • 一致性
    • 隔离性
    • 持久性

事务控制一般设置在service层,只要业务逻辑出错或者异常就回滚,粒度较小,而dao层不包括业务逻辑,只是单纯地操作数据库,粒度较大

比如说,给service层配置事务,因为一个service层的方法可以包含多个dao层的方法,也就是会多次操作数据库。如果这些操作中有失败就全部回滚,成功则全部提交

15.2 Spring中的事务管理

  • 声明式事务:基于AOP
    * 只要告知Spring哪个方法是事务方法即可,Spring会自动进行事务控制
  • 编程式事务:在代码中进行事务管理
TransactionFilter {
    try {
        // 获取连接
        // 设置非自动提交
        chain.doFilter();
        // 提交
    } catch (e) {
        // 回滚
    } finally {
        // 关闭连接
    }
}

一般使用声明式事务,而不是用编程式事务,因为编程式事务会改变代码

  1. 在xml中配置声明式事务

使用Spring提供的DataSourceTransactionManager事务管理器,可以在目标方法运行前后进行事务控制

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--控制数据源-->
    <property name="dataSource" value="dataSource"/>
</bean>
  1. 结合AOP实现事务织入
<!--配置事务通知-->
<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" propagation="REQUIRED"/>
        <tx:method name="query" read-only="true"/>
        <!--配置所有方法-->
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

<!--配置事务切入-->
<aop:config>
    <aop:pointcut id="txPointCut" expression="execution(* com.hjc.service.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>

切入点表达式expression只是让事务管理器要切入这些方法,至于要不要控制事务是用tx:method来指定

这一步也可以使用基于注解的事务控制

首先开启基于注解的事务控制,开启的方法有两种

一种是xml配置方式

<tx:annotation-driven transaction-manager="transactionManager"/>

另一种为注解开启,在配置类的上方标注@EnableTransactionManagement注解开启事务管理功能。另外,配置事务管理器的方法也有两种,一种上文中使用xml配置,另一种是使用配置类配置

接下来就可以在UserService类中的事务方法加注解@Transactional

public class UserServiceImpl implements UserService {
    
    @Transactional
    public void add() {
        //...
    }

    @Transactional
    public void delete() {
        //...
    }

    @Transactional
    public void update() {
        //...
    }

    @Transactional
    public void query() {
        //...
    }
}

这样就给UserService类中的四个方法加上了事务控制,如果一个方法内有多个数据库操作,其中一个操作失败了,其余操作也不会改变数据库内的数据

通过这两种方法就可以对类中的方法进行事务控制,在容器中,有事务控制的类被保存为这个类的代理对象(因为是通过AOP进行事务控制的,而AOP是由动态代理实现的)

15.3 事务细节

在注解@Transactional或者xml配置中还有一些可用的属性,这些属性涉及到事务的细节

  1. timeout

timeout属性表示事务超出指定执行时长后自动终止并回滚,以秒为单位

  1. readOnly

readOnly属性可以设置事务是否为只读,如果方法内的操作只是对数据库的读出,而没有写入,那么可以设置readOnly=true,可以进行事务优化,加快数据库的查询速度

  1. noRollBackFor

对于异常,可以分为运行时异常和编译时异常

  • 运行时异常:可以不用处理,默认都回滚
  • 编译时异常:要么try-catch,要么throws,默认不回滚

那么对于事务的回滚,默认发生运行时异常时都会回滚,而发生编译时异常时不会回滚

所以noRollBackFor属性可以设置发生原本会回滚的异常(运行时异常)时事务不进行回滚,值是一个数组,数组内元素为异常的类型,设置完后当方法内出现特定异常时,事务不会回滚

  1. rollBackFor

rollBackFor属性和noRollBackFor属性作用相反,表示原本不回滚的异常(编译时异常)指定让其回滚,值是一个数组,数组内元素为异常的类型,设置完后当方法内出现特定异常时,事务会回滚

  1. isolation

数据库支持了四种隔离级别,分别是READ_UNCOMMITTEDREAD_COMMITTEDREPEATABLE_READSERIALIZABLE,以防止脏读,幻读和不可重复读的行为

  1. propagation

propagation属性表示事务的传播行为。思考一个问题,如果有多个事务嵌套运行,子事务是否需要和父事务共用一个事务

AService {
    tx_a() {
        tx_b() {
            //...
        }
        tx_c() {
           //...
        }
    }
}

如上所示,如果c事务发生了异常,b事务是否需要进行回滚,这是一个可以设定控制的问题,可以通过设置事务的传播行为来设置

propagation属性的值有多种

  • REQUIRED:如果有事务在运行,当前的方法就在这个事务中运行,否则就启动一个新的事务,在新的事务中运行
  • REQUIRES_NEW:当前的方法必须启动新的事务,并在新的事务中运行,如果有事务正在运行,应该将它挂起
  • SUPPORTS:如果有事务在运行,当前的方法就在这个事务内运行,否则它可以不运行在这个事务中
  • NOT_SUPPORTED:当前的方法不应该运行在事务中,如果有正在运行的事务,应该将它挂起
  • MANDATORY:当前的方法必须运行在事务内,如果没有正在运行的事务,就抛出异常
  • NEVER:当前的方法不应该运行在事务中,如果有正在运行的事务,就抛出异常
  • NESTED:如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则,就启动一个新的事务,并在它自己的事务内运行

当一个事务的propagation被设置为REQUIRED时,意味着,这个事务和它的父事务共用一个事务,即整个事务的任何一处出现异常,整个事务都会回滚。而且,在子事务中设置的其他属性,比如timeout都不起作用,只有在父事务中设置的属性对整体事务起作用。
而当一个事务的propagation被设置为REQUIRES_NEW时,意味着,这个事务和它的父事务不共用一个事务,即一个事务出现异常要回滚时,另一个事务不受影响。

假设有如下事务

multx() {
    //REQUIRED
    A() {
        //REQUIRES_NEW
        B() {}
        //REQUIRED
        C() {}
    }
    //REQUIRES_NEW
    D() {
        //REQUIRED
        E() {
            //REQUIRES_NEW
            F() {}
        }
        //REQUIRES_NEW
        G() {}
    }
    int i = 10 / 0;
}

上述代码在 int i = 10 / 0; 处出现异常,根据每个事务的传播行为的设定,可以看到A事务会回滚,B事务不会回滚,C事务会回滚,D事务不会回滚,E事务不会回滚,F事务不会回滚,G事务不会回滚

posted @ 2020-07-16 16:44  Kinopio  阅读(370)  评论(0编辑  收藏  举报