Spring 5 基础

一、Spring 概述

1.1 简介

  • Spring:春天 --> 给软件行业带来了春天;

  • 2002,首次推出了 Spring 框架的雏形 interface21 框架;

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

  • Rod Johnson:Spring Framework 创始人;

  • Spring 理念:使现有的技术更加容易使用,本身是一个大杂烩,整合了现有的技术框架。

  • SSH:Struct2 + Spring + Hibernate

  • SSM:SpringMVC + Spring + Mybatis

  • 官网:链接

  • 下载地址:链接

  • Github:链接

  • Maven 依赖:

    • 导入 webmvc 会自动导入相关依赖;
    • jdbc 用于和 Mybatis 整合;

    org.springframework
    spring-webmvc
    5.3.17



    org.springframework
    spring-jdbc
    5.3.17

1.2 优点

  • Spring 是一个开源的免费的框架(容器);
  • Spring 是一个轻量级的、非入侵式的框架;
  • 控制反转(IOC)、面向切面编程(AOP);
  • 支持事务的处理,对框架整合的支持;
  • ==总结一句话:Spring 就是,轻量级的控制反转(IOC)和面向切面(AOP)编程的框架==

1.3 组成

  • Spring 框架是一个分层架构,由 7 个定义良好的模块组成;

  • Spring 模块,构建在核心容器之上,核心容器定义了创建、配置和管理 bean 的方式;

  • 组成 Spring 框架的每个模块(或组件)都可以单独存在,或者与其他一个或多个模块联合实现;

模块简介:

  • 核心容器
    • 提供 Spring 框架的基本功能;
    • 主要组件是 BeanFactory,它是工厂模式的实现,使用控制反转(IOC)模式,将应用程序的配置,和依赖性规范与实际的应用程序代码分开;
  • Spring 上下文
    • 是一个配置文件,向 Spring 框架提供上下文信息,包括企业服务,例如:JNDIEJB、电子邮件、国际化、校验和调度功能;
  • Spring AOP
    • 面向切面的编程功能,可管理任何支持 AOP 的对象;
    • 基于 Spring 的应用程序中的对象,提供了事务管理服务;
    • 不用依赖组件,就可以将声明性事务管理,集成到应用程序中;
  • Spring DAO
    • JDBC DAO 抽象层,提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息;
    • 异常层次结构,简化了错误处理,降低了需要编写的异常代码数量(例如打开和关闭连接);
    • Spring DAO 的面向 JDBC 的异常,遵从通用的 DAO 异常层次结构;
  • Spring ORM
    • Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括 JDOHibernateiBatis SQL Map
    • 遵从 Spring 的通用事务和 DAO 异常层次结构;
  • Spring Web 模块
    • 建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文;
    • Spring 框架支持与 Jakarta Struts 的集成;
    • 简化了处理多部分请求,以及将请求参数绑定到域对象的工作;
  • Spring MVC 框架
    • 全功能的构建 Web 应用程序的 MVC 实现;
    • 通过策略接口,MVC 框架变成为高度可配置的;
    • MVC 容纳了大量视图技术,其中包括 JSPVelocityTilesiTextPOI

1.4 拓展

  • Spring Boot:

    • 快速开发的脚手架;
    • 基于 Spring Boot,可以快速的开发单个微服务;
    • 约定大于配置;
  • Spring Cloud:

    • SpringCloud 是基于 SpringBoot 实现的;
  • 学习 SpringBoot 的前提,需要完全掌握 Spring 以及SpringMVC,承上启下的作用;

  • 弊端:发展了太久之后,违背了原来的理念,配置十分繁琐;

二、IOC 基础

  • 新建空白的 maven 项目;

2.1 IOC 理论推导

以前代码的实现方式:

  1. 创建 UserDao 接口:
public interface UserDao {
    public void getUser();
}
  1. 创建 UserDaoImpl 实现类 :
public class UserDaoImpl implements UserDao{
    @Override
    public void getUser() {
        System.out.println("获取用户数据");
    }
}
  1. 创建 UserService 业务接口:
public interface UserService {
    public void getUser();
}
  1. 创建 UserServiceImpl 业务实现类:
public class UserServiceImpl implements UserService {
    private UserDao userDao = new UserDaoImpl();
    
    @Override
    public void getUser() {
        userDao.getUser();
    }    
}    
  1. 测试:
public class MyTest {
    @Test
    public void testUser() {
        UserService service = new UserServiceImpl();
        service.getUser();
    }
}

以前增加需求的实现方式:

  1. 增加 Userdao 的实现类:
public class UserDaoOracleImpl implements UserDao{
    @Override
    public void getUser() {
        System.out.println("Oracle数据");
    }
}
  1. 在 UserServiceImpl 实现类里,修改对应的实现:
public class UserServiceImpl implements UserService {
    // 修改UserDao的对应实现类
    private UserDao userDao = new UserDaoOracleImpl();
    
    @Override
    public void getUser() {
        userDao.getUser();
    }    
} 
  • 发现问题:用户的需求变化,会影响内部的实现代码,需要根据用户的需求去修改源代码;

解决方案:

  • 使用 Set 接口,实现不同的需求:
public class UserServiceImpl implements UserService { 
    private UserDao userDao;

    // 利用set进行动态实现值的注入
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public void getUser() {
        userDao.getUser();
    }
}
  • 测试:
public class MyTest {
    @Test
    public void testUser() {
        UserServiceImpl service = new UserServiceImpl();
        service.setUserDao(new UserDaoImpl());
        service.getUser();
        // 用Oracle去实现
        service.setUserDao(new UserDaoOracleImpl());
        service.getUser();
    }
}

小结:

  • 实现方式对比:

  • 之前,程序是主动创建对象,控制权在程序员;

  • 使用了 set 注入后,程序不再具有主动性,而是变成了被动的接收对象;

  • 这种思想,从本质上解决了问题,程序员不用再去管理对象的创建,只专注于业务的实现,使系统的耦合性大大降低,这就是 IOC 的原型;

2.2 IOC 本质

  • 控制反转 IOC(Inversion of Control),是一种设计思想,DI(依赖注入)是实现 IOC 的一种方法(也有人认为 DI 只是 IOC 的另一种说法);

  • 没有 IOC 的程序中,使用面向对象编程,对象的创建与对象间的依赖关系,完全硬编码在程序中,对象的创建由程序自己控制,控制反转后,将对象的创建转移给第三方;

  • 所谓控制反转,就是获得依赖对象的方式反转了;

  • IOC 是 Spring 框架的核心内容,实现方式:

    • 使用 XML 配置;
    • 使用注解;
    • 新版本的 Spring 可以零配置实现 IOC;
  • Spring 容器在初始化时,先读取配置文件,根据配置文件或元数据创建与组织对象存入容器中,程序使用时,再从 IOC 容器中取出需要的对象;

  • 采用 XML 方式配置 Bean 时,Bean 的定义信息是和实现分离的,而采用注解的方式,可以把两者合为一体,Bean 的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的;

  • 控制反转,是一种通过描述(XML 或注解)并通过第三方,去生产或获取特定对象的方式;

  • 在 Spring 中,实现控制反转的是 IOC 容器,其实现方法是,依赖注入(Dependency Injection,DI);

三、Hello Spring

3.1 搭建环境

  • 导入相关依赖,spring 需要导入commons-logging 进行日志记录,maven 会自动下载对应的依赖项;

    org.springframework
    spring-webmvc
    5.3.17

3.2 编码代码

  • 创建实体类:Hello.java
public class Hello {    
    private String str;

    public String getStr() {
        return str;
    }

    public void setStr(String str) {
        this.str = str;
    }

    public void show() {
        System.out.println("Hello " + str);
    }
}
  • 创建 spring 配置文件,beans.xml(文件名可自定义):



    
    
    
        
        
    

  • 测试:
public class MyTest {
    @Test
    public void helloTest() {
        // 获取beans.xml:拿到Spring管理对象的容器
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        // genBean:参数就是Spring配置文件中bean的id(对象名)
        Hello hello = (Hello) context.getBean("hello");
        hello.show();
    }
}

3.3 思考

  • Hello 对象是谁创建?
    • 由 Spring 创建;
  • Hello 对象的属性是怎么设置的?
    • 由 Spring 容器设置的;

这个过程就叫做 控制反转

  • 控制:控制对象的创建;

    • 传统应用程序,对象是由程序本身控制创建
    • 使用 Spring 后,对象是由 Spring 来创建
  • 反转:程序本身不创建对象,而变成被动的接收对象;

  • 依赖注入:利用 set 方法来进行注入;

  • IOC 是一种编程思想,由主动的编程,变成被动的接收;

  • 可以通过 new ClassPathXmlApplicationContext 查看底层源码;

3.4 修改之前代码

  • IDEA 快捷创建 beans.xml 文件,自动导入 spring 配置信息:

  • 配置上下文:按提示操作

  • bean 对象添加:




    
    

  • 测试:
@Test
public void testSpring() {
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    UserService service = (UserService) context.getBean("service");
    service.getUser();
}

小结:

  • 要实现不同的操作,不用在程序中去改动,只需要在 xml 配置文件中进行修改;
  • 所谓的 IOC,就是对象由 Spring 来创建、管理、装配;

四、IOC 创建对象方式

4.1 通过无参构造(默认)

  • 创建实体类:无参构造
public class User {
    private String name;

    public User() {
        System.out.println("无参构造");
    }

    public String getName() {
        return name;
    }

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

    public void show() {
        System.out.println("name=" + name);
    }
}
  • 创建配置文件:beans.xml

    

  • 测试:
@Test
public void testUser() {
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    // 在执行getBean的时候,user已经通过无参构造创建好了
    User user = (User) context.getBean("user");
    // 调用对象的方法
    user.show();
}
  • 运行结果:

  • 在调用 show 方法之前,User 对象,已经通过无参构造初始化了;

4.2 通过有参构造

  • 创建实体类:有参构造
public class User2 {
    private String name;

    public User2(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public void show() {
        System.out.println("name=" + name);
    }
}

有参构造 beans.xml 的三种创建方式

  1. 下标赋值:


    

  1. 类型赋值:不建议使用,重复类型难以分辨


    

  1. 参数名赋值:

    
    

