Spring 基础

Spring 基础

参考资料

Spring 的核心包括 2 个概念:控制反转 (IOC)面向切面 (AOP)

我的Spring Boot学习之路! - 知乎 (zhihu.com)

控制反转 (IOC) 的核心思想:把主动权交给用户。

  1. 狂神说Java
    1. 《Spring5 最新完整教程 IDEA 版》
    2. 《SpringBoot 最新教程 IDEA 版》
  2. 开源项目SpringBoot-Learning
  3. 尚硅谷 Spring5 框架最新版教程

Spring Framework 中文文档 - Spring Framework 5.1.3.RELEASE Reference | Docs4dev

控制反转(IOC)理论推导

3、IOC 理论推导_哔哩哔哩_bilibili

<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.0.RELEASE</version>
    <scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.0.RELEASE</version>
</dependency>

业务的常规实现:

  1. UserDao 实现接口
  2. UserDaoImpl 实现类
  3. UserService 业务接口
  4. UserServiceImpl 业务实现类

在我们之前的业务中,用户的需求可能会影响我们原来的代码,我们需要根据用户的需求去修改原来的代码!

如果程序代码量十分大,修改一次的成本代价十分昂贵。

业务接口层,将:

private UserDao userDao = new UserDaoMysqlImpl();

替换为:使用一个 Set 接口实现

private UserDao userDao;
// 利用 set 进行动态实现值的注入!
public void setUserDao(UserDao userDao) {
    this.userDao = userDao;
}
  • 之前,程序是主动创建对象!控制权在程序员手上
  • 使用 set 注入后,程序不再具有主动性,而是变成了被动的接受对象

这种思想从本质上解决了问题,程序员不再需要去管理对象的创建了,系统的耦合性大大降低,可以更加专注在业务的实现上,这就是 IOC 的原型。

IOC 本质

IOC 是一种设计思想,DI(依赖注入)是实现 IoC 的一种方法。没有 IoC 的程序中,我们使用面向对象编程,对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转后将对象的创建转移给第三方,个人认为所谓控制反转就是:获得依赖对象的方式反转了。

Spring 容器在初始化时先读取配置文件,根据配置文件或元数据创建与组织对象存入容器中,程序使用时再从 IoC 容器中取出需要的对象。

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

HelloSpring

Hello.java

注意练习 alt+insert 的使用:执行自动插入

package com.locke.pojo;

public class Hello {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Hello{" +
                "name='" + name + '\'' +
                '}';
    }
}

配置元数据

Core Technologies (spring.io)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="..." class="...">  (1) (2)
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <bean id="..." class="...">
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions go here -->

</beans>
  • id:该属性是表示单个 bean 定义的字符串
  • class:该属性定义 bean 的类型并使用完全限定的类名
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--使用 Spring 来创建对象,在 Spring 中这些都称为 Bean
    类型 变量名 = new 类型();
    Hello hello = new Hello();

    id = 对象名(bean 的唯一标识符)
    class = new 的对象(bean 对象所对应的全限定名:包名 + 类型)
	name = 别名,可以同时取多个别名,比 alias 更高级

        property 相当于给对象中的属性设置一个值!
        ref: 引用 Spring 容器中创建好的对象
        bean = 对象  new Hello();
    -->
    <bean id="hello" class="com.locke.pojo.Hello">
        <property name="name" value="狂神说Java"/>

    </bean>
</beans>

实例化容器

提供给 ApplicationContext 构造函数的位置路径是资源字符串,这些资源字符串使容器可以从各种外部资源 (例如本地文件系统,Java CLASSPATH 等) 加载配置元数据。

public static void main(String[] args) {
        // 获取 Spring 的上下文对象
        // 解析 beans.xml 文件,生成管理相应的 Bean 对象
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        // 我们的对象现在都在 Spring 中的管理了,我们要使用,直接去里面取出来就可以
        // getBean: 参数即为 spring 配置文件中的 bean 的 id
        Hello hello = (Hello) context.getBean("hello");

        System.out.println(hello.toString());

    }

// 输出:Hello{name='Spring'}

思考问题:

  1. Hello 对象是谁创建的?
    1. hello 对象是 Spring 创建的
  2. Hello 对象的属性是怎么设置的?
    1. hello 对象的属性是由 Spring 容器设置的

这个过程就叫控制反转

  • 控制:谁来控制对象的创建,传统应用程序的对象是由程序本身控制创建的,使用 Spring 后,对象是由 Spring 来创建的
  • 反转:程序本身不创建对象,而变成被动的接收对象
  • 依赖注入:就是利用 set 方法来进行注入的
    • 依赖:bean 对象的创建依赖于容器
    • 注入:bean 对象中的所有属性,由容器来注入

