SSM整合02:Spring 5

147 阅读26分钟

Spring 5

1 Spring 概述

官网 : spring.io/

官方下载地址 : repo.spring.io/libs-releas…

GitHub : github.com/spring-proj…

1.1 Spring 简介

Spring是轻量级的开源的 JavaEE 解决方案,整合了众多优秀的设计模式,可以降低企业应用开发的复杂性

  • 轻量级

    1. 对于运⾏环境是没有额外要求的; 开源:tomcat、resion、jetty 收费:weblogic、websphere
    2. 代码移植性⾼:不需要实现额外接⼝。
  • JavaEE 解决方案

image-20200916202152905

三层架构

  • 控制器 Controller
  • 业务层 Service
  • 持久层 DAO 数据访问和操作
  • 整合的设计模式

    1. ⼯⼚模式

    2. 代理模式

    3. 模板模式

    4. 策略模式

    设计模式

    广义上,设计模式是指在面向对象设计中,解决特定问题的经典代码

    狭义上,设计模式是指GOF定义的23种设计模式:工厂、适配器、修饰器、门面、代理、模板……

Spring 特性

  • 非侵入式:基于Spring开发的应用中的对象可以不依赖于Spring的API
  • 控制反转:IOC,指的是将对象的创建权交给 Spring 框架去创建
  • 依赖注入:DI,是指依赖的对象不需要手动调用 setXX 方法去设置属性的值,而是通过配置赋值。
  • 面向切面编程:AOP,不修改源代码进行功能增强
  • 容器:Spring 是一个容器,因为它包含并且管理应用对象的生命周期
  • 组件化:用简单的组件配置组合成一个复杂的应用,可以使用XML和Java注解组合这些对象。
  • 一站式:在 IOC 和 AOP 的基础上可以整合各种企业应用的开源框架和优秀的第三方类库

Spring 拓展

  • Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务;
  • Spring Cloud是基于Spring Boot实现的;
  • Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架;
  • Spring Boot使用了约束优于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置 , Spring Cloud很大的一部分是基于Spring Boot来实现,Spring Boot可以离开Spring Cloud独立使用开发项目,但是Spring Cloud离不开Spring Boot,属于依赖的关系。
  • SpringBoot在SpringClound中起到了承上启下的作用,如果要学习SpringCloud必须学习SpringBoot。

Spring 体系

Spring 体系结构

核心容器(Core Container)

核心容器由spring-core,spring-beans,spring-context,spring-context-support和spring-expression(SpEL,Spring表达式语言,Spring Expression Language)等模块组成,它们的细节如下:

  • spring-core模块提供了框架的基本组成部分,包括 IoC 和依赖注入功能。依赖于commons-logging。
  • spring-beans 模块提供 BeanFactory,工厂模式的微妙实现,它移除了编码式单例的需要,并且可以把配置和依赖从实际编码逻辑中解耦。依赖于spring-core。
  • context模块建立在由corebeans 模块的基础上建立起来的,它以一种类似于JNDI注册的方式访问对象。依赖于spring-core、spring-beans、spring-aop、spring-expression。
    • Context模块继承自Bean模块,并且添加了国际化(比如,使用资源束)、事件传播、资源加载和透明地创建上下文(比如,通过Servelet容器)等功能。Context模块也支持Java EE的功能,比如EJB、JMX和远程调用等。
    • ApplicationContext接口是Context模块的焦点。spring-context-support提供了对第三方库集成到Spring上下文的支持,比如缓存(EhCache, Guava, JCache)、邮件(JavaMail)、调度(CommonJ, Quartz)、模板引擎(FreeMarker, JasperReports, Velocity)等。
  • spring-expression模块提供了强大的表达式语言,用于在运行时查询和操作对象图。它是JSP2.1规范中定义的统一表达式语言的扩展,支持set和get属性值、属性赋值、方法调用、访问数组集合及索引的内容、逻辑算术运算、命名变量、通过名字从Spring IoC容器检索对象,还支持列表的投影、选择以及聚合等。依赖于spring-core。

中间层

  • AOP 模块提供了面向方面(切面)的编程实现,允许你定义方法拦截器和切入点对代码进行干净地解耦,从而使实现功能的代码彻底的解耦出来。使用源码级的元数据,可以用类似于.Net属性的方式合并行为信息到代码中。
  • Aspects 模块提供了与 AspectJ 的集成,这是一个功能强大且成熟的面向切面编程(AOP)框架。
  • Instrumentation 模块在一定的应用服务器中提供了类 instrumentation 的支持和类加载器的实现。
  • Messaging 模块为 STOMP 提供了支持作为在应用程序中 WebSocket 子协议的使用。它也支持一个注解编程模型,它是为了选路和处理来自 WebSocket 客户端的 STOMP 信息。
  • 测试模块支持对具有 JUnit 或 TestNG 框架的 Spring 组件的测试。

数据访问/集成(Data Access / Integration)

数据访问/集成层包括 JDBC(Java Data Base Connectivity),ORM(Object Relational Mapping),OXM(Object XML Mapping),JMS(Java Message Service) 和事务处理(Transactions)模块:

  • JDBC 模块提供了JDBC抽象层,它消除了冗长的JDBC编码和对数据库供应商特定错误代码的解析。
  • ORM 模块提供了对流行的对象关系映射API的集成,包括JPA、JDO和Hibernate等。通过此模块可以让这些ORM框架和spring的其它功能整合,比如前面提及的事务管理。
  • OXM 模块提供了对OXM实现的支持,比如JAXB、Castor、XML Beans、JiBX、XStream等。
  • JMS 模块包含生产(produce)和消费(consume)消息的功能。从Spring 4.1开始,集成了spring-messaging模块。
  • 事务模块为实现特殊接口类及所有的 POJO 支持编程式和声明式事务管理。

网络层(Web:MVC / Remoting)

Web 层由 Web,Web-MVC,Web-Socket 和 Web-Portlet 组成:

  • Web 模块提供面向web的基本功能和面向web的应用上下文,比如多部分(multipart)文件上传功能、使用Servlet监听器初始化IoC容器等。它还包括HTTP客户端以及Spring远程调用中与web相关的部分。
  • Web-MVC 模块为web应用提供了模型视图控制(MVC)和REST Web服务的实现。Spring的MVC框架可以使领域模型代码和web表单完全地分离,且可以与Spring框架的其它所有功能进行集成。
  • Web-Socket 模块为 WebSocket-based 提供了支持,而且在 web 应用程序中提供了客户端和服务器端之间通信的两种方式。
  • Web-Portlet 模块提供了用于Portlet环境的MVC实现,并反映了spring-webmvc模块的功能。

1.2 工厂设计模式

概念:通过工厂类创建对象

好处:解耦合

耦合:代码间的强关联关系,⼀⽅的改变会影响到另⼀⽅;

问题:把接口的实现类硬编码在程序中,不利于代码维护

传统代码

User.class

package com.hihanying.basic;
import java.io.Serializable;
public class User implements Serializable {
    private String name;
    private String password;
	// 构造器
    // Getter 和 Setter
    // toString函数
}

UserDAO.class

package com.hihanying.basic;
public interface UserDAO {
    public void save(User user);
    public void queryUserByNameAndPassword(String name, String password);
}

UserDAOImpl.class

package com.hihanying.basic;
public class UserDAOImpl implements UserDAO {
    @Override
    public void save(User user) {
        System.out.println("insert into user = " + user);
    }
    @Override
    public void queryUserByNameAndPassword(String name, String password) {
        System.out.println("query User name = " + name + "password = " + password);
    }
}

UserService.class

package com.hihanying.basic;
public interface UserService {
    public void register(User user);
    public void login(String name, String password);
}

UserServiceImpl.class

package com.hihanying.basic;
public class UserServiceImpl implements UserService {
    private UserDAO userDao = new UserDAOImpl();
    @Override
    public void register(User user) {
        userDao.save(user);
    }
    @Override
    public void login(String name, String password) {
        userDao.queryUserByNameAndPassword(name, password);
    }
}