  • 测试:
@Test
public void testUser2() {
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    User2 user2 = (User2) context.getBean("user2");
    user2.show();
}

小结:

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

五、Spring 配置

5.1 别名

  • alias:设置别名;


5.2 Bean 的配置

  • bean 就是 java 对象,由 Spring 创建和管理:


    

5.2 import

  • 一般用于团队开发,可以将多个配置文件,导入合并为一个;
  • applicationContext.xml(总配置文件):
    • 使用时,直接使用总的配置就可以;


    
    
    
    

六、依赖注入(DI)

6.1 构造器注入

  • 查看上文;

6.2 set 注入(重点)

  • 依赖注入(Dependency Injection,DI):set 方法注入
    • 依赖:指 Bean 对象的创建,依赖于容器;
    • 注入:Bean 对象中的所有属性,由容器来注入;

搭建环境

  • 要求被注入的属性,必须有 set 方法:

    • 方法名由 set + 属性首字母大写;
    • 属性是 boolean 类型,没有set方法,是 is;
  • 创建实体类:复杂类型(引用类)

public class Address {
    private String address;
    // get、set、toString
}
  • 真实测试对象:
public class Student {
    private String name;
    private Address address;
    private String[] books;
    private List hobbys;
    private Map card;
    private Set games;
    private String wife;
    private Properties info;
    // get、set、toString
}
  • 配置文件:applicationContext.xml



    
        
        
    

  • 测试:
@Test
public void studentTest() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    Student student = (Student) context.getBean("student");
    System.out.println(student.getName());
}
  • 完善注入信息:



    
    
    
    
    
    
        
            西游记
            红楼梦
            水浒传
        
    
    
    
        
            爬山
            阅读
            听歌
        
    
    
    
        
            
            
        
    
    
    
        
            LOL
            BOB
            COC
        
    
    
    
        
    
    
    
        
            com.mysql.cj.jdbc.Driver
            jdbc:mysql://localhost:3306/数据库名?
            root
            123456
        
    

  • 测试:
@Test
public void studentTest() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    Student student = (Student) context.getBean("student");
    System.out.println(student.toString());

    /*
        Student{
            name='学生1',
            address=Address{address='null'},
            books=[西游记, 红楼梦, 水浒传],
            hobbys=[爬山, 阅读, 听歌],
            card={建行=217842215439, 工行=54358942439},
            games=[LOL, BOB, COC],
            wife='null',
            info={
                password=123456,
                driver=com.mysql.cj.jdbc.Driver,
                url=jdbc:mysql://localhost:3306/数据库名?,
                username=root
                }
          }
     */
    }
}

6.3 拓展方式注入

  • 使用 cp 命名空间,进行注入;

  • 官方解释:

  • 创建实例类:

public class User {
    private String name;
    private int age;
    // // get、set、toString
}
  • 创建配置文件:beans.xml



    
    
    
    

  • 测试:
@Test
public void testUser() {
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    User user = (User) context.getBean("user");
    User user2 = (User) context.getBean("user2");
    System.out.println(user);
    System.out.println(user2);
}
  • 注意点:
    • p 和 c 命名空间,不能直接使用,需要导入 xml 头文件约束;
    • c 命名空间,通过构造器注入,必须定义无参构造(否则报错);

xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"

6.4 Bean 的作用域

  • 六种模式:

单例模式(Spring 默认机制):singleton

  • get 到的都是同一个对象:

  • 配置:


  • 测试:
@Test
public void testUser2() {
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    User user = (User) context.getBean("user");
    User user2 = (User) context.getBean("user");
    System.out.println(user==user2);    // true
}

原型模式:prototype

  • 每次从容器中 get 时,都产生一个新的对象:

  • 配置:


  • 测试:
@Test
public void testUser2() {
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    User user = (User) context.getBean("user");
    User user2 = (User) context.getBean("user");
    System.out.println(user==user2);    // false
}
  • requestsessionapplication 只能在 web 开发中使用;

七、Bean 的自动装配

  • 自动装配是 Spring 满足 bean 依赖的一种方式;
  • Spring 会在上下文中自动寻找,并自动给 bean 装配属性;
  • Spring 中的三种装配方式:
    • 在 xml 中显式配置;
    • 在 Java 中显式配置;
    • 隐式的自动装配 bean(重点);

7.1 搭建测试环境

创建项目:

  • 一个人有两个宠物;

  • 实体类:Cat

public class Cat {
    public void shout() {
        System.out.println("miao~");
    }
}
  • 实体类:Dog
public class Dog {
    public void shout() {
        System.out.println("wang~");
    }
}
  • 实体类:People
public class People {
    private Cat cat;
    private Dog dog;
    private String name;
    // get、set、toString
}
  • 创建 Spring 配置文件:



    
    
    
        
        
        
    

  • 测试:
@Test
public void myTest() {
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    People people = (People) context.getBean("people");
    people.getCat().shout();
    people.getDog().shout();
}
  • 查看运行结果:

7.2 ByName 自动装配

  • 修改 bean 配置,增加属性 autowire="byName"


    

  • 当 bean 节点有 autowire="byName" 属性时:
    • 查找此类中,所有的 set 方法名(去掉 set 后,首字母小写 );
    • 去 spring 容器中,寻找对应 set 方法名的 id 对象;
    • 如果有,就取出注入,如果没有,就报空指针异常;