到了现在,我们彻底不同在程序中去改动了,要实现不同的操作,只需要在 xml 配置文件中进行修改,对象由 Spring 来创建、管理、装配!

IOC 创建对象的方式

  1. 默认使用无参构造创建对象;

  2. 假设我们要使用有参构造创建对象:

    1. 下标赋值
    <!--第一种:下标赋值-->
    <bean id="user" class="com.locke.pojo.User">
        <constructor-arg index="0" value="狂神说 Java"/>
    </bean>
    
    1. 类型赋值
    <!--第二种:类型赋值,不建议使用-->
    <bean id="user" class="com.locke.pojo.User">
        <constructor-arg type="java.lang.String" value="狂神说 Java"/>
    </bean>
    
    1. 直接通过参数名赋值
    <!--第三种:直接通过参数名-->
    <bean id="user" class="com.locke.pojo.User">
        <constructor-arg name="name" value="狂神说 Java"/>
    </bean>
    

总结:在配置文件加载的时候,容器中管理的对象就已经初始化了

Spring 配置

别名

<bean id="user" class="com.locke.pojo.User">
    <constructor-arg name="name" value="狂神说 Java"/>
</bean>
<alias name="user" alias="user_alias"/>

MyTest.java 中:可以用别名来取出这个变量

public class MyTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        User user = (User) context.getBean("user_alias");
        user.show();
    }
}

Bean 的配置

参见 HelloSpring 下的配置元数据章节。

import

import 一般用于团队开发使用,他可以将多个配置文件,导入合并为一个。

假设现在项目中有多个人开发,三个人负责不同的类开发,不同的类需要注册在不同的 bean 中,我们可以利用 import 将所有人的 beans.xml 合并为一个总的。使用的时候,直接使用总的配置就可以了。

applicationContext.xml 中包含:

<import resource="beans.xml">
<import resource="beans2.xml">

依赖注入

注入方式

有三种方式:

  1. 构造器注入
  2. set 方式注入
    1. 依赖:bean 对象的创建依赖于容器
    2. 注入:bean 对象中的所有属性,由容器来注入
<bean id="moreComplexObject" class="example.ComplexObject">
    <!-- results in a setAdminEmails(java.util.Properties) call -->
    <property name="adminEmails">
        <props>
            <prop key="administrator">administrator@example.org</prop>
            <prop key="support">support@example.org</prop>
            <prop key="development">development@example.org</prop>
        </props>
    </property>
    <!-- results in a setSomeList(java.util.List) call -->
    <property name="someList">
        <list>
            <value>a list element followed by a reference</value>
            <ref bean="myDataSource" />
        </list>
    </property>
    <!-- results in a setSomeMap(java.util.Map) call -->
    <property name="someMap">
        <map>
            <entry key="an entry" value="just some string"/>
            <entry key="a ref" value-ref="myDataSource"/>
        </map>
    </property>
    <!-- results in a setSomeSet(java.util.Set) call -->
    <property name="someSet">
        <set>
            <value>just some string</value>
            <ref bean="myDataSource" />
        </set>
    </property>
</bean>
  1. 扩展方式
    1. p-namespace
    2. c-namespace

bean 的作用域(scope)

img

  1. 单例模式(默认)
<bean id="user" class="com.locke.pojo.User" c:age="18" c:name="狂神" scope="singleton"/>
  1. 原型模式:每次从容器中 get 的时候,都会产生一个新对象
<bean id="user" class="com.locke.pojo.User" c:age="18" c:name="狂神" scope="prototype"/>

Bean 的自动装配

自动装配是 Spring 满足 bean 依赖的一种方式,Spring 会在上下文中自动寻找,并自动给 bean 装配属性。

在 Spring 中有三种装配的方式:

  1. 在 xml 中显式的配置
  2. 在 java 中显式的配置
  3. 隐式的自动专配 bean【重要】

Spring 的自动装配需要从两个角度来实现,或者说是两个操作:

  1. 组件扫描 (component scanning):spring 会自动发现应用上下文中所创建的 bean;
  2. 自动装配 (autowiring):spring 自动满足 bean 之间的依赖,也就是我们说的 IoC/DI;

测试

  1. 环境搭建
    1. 一个人有两个宠物
<?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">

    <bean id="cat" class="com.locke.pojo.Cat"/>
    <bean id="dog" class="com.locke.pojo.Dog"/>

    <bean id="people" class="com.locke.pojo.People">
        <property name="name" value="小狂神呀"/>
        <property name="dog" ref="dog"/>
        <property name="cat" ref="cat"/>
    </bean>