TestSpring.class

package com.hihanying.basic;
import org.junit.Test;
public class TestSpring {
    @Test
    public void test1() {
        UserService userService = new UserServiceImpl();
        userService.login("name", "suns");
        User user = new User("suns", "123456");
        userService.register(user);
    }
}

简单工厂

问题分析

TestSpring.class 文件中,UserServiceImpl userService = new UserServiceImpl();语句中存在耦合,我们把 UserServiceImpl 实现类硬编码在了程序中,因此代码的维护性变差,如果日后想用新的实现类替换掉 UserServiceImpl ,就需要改动 TestSpring.class 代码文件。

解决方案

通过工厂类设计对象

BeanFactory.class

package com.hihanying.basic;
// 工厂类属于工具类,因此将方法修饰为static
public class BeanFactory {
    public static UserService getUserService() {
        return new UserServiceImpl();
    }
}

此是就可以将测试类中的 UserServiceImpl userService = new UserServiceImpl(); 替换为 UserService userService = BeanFactory.getUserService(); ,此时,在测试类中没有耦合了。

反射工厂

但是,上述简单工厂中,在我们新创建的 BeanFactory 类中,生产 UserService 类时又出现了耦合,如何解决呢?

答案:反射

对象的创建方式:

  1. 直接调用构造方法创建对象 :UserService userService = new UserServiceImpl();
  2. 通过反射的形式创建对象(解耦合) Class clazz = Class.forName("com.baizhiedu.basic.UserServiceImpl"); UserService userService = (UserService)clazz.newInstance();

BeanFactory.class