7.3 ByType 自动装配

  • 修改 bean 配置,autowire="byType"
    • 被引用的 bean 不再需要 id;
    • 类型:必须保证全局唯一,否则报错;




    

小结:

  • byName
    • 必须保证所有 bean 的 id 唯一;
    • bean 需要和自动注入属性的 set 方法值一致;
  • byType
    • 必须保证所有 bean 的 class 唯一;
    • bean 需要和自动注入属性的类型一致;

7.4 使用注解实现自动装配

  • jdk1.5 支持注解,Spring2.5 开始支持注解;

  • 使用注解实现自动装配,需重新配置 xml 头部信息:

    • 导入 context 约束;
    • 开启属性注解支持:



    
    


@Autowired

  • @Autowired 是按类型自动转配的,不支持 id 匹配;
  • 需要导入 spring-aop 的包,或依赖;
  • 使用方式:
    • 直接在属性上使用即可;
    • 也可以在 set 方式上使用;
    • 注:使用 @Autowired 可以不用写 Set 方法,前提:
      • 自动装配的属性在 IOC(Spring)容器中存在;
      • 且符合名字 byName;
  • 修改:在类中去掉 Set 方法,使用 @Autowired 注解;
public class People {
    @Autowired
    private Cat cat;
    @Autowired
    private Dog dog;
    private String name;
    // get、toString
}    
  • 修改配置文件:






拓展

  • @Nullable:字段可以为 null

    public People(@Nullable String name) {
        this.name = name;
    }
    
  • @Autowired(required=false)

    • 默认:true(必须存在对象,不能为 null)
    // Autowired 源码
    public @interface Autowired {
        boolean required() default true;
    }
    
    • false:对象可以为 null;
    // 如果允许对象为 null,设置required = false,默认为true
    @Autowired(required = false)
    private Cat cat;
    

@Qualifier

  • @Autowired 是根据类型自动装配,加上@Qualifier 就可以根据 byName 的方式自动装配;

  • @Qualifier 不能单独使用

  • 测试:

    • 修改配置文件内容,类型不变,名字不为类的默认名字:



  • 在属性上添加 @Qualifier 注解:
@Autowired
@Qualifier(value = "cat1")
private Cat cat;
@Autowired
@Qualifier(value = "dog1")
private Dog dog;

小结:

  • 没有加 @Qualifier 测试,会直接报错;
  • 如果 @Autowired 自动装配的环境比较复杂,自动装配无法通过 @Autowired 一个注解完成时,可以使用 @Qualifier(value=“xxx”) 去配置 @Autowired 的使用,指定一个唯一的 bean 对象注入;

@Resource:Java 注解

  • JDK11 以上,包被移除了,需要导入包或依赖:

    
        javax.annotation
        javax.annotation-api
        1.3.2
    
    
  • 修改实体类注解:

    @Resource(name = "cat11")
    private Cat cat;  
    @Resource
    private Dog dog;
    private String name;
    
  • 修改配置文件:

    
    
    
    

小结:

  • @Resource 和 @Autowired 的区别:

    • 都是用来 自动装配 的,都可以放在属性字段上;

    • @Autowired 通过 byType 的方式实现,而且必须要求这个对象存在;【常用】

    • @Resource 默认通过 byName 的方式实现,如果找不到名字,则通过 byType 实现,如果两个都找不到的情况下,就报错;【常用】

    • 执行顺序不同@Autowired 通过 byType 的方式实现,@Resource 默认通过 byName 的方式实现;

  • 注意:byType 的类型,必须全局唯一;

八、使用注解开发

环境配置:

  • spring 4 之后,想要使用注解,必须要引入 aop 的包;

  • 配置文件中,要引入 context 约束,增加注解的支持:



       

8.1 Bean 的实现

  • 之前是使用 bean 的标签,进行 bean 注入,但实际开发中,一般会使用注解;
  • 配置扫描哪些包下的注解:


  • 在 dao 包下创建类,增加注解:
/*
    @Component:组件
    相当于配置文件中 
    可以指定对象名:@Component("对象名"),默认:类名首字母小写
 */
@Component
public class User {
    public String name;
}

8.2 属性注入

  • 不需要 set 方法,属性名上直接添加:@value("值")
@Component
public class User {
    // 相当于配置文件中 
    @Value("测试")
    public String name;
}
  • 如果提供了 set 方法,在 set 方法上添加 @value("值")
@Value("测试")
public void setName(String name) {
    this.name = name;
}
  • 测试:
@Test
public void testUser() {
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    User user = (User) context.getBean("user");
    System.out.println(user.name);
}

8.3 衍生注解

  • @Component 的衍生注解:web 开发中,按 mvc 三层架构分层;

    • @Repository:dao 层;
    • @Service:service 层;
    • @Controller:web 层;
  • 四个注解功一样:将类注册到 Spring 中,装配 Bean;