</beans>

byName & byType 自动装配

<?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">

    <bean id="cat" class="com.locke.pojo.Cat"/>
    <bean id="dog" class="com.locke.pojo.Dog"/>

	<!--
    byName:会自动在容器上下文中查找,和自己对象 set 方法后面的值对应的 bean id
	byType:会自动在容器上下文中查找,和自己对象属性类型相同的 bean id
    -->
    <bean id="people" class="com.locke.pojo.People" autowire="byName">
        <property name="name" value="小狂神呀"/>

    </bean>

</beans>

小结:

  • byName:需要保证所有的 bean id 唯一,并且这个 bean 需要和自动注入的属性的 set 方法的值一致
  • byType:需要保证所有 bean 的 class 唯一,并且这个 bean 需要和自动注入的属性的类型一致

使用注解实现自动装配

要使用注解须知:

  1. 导入约束:<context:annotation-config/>
  2. 配置注解的支持:直接在属性上使用 @Autowired,甚至可以省略掉 set 方法
<?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/>

</beans>
public class People {
    @Autowired
    private Cat cat;
    @Autowired
    private Dog dog;
    private String name;

    public Cat getCat() {
        return cat;
    }
    ..
}

科普:

@Nullable 字段标记了这个注解,说明这个字段可以为 null;
public class People {
    // 如果显示定义了 Autowired 的 required 属性为 false,说明这个对象可以为 null,否则不允许为空
    @Autowired(required = false)
    private Cat cat;
    @Autowired
    private Dog dog;
    private String name;
}

如果 @Autowired 自动装配的环境比较复杂,自动装配无法通过一个注解完成的时候,我们可以使用 @Qualifier(value="xxx") 去配置 @Autowired 的使用,指定一个唯一的 bean 对象注入。

小结:@Resource@Autowired 的区别:

  • 都是用来自动装配的,都可以放在属性字段上
  • @Autowired 通过 byType 的方式实现,而且必须要求这个对象存在
  • @Resource 默认通过 byName 的方式实现,如果找不到名字,则通过 byType 实现,如果两个都找不到的情况下,就报错

使用注解开发

  • @Component:组件,放在类上,说明这个类被 Spring 管理了,就是 bean!
    • 衍生的注解,在 web 开发中,会按照 mvc 三层架构分层
      • dao:@Repository
      • service:@Service
      • controller:@Controller
      • 以上四个注解功能都是一样的,都是代表将某个类注册到 Spring 中,装配 bean
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       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:component-scan base-package="com.locke.pojo"/>
    <context:annotation-config/>

</beans>
import org.springframework.stereotype.Component;
// 等价于 <bean id="cat" class="com.locke.pojo.Cat">
@Component
public class Cat {
    // public String name="花花";

    // 等价于
    /*
    <bean id="cat" class="com.locke.pojo.Cat">
        <property name="name" value="花花"/>
    </bean>
    **/
    @Value("花花")
    public String name;
}
  • @Scope:作用域,在类上标注 @Scope(singleton)

xml 和 注解的最佳实践

  1. xml 用来管理 bean
  2. 注解只负责完成属性的注入
  3. 我们在使用的过程中,只需要注意一个问题:必须让注解生效,就需要开启注解的支持

使用 java 的方式配置 Spring

JavaConfig 是 Spring 的一个子项目。

User.java@Component

// 这个注解的含义:说明类被 Spring 接管了,注册到了容器中
@Component
public class User {
    private String name;

    public String getName() {
        return name;
    }
    @Value("花花")
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }
}

LockeConfig.java@Configuration 代表这是一个配置类,就和我们之前看的 beans.xml 一样的

@Configuration
@ComponentScan("com.locke.pojo")
public class LockeConfig {
    // 注册一个 bean,就相当于我们之前写的一个 bean 标签
    // 这个方法的名字,就相当于 bean 标签中的 id 属性:getUser()
    // 这个方法的返回值,就相当于 bean 标签中的 class 属性:User
    @Bean
    public User getUser() {
        return new User(); // 就是返回要注入到 bean 的对象
    }
}

MyTest.java

public class MyTest {
    public static void main(String[] args) {
        // 如果完全使用了配置类方法去做,我们就只能通过 AnnotationConfig 上下文来获取容器,通过配置类的 class 对象加载
        ApplicationContext context = new AnnotationConfigApplicationContext(LockeConfig.class);
        User getUser = (User) context.getBean("getUser");
        System.out.println(getUser.getName());
    }
}

这种纯 Java 的配置方式,在 SpringBoot 中随处可见!