package com.hihanying.basic;
public class BeanFactory {
    public static UserService getUserService() {
        UserService userService = null;
        try {
            Class aClass = Class.forName("com.hihanying.basic.UserServiceImpl");
            userService = (UserService) aClass.newInstance();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return userService;
    }
}

此时,我们发现,耦合仅存在于 Class aClass = Class.forName("com.hihanying.basic.UserServiceImpl"); 语句中的字符串参数中。

对于字符串耦合的问题,我们可以通过配置文件解决,即将耦合字符串信息转移到配置文件中,我们可以新建一个 properties文件用来保存配置信息。

image-20200917154532672

applicationContext.properties

# properties文件中存储的是键值对
# 可以通过 Properties 类将文件中的键值对读取到一个 Map
userService = com.hihanying.basic.UserServiceImpl

BeanFactory.class

package com.hihanying.basic;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class BeanFactory {
    private static Properties env = new Properties();
    static{
        try {						// 1. 获得IO输入流
            InputStream inputStream = BeanFactory.class.getResourceAsStream("/applicationContext.properties"); 
            env.load(inputStream); // 2. 文件内容封装到 Properties 集合中
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static UserService getUserService() {
        UserService userService = null;
        try {
            Class aClass = Class.forName(env.getProperty("userService"));
            userService = (UserService) aClass.newInstance();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return userService;
    }
}

此时,BeanFactory.class 中的耦合字符串通过配置文件转移到了 applicationContext.properties 中,BeanFactory 类中再也没有耦合了。

除此之外,在 UserServiceImpl.class 文件中,我们通过 private UserDAO userDao = new UserDAOImpl(); 语句将 UserDAOImpl 类的实例化硬编码在了代码中,同样存在耦合,我们可以用上述反射工厂+配置文件的方法去掉耦合。

BeanFactory 类中加入 getUserDAO() 方法:

public static UserDAO getUserDAO() {
    UserDAO userDAO = null;
    try {
        Class aClass = Class.forName(env.getProperty("userDAO"));
        userDAO = (UserDAO)aClass.newInstance();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return userDAO;
}

此时,就可以将UserServiceImpl类中的 private UserDAO userDao = new UserDAOImpl(); 替换成 private UserDAO userDao = BeanFactory.getUserDAO();,耦合转移到配置文件中。

通用工厂

问题:观察 BeanFactory 类中的 getUserDAO() 方法和 getUserService 方法,我们发现其中有很多重复代码

解决:我们可以设计一个通用的工厂方法。

public static Object getBean(String key) {
    Object ret = null;
    try {
        Class aClass = Class.forName(env.getProperty(key));
        ret = aClass.newInstance();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return ret;
}

优化:

  • UserServiceImpl类中的 private UserDAO userDao = BeanFactory.getUserDAO(); 替换成private UserDAO userDao = (UserDAO)BeanFactory.getBean("userDao");
  • 将测试方法中的UserService userService = BeanFactory.getUserService();替换为 UserService userService = (UserService)BeanFactory.getBean("userService");

到此,通用工厂设计以及使用完毕。

使用方式

  1. 定义类型(类)
  2. 通过配置文件的配置告知工厂(applicationContext.properties)
  3. 通过工厂获得类的对象(Object ret = BeanFactory.getBean(“key”))

小结

Spring 本质:工厂 ApplicationContext(applicationContext.xml)

1.3 Spring 5 案例

软件版本:Maven 3.6 和 IDEA2019 存在Bug

1. IDEA创建工程

  • 普通Java工程
    • 打开 IDEA 开发工具,创建普通 Java 工程
    • 下载 Spring 最新稳定版本:,下载 Apache Commons Logging API 最新版本:Download
    • 在 src 同级目录下建立 lib 目录,将以下 jar 包放入 lib,然后在项目结构->模块中导入这些 jar 包

image-20200908232632497

  • Maven工程

    • 打开 IDEA 开发工具,创建 Maven 工程
    • pom.xml 文件中导入依赖,maven仓库
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>
    

2. 创建配置文件

  • 配置⽂件的位置没有硬性要求,建议放置在 src/main/java/resources 目录下
  • 配置⽂件的命名没有硬性要求,建议使用 applicationContext.xml

配置文件的位置和命名没有要求,因此⽇后应⽤Spring框架时,需要进⾏配置⽂件路径的设置。

通过以下方式可以创建配置文件

image-20200918152242497

文件初始内容为:

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

3. Spring 核⼼API

Spring提供的 ApplicationContext 这个⼯⼚,⽤于对象的创建,方便解耦合

ApplicationContext 是一个接口,可以屏蔽不同场景下实现的差异,两个主要实现:

  • 非 Web 环境(main junit):ClassPathXmlApplicationContext
  • Web 环境:XmlWebApplicationContext

导入 spring-webmvc 依赖后,展开 ApplicationContext 的继承关系如下:

image-20200918153745020

ApplicationContext 是一个重量级资源,其实现类会占用大量内存,因此不会频繁的创建对象,一个应用只会创建一个工厂对象,且一定是线程安全的。

4. 程序开发

  1. 创建类型

    package com.hihanying.basic;
    public class Person {
    }
    
  2. 在配置文件中配置

    <bean id="person" class="com.hihanying.basic.Person" />
    
  3. 创建测试方法:通过工厂类获得对象

package com.hihanying.basic;
import org.junit.Test;
public class TestSpring {
    @Test
    public void test3() {
        // 获得 Spring 的工厂
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 通过工厂获得对象(类似上一小节中我们自己创建的工厂的getBean方法调用)
        Person person = (Person) ctx.getBean("person");
         //输出:person = com.hihanying.basic.Person@576d5deb
        System.out.println("person = " + person);
    }
}

5. 细节分析

Spring⼯⼚创建的对象,叫做bean或者组件(componet)

ApplicationContext 工厂的相关方法

//通过这种⽅式获得对象,就不需要强制类型转换
Person person = ctx.getBean("person", Person.class);
System.out.println("person = " + person);
//如果当前 Spring 的配置⽂件中只有⼀个 Person 类型的bean,可以使用这种方法
Person person = ctx.getBean(Person.class);
System.out.println("person = " + person);
//获取的是 Spring⼯⼚配置⽂件中所有bean标签的id值 person person1
String[] beanDefinitionNames = ctx.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
    System.out.println("beanDefinitionName = " + beanDefinitionName);
}
//根据类型获得Spring配置⽂件中对应的id值
String[] beanNamesForType = ctx.getBeanNamesForType(Person.class);
for (String id : beanNamesForType) {
    System.out.println("id = " + id);
}

配置⽂件中需要注意的细节

  1. 可以只配置class属性 <bean class="com.baizhiedu.basic.Person"/>

    • Spring 会提供默认的 id 值:com.baizhiedu.basic.Person#0
    • 应用场景:如果这个 bean 只需要使用一次且不会被其他的bean引用,那么可以省略 id 值
  2. name 属性用于为bean对象定义别名(小名)

    • 相同点

      • 通过 name 和 id 都可以通过getBean方法获得对象
      • <bean name="" class=""等效于 <bean id="" class=""
    • 不同点

      • name 属性可以定义多个(用逗号、分号、空格隔开),但id值只能有一个

      • 历史问题:XML的 id 属性的值,命名要求:必须以字⺟开头,不能以特殊字符开头;name属性命名没有要求 /person name属性会应⽤在特殊命名的场景下:/person (spring+struts1) XML发展到了今天:id 属性的限制不存在了,可以使用 /person 命名 id

      • 工厂的方法使用

        //⽤于判断是否存在指定id值得bean,不能判断name值
        if (ctx.containsBeanDefinition("person")) {
        System.out.println("true = " + true);
        }else{
        System.out.println("false = " + false);
        }
        //⽤于判断是否存在指定id值得bean,也可以判断name值
        if (ctx.containsBean("p")) {
        System.out.println("true = " + true);
        }else{
        System.out.println("false = " + false);
        }
        

6. Spring 底层

ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

  1. Spring 工厂读取配置文件得到bean的关键信息,包括 id 和 class 属性

  2. Spring通过反射创建对象

    Class<?> aclass = new Class.forName(class的值);
    id 的值 = aclass.newInstance();
    
  3. 反射创建对象,底层也会调用对象自己的构造方法,并且可以调⽤对象私有构造⽅法创建对象

Person person = (Person)ctx.getBean("person");

  • 通过工厂的 getBean 方法就可以拿到创建好的对象

注意:创建对象是上一行代码完成的

小结

问题:未来在开发过程中,是不是所有的对象,都会交给Spring⼯⼚来创建呢? 回答:理论上是的,但是有特例 :实体对象(entity,一般一个实体类对应一张数据库的表)是不会交给Spring创建,它是由持久层框架进⾏创建

1.4 Spring5.x与⽇志框架的整合

Spring与⽇志框架进⾏整合,⽇志框架就可以在控制台中,输出Spring框架运⾏过程中的⼀些重要的信息。便于了解Spring框架的运⾏过程,利于程序的调试。

Spring 默认⽇志框架

  • Spring1.2.3早期都是于 commons-logging.jar
  • Spring5.x默认整合的⽇志框架 logback 或者 log4j2

Spring5.x 整合 log4j

  • pom.xml 文件中引⼊ log4j 的 jar 包

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.25</version>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    
  • 在 resources 目录下引⼊ log4j.properties 配置⽂件

    ###### 配置根
    log4j.rootLogger = debug,console
    ###### ⽇志输出到控制台显示
    log4j.appender.console=org.apache.log4j.ConsoleAppender
    log4j.appender.console.Target=System.out
    log4j.appender.console.layout=org.apache.log4j.PatternLayout
    log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
    

1.5 Spring IOC 与 DI

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI)。

Class A中用到了Class B的对象b,即A依赖于B,一般情况下,需要在A的代码中显式的new一个B的对象。采用依赖注入技术之后,A的代码只需要定义一个私有的B对象,不需要直接new来获得这个对象,而是通过相关的容器控制程序来将B对象在外部new出来并注入到A类里的引用中。而具体获取的方法、对象被获取时的状态由配置文件(如XML)来指定。

控制反转:

  • **解释:**把对象创建及其初始化(成员变量赋值)赋值的控制权,从代码中反转(转移)到 Spring 容器中,通过Spring 工厂和配置文件控制对象的创建及其初始化。
  • **目的:**解耦合
  • **底层原理:**XML解析、工厂模式、反射

依赖注入:

  • **解释:**当调用者需要被调用者(依赖)协助时,无需在代码中创建被调用者,而是通过 Spring 容器创建并注入被调用者(依赖)。
  • **目的:**通过分离调用者和依赖,从而解耦,提高代码可读性以及重用性

2 Spring 注入

2.1 Spring 注入简介

为成员变量赋值的传统方式

ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Person person = (Person) ctx.getBean("person");
person.setId(1);
person.setName("suns");
System.out.println("person = " + person);	// person = Person{id=1, name='hanying'} 
  • 问题:调用 Person 类的 set 方法为成员变量赋值的方式,会将赋值的内容硬编码到程序中,存在耦合。
  • 解决:注入(Injection),即通过 Spring ⼯⼚及配置⽂件,为所创建对象的成员变量赋值。
  • 好处:可以将为成员变量赋值时存在的耦合转移到配置文件中。

通过配置文件给成员变量赋值

  1. 类的成员变量提供 Setter 和 Getter 方法

  2. 配置 applicationContext.xml 文件

    <bean id="person" class="com.hihanying.basic.Person">
        <property name="id">
            <value>10</value>
        </property>
        <property name="name">
            <value>hy</value>
        </property>
    </bean>
    
  3. 通过测试方法测试想过

    ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    Person person = (Person) ctx.getBean("person");
    System.out.println("person = " + person);// person = Person{id=1, name='hy'} 
    

Spring 注入的简易原理分析

从类路径 resource 的 [applicationContext.xml] 文件中加载了 1 个 bean 定义:

  1. <bean id="person" class="com.hihanying.basic.Person">等效于 Person pserson = new Person();
  2. <property name="id"><value>10</value></property>等效于person.setId(10)
  3. <property name="name"><value>hy</value></property>等效于person.setName("hy")

底层原理:通过反射获取类的对应 setter 方法的 Method 对象,并调用其 invoke 方法赋值

  1. 根据全限定类名获取类的 Class 对象并创建实例

     Class aClass = Class.forName("key");
     Object bean = aClass.newInstance();
    
  2. 根据属性名获取对应 setter 方法的 Method 对象并赋值 aval

    Method method = aClass.getMethod("setter",aVal.getClass());
    method.invoke(bean,aVal);
    

2.2 详解 Set 注入

上一小节中展示了简单的 Set 注入普通类成员变量的方法,Set 注入需要成员变量的 Settter 方法。

而针对不同成员类型的成员变量,需要在<property> 标签中嵌套使用其他标签。主要分为两大类:

image-20200920175158957

JDK 内置类型

1. String + 8种基本类型

<value>suns</value>

2. 数组

<list>
    <value>value1</value>
    <value>value2</value>
    <value>value3</value>
</list>

3. set 集合

<set>
    <value>11111</value>
    <value>112222</value>
</set>
<!--集合中可以有其他类型-->
<set>
    <list
    <set
</set>

4. list 集合

<list>
    <value>11111</value>
    <value>2222</value>
</list>
<!--集合中可以有其他类型-->
<list>
    <list
    <set
</list>

5. map 集合

<map>
    <entry>
        <key><value>key1</value></key>
        <value>3434334343</value>
    </entry>
    <entry>
        <key><value>key2</value></key>
        <list><!--list--></list>
    </entry>
</map>

entry 表示键值对,其中 key 有特定的标签 <key></key> ,而值根据对应类型选择对应的标签。

6. Properites

<props>
    <prop key="key1">value1</prop>
    <prop key="key2">value2</prop>
</props>

7. 复杂的JDK类型 (Date)

需要程序员⾃定义类型转换器进行处理,后续学习。

用户自定义类型

1. 内连接

<bean id="userService" class="com.hihanying.spring.basic.UserServiceImpl">
    <property name="userDAO">
        <bean class="com.hihanying.spring.basic..UserDAOImpl"/>
    </property>
</bean>

问题:如果在程序中需要注入多个 userDAO 对象,就需要创建多次,会浪费内存资源,且存在代码冗余。

2. 外连接

<bean id="userDAO" class="xxx.UserDAOImpl"/>
<bean id="userService" class="xxx.UserServiceImpl">
    <property name="userDAO">
        <ref bean="userDAO"/>
    </property>
</bean>

注意:Spring4.x 废除了 <ref local=""/> 基本等效 <ref bean=""/>,前者只能引用当前问配置文件中的 bean,后者还可以引用父容器中的 bean

Set 注入的简化

1. 基于属性的简化

JDK类型注⼊,其中 value 属性只能简化8种基本类型+String的注⼊

<property name="name">
    <value>suns</value>
</property>
<!--简化后-->
<property name="name" value="suns"/>

⽤户⾃定义类型

<property name="userDAO">
    <ref bean="userDAO"/>
</property>
<!--简化后-->
<property name="userDAO" ref="userDAO"/>

2. 基于p命名空间的简化

在 bean 标签中的类路径后输入 p:,等待一会按下 Alt + Enter 可以自动插入 p 命名空间

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

JDK类型注⼊,其中 value 属性只能简化8种基本类型+String的注⼊

<bean id="person" class="xxx.Person">
    <property name="name">
        <value>suns</value>
    </property>
</bean>
<!--简化后-->
<bean id="person" class="xxx.Person" p:name="suns"/>

⽤户⾃定义类型

<bean id="userService" class="xx.UserServiceImpl">
    <property name="userDAO">
        <ref bean="userDAO"/>
    </property>
</bean>
<!--简化后-->
<bean id="userService" class="xxx.UserServiceImpl" p:userDAOref="userDAO"/>

2.3 构造注入

注入是通过 Spring 的配置文件为成员变量赋值, Set 注入和构造注入的区别是

  • Set 注⼊:Spring调⽤Set⽅法,通过配置⽂件为成员变量赋值
  • 构造注⼊:Spring调⽤构造⽅法,通过配置⽂件为成员变量赋值

因此,构造注入需要类提供构造方法。

配置文件的配置

<bean id="customer"
      class="com.hihanying.basic.constructer.Customer">
    <constructor-arg>
        <value>hy</value>
    </constructor-arg>
    <constructor-arg>
        <value>102</value>
    </constructor-arg>
</bean>

注意,<constructor-arg>标签的数量和顺序应该与构造函数的参数一致。

构造⽅法重载

  • 构造器参数个数不同时,通过控制<constructor-arg>标签的数量进⾏区分
  • 构造参数个数相同时,通过在标签引⼊ type 属性进⾏类型的区分:`

2.4 小结

image-20200920183152206

未来的实战中,应⽤set注⼊还是构造注⼊?

答案:set注⼊更多

  1. 构造注⼊麻烦 ,需要考虑重载
  2. Spring框架底层⼤量应⽤了 set 注⼊

3. Spring ⼯⼚创建对象

Spring 工厂创建的对象可以分为两类:

  • 简单对象:可以通过 new 构造方法创建的对象(UserService、UserDAO等)

  • 复杂对象:不能直接通过 new 构造方法创建的对象(Connection、SqlSessionFactory等)

3.1 创建复杂对象方式

FactoryBean 接⼝

BeanFactory 和 FactoryBean

  • BeanFactory 是个 Factory,也就是IOC容器或对象工厂,而FactoryBean是个Bean。
  • BeanFactory 是容器的顶级接⼝,所有的Bean都是由 BeanFactory (也就是IOC容器)来进行管理的。
  • FactoryBean 是一个特殊的 Bean ,能够生产或者修饰对象生成的工厂 Bean。

FactoryBean 接口的实现

以 Connection 为例:提前导入 mysql-connector-java 驱动 jar 包

package com.hihanying.factorybean;
import org.springframework.beans.factory.FactoryBean;
import java.sql.Connection;
import java.sql.DriverManager;

public class ConnectionFactoryBean implements FactoryBean {    
    @Override	//用于书写创建复杂对象的代码,并把复杂对象作为方法的返回值返回
    public Object getObject() throws Exception {
        Class.forName("com.mysql.jdbc.Driver");
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "root");
        return conn;
    }    
    @Override	// 用于返回所创建复杂对象的 class 对象
    public Class<?> getObjectType() {
        return Connection.class;
    }    
    @Override	// 用于控制所创建复杂对象的创建次数
    public boolean isSingleton() {
        return false;
    }
}

配置文件的配置

<bean id="conn" class="com.hihanying.factorybean.ConnectionFactoryBean"/>

注意:如果Class中指定的类型是FactoryBean接⼝的实现类,那么通过id值获得的是这个类所创建的复杂对象 Connection

测试

ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Connection conn = (Connection) ctx.getBean("conn");
System.out.println("conn = " + conn);//conn = com.mysql.jdbc.JDBC4Connection@301eda63

细节分析

  1. 如果就想获得 FactoryBean 类型的对象,使用 ctx.getBean("&conn") ,即可获得 ConnectionFactoryBean 对象
  2. isSingleton ⽅法返回 true 只会创建⼀个复杂对象,返回 false 每⼀次都会创建新的对象
  3. mysql ⾼版本连接创建时,需要制定SSL证书,解决问题的⽅式是 url = jdbc:mysql://localhost:3306/mysql?useSSL=false

依赖注入

ConnectionFactoryBean 实现类中包括四个重要的字符串,可以通过配置文件注入,以进一步解耦合

在ConnectionFactoryBean 实现类中创建四个字符串类型的成员变量,并提供 Getter 和 Setter 方法

private String driveClassName;
private String url;
private String username;
private String password;

在配置文件中注入所创建对象的属性

<bean id="conn" class="com.hihanying.factorybean.ConnectionFactoryBean">
    <property name="driveClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/mysql"/>
    <property name="username" value="root"/>
    <property name="password" value="root"/>
</bean>

在实现方法中将耦合的字符串编码改为对应成员变量

public Object getObject() throws Exception {
    Class.forName(driveClassName);
    Connection conn = DriverManager.getConnection(url, username, password);
    return conn;
}

FactoryBean 的实现原理

  1. 通过 bean 的 id 获取 ConnectionFactoryBean 类对象
  2. 通过 instanceof 判断类对象是否为 FactoryBean 接⼝的实现
  3. 如果是,则通过 getBean 方法获取 Connection 对象

实例工厂

目的:

  • 防止 Spring 框架的侵入(创建复杂对象依赖于 FactoryBean 接口)
  • 整合遗留系统(通过遗留实例工厂的 .class 文件获取复杂对象)

方法:

实例工厂

package com.hihanying.factorybean;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class ConnectionFactory {
    public Connection getConnection() {
        Connection conn = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "root");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return conn;
    }
}

配置文件

<bean id="connFactory" class="com.hihanying.factorybean.ConnectionFactory"></bean>
<bean id="conn" factory-bean="connFactory" factory-method="getConnection"/>

静态工厂

如果获取复杂对象的方法(getConnection)是 static 的,修改配置文件为以下方式

<bean id="conn" class="com.hihanying.factorybean.StaticConnectionFactory" factory-method="getConnection"/>

3.2 控制创建对象次数

控制创建对象次数的目的:节省不必要的内存浪费

  • 只创建一次的对象:SqlSessionFactory、DAO、Service
  • 每次都要创建的对象:Connection、SqlSession | Session、Struts2 Action

控制简单对象的创建次数

通过 bean 标签中的 scope 属性值控制

<bean id="account" scope="singleton 或 prototype" class="xxxx.Account"/>
  • sigleton:只会创建⼀次简单对象(默认值)
  • prototype:每⼀次都会创建新的对象

控制复杂对象的创建次数

通过 isSingleton() 方法的返回值控制

  • true:只会创建一次
  • false:每一次都会创建新的

如没有 isSingleton ⽅法,还是通过 scope 属性进⾏对象创建次数的控制

4. Spring 工厂高级特性

4.1 Bean 的生命周期

Bean 的生命周期概括起来就是 4 个阶段:

1. 实例化(Instantiation)

在 applicationContext.xml 文件中,bean 标签中 scope 属性的值会影响对象实例化的时间

  • scope="singleton":创建 Spring ⼯⼚的同时实例化对象
  • scope="prototype":Spring ⼯⼚获取对象时实例化对象

如果在 scope="singleton" 时,需要 Spring ⼯⼚获取对象时实例化对象,可以在 bean 标签中设置 lazy-init="true"

2. 属性赋值(Populate)

即注入

3. 初始化(Initialization)

Spring ⼯⼚在实例化完对象后,会调⽤对象的初始化⽅法,完成对应的初始化操作。初始化方法由程序员提供,Spring 工厂完成调用。

初始化主要完成资源的初始化(数据库、IO、⽹络……)

初始化有两种形式:

  • 实现 InitializingBean 接⼝
  • 在对象中提供一个普通的方法,并在 bean 中提供 init-method 属性指向该方法名

如果对象中同时包含初始化的两种形式,则先执行前者,再执行后者

4. 销毁(Destruction)

Spring什么时候调用对象的销毁方法?在 Spring 工厂销毁对象前

Spring什么时候销毁所创建的对象?Spring 工厂执行 close() 方法时

销毁方法有什么作用?程序员根据⾃⼰的需求,定义销毁⽅法,完成销毁操作(主要是资源释放操作)。

销毁方法有两种方式:

  • 实现 DisposableBean 接口
  • 在对象中定义一个普通的方法,并在 bean 中提供 destroy-method 属性指向该方法名

销毁⽅法的操作只适⽤于 bean 中 scope="singleton" 时。

如果对象中同时包含销毁的两种形式,则先执行前者,再执行后者

代码示例

Product 类

package com.hihanying.life;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class Product implements InitializingBean, DisposableBean {
    private String pname;
    private int pid;
    public Product () {
        System.out.println("Product.Product");
    }
    // Setter and Getter
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("Product.afterPropertiesSet");
    }
    public void myInit() {
        System.out.println("Product.init");
    }
    @Override
    public void destroy() throws Exception {
        System.out.println("Product.destroy");
    }
    public void myDestroy () {
        System.out.println("Product.myDestroy");
    }
}

xml 文件配置

<bean id="product" class="com.hihanying.life.Product" init-method="myInit" destroy-method="myDestroy">
    <property name="pid" value="123"/>
    <property name="pname" value="phone"/>
</bean>

测试方法

@Test
public void test(){
    ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    Product product = (Product) ctx.getBean("product");
    System.out.println("product = " + product);
    ctx.close();
}

输出

Product.Product
Product.afterPropertiesSet
Product.init
product = com.hihanying.life.Product@7ac296f6
Product.destroy
Product.myDestroy

Bean 的完整生命周期示意

Spring生命周期(概要)

4.2 配置文件参数化

问题

大型项目的 Spring 配置⽂件会有上千行配置信息,当修改部分配置时不易查找。

解决

将需要经常修改的字符串信息转移到一个更小的配置文件中,使配置文件的维护更加方便

  • Spring的配置⽂件中存在需要经常修改的字符串的代表:数据库连接相关的参数
  • 小的配置文件一般是 .properties 文件

步骤

  1. 在 resource 目录下创建一个配置文件 db.properties (文件名和文件位置随意),并将需要转移的配置信息填入。

    jdbc.driverClassName = com.mysql.jdbc.Driver
    jdbc.url = jdbc:mysql://localhost:3306/sqlName?useSSL=false
    jdbc.username = root
    jdbc.password = root
    
  2. 将 Spring 的配置⽂件(applicationContext.xml)与⼩配置⽂件(db.properties)进⾏整合,需要在 beans 标签下添加一下内容

    <context:property-placeholder location="classpath:/db.properties"/>
    
  3. 将 Spring 的配置⽂件(applicationContext.xml)中的字符串改为${Key}的形式引入小配置文件(db.properties)的值。

    <bean id="conn" class="com.hihanying.factorybean.ConnectionFactoryBean">
        <property name="driveClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    

Maven 项目在编译时会将 java 目录和 resource 目录合并,因此 resource 目录中的配置文件和 com 目录同级。

4.3 ⾃定义类型转换器

背景

Student 类

package com.hihanying.conversion;
import java.util.Date;
public class Student {
    int number;
    String name;
    Date birthday;
	// Getter and Setter
}

applicationContext.xml 文件

<bean id="student" class = "com.hihanying.conversion.Student">
    <property name="number" value="1"/>
    <property name="name" value="China"/>
    <property name="birthday" value="1949/10/01"/>
</bean>

在上述 xml 文件中,我们通过 set 注入的方式给属性 number 赋值。value 的值默认是 String 类型,而 number 属性是 int 类型。Spring 通过类型转换器,将配置文件中的字符串类型的数据,转换成对象中成员变量对应类型的数据,进而完成注入。

问题

Spring框架内置了⽇期类型的转换器,但只是支持 yyyy/mm/dd 格式的日期,如果我们使用的是其他格式的日期呢?比如 yyyy-mm-dd

实现

当 Spring 内部没有提供特定类型转换器时,那么就需要程序员自己定义类型转换器。

  1. 继承 Converter 接⼝并实现 convert 方法,创建自定义对象转换器并注册

    package com.hihanying.conversion;
    import org.springframework.core.convert.converter.Converter;
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    public class dateConverter implements Converter<String, Date> {
        @Override
        public Date convert(String s) {
            Date date = null;
            try {
                SimpleDateFormat sdm = new SimpleDateFormat("yyyy-mm-dd");
                date = sdm.parse(s);
            } catch (ParseException e) {
                e.printStackTrace();
            }
            return date;
        }
    }
    
  2. 配置 applicationContext.xml 文件,

    <bean id="dateConverter" class="com.hihanying.conversion.dateConverter"/>
    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <ref bean="dateConverter"/>
            </set>
        </property>
    </bean>
    

细节

  1. 在 继承 Converter 接⼝并实现 convert 方法时,dateConverter类依赖于 "yyyy-mm-dd" 日期格式字符串,因此我们可以通过依赖注入的方式进行解耦,由配置文件完成赋值。

    public class dateConverter implements Converter<String, Date> {
        private String pattern;
        public String getPattern() {
            return pattern;
        }
        public void setPattern(String pattern) {
            this.pattern = pattern;
        }
        @Override
        public Date convert(String s) {
            Date date = null;
            try {
                SimpleDateFormat sdm = new SimpleDateFormat(pattern);
                date = sdm.parse(s);
            } catch (ParseException e) {
                e.printStackTrace();
            }
            return date;
        }
    }
    

    applicationContext.xml 文件

    <bean id="dateConverter" class="com.hihanying.conversion.dateConverter">
        <property name="pattern" value="yyyy-mm-dd"/>
    </bean>
    
  2. applicationContext.xml 文件中,ConversionSeviceFactoryBean 类的 id 属性值必须是 conversionService

  3. Spring 框架内置⽇期类型的转换器支持 yyyy/mm/dd 格式的日期。

4.4 后置处理 Bean

后置处理

BeanPostProcessor 接口:对 Spring ⼯⼚所创建的对象,进⾏再加⼯。

运行原理分析

image-20201005171501244

实际操作中很少处理 Spring 初始化操作,没必要区分初始化前后,通常只需要实现 postProcessAfterInitiallization 方法。

开发步骤

  1. Book 类

    package com.hihanying.postbean;
    public class Book {
        Integer id;
        String name;
    	// Getter and Setter
    }
    
  2. 编写 MyBeanPostProcessor 类继承 BeanPostProcessor 接口并实现其方法

    package com.hihanying.postbean;
    import org.springframework.beans.BeansException;
    import org.springframework.beans.factory.config.BeanPostProcessor;
    public class MyBeanPostProcessor implements BeanPostProcessor {
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            return bean;
        }
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            if (bean instanceof Book) {
                Book book = (Book) bean;
                book.setName("<活着>");
            }
            return bean;
        }
    }
    
  3. 配置文件 applicationContext.xml

    <bean id="book" class="com.hihanying.postbean.Book">
        <property name="id" value="1"/>
        <property name="name" value="《文化苦旅》"/>
    </bean>
    <bean id="myBeanPostProcessor" class="com.hihanying.postbean.MyBeanPostProcessor"/>
    

细节

MyBeanPostProcessor 会对 Spring 工厂中创建的所有对象进行加工,因此在实现 postProcessAfterInitialization 时,加了一条保护语句 if (bean instanceof Book)

5. Spring AOP 编程

5.1 静态代理设计模式

问题

在 JavaEE 分层开发中,最为重要的时 Service 层,在 Service 层中,主要包含核心功能的代码(包括业务运算、DAO调用等)和附加功能的代码(包括事务、日志、性能监控等),其中附加功能具有以下特点:不属于业务核心功能、可有可无、代码量较小等。

在 Service 层调用者(Controller)的角度来看,这些 Service 层附加功能是有必要的;而站在软件设计者的角度来看,Service 层的附加功能可有可无、易更改的特点不便于代码的维护。

解决方案:代理设计模式

  • 定义:给目标类提供一个代理类,并由代理对象控制对目标对象的引用。代理类可以为目标类增加附加功能。

  • 作用:间接访问目标类,防止直接访问目标类给系统带来的不必要复杂性,利于目标类的维护。

  • 代理类:

    • 代理类 = 目标类 + 附加功能
    • 代理类和目标类实现同样的接口
  • 静态代理:为每⼀个目标类编写⼀个代理类

静态代理代码实现

创建一个代理类实现 Service 接口,在代理类中创建 Service 接口的实现类对象,编写附加功能,调用实现类对象的对应方法。

image-20201006155112586

User 类:

package com.hihanying.spring5.aop.proxy;
public class User {
    String name;
    String password;
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
}

UserService 接口:

package com.hihanying.spring5.aop.proxy;
public interface UserService {
    void register(User user);
    boolean login(String name, String password);
}

UserServiceImpl 类

package com.hihanying.spring5.aop.proxy;
public class UserServiceImpl implements UserService {
    @Override
    public void register(User user) {
        System.out.println("UserServiceImpl.register");
    }
    @Override
    public boolean login(String name, String password) {
        System.out.println("UserServiceImpl.login");
        return false;
    }
}

UserServiceProxy 类

package com.hihanying.spring5.aop.proxy;
public class UserServiceProxy implements UserService {
    UserServiceImpl usi = new UserServiceImpl();
    @Override
    public void register(User user) {
        System.out.println("UserServiceProxy.register");
        usi.register(user);
    }
    @Override
    public boolean login(String name, String password) {
        System.out.println("UserServiceProxy.login");
        usi.login(name, password);
        return false;
    }
}

测试类

package com.hihanying.spring5.aop.proxy;
import org.junit.Test;
public class TestProxy {
    @Test
    public void test1() {
        UserService usp = new UserServiceProxy();
        usp.login("suns", "123456");
        usp.register(new User());
    }
}

输出:

UserServiceProxy.login
UserServiceImpl.login
UserServiceProxy.register
UserServiceImpl.register

静态代理问题分析

  • 每一个 Service 类都对应了一个代理类,导致类文件数量过多,不利于项目管理

  • 如果统一修改代理类中的附加功能只能一个一个修改,不利于代码维护

5.2 Spring的动态代理开发

动态代理的概念

通过代理类为原始类(⽬标类)增加额外功能

开发步骤

  1. 搭建开发环境

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>5.1.14.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.8.8</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.3</version>
    </dependency>
    
  2. 创建 Service 类的实现类(目标类)并在 applicationContext.xml 文件中配置

    package com.hihanying.spring5.aop.proxy;
    public class UserServiceImpl implements UserService {
        @Override
        public void register(User user) {
            System.out.println("UserServiceImpl.register");
        }
        @Override
        public boolean login(String name, String password) {
            System.out.println("UserServiceImpl.login");
            return false;
        }
    
    
    <bean id="userService" class="com.hihanying.spring5.aop.proxy.UserServiceImpl"/>
    
  3. 实现 MethodBeforeAdvice 接⼝的 before 方法,其中编写的附加功能将在目标方法执行之前执行,并在 applicationContext.xml 文件中配置

    package com.hihanying.spring5.aop.proxy;
    import org.springframework.aop.MethodBeforeAdvice;
    import java.lang.reflect.Method;
    public class Before implements MethodBeforeAdvice {
        @Override
        public void before(Method method, Object[] objects, Object o) throws Throwable {
            System.out.println("Before.before");
        }
    }
    
    <bean id="before" class="com.hihanying.spring5.aop.proxy.Before"/>
    
  4. 在 applicationContext.xml 文件中配置切入点(目标对象),并将其与附加功能(MethodBeforeAdvice 接口的实现)关联

    <aop:config>
        <aop:pointcut id="pc" expression="execution(* *(..))"/>
        <aop:advisor advice-ref="before" pointcut-ref="pc"/>
    </aop:config>
    

    其中, execution(* *(..)) 表示所有的目标对象

  5. 测试

    public void test2() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = (UserService) ctx.getBean("userService");
        userService.login("suns", "123456");
        userService.register(new User());
    }
    

    注意:

    • Spring的⼯⼚通过目标对象的id值获得的是代理对象
    • 获得代理对象后,可以通过声明接⼝类型,进⾏对象的存储

    输出

    Before.before
    UserServiceImpl.login
    Before.before
    UserServiceImpl.register
    

细节分析

  1. MethodBeforeAdvice 分析
    • 作⽤:附加功能运⾏在目标⽅法执⾏之前,进⾏附加功能操作。
    • before 方法的参数:
      • Method method:目标方法
      • Object[] args:目标方法的参数
      • Object target:目标方法所在的目标对象
  2. Spring 创建的动态代理类在哪⾥?
    • Spring框架在运⾏时,通过动态字节码技术,在JVM创建的运⾏在JVM内部,等程序结束后会和JVM⼀起消失
    • 动态字节码技术:通过第三方动态字节码框架(ASM、Javassist、Cglib等),在JVM中创建对应类的字节码,进⽽创建对象,当虚拟机结束,动态字节码跟着消失。
    • 动态代理不需要定义代理类⽂件,都是JVM运⾏过程中动态创建的,所以不会产生代理类⽂件数量过多问题。
  3. Spring 动态代理的好处
    • 当附加功能不变而增加其应用的目标对象时,只需要创建增加的目标对象并在配置文件中重新配置 aop:pointcu 即可。
    • 当附加功能改变而其应用的目标对象不变时,只需要编写更新的附加功能并在配置文件中重新配置 aop:advisor 即可。

5.3 Spring动态代理详解

在 5.2 节中,动态代理的开发步骤包括:创建目标对象、实现 MethodBeforeAdvice 接口的 before 方法、配置切入点以及将其与 before 进行关联。

MethodInterceptor 接口

**问题:**在实现 MethodBeforeAdvice 接口的 before 方法时,我们发现在 before 方法中添加的内容只能运行在目标方法之前,那如果我们想要将附加功能运行在目标方法之后、或者环绕目标方法、甚至在目标方法抛出异常时呢?

解决:

实现 MethodInterceptor 接口

package com.hihanying.spring5.aop.dynamic;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class Arround implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("运行在目标方法之前的附加功能");
        Object o = null;
        try {
            o = methodInvocation.proceed();
        } catch (Throwable throwable) {
            System.out.println("运行在目标方法抛出异常时的附加功能");
            throwable.printStackTrace();
        }
        System.out.println("运行在目标方法之后的附加功能");
        return o;
    }
}
  • 在实现 MethodInterceptor 接口过程中,重写其 invoke 方法,该方法传入的参数 methodInvocation 就是目标方法

  • methodInvocation.proceed(); 表示执行目标方法,因此在该语句前的代码就会运行在目标方法之前,反之,在目标代码之后运行。

  • 捕获 methodInvocation.proceed(); 语句执行时所抛出的异常,并提供相应的附加功能。为了测试捕获异常时运行的附加功能,可以修改 com.hihanying.spring5.aop.proxy.UserServiceImpl#register 方法

    @Override
    public void register(User user) {
        System.out.println("UserServiceImpl.register");
        throw new RuntimeException("测试异常");
    }
    

配置 applicationContext.xml 文件

<bean id="arround" class="com.hihanying.spring5.aop.dynamic.Arround"/>
<aop:config>
    <aop:pointcut id="pc" expression="execution(* *(..))"/>
    <aop:advisor advice-ref="arround" pointcut-ref="pc"/>
</aop:config>

沿用 5.2 章节的测试代码,测试输出

运行在目标方法之前的附加功能
UserServiceImpl.login
运行在目标方法之后的附加功能
运行在目标方法之前的附加功能
UserServiceImpl.register
运行在目标方法抛出异常时的附加功能
java.lang.RuntimeException: 测试异常

注意

  1. 什么样的额外功能运⾏在原始⽅法执⾏之前,之后都要添加?

    事务的 begin、commit

  2. MethodInterceptor 接口的实现可以影响目标⽅法的返回值,而MethodInterceptor不会。

    在重写 invoke 方法时,该方法的返回值可以覆盖目标方法的返回值。

切入点详解

在 5.2 章节中配置切入点时,我们使用了如下 xml 语句

<aop:pointcut id="pc" expression="execution(* *(..))"/>

其中,execution 是一种切入点函数、而 * *(..) 是切入点表达式,表示所有的方法

切入点表达式

切入点表达式由三部分组成,三部分分别如下所示,而其中的 * 以及 .. 表示不限

*    			*  (..)
修饰符和返回值 方法名 参数列表
  1. 方法切入点

    • 指定名为 login 方法为切入点:execution(* login(..))

    • 指定仅有两个字符串类型的参数的名为 login ⽅法为切⼊点:execution(* login(String, String))

    • 指定仅有一个 User 类型参数的名为 register 方法为切入点,由于 User 属于非 java.lang 包中的类型,需写全限定类名:

      execution(* register(com.hihanying.spring5.aop.proxy.User))

    • 指定第一个参数为字符串类型的名为 login ⽅法为切⼊点:execution(* login(String..))

    • 指定精确位置的 login 方法为切入点:execution(* com.hihanying.spring5.aop.proxy.UserServiceImpl.login(..))

  2. 类切入点

    指定特定的类作为切入点,表示类中的所有方法都会添加对应的附加功能。指定某类中所有方法为切入点:

    execution(* com.hihanying.spring5.aop.proxy.UserServiceImpl.*(..))

    • 如果该类是在一级包下,可以简写为:execution(* *.UserServiceImpl.*(..))
    • 如果该类在多级包下,可以简写为:execution(* *..UserServiceImpl.*(..))
  3. 包切入点

    指定特定的包作为切入点,表示包中的所有类及其方法都会添加对应的附加功能。

    • 如果不包括该包中的子包中的类:execution(* com.hihanying.spring5.aop.proxy.*.*(..))
    • 如果包括该包中的子包中的类:execution(* com.hihanying.spring5.aop.proxy..*.*(..))

切入点函数

切入点函数用于执行切入点表达式

  1. execution 是功能最全的切入点函数,但切入点表达式书写复杂,其他的切入点函数主要简化书写复杂度

  2. args 主要用于方法参数的匹配:args(String, String) 等价于execution(* login(String, String))

  3. within 主要用于进行包、类切入点的表达式的匹配

    • within(*..UserServiceImpl) 等价于 execution(* *..UserServiceImpl.*(..))
    • within(com.hihanying.spring5.aop.proxy..*) 等价于 execution(* com.hihanying.spring5.aop.proxy..*.*(..))
  4. @annotation 指定具有特殊注解的⽅法为切入点:expression="@annotation(com.hihanying.spring5.Log)"

    自定义注解

    package com.hihanying.spring5;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    @Target(ElementType.METHOD)     	// 用在方法上
    @Retention(RetentionPolicy.RUNTIME) // 在什么时候起作用
    public @interface Log {
    }
    

切入点函数可以进行逻辑运算

  1. and 表示与,通常用于同种切入点函数

    指定名为 login 且参数为 2 个字符串的方法为切入点:execution(* login(..)) and args(String,String)

  2. or 表示或

    指定名为 register 或名为 login 的⽅法作为切⼊点:execution(* login(..)) or execution(* register(..))

5.4 Spring AOP

AOP 概念

面向对象编程(Object-oriented Programming,OOP)是指以对象为基本单位的程序开发,通过对象间的彼此协同,相互调⽤,完成程序的构建。而面向切面编程(Aspect-oriented Programming,AOP)是指以切⾯为基本单位的程序开发,通过切⾯间的彼此协同,相互调⽤,完成程序的构建。

  • Aspect(切面):跨越多个类(class)的关注点的模块化,例如:事务管理的模块化可以跨越多种类型和对象。

    image-20201008152143642

  • Join point(连接点):应用程序的程序执行中的候选点,例如方法的执行或异常的处理。所有的 Join point 都可以插入Aspect

  • Advice(附加功能):Aspect 在指定 Joint point 处执行的操作,可以分为 “around”, “before” and “after” advice。大多数的 AOP 框架都会将 advice 建模为 interceptor (拦截器)

  • Pointcut(切入点):一组特定的 join point。Advice 与 Pointcut 表达式相关联,并在切入点表达式指定的 join point 上运行。

  • Introduction:

  • Target object(目标对象):要由 Aspects 加入 Advice 的对象。

  • AOP proxy(代理):在 Spring 框架中,代理包括 JDK 动态代理或 CGLIB 代理。

  • Weaving:

Advice 的类型

  • Before advice: 在 join point 之前执行但不会阻止执行流程继续进行到连接点的 Advice
  • After returning advice: 在 join point 正常完成(不引发异常)之后执行的 Advice
  • After throwing advice: 在抛出异常时执行的 Advice
  • After (finally) advice: 无论 join point 的退出方式如何均会执行的 Advice
  • Around advice: 表示围绕连接点的 Advice 也是功能最全面的 Advice 类型,它可以在目标方法之前或之后执行,还可以改变返回值。

尽管 Spring AOP提供了各种 Advice 类型,但推荐使用功能专一的 Advice 类型,而不是都使用 Around advice,以便减少出错的可能性。

总结

  • AOP 本质就是 Spring 的动态代理开发,通过代理类为目标类加入附加功能。这利于目标类的维护。
  • Pointcut 使 Advice 的目标独立于面向对象的层次结构。例如,可以将提供声明性事务管理的 Around advice 应用于跨越多个对象(例如服务层中的所有业务操作)的一组方法。因此,AOP 是 OOP 的有效补充。

5.5 AOP的底层实现原理

5.4 节中讲到 Spring AOP 的本质是 Spring 的动态代理开发,那要解决的核心问题是:

  1. AOP 如何创建动态代理类:动态字节码技术
  2. Spring 工厂如何创建代理对象:通过目标对象的 id 值获得的是目标对象的代理对象

1. 创建动态代理类的两种方式

JDK 的动态代理

第一步,需要创建目标对象:

UserService userService = new UserServiceImpl();

使用 JDK 进行动态代理对象的创建需要借助 java.lang.reflect.Proxy 的 newProxyInstance 方法,通过阅读源码发现有三个参数:

  • ClassLoader loader:类加载器,可以将用一个类加载器创建代理类的 Class 对象,进而创建代理对象

    类加载器的作用:

    1. 类加载器可以把对应类的字节码(.class)文件加载进 JVM
    2. 类加载器根据字节码文件创建类的 Class 对象,进而创建这个类的对象

    每一个类的 .class 文件都会自动分配与之对应的类加载器

    JVM 如何创建动态代理类的对象:

    1. 通过动态字节码技术(Proxy.newProxyInstance 方法)创建动态代理类的字节码文件并直接写在 JVM 中
    2. 因为动态代理类没有.class文件,JVM 也就没有分配与之对应的类加载器,只能借用一个类加载器来创建动态代理类的 Class 对象,进而创建这个类的对象
  • Class<?>[] interfaces:接口代理类要实现的接口列表,即目标类实现的接口

  • InvocationHandler h:表示当动态代理对象调用方法的时候会被转发到实现该 InvocationHandler 接口类的 invoke 方法来调用,简而言之,附加功能写在实现 InvocationHandler 接口类的 invoke 方法中。

@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h) {
    Objects.requireNonNull(h);
    final Class<?> caller = System.getSecurityManager() == null ? null : Reflection.getCallerClass();
    //Look up or generate the designated proxy class and its constructor.
    Constructor<?> cons = getProxyConstructor(caller, loader, interfaces);
    return newProxyInstance(caller, cons, h);

第二步,需要创建 InvocationHandler 接口的实现类来重写其 invoke 方法,该方法具有以下参数:

  • Object proxy:代理类代理的真实代理对象com.sun.proxy.$Proxy0(忽略)
  • Method method:目标类的 Method 类对象
  • Object[] args:目标方法的参数

为了确定附加功能所插入的位置,我们需要在 invoke 方法中执行方法,参数中已经包含了目标类的 Method 类对象以及目标方法的参数,因此我们可以通过反射执行目标类的方法:method.invoke(userService, args);

InvocationHandler handler = new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("执行目标方法之前插入的附加功能");
        Object ret = method.invoke(userService, args);
        return ret;
    }
};

第三步,Proxy.newProxyInstance() 方法的三个参数都已经得到,因此只需调用并返回一个代理对象并测试即可

UserService userServiceProxy = (UserService) Proxy.newProxyInstance(TestJdkProxy.class.getClassLoader(), UserServiceImpl.class.getInterfaces(), handler);
userServiceProxy.register(new User());
userServiceProxy.login("suns", "123456");

其中第2个参数,也可以写作:userService.getClass().getInterfaces()

输出

执行目标方法之前插入的附加功能
UserServiceImpl.register
执行目标方法之前插入的附加功能
UserServiceImpl.login

CGlib 的动态代理

通过⽗⼦继承关系创建代理对象,目标类作为⽗类,代理类作为⼦类,这样既可以保证两者⽅法⼀致,同时在代理类中提供新的实现:附加功能 + 原始⽅法(调用父类的方法 super.,method)

CGlib 的核心类是 Enhancer,通过 setXxx 方法配置 Enhancer 对象,最后通过 create() 方法创建对象。

  • setClassLoader():借用一个类加载器
  • setSuperclass():目标类的 Class 对象
  • setCallback():附加功能,实现 MethodInterceptor 接口
package com.hihanying.spring5.aop.cglib;

import com.hihanying.spring5.aop.jdk.TestJdkProxy;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class TestCglib {
    public static void main(String[] args) {
        // 1. 创建目标对象
        UserService userService = new UserService();
        // 2. 配置三要素
        Enhancer enhancer = new Enhancer();
        enhancer.setClassLoader(TestJdkProxy.class.getClassLoader());
        enhancer.setSuperclass(userService.getClass());
        MethodInterceptor interceptor = new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                System.out.println("执行目标方法之前插入的附加功能");
                return method.invoke(userService, objects);
            }
        };
        enhancer.setCallback(interceptor);
        
		// 3. 创建动态代理对象
        UserService userServiceProxy = (UserService) enhancer.create();
        userServiceProxy.register(new User());
        userServiceProxy.login("suns", "123456");
    }
}

创建动态代理对象的方式:

  1. JDK动态代理通过接⼝创建代理的实现类 Proxy.newProxyInstance()
  2. Cglib动态代理通过继承⽗类创建的代理类 Enhancer 类的对象的 create 方法

2. 如何加工原始对象

在 4.4 章节中,我们介绍了后置处理 Bean,通常加工原始对象的方式是在后置处理 Bean 中,编写 ProxyBeanPostProcessor类继承 BeanPostProcessor 接口并实现其方法,在 postProcessAfterInitialization 方法中创建动态代理对象并返回。

image-20201008223953008

代码:

后置处理:ProxyBeanPostProcessor 类实现 BeanPostProcessor 接口

package com.hihanying.spring5.aop.factory;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

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

public class ProxyBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("在后置处理 Bean 中创建动态代理对象添加的附加功能");
                return method.invoke(bean, args);
            }
        };
        return Proxy.newProxyInstance(ProxyBeanPostProcessor.class.getClassLoader(),
                bean.getClass().getInterfaces(),
                handler);
    }
}

配置文件:applicationContext1.xml

<bean id="userService" class="com.hihanying.spring5.aop.factory.UserServiceImpl"/>
<bean id="proxyBeanPostProscessor" class="com.hihanying.spring5.aop.factory.ProxyBeanPostProcessor"/>

测试类:

package com.hihanying.spring5.aop.factory;

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

public class Test {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext1.xml");
        UserService userService = (UserService) ctx.getBean("userService");
        userService.register(new User());
        userService.login("suns", "123456");
    }
}

5.6 基于注解的AOP编程

开发步骤

创建切面类

  • @Aspect 表示这个类是一个切面类
  • @Around("execution(* login(..))") 表示将该方法(附加功能)插入到切入点
package com.hihanying.spring5.aop.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class MyAspect {
    @Around("execution(* login(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("附加功能");
        Object ret = joinPoint.proceed();
        return ret;
    }
}

配置文件

  • <aop:aspectj-autoproxy/>:表示开启Spring基于注解进⾏AOP编程
<bean id="userService" class="com.hihanying.spring5.aop.factory.UserServiceImpl"/>
<bean id="around" class="com.hihanying.spring5.aop.aspect.MyAspect"/>
<aop:aspectj-autoproxy/>

测试类

package com.hihanying.spring5.aop.aspect;

import com.hihanying.spring5.aop.factory.User;
import com.hihanying.spring5.aop.factory.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestAspectProxy {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = (UserService) ctx.getBean("userService");
        userService.register(new User());
        userService.login("suns", "123456");
    }
}

输出

UserServiceImpl.register
附加功能
UserServiceImpl.login

细节

切入点复用:在切⾯类中定义⼀个方法,使用@Pointcut注解,定义切⼊点表达式。

package com.hihanying.spring5.aop.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class MyAspect {
    @Pointcut("execution(* login(..))")
    public void myPointcut(){}
    
    @Around(value = "myPointcut()")
    public Object around1(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("附加功能1");
        Object ret = joinPoint.proceed();
        return ret;
    }
    
    @Around(value = "myPointcut()")
    public Object around2(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("附加功能2");
        Object ret = joinPoint.proceed();
        return ret;
    }

}

默认情况下底层应⽤ JDK 动态代理创建⽅式,如果切换 Cglib:

  • 基于注解AOP开发:<aop:aspectj-autoproxy proxy-target-class="true" />
  • 传统的AOP开发: <aop:config proxy-target-class="true"></aop>

5.7 AOP开发的问题

在同⼀个业务类中,进⾏业务⽅法间的相互调⽤,只有最外层的⽅法,才是加⼊了附加功能(内部的⽅法,通过普通的⽅式调⽤,都调⽤的是目标⽅法)。如果想让内层的⽅法也调⽤代理对象的⽅法,就要使目标类实现 AppicationContextAware 接口获得⼯⼚,进⽽获得代理对象。

public class UserServiceImpl implements UserService,
ApplicationContextAware {
    private ApplicationContext ctx;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.ctx = applicationContext;
    }
    @Log
    @Override
    public void register(User user) {
        System.out.println("UserServiceImpl.register");
        // 内部业务的互相调用
        // this.login("suns", "123456");
        // 更改为从工厂中获取对象
        UserService userService = (UserService)
            ctx.getBean("userService");
        userService.login("suns", "123456");
    }
    @Override
    public boolean login(String name, String password) {
        System.out.println("UserServiceImpl.login");
        return true;
    }
} 

AOP 总结

image-20201008233412766

END