8.4 自动装配注解

  • @Autowired
    • 自动装配,通过类型,名字;
    • 如果 @Autowired 不能唯一自动装配上属性,则需要通过 @Qualifier(value="xxx")
  • @Nullable:字段标记了这个注解,说明这个字段可以为 null;
  • @Resource(Java 注解):自动装配,通过名字,类型;

8.5 作用域

  • @Scope:
    • singleton(默认):单例模式,创建这个对象,关闭工厂,所有的对象都会销毁;
    • prototype:原型模式,关闭工厂,所有的对象不会销毁,内部的垃圾回收机制会回收;
@Component
@Scope("prototype")
public class User {
    public String name;
}

小结:

  • xml 与注解:
    • xml 更加万能,适用于任何场合,维护简单方便;
    • 注解,不是自己的类无法使用,维护相对复杂;
  • xml 与注解整合开发,推荐最佳实践方式:
    • xml 用来管理 bean
    • 注解,只负责完成属性的注入
  • 在使用的过程中,需要注意:
    • 让注解生效,必须开启注解的支持;



九、使用 Java 的方式配置 Spring

  • JavaConfig 原来是 Spring 的一个子项目;
  • 它通过 Java 类的方式,提供 Bean 的定义信息;
  • 在 Spring4 的版本,JavaConfig 已正式成为 Spring4 的核心功能;

搭建环境

  • 创建项目;
  • 创建实体类:
// @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 + '\'' +
                '}';
    }
}
  • 新建 config 配置包,创建 MyConfig 配置类:
/*
    @Configuration:
        代表这是一个配置类,等同于beans.xml
        本身就是一个组件(@Component),
        也会被Spring容器托管,注册到容器中
 */
@Configuration
// 可以设置扫描包
@ComponentScan("com.study.spring.config")
// 可以引入其它配置类
@Import(MyConfig2.class)
public class MyConfig {

    /*
        注册一个bean,相当于bean标签
        方法名:相当于bean标签中的id属性
        方法的返回值:相当于bean标签中的class属性
     */
    @Bean
    public User getUser() {
        // 返回要注入到bean的对象
        return new User();
    }
}
  • 测试:
@Test
public void testUser() {
    // 使用了配置类方式,只能通过AnnotationConfig上下文来获取容器,通过配置类的class对象加载
    ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
    // getBean的参数为,@Bean对应的方法名,非类名首字母小写!!
    User user = (User) context.getBean("getUser");
    System.out.println(user.getName());
}
  • 纯 Java 的配置方式,SpringBoot 中比较常见;

十、代理模式

  • 为什么要学习代理模式?

    • Spring AOP 的底层就是代理模式;
  • 代理模式的分类:

    • 静态代理;
    • 动态代理;

10.1 静态代理

  • 静态代理角色分析:
    • 抽象角色:一般使用接口或者抽象类来实现;
    • 真实角色:被代理的角色;
    • 代理角色:代理真实角色,代理后 , 会增加附属操作;
    • 客户:访问代理对象的人;

代码实现

  • 抽象角色:租房
// 租房
public interface Rent {
    public void rent();
}
  • 真实角色:房东
// 房东:实现租房接口
public class Host implements Rent {
    @Override
    public void rent() {
        System.out.println("房东出租房屋...");
    }
}
  • 代理角色:代理人
// 代理:代理房东、实现租房接口、并增加附属操作
public class Proxy implements Rent {
    private Host host;

    public Proxy() {
    }

    // 有参构造方式,注入真实对象
    public Proxy(Host host) {
        this.host = host;
    }

    @Override
    public void rent() {
        seeHouse();
        // 房东租房
        host.rent();
        hetong();
        fare();
    }

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

    // 签合同
    public void hetong() {
        System.out.println("签订合同");
    }

    // 收中介费
    public void fare() {
        System.out.println("收中介费");
    }
}
  • 客户:客户端访问代理角色
// 客户
public class Client {
    public static void main(String[] args) {
        // 房东要租房子
        Host host = new Host();
        // 代理:中介代理房东出租房子,但是,代理会增加附属操作
        Proxy proxy = new Proxy(host);
        // 客户不用面对房东,直接找中介即可
        proxy.rent();
    }
}
  • 代理模式的好处:
    • 可以使真实角色的操作更加纯粹,不用去关注一些公共的业务;
    • 公共业务交给代理角色,实现了业务的分工;
    • 公共业务发生扩展时,方便集中管理;
  • 缺点:
    • 一个真实角色,就会产生一个代理角色,代码量会翻倍,开发效率会变低;

10.2 加深理解

静态代理实现 CRUD

  • 抽象角色:用户业务(增、删、改、查)
// 抽象角色:增删改查业务
public interface UserService {
    public void add();

    public void delete();

    public void update();

    public void query();
}
  • 真实对象:完成增、删、改、查操作
// 真实对象:完成增删改查操作
public class UserServiceImpl implements UserService {
    @Override
    public void add() {
        System.out.println("增加一个用户");
    }

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

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

    @Override
    public void query() {
        System.out.println("查询用户");
    }
}
  • 代理角色:通过代理类,增加日志功能
// 代理角色:增加日志的实现
public class UserServiceProxy implements UserService {
    public UserServiceImpl userService;