代理模式

  1. 为什么要学习代理模式?

    1. 因为这就是 SpringAOP 的底层!
  2. 代理模式的分类:

    1. 静态代理
    2. 动态代理

静态代理

  1. 角色分析
    1. 抽象角色:一般会使用接口或者抽象类来解决
    2. 真实角色:被代理的角色
    3. 代理角色:代理真实角色,代理真实角色后,我们一般会做一些附属操作
    4. 客户:访问代理对象的人

Rent.java:接口

public interface Rent {
    public void rent();
}

Host.java:真实角色

public class Host implements Rent{
    @Override
    public void rent() {
        System.out.println("房东要出租房子!");
    }
}

Proxy.java:代理角色

public class Proxy implements Rent {
    // 尽量用组合而不是继承
    private Host host;

    // 无参构造
    public Proxy() {
    }

    // 有参构造
    public Proxy(Host host) {
        this.host = host;
    }
    // 传给它的是哪个客户,就调用哪个客户去租房
    @Override
    public void rent() {
        seeHouse();
        host.rent();
    }

    // 看房
    public void seeHouse(){
        System.out.println("中介带你看房");
    }
}

Client.java:客户端

public class Client {
    public static void main(String[] args) {
        // 房东要租房子
        Host host = new Host();
        // 中介帮房租租房子
        // 但是代理角色一般会有一些附属操作
        Proxy proxy = new Proxy(host);
        // 你不同面对房顶,直接找中介租房
        proxy.rent();
    }
}
  1. 代理模式的好处

    1. 可以使真实角色的操作更加纯粹,不用再去关注一些公共的业务;
    2. 公共业务也就交给了代理角色,实现了业务的分工;
    3. 公务业务发生扩展的时候,方便集中管理
  2. 缺点

    1. 一个真实角色就会产生一个代理角色;代码量会翻倍,开发效率会变低

动态代理

  1. 动态代理的代理类是动态生成的,不是我们直接写好的

  2. 动态代理分为两大类:

    1. 基于接口:JDK 动态代理
    2. 基于类:cglib
    3. java 字节码实现:javasist
  3. 动态代理的好处:

    1. 静态代理的好处都有
    2. 一个动态代理类代理的是一个接口,一般就是对应的一类业务
    3. 一个动态代理类可以代理多个类,只要是使用了同一个接口

Rent.java:接口

public interface Rent {
    public void rent();
}

Host.java

public class Host implements Rent {
    @Override
    public void rent() {
        System.out.println("房东要出租房子!");
    }
}

ProxyInvocationHandler.java

public class ProxyInvocationHandler implements InvocationHandler {
    // 被代理的接口
    private Rent rent;

    public void setRent(Rent rent) {
        this.rent = rent;
    }

    // 生成得到代理类
    public Object getProxy() {
        return Proxy.newProxyInstance(this.getClass().getClassLoader(),
                rent.getClass().getInterfaces(), this);
    }

    // 处理代理实例,并返回结果
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 动态代理的本质,就是使用反射机制实现!
        // proxy:调用该方法的代理实例
        // method:所述方法对应于调用代理实例上的接口方法的实例
        // args:包含的方法调用传递代理实例的参数值的对象的阵列
        seeHouse();
        Object result = method.invoke(rent, args);
        fare();
        return result;
    }

    public void seeHouse() {
        System.out.println("中介带看房子!");
    }

    public void fare() {
        System.out.println("收中介费!");
    }
}

Client.java

public class Client {
    public static void main(String[] args) {
        // 真实角色
        Host host = new Host();

        // 代理角色:现在没有
        ProxyInvocationHandler pih = new ProxyInvocationHandler();
        // 通过调用程序处理角色来处理我们要调用的接口对象
        pih.setRent(host);
        // 这里的 proxy 就是动态生成的,我们并没有写
        Rent proxy = (Rent) pih.getProxy();
        proxy.rent();
    }

}

动态代理再抽象:

UserService.java:接口

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

UserServiceImpl.java

public class UserServiceImpl implements UserService {
    public void add() {
        System.out.println("增加了一个用户");
    }

    public void delete() {
        System.out.println("删除了一个用户");
    }

    public void update() {
        System.out.println("修改了一个用户");
    }

    public void query() {
        System.out.println("查询了一个用户");
    }
}

ProxyInvocationHandler.java

需求:新增一个日志功能。使用代理来做,能够在不改变原来业务的情况下,实现此功能。

public class ProxyInvocationHandler implements InvocationHandler {
    // 被代理的接口

    private Object target;