    // set方法,注入真实对象(注入方式:有参构造、set方法)
    public void setUserService(UserServiceImpl userService) {
        this.userService = userService;
    }

    @Override
    public void add() {
        log("add");
        userService.add();
    }

    @Override
    public void delete() {
        log("delete");
        userService.delete();
    }

    @Override
    public void update() {
        log("update");
        userService.update();
    }

    @Override
    public void query() {
        log("query");
        userService.query();
    }

    // 日志方法
    public void log(String msg) {
        System.out.println("[Debug] 使用了" + msg + " 方法");
    }
}
  • 测试:
@Test
public void testUser() {
    // 真实业务
    UserServiceImpl userService = new UserServiceImpl();
    // 代理类
    UserServiceProxy proxy = new UserServiceProxy();
    // 使用代理类,增加了日志功能的实现
    proxy.setUserService(userService);
    proxy.add();
    proxy.delete();
    proxy.update();
    proxy.query();
}
  • 运行结果:

  • AOP 核心思想

    • 不改变原有代码,通过代理,实现对原有功能的增强

10.3 动态代理

  • 动态代理和静态代理角色一样;

  • 动态代理的代理类,是动态生成的,不是直接写好的;

  • 动态代理分为两大类:

    • 基于接口的动态代理:JDK 动态代理;
    • 基于类的动态代理:cglib
    • Java 字节码实现:javasisit
  • JDK 动态代理,需要了解两个类:

    • Proxy:代理;

    • InvocationHandler:调用处理程序;

InvocationHandler

  • 用来生成代理的类,不需要去为每个代理,都单独生成一个类;