    public void setTarget(Object target) {
        this.target = 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 {
        // 动态代理的本质,就是使用反射机制实现!
        log(method.getName());
        Object result = method.invoke(target, args);
        return result;
    }

    public void log(String msg) {
        System.out.println("执行了" + msg + "方法");
    }

}

Client

public class Client {
    public static void main(String[] args) {
        // 真实角色
        UserServiceImpl userService = new UserServiceImpl();
        // 代理角色,不存在
        ProxyInvocationHandler pih = new ProxyInvocationHandler();
        // 设置要代理的对象:注入
        pih.setTarget(userService);
        // 动态生成代理类
        UserService proxy = (UserService) pih.getProxy();
        proxy.delete();
    }
}

AOP

什么是 AOP

AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 OOP 的延续,是软件开发的一个热点,也是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

Aop 在 Spring 中的作用

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

  • 横切关注点:跨越应用程序多个模块的方法或功能。即,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志、安全、缓存、事务等
  • 切面(Aspect):横切关注点被模块化的特殊对象,即,它是一个类
  • 通知(Advice):切面必须要完成的工作,即,它是类中的一个方法
  • 目标(Target):被通知对象
  • 代理(Proxy):向目标对象应用通知之后创建的对象
  • 切入点(PointCut):切面通知执行的“地点”的定义
  • 连接点(JointPoint):与切入点匹配的执行点

使用 Spring 实现 Aop

需要导入一个依赖包:

<dependencies>
    <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.19</version>
        <scope>runtime</scope>
    </dependency>

</dependencies>

使用 Spring 的 API 接口

  1. 新建 service 包:

    1. UserService.java 接口
    2. UserServiceImpl.java 实现类
  2. 新建 log

    1. Log.java
    public class Log implements MethodBeforeAdvice {
        // method: 要执行的目标对象的方法
        // args: 参数
        // target: 目标对象
        @Override
        public void before(Method method, Object[] args, Object target) throws Throwable {
    
            assert target != null;
            System.out.println(target.getClass().getName() + "的" + method.getName() + "被执行了");
        }
    }
    
    1. AfterLog.java
    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);
        }
    }
    
  3. resources 包中新建文件:applicationContext.xml

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

    <!--注册 bean-->
    <bean id="userService" class="com.locke.service.UserServiceImpl"/>
    <bean id="log" class="com.locke.log.Log"/>
    <bean id="afterLog" class="com.locke.log.AfterLog"/>

    <!--方式一:使用原生 Spring API 接口-->
    <!--配置 aop:需要导入 aop 约束-->
    <aop:config>
        <!--切入点:expression:表达式,execution:要执行的位置-->
        <aop:pointcut id="pointcut" expression="execution(* com.locke.service.UserServiceImpl.*(..))"/>
        <!--执行环绕增加-->
        <aop:advisor advice-ref="log" pointcut-ref="pointcut"/>
        <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
    </aop:config>

</beans>
  1. test.java 文件夹新建测试文件
public class MyTest {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 注意:动态代理代理的是接口
        UserService userService = (UserService) context.getBean("userService");
        userService.add();
    }
}

自定义类来实现 AOP

  1. locke 文件夹下新建 diy 包:DiyPointCut
public class DiyPointCut {
    public void before() {
        System.out.println("=======方法执行前=======");
    }

    public void after() {
        System.out.println("=======方法执行后=======");
    }
}
  1. resources 包中新建文件:applicationContext.xml
<!--方式二:自定义类-->
<bean id="diy" class="com.locke.diy.DiyPointCut"/>
<aop:config>
    <!--自定义切面, ref 要引用的类-->
    <aop:aspect ref="diy">
        <!--切入点-->
        <aop:pointcut id="point" expression="execution(* com.locke.service.UserServiceImpl.*(..))"/>
        <!--通知-->
        <aop:before method="before" pointcut-ref="point"/>
        <aop:after method="after" pointcut-ref="point"/>
    </aop:aspect>
</aop:config>

重点是切面定义。

使用注解实现

  1. diy 包中新建 AnnotationPointCut.java 文件
// 方式三:使用注解方式实现 AOP
// 标注这个类是一个切面
@Aspect
public class AnnotationPointCut {
    @Before("execution(* com.locke.service.UserServiceImpl.*(..))")
    public void before() {
        System.out.println("=======方法执行前=======");
    }

    @After("execution(* com.locke.service.UserServiceImpl.*(..))")
    public void after() {
        System.out.println("=======方法执行后=======");
    }

    // 在环绕增强中,我们可以给定一个参数,代表我们要获取处理切入的点
    @Around("execution(* com.locke.service.UserServiceImpl.*(..))")
    public void around(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("环绕前");
        // 获得签名
        Signature signature = jp.getSignature();
        // System.out.println("signature:" + signature);

        // 执行方法
        Object proceed = jp.proceed();
        System.out.println("环绕后");
    }
}
  1. resources 包中新建文件:applicationContext.xml
<!--方式三:使用注解实现-->
<bean id="annotationPointCut" class="com.locke.diy.AnnotationPointCut"/>
<!--开启注解支持-->
<aop:aspectj-autoproxy/>
  1. 输出:
环绕前
=======方法执行前=======

增加了一个用户

环绕后
=======方法执行后=======

整合 Mybatis

步骤:

  1. 导入相关 jar 包
    1. junit
    2. mybatis
    3. mysql 数据库
    4. spring 相关
    5. aop 置入
    6. mybatis-spring [new]
<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.30</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.1.9.RELEASE</version>
    </dependency>
    <!--Spring 操作数据库的话,还需要一个 spring-jdbc-->
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.1.9.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.19</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>3.0.1</version>
    </dependency>

</dependencies>
  1. 编写配置文件

  2. 测试

回忆 mybatis

  1. 编写实体类:com.locke.pojo User
public class User {
    private int id;  //id
    private String name;   //姓名
    private String pwd;   //密码

    //非常重要:构造,有参,无参
    //set/get
    //toString()

}
  1. 编写核心配置文件:resources
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <typeAliases>
        <package name="com.locke.pojo"/>
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=utf8"/>
                <property name="username" value="root"/>
                <property name="password" value="130914"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <package name="com.locke.mapper"/>
    </mappers>
</configuration>
  1. 编写接口:com.locke.mapper UserMapper
public interface UserMapper {
    List<User> selectUser();
}
  1. 编写 UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.locke.mapper.UserMapper">
    <select id="selectUser" resultType="com.locke.pojo.User">
        select * from user
    </select>
</mapper>
  1. 测试
public class MyTest {
    @Test
    public void selectUser() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        List<User> userList = mapper.selectUser();
        for (User user: userList){
            System.out.println(user);
        }
        sqlSession.close();
    }
}

Mybatis-Spring

Mybatis-Spring 可以帮助你将 MyBatis 代码无缝地整合到 Spring 中,它允许 MyBatis 参与到 Spring 的事务管理之中,创建映射器 mappersqlSession 并注入到 bean 中,以及将 Mybatis 的异常转换为 Spring 的 DataAccessException。最终,可以做到应用代码不依赖于 MyBatis,Spring 或 MyBatis-Spring。

如果使用 Maven 作为构建工具,仅需要在 pom.xml 中加入以下代码即可:

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.0.2</version>
</dependency>

要和 Spring 一起使用 MyBatis,需要在 Spring 应用上下文中定义至少两样东西:一个 SqlSessionFactory 和至少一个数据映射器类

在 MyBatis-Spring 中,可使用 SqlSessionFactoryBean 来创建 SqlSessionFactory。 要配置这个工厂 bean,只需要把下面代码放在 Spring 的 XML 配置文件中:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
</bean>

注意:SqlSessionFactory 需要一个 DataSource(数据源)。 这可以是任意的 DataSource,只需要和配置其它 Spring 数据库连接一样配置它就可以了。

在基础的 MyBatis 用法中,是通过 SqlSessionFactoryBuilder 来创建 SqlSessionFactory 的。 而在 MyBatis-Spring 中,则使用 SqlSessionFactoryBean 来创建。

在 MyBatis 中,你可以使用 SqlSessionFactory 来创建 SqlSession。一旦你获得一个 session 之后,你可以使用它来执行映射了的语句,提交或回滚连接,最后,当不再需要它的时候,你可以关闭 session。

SqlSessionFactory 有一个唯一的必要属性:用于 JDBC 的 DataSource。这可以是任意的 DataSource 对象,它的配置方法和其它 Spring 数据库连接是一样的。

一个常用的属性是 configLocation,它用来指定 MyBatis 的 XML 配置文件路径。它在需要修改 MyBatis 的基础配置非常有用。通常,基础配置指的是 <settings><typeAliases> 元素。

需要注意的是,这个配置文件并不需要是一个完整的 MyBatis 配置。确切地说,任何环境配置(<environments>),数据源(<DataSource>)和 MyBatis 的事务管理器(<transactionManager>)都会被忽略SqlSessionFactoryBean 会创建它自有的 MyBatis 环境配置(Environment),并按要求设置自定义环境的值。

SqlSessionTemplate 是 MyBatis-Spring 的核心。作为 SqlSession 的一个实现,这意味着可以使用它无缝代替你代码中已经在使用的 SqlSession