/*
    处理代理实例,并返回结果
    参数:
    proxy:代理类,代理的真实代理对象;
    method:要调用某个对象真实的方法的Method对象;
    args:代理对象方法传递的参数;
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object result = method.invoke(target, args);
    return result;    
}

Proxy

  • Proxy 类:用来创建一个代理对象的类;
  • 常用方法:newProxyInstance()

代码实现

  • 抽象角色、真实角色,参照 10.2

  • 创建自动生成代理的类:

// 用这个类,自动生成代理类
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("[Debug] 使用了" + msg + " 方法");
    }
}
  • 测试:
    1. 创建真实角色;
    2. 创建动态代理创建的实体;
    3. 设置;
    4. 使用;
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.add();
        proxy.delete();
        proxy.update();
        proxy.query();
    }
}
  • 如果需要生成多个代理对象,只需要去创建不同的 userService 和其方法,无需再对每个代理进行设置;

动态代理的好处

  • 可以使真实角色的操作更加纯粹,不用去关注一些公共的业务;
  • 公共业务交给代理角色,实现了业务的分工;
  • 公共业务发生扩展时,方便集中管理;
  • 一个动态代理类,代理的是一个接口,一般就是对应的一类业务;
  • 一个动态代理类,可以代理多个类,需要实现同一个接口;

十一、AOP

11.1 什么是 AOP

  • AOP(Aspect Oriented Programming):面向切面编程

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

11.2 Aop 在 Spring 中的作用

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

  • 横切关注点:

    • 跨越应用程序多个模块的方法或功能;
    • 与业务逻辑无关,但是需要关注的部分,就是横切关注点,如日志、安全、缓存、事务,等等;
  • 切面(ASPECT):横切关注点,被模块化的特殊对象,是一个类;

  • 通知(Advice):切面必须要完成的工作,是类中的一个方法;

  • 目标(Target):被通知对象;

  • 代理(Proxy):向目标对象应用通知之后,创建的对象;

  • 切入点(PointCut):切面通知,执行的 地点 的定义;

  • 连接点(JointPoint):与切入点匹配的执行点;

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

  • 即 Aop 在不改变原有代码的情况下,去增加新的功能;

11.3 使用 Spring 实现 Aop

  • 使用 AOP 织入,需要导入 aspectjweaver 依赖:

    org.aspectj
    aspectjweaver
    1.9.8    

第一种方式:通过 Spring API 实现

  • 创建接口:
public interface UserService {
    public void add();

    public void delete();

    public void update();

    public void select();
}
  • 创建实现类:
public class UserServiceImpl implements UserService{
    @Override
    public void add() {
        System.out.println("增加一个用户");
    }

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

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

    @Override
    public void select() {
        System.out.println("查询用户");
    }
}
  • 前置增强 MethodBeforeAdvice:AOP 增加的业务(前置日志)
public class Log 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() + "被执行了");
    }
}
  • 后置增强 AfterReturningAdvice:AOP 增加的业务(后置日志)
public class AfterLog implements AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        // returnValue:返回值
        System.out.println("执行了" + method.getName() + "方法,返回值:" + returnValue);
    }
}
  • 在 spring 的配置文件中,注册 bean,并实现 aop 切入:
    • 头文件,需导入 Aop 约束;



    
    
    
    

    
    
    
        
        

        
        
        
    

  • 测试:
@Test
public void testUser() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    // 增加 UserService.class 后,不再需要强制转换
    // 注意点:动态代理,代理的是接口,不是实现类
    UserService userService = context.getBean("userService", UserService.class);
    userService.add();
    userService.delete();
    userService.update();
    userService.select();
}
  • 注意点:动态代理,代理的是接口,不是实现类;

第二种方式:自定义类(切面),实现 Aop

  • 切面定义:自定义一个需要被插入的类;

  • 自定义类(切面):

// 自定义切面(需要插入的类)
public class DiyPointCut {
    public void before() {
        System.out.println("=========方法执行前=========");
    }

    public void after() {
        System.out.println("=========方法执行后=========");
    }
}
  • 配置 xml 文件:





    
    
        
        
        
        
        
    

  • 测试:其它代码不变

切入点位置表达式

  • 格式:
execution(* com.service ..*.*(..))
  • 解释:
符号 含义
execution() 执行,表达式的主体
第一个 * 表示返回值的类型任意
com.service AOP 所切入的服务的包名,业务部分
包名后面的 .. 表示当前包及子包
第二个 * 表示类名,即所有类
.*(..) 表示任何方法名,括号表示参数,两个点表示任何参数类型
  • 第二种方式,实现更加简单,但是实现的功能少了,比如:不能使用反射,获取执行方法的名称;

第三种方式:使用注解实现

  • 创建用注解实现的增强类:
// @Aspect:标注这个类是一个切面
@Aspect
public class AnnotationPointCut {
    @Before("execution(* com.study.spring.service.UserServiceImpl.*(..))")
    public void before() {
        System.out.println("=========方法执行前=========");
    }

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

    // 环绕增强:可以给定义一个参数,代表要获取处理切入的点
    @Around("execution(* com.study.spring.service.UserServiceImpl.*(..))")
    public void around(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("环绕前");
        // getSignature():方法签名,查看目标方法名
        System.out.println(jp.getSignature());
        // 执行目标方法
        Object proceed = jp.proceed();
        System.out.println(proceed);
        System.out.println("环绕后");
    }
}
  • 配置 xml 文件,注册 bean,并增加支持注解的配置:





十二、整合 Mybatis

12.1 搭建环境

导入相关依赖

  • junit:


    junit
    junit
    4.13.2

  • spring 相关:


    org.springframework
    spring-webmvc
    5.3.17

  • Java 注解:(JDK11 以上需要)


    javax.annotation
    javax.annotation-api
    1.3.2

  • mybatis:


    org.mybatis
    mybatis
    3.5.9

  • mybatis-spring 整合包 (重点)


    org.mybatis
    mybatis-spring
    2.0.7

  • mysql:


    mysql
    mysql-connector-java
    8.0.28

  • spring-jdbc:


    org.springframework
    spring-jdbc
    5.3.17

  • AOP 织入:


    org.aspectj
    aspectjweaver
    1.9.8

  • Maven 静态资源过滤:

    
        
            src/main/java
            
                **/*.properties
                **/*.xml
            
            true
        
    

12.2 MyBatis 项目回顾

  • 创建实体类:
public class User {
    private int id;
    private String name;
    private String pwd;
    // get、set、toString
}
  • 编写核心配置文件:




    
        
    

    
        
            
            
                
                
                
                
            
        
    
    
    
        
    

  • 创建接口类:
public interface UserMapper {
    public List selectUser();
}
  • 创建 Mapper.xml



    

  • 测试:
@Test
public void testUser() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream in = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
    SqlSession sqlSession = sqlSessionFactory.openSession(true);

    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    List userList = mapper.selectUser();
    for (User user : userList) {
        System.out.println(user);
    }

    sqlSession.close();
}
  • 运行结果:

12.3 MyBatis-Spring

  • 官方文档

  • MyBatis-Spring:将 MyBatis 代码,无缝整合到 Spring 中;

  • MyBatis-Spring 对应版本:

  • MyBatis-Spring 相关依赖:



    org.mybatis
    mybatis-spring
    2.0.7

  • 要和 Spring 一起使用 MyBatis,需要在 Spring 应用上下文中定义:
    • 一个 SqlSessionFactory
      • SqlSessionFactory 需要一个 DataSource(数据源);
    • 至少一个数据映射器类;

整合实现方式一:SqlSessionTemplate

  • 创建 Mybatis 配置文件:mybatis-config.xml


    


    

  • 创建配置文件:spring-dao.xml
    • 替换 mybaits 的数据源;
    • 配置 SqlSessionFactory,关联 MyBatis;
    • 注册 sqlSessionTemplate,关联 sqlSessionFactory;



    
    
        
        
        
        
    

    
    
        
        
        
        
        
    

    
    
        
        
    

  • 增加 UserMapper 接口的实现类,私有化 sqlSessionTemplate:
public class UserMapperImpl implements UserMapper {
    // 以前使用SqlSession操作,现在都使用SqlSessionTemplate
    private SqlSessionTemplate sqlSession;

    public void setSqlSession(SqlSessionTemplate sqlSession) {
        this.sqlSession = sqlSession;
    }

    // 在接口实现类中,实现以前测试类的方法,并将方法值返回(使用时在spring中直接调用方法即可)
    @Override
    public List selectUser() {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        return mapper.selectUser();
    }
}
  • 创建总配置文件:applicationContext.xml
    • 引入 spring-dao.xml
    • 注册实现类:UserMapperImpl



    

    
        
    

  • 测试:
@Test
public void testSpringMybatis() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserMapper userMapper = context.getBean("userMapper", UserMapper.class);
    for (User user : userMapper.selectUser()) {
        System.out.println(user);
    }
}

整合实现方式二:继承 SqlSessionDaoSupport

  • mybatis-spring1.2.3 版以上的才有;
  • dao 层通过继承 Support 类,直接利用 getSqlSession() 获得,然后直接注入 SqlSessionFactory;
  • 与方式一相比,不需要管理 SqlSessionTemplate,而且对事务的支持更加友好,可跟踪源码查看;

代码实现:

  • 创建 UserMapper 接口实现类:
public class UserMapperImpl2 extends SqlSessionDaoSupport implements UserMapper {
    @Override
    public List selectUser() {
        // getSqlSession()直接获取,不需要创建
        return getSqlSession().getMapper(UserMapper.class).selectUser();
    }
}
  • 注册 bean:


    

  • 测试:
@Test
public void testSpringMybatis2() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserMapper userMapper = context.getBean("userMapper2", UserMapper.class);
    for (User user : userMapper.selectUser()) {
        System.out.println(user);
    }
}

十三、声明式事务

13.1 回顾事务

  • 事务:把一系列的动作,当成一个独立的工作单元,这些动作要么全部完成,要么全部不起作用;

  • 事务四个属性:ACID

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

未开启事务测试:

  • 复制上例中的代码到新项目中;
  • 在接口 UserMapper 中新增两个方法,增加和删除用户:
// 增加用户
public int addUser(User user);
// 删除用户
public int deleteUser(int id);
  • 修改 Mapper.xml 文件,把 deletes 写错(模拟程序出错)

    insert into user (id, name, pwd)
    values (#{id}, #{name}, #{pwd});



    
    deletes from user where id = #{id};

  • 在接口实现类,添加增加、删除对应的方法:
public class UserMapperImpl extends SqlSessionDaoSupport implements UserMapper {
    @Override
    public List selectUser() {
        User user = new User(4, "小王", "123");
        UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
        mapper.addUser(user);
        mapper.deleteUser(1);
        return mapper.selectUser();
    }

    @Override
    public int addUser(User user) {
        return getSqlSession().getMapper(UserMapper.class).addUser(user);
    }

    @Override
    public int deleteUser(int id) {
        return getSqlSession().getMapper(UserMapper.class).deleteUser(id);
    }
}
  • 测试:
@Test
public void testSpringMybatis() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserMapper userMapper = context.getBean("userMapper", UserMapper.class);
    for (User user : userMapper.selectUser()) {
        System.out.println(user);
    }
}
  • 运行结果:

    • 报错:sql 异常,delete 写错了;

    • 结果:插入成功;

小结:

  • 没有进行事务的管理,程序部分出现错误,依旧可以提交;
  • 如果想让一组程序,都成功时才成功,有一个失败,就都失败,就需要事务;

13.2 Spring 中的事务管理

  • 需要用到的依赖:

    • aspectjweaver

      
      
          org.aspectj
          aspectjweaver
          1.9.8
      
      
    • spring-jdbc

      
      
          org.springframework
          spring-jdbc
          5.3.17
      
      
  • Spring 在不同的事务管理 API 之上定义了一个抽象层,使得开发人员,不必了解底层的事务管理 API,就可以使用 Spring 的事务管理机制;

  • Spring 支持:

    • 编程式事务管理:
      • 代码嵌到业务方法中,来控制事务的提交和回滚;
      • 缺点:必须在每个事务操作业务逻辑中,包含额外的事务管理代码;
    • 声明式事务管理:AOP
      • 代码与业务方法分离,以声明的方式,来实现;
      • 将事务管理,作为横切关注点,通过 AOP 方法模块化;
      • Spring 中通过 Spring AOP 框架,支持声明式事务 管理;

spring 七种事务类型

事务类型 说明
REQUIRED 默认)支持当前事务,无事务,另起新事物
SUPPORTS 支持当前事务,无事务,以非事务执行
MANDATORY 以事务方式执行,无事务,抛异常
REQUIRES_NEW 新建事务,若有旧事务,挂起
NOT_SUPPORTED 不支持事务,如有事务,挂起
NEVER 以非事务执行,有事务,抛异常
NESTED 内切事务

配置 spring-dao.xml 文件

  • 使用 Spring 管理事务,注意头文件的约束导入:tx
xmlns:tx="http://www.springframework.org/schema/tx"

http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
  • 配置声明式事务:JDBC 事务


    

  • 配置事务的通知:



    
    
    
        
        
        
        
        
    

  • 配置事务切入:AOP(注意导入头文件)


    
    

  • 测试:

    • 运行出现异常(deletes 写错);

    • 查看数据库,未插入成功;

    • 更改删除语句后,运行正常;

小结:

  • 为什么需要配置事务:
    • 如果不配置事务,可能存在数据提交不一致的情况;
    • 如果不在 Spring 中去配置声明式事务,就需要在代码中,手动配置事务;
    • 事务在项目的开发中十分重要,涉及到数据的一致性,和完整性问题,不容马虎;

你可能感兴趣的:(Spring 5 基础)