模板可以参与到 Spring 的事务管理中,并且由于其是线程安全的,可以供多个映射器类使用,你应该总是SqlSessionTemplate 来替换 MyBatis 默认的 DefaultSqlSession 实现。在同一应用程序中的不同类之间混杂使用可能会引起数据一致性的问题。

可以使用 SqlSessionFactory 作为构造方法的参数来创建 SqlSessionTemplate 对象。

<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
  <constructor-arg index="0" ref="sqlSessionFactory" />
</bean>

现在,这个 bean 就可以直接注入到你的 Mapper bean 中了。你需要在你的 bean 中添加一个 SqlSession 属性,就像下面这样:

public class UserMapperImpl implements UserMapper {
  private SqlSession sqlSession;
  public void setSqlSession(SqlSession sqlSession) {
    this.sqlSession = sqlSession;
  }
  public User getUser(String userId) {
    return sqlSession.getMapper...;
  }
}

按下面这样,注入 SqlSessionTemplate

<bean id="userMapper" class="com.locke.mapper.UserMapperImpl">
    <property name="sqlSession" ref="sqlSession"/>
</bean>

整合实现一:

配置:resource/mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--别名-->
    <typeAliases>
        <package name="com.locke.pojo"/>
    </typeAliases>


    <!--设置-->
    <!--<settings>
        <setting name="" value=""/>
    </settings>-->

    <mappers>
        <package name="com.locke.mapper.UserMapper"/>
    </mappers>
</configuration>

配置 resource/spring-dao.xml

  1. 引入 Spring 配置文件:
<?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">
  1. 配置数据源替换 mybatis 的数据源:
<!--配置数据源:数据源有非常多,可以使用第三方的,也可使用 Spring-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=utf8"/>
    <property name="username" value="root"/>
    <property name="password" value="130914"/>
</bean>
  1. 配置 SqlSessionFactory,关联 MyBatis:
<!--配置 SqlSessionFactory-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <!--关联 Mybatis-->
    <property name="configLocation" value="classpath:mybatis-config.xml"/>
    <property name="mapperLocations" value="classpath:com/locke/mapper/*.xml"/>
</bean>
  1. 注册 sqlSessionTemplate,关联 sqlSessionFactory
<!--注册 sqlSessionTemplate , 关联 sqlSessionFactory-->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
    <!--利用构造器注入-->
    <constructor-arg index="0" ref="sqlSessionFactory"/>
</bean>
  1. 注册 bean 实现
<bean id="userMapper" class="com.locke.mapper.UserMapperImpl">
        <property name="sqlSession" ref="sqlSession"/>
</bean>

增加 Mapper 接口的实现类,私有化 sqlSessionTemplate

public class UserMapperImpl implements UserMapper {
    // sqlSession 不用我们自己创建了,Spring来管理
    private SqlSessionTemplate sqlSession;
    public void setSqlSession(SqlSessionTemplate sqlSession) {
        this.sqlSession = sqlSession;
    }
    public List<User> selectUser() {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        return mapper.selectUser();
    }
}

测试

public class MyTest {
    @Test
    public void test2(){
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        UserMapper mapper = (UserMapper) context.getBean("userMapper");
        List<User> user = mapper.selectUser();
        System.out.println(user);
    }
}

成功输出:

User{id=1, name='狂神', pwd='123456'}
User{id=2, name='张三', pwd='abcdef'}
User{id=3, name='李四', pwd='987654'}

整合实现二:

dao 继承 Support 类,直接利用 getSqlSession () 获得,然后直接注入 SqlSessionFactory。 比起方式 1 , 不需要管理 SqlSessionTemplate , 而且对事务的支持更加友好。

  1. 修改 UserMapperImpl
public class UserMapperImpl extends SqlSessionDaoSupport implements UserMapper {
    public List<User> selectUser() {
        UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
        return mapper.selectUser();
    }
}
  1. 修改 bean 的配置
<bean id="userMapper" class="com.locke.mapper.UserMapperImpl">
    <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
  1. 测试
@Test
public void test2(){
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    UserMapper mapper = (UserMapper) context.getBean("userMapper");
    List<User> user = mapper.selectUser();
    System.out.println(user);
}

声明式事务

回顾事务

  • 事务就是把一系列的动作当成一个独立的工作单元,这些动作要么全部完成,要么全部不起作用。
  • 事务在项目开发过程中非常重要,涉及到数据的一致性的问题,不容马虎!
  • 事务管理就是企业级应用程序开发中必备技术,用来确保数据的完整性和一致性。

事务的四个属性 ACID

  • 原子性 (atomicity):事务时原子性操作,由一系列动作组成,事务的原子性确保动作要么全部完成,要么完全不起作用
  • 一致性 (consistency):一旦所有事务动作完成,事务就要被提交,数据和资源处于一种满足业务规则的一致性状态中
  • 隔离性 (isolation): 可能多个事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏
  • 持久性 (durability): 事务一旦完成,无论系统发生什么错误,结果都不会受到影响。通常情况下,事务的结果被写到持久化存储器中

测试

将上面的代码拷贝到一个新项目中。

在之前的案例中,我们给 userMapper 接口新增两个方法:

// 添加一个用户
int addUser(User user);
// 根据 id 删除用户
int deleteUser(int id);

mapper 文件,我们故意把 deletes 写错,测试:

<insert id="addUser" parameterType="com.locke.pojo.User">
    insert into user (id,name,pwd) values (#{id},#{name},#{pwd})
</insert>
<delete id="deleteUser" parameterType="int">
    <!--故意写错 sql 语句: 将 delete 写成 deletes-->
    deletes from user where id = #{id}
</delete>

编写接口的实现类,在实现类中,我们去操作:

public class UserMapperImpl extends SqlSessionDaoSupport implements UserMapper {
    // 增加一些操作
    public List<User> selectUser() {
        User user = new User(4,"小明","123456");
        UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
        mapper.addUser(user);
        mapper.deleteUser(4);
        return mapper.selectUser();
    }
    // 新增
    public int addUser(User user) {
        UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
        return mapper.addUser(user);
    }
    // 删除
    public int deleteUser(int id) {
        UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
        return mapper.deleteUser(id);
    }
}

测试:

public class MyTest {
    @Test
    public void test2(){
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserMapper mapper = (UserMapper) context.getBean("userMapper");
        for (User user : mapper.selectUser()) {
            System.out.println(user);
        }
}
}

报错:sql 异常,delete 写错了。没有删除,但是插入成功了。这肯定不符合我们的要求。

没有进行事务的管理;我们想让他们都成功才成功,有一个失败,就都失败,我们就应该需要事务!

以前我们都需要自己手动管理事务,十分麻烦!但是 Spring 给我们提供了事务管理,我们只需要配置即可。

Spring 中的事务管理

Spring 在不同的事务管理 API 之上定义了一个抽象层,使得开发人员不必了解底层的事务管理 API 就可以使用 Spring 的事务管理机制。Spring 支持编程式事务管理和声明式的事务管理。

编程式事务管理

  • 将事务管理代码嵌到业务方法中来控制事务的提交和回滚
  • 缺点:必须在每个事务操作业务逻辑中包含额外的事务管理代码

声明式事务管理

  • 一般情况下比编程式事务好用
  • 将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理
  • 将事务管理作为横切关注点,通过 aop 方法模块化。Spring 中通过 Spring AOP 框架支持声明式事务管理

使用 Spring 管理事务,注意头文件的约束导入

xmlns:tx="http://www.springframework.org/schema/tx"
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

事务管理器

  • 无论使用 Spring 的哪种事务管理策略(编程式或者声明式)事务管理器都是必须的
  • 就是 Spring 的核心事务管理抽象,管理封装了一组独立于技术的方法

具体操作

  1. JBDC 事务
<!--配置声明式事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>
  1. 配置事务的通知
<!--配置事务通知-->
<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="search*" propagation="REQUIRED"/>
        <tx:method name="get" read-only="true"/>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

spring 事务传播特性

事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。spring 支持 7 种事务传播行为:

  • propagation_requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。
  • propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。
  • propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。
  • propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。
  • propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。
  • propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 propagation_required 类似的操作

Spring 默认的事务传播行为是 PROPAGATION_REQUIRED,它适合于绝大多数的情况。

假设 ServiveX#methodX () 都工作在事务环境下(即都被 Spring 事务增强了),假设程序中存在如下的调用链:Service1#method1 ()->Service2#method2 ()->Service3#method3 (),那么这 3 个服务类的 3 个方法通过 Spring 的事务传播机制都工作在同一个事务中。

就好比,我们刚才的几个方法存在调用,所以会被放在一组事务当中!

配置 AOP

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

进行测试

public class MyTest {
    @Test
    public void test2(){
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserMapper mapper = (UserMapper) context.getBean("userMapper");
        for (User user : mapper.selectUser()) {
            System.out.println(user);
        }
}
}

完全没有改动项目代码,使用 AOP 实现横切,加入了新的功能。

为什么需要配置事务

  • 如果不配置,就需要我们手动提交控制事务;
  • 事务在项目开发过程中非常重要,涉及到数据一致性和完整性的问题,不容马虎
posted @ 2024-02-10 16:25  Lockegogo  阅读(102)  评论(0编辑  收藏  举报