Spring(上)

147 阅读22分钟

Spring

第一章 简介

1.1 Spring概述

Spring是一个为简化企业级开发而生的开源框架。使用Spring开发可以将Bean对象,Dao组件对象,Service组件对象等交给Spring容器来管理,这样使得很多复杂的代码在Spring中开发却变得非常的优雅和简洁,有效的降低代码的耦合度,极大的方便项目的后期维护、升级和扩展。

Spring是一个IOC(DI)和AOP容器框架。

Spring的优良特性:

1) 非侵入式:基于Spring开发的应用中的对象可以不依赖于Spring的API。

2) 控制反转:IOC——Inversion of Control,指的是将对象的创建权交给Spring去创建。使用Spring之前,对象的创建都是由我们自己在代码中new创建。而使用Spring之后。对象的创建都是由给了Spring框架。

3) 依赖注入:DI——Dependency Injection,是指依赖的对象不需要手动调用setXXX方法去设置,而是通过配置赋值。

4) 面向切面编程:AOP——Aspect Oriented Programming,在不修改源代码的基础上进行功能扩展。

5) 容器:Spring是一个容器,因为它包含并且管理应用对象的生命周期。

6) 组件化:Spring实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用XML和Java注解组合这些对象。

7) 一站式:在IOC和AOP的基础上可以整合各种企业应用的开源框架和优秀的第三方类库(实际上Spring 自身也提供了表述层的SpringMVC和持久层的JDBCTemplate)。

1.2 Spring模块

Spring框架分为五大模块:

1) Core Container:核心容器

  • Beans提供 BeanFactory,它是一个工厂模式的复杂实现。
  • Core提供了框架的基本组成部分,包括IOC和DI功能。
  • Context建立在由核心和 Bean 模块提供的基础上,它是访问定义和配置的任何对象的媒介。ApplicationContext 接口是上下文模块的重点。
  • SpEL在运行时提供了查询和操作一个对象的强大的Spring表达式语言。

2) AOP&Aspects:提供了面向切面编程的实现。

3) DataAccess/Integration:提供了对数据访问/集成的功能。

4) Web:提供了面向Web应用程序的集成功能。

5) Test:提供了对JUnit 或 TestNG 框架的测试功能。

1.3 HelloWorld

创建HelloWorld的步骤如下:

1) 创建Java工程,并导入以下jar包

<!--导入spring-context-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.1</version>
</dependency>
<!--导入junit4.12-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

2) 创建Java类HelloWorld

  • 声明一个name属性
  • 提供setName方法
  • 创建sayHello方法
public class HelloWorld {
    private String name;
​
    public void setName(String name) {
        this.name = name;
    }
​
    public void sayHello(){
        System.out.println("Hello :"+name);
    }
}
​

3) 在src目录下创建Spring Config配置文件,并命名为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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
​
    <!--配置bean
        id属性:设置bean的唯一标识
        class属性:设置类的全类名,IOC容器通过反射创建对象
    -->
    <bean id="helloWorld" class="com.atguigu.spring.helloworld.HelloWorld">
        <property name="name" value="Spring"></property>
    </bean>
</beans>

4) 创建单元测试类HelloWorldTest

public class HelloWorldTest {
    /*
       测试HelloWorld
    */
        @Test
    public void testHelloWorld(){
        //1.创建IOC容器对象
        ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
        //2.获取IOC容器中的HelloWorld对象
        HelloWorld helloWorld = (HelloWorld) ioc.getBean("helloWorld");
        //3.调用sayHello方法
        helloWorld.sayHello();
    }
}
​
​
  • 目的

    • 创建Spring的IOC容器
    • 让容器帮助我们创建一个对象
    • 让容器为这个对象中的某个属性进行赋值
  • 步骤

    • 导入Spring的Jar包
    • 创建一个类,并且设置一些属性
      • 创建Spring的配置文件

        • <!--1. 创建Employee对象
              bean标签
                  id属性: 自定义唯一标识
                  class属性:  你想创建对象的全类名
          -->
          <bean id="emp01" class="com.atguigu.bean.Employee">
              <!--为属性赋值:set注入-->
              <property name="id" value="101"></property>
              <property name="name" value="张三"></property>
              <property name="gender" value="1"></property>
              <property name="salary" value="8888"></property>
          </bean><bean id="emp02" class="com.atguigu.bean.Employee">
              <property name="id" value="102"></property>
              <property name="name" value="李四"></property>
          </bean>
          
      • 测试

        • @Test
          public void test01(){
              //1. 创建IOC容器
              ApplicationContext ioc=new ClassPathXmlApplicationContext("applicationContext.xml");
              //2. 获取到IOC容器中的对象
              Employee emp01 = (Employee) ioc.getBean("emp01");
              System.out.println("emp01 = " + emp01);
          ​
              Employee emp02 = (Employee) ioc.getBean("emp02");
              System.out.println("emp02 = " + emp02);
          }1.4 IOC和DI
          

1.4 IOC和DI

1.4.1 IOC(Inversion of Control):反转控制

在应用程序中的组件需要获取资源时,传统的方式是组件主动的从容器中获取所需要的资源,在这样的模式下开发人员往往需要知道在具体容器中特定资源的获取方式,增加了学习成本,同时降低了开发效率。

反转控制的思想完全颠覆了应用程序组件获取资源的传统方式:反转了资源的获取方向——改由容器主动的将资源推送给需要的组件,开发人员不需要知道容器是如何创建资源对象的,只需要提供接收资源的方式即可,极大的降低了学习成本,提高了开发的效率。这种行为也称为查找的被动形式。

1.4.2 DI(Dependency Injection):依赖注入

IOC的另一种表述方式:即组件以一些预先定义好的方式(例如:setter 方法)接受来自于容器的资源注入。相对于IOC而言,这种表述更直接。

总结: IOC 就是一种反转控制的思想, 而DI是对IOC的一种具体实现。

1.4.3 IOC容器在Spring中的实现

在创建Bean之前,首先需要创建IOC容器。Spring提供了IOC容器的两种实现方式:

1) BeanFactory:IOC容器的基本实现,是Spring内部的使用接口,是面向Spring本身的,不是提供给开发人员使用的。

2) ApplicationContext:BeanFactory的子接口,提供了更多高级特性。面向Spring的使用者,几乎所有场合都使用ApplicationContext而不是底层的BeanFactory。

1.4.4 ApplicationContext的主要实现类

1) ClassPathXmlApplicationContext:对应类路径下的XML格式的配置文件

2) FileSystemXmlApplicationContext:对应文件系统中的XML格式的配置文件

3) ConfigurableApplicationContext:是ApplicationContext的子接口,包含一些扩展方法refresh()和close(),让ApplicationContext具有启动、关闭和刷新上下文的能力。

4) WebApplicationContext:专门为WEB应用而准备的,它允许从相对于WEB根目录的路径中完成初始化工作。

1.5 获取bean的方法

1) 根据bean的id获取

  • 需要强转
  HelloWorld helloWorld = (HelloWorld)ioc.getBean("helloWorld");  

2) 根据bean的类型获取

  • 如果在IOC容器中有多个该类型的bean则会抛出异常
  HelloWorld helloWorld =  ioc.getBean(HelloWorld.class);     

3) 根据bean的id和类型获取

  HelloWorldhelloWorld=ioc.getBean("helloWorld",HelloWorld.class;   

第二章 基于XML的方式管理Bean

2.1 bean对象的创建

让Spring的IOC容器帮我们创建Bean,只需要在Spring 的配置文件中通过bean标签配置即可。

bean标签中常用属性说明:

1) Id属性:给IOC容器中的bean设置的唯一标识。

2) class属性:配置让IOC容器管理的类的全类名,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">
​
    <bean id="book" class="com.atguigu.spring.beans.Book"></bean>
</beans>

2.2 给bean的属性赋值

2.2.1 set注入 ★

  • 调用set方法实现属性的赋值
  • <bean id="emp01" class="com.atguigu.bean.Employee">
        <!--为属性赋值:set注入
            property标签:
                name属性:设置属性名(原理是调用set方法)
                value属性: 普通类型属性值
                ref属性:  引用类型属性值
        -->
        <property name="id" value="101"></property>
        <property name="name" value="张三"></property>
        <property name="gender" value="1"></property>
        <property name="salary" value="8888"></property>
        <!--为department注入数据 : set注入-->
        <property name="department" ref="dept01"></property>
    </bean>
    

2.2.2 构造器注入

  • 调用构造器实现属性的赋值
  • <bean id="emp03" class="com.atguigu.bean.Employee">
        <!--采用构造器注入
            constructor-arg标签
                value属性:  设置值
                ref属性:    设置其他对象的id值
                name属性:   设置名字
                index属性:   设置索引
                type属性:   设置类型
        -->
        <constructor-arg value="103" index="0"></constructor-arg>
        <constructor-arg value="1" index="2"></constructor-arg>
        <constructor-arg value="王五" index="1"></constructor-arg>
        <constructor-arg value="6666" index="3"></constructor-arg>
        <constructor-arg ref="dept01" index="4"></constructor-arg>
    </bean>
    

2.2.3 特殊值

  • 字面量

    • 普通的数据
  • 赋null值

    • 通过
  • 特殊字符

    • 有可能破坏xml的结构

    • 通过实体兑换

    • CDATA区域

      • <property name="name">
            <value><![CDATA[<法务部>]]></value>
        </property>
        

2.2.4 p名称空间

<bean id="emp04" class="com.atguigu.bean.Employee" p:id="104" p:name="赵六" p:gender="0" p:salary="8888" p:department-ref="dept01"></bean>

2.2.5 引用外部已声明的bean ★

  • ref
<!--为department注入数据 : set注入-->
<property name="department" ref="dept01"></property>

2.2.6 内部bean

  • 只能当前对象使用,其他对象无法使用

    • <!--为department注入数据 : set注入-->
      <property name="department">
          <bean class="com.atguigu.bean.Department">
              <property name="id" value="2"></property>
              <property name="name" value="董事会"></property>
          </bean>
      </property>
      

3.2.7 级联属性赋值

<bean id="emp05" class="com.atguigu.bean.Employee">
    <property name="id" value="105"></property>
    <property name="name" value="田七"></property>
    <!--department赋值-->
    <property name="department" ref="dept01"></property>
    <!--修改dept01对象的属性值:会对dept01对象产生影响-->
    <property name="department.id" value="10"></property>
    <property name="department.name" value="法务部Plus"></property>
</bean>

2.2.8 集合类型属性赋值 ★

<bean id="stu01" class="com.atguigu.bean.Student">
    <property name="id" value="1001"></property>
    <property name="name" value="马云"></property>
    <!--数组类型的属性-->
    <property name="scores">
        <array>
            <value>60</value>
            <value>59</value>
            <value>61</value>
            <value>99</value>
        </array>
    </property>
    <!--List<String>集合类型属性-->
    <property name="hobbys">
        <list>
            <value>演讲</value>
            <value>唱歌</value>
            <value>拍电影</value>
            <value>跳舞</value>
        </list>
    </property>
    <!--List<Car>-->
    <property name="cars" ref="carList01">
        <!--<list>
            <bean class="com.atguigu.bean.Car">
                <property name="id" value="1"></property>
                <property name="brand" value="拖拉机"></property>
                <property name="price" value="30000"></property>
            </bean>
            <ref bean="car01"></ref>
            <ref bean="car02"></ref>
        </list>-->
    </property>
    <!--Map<String,String>-->
    <property name="hobbyMap">
        <map>
            <entry key="hobby01" value="阿里"></entry>
            <entry key="hobby02" value="淘宝"></entry>
            <entry key="hobby03" value="天猫"></entry>
            <entry key="hobby04" value="支付宝"></entry>
            <entry key="hobby05" value="蚂蚁金服"></entry>
        </map>
    </property>
    <!--Map<String,Car>-->
    <property name="carMap">
        <map>
            <entry key="car001" value-ref="car01"></entry>
            <entry key="car002" value-ref="car02"></entry>
            <entry key="car003">
                <bean class="com.atguigu.bean.Car">
                    <property name="id" value="3"></property>
                    <property name="brand" value="二八大杠"></property>
                    <property name="price" value="50"></property>
                </bean>
            </entry>
        </map>
    </property>
    <!--Set<Car>-->
    <property name="setCar">
        <set>
            <bean class="com.atguigu.bean.Car">
                <property name="id" value="4"></property>
                <property name="brand" value="捷达"></property>
                <property name="price" value="80000"></property>
            </bean>
            <ref bean="car01"></ref>
            <ref bean="car02"></ref>
        </set>
    </property>
    <!--Properties-->
    <property name="properties">
        <props>
            <prop key="driverClassName">com.mysql.cj.jdbc.Driver</prop>
            <prop key="username">root</prop>
        </props>
    </property>
</bean><bean id="car01" class="com.atguigu.bean.Car">
    <property name="id" value="2"></property>
    <property name="brand" value="雅迪"></property>
    <property name="price" value="2000"></property>
</bean>
<bean id="car02" class="com.atguigu.bean.Car">
    <property name="id" value="3"></property>
    <property name="brand" value="五菱宏光"></property>
    <property name="price" value="50000"></property>
</bean>

2.2.9 集合类型的bean

<!--将集合提取出来-->
<util:list id="carList01">
    <bean class="com.atguigu.bean.Car">
        <property name="id" value="1"></property>
        <property name="brand" value="拖拉机Plus"></property>
        <property name="price" value="40000"></property>
    </bean>
    <ref bean="car01"></ref>
    <ref bean="car02"></ref>
</util:list>
  • 名称空间

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

2.2.10 自动装配

  • byType

    • 根据类型进行自动装配
  • byName

    • 根据名字进行自动装配
<!--
        bean标签
            autowire属性:根据类型进行装配
                byType根据类型进行装配,要求ioc容器中只有一个目标对象
                byName根据名字进行装配,属性名和id属性值一样则可以装配成功
    -->
    <bean id="emp01" class="com.atguigu.bean.Employee" autowire="byName">
        <property name="name" value="贾跃亭"></property>
        <!--department-->
<!--        <property name="department" ref="dept01"></property>-->
    </bean>

2.2.11 引入外部属性文件 ★

  • properties文件

    • db.properties
<!--加载外部属性文件-->
<context:property-placeholder location="db.properties"></context:property-placeholder><!--使用数据库连接池,Druid-->
<!--创建数据库连接池对象
    步骤:
        导入jar包
            mysql驱动
            druid的jar包
        创建数据库连接池对象
-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <!--为属性赋值-->
    <property name="driverClassName" value="${jdbc.driverClassName}"></property>
    <property name="url" value="${jdbc.url}"></property>
    <property name="username" value="${jdbc.username}"></property>
    <property name="password" value="${jdbc.password}"></property>
</bean>

3.2.12 FactoryBean

  • 功能:将自己new的对象,添加到ioc容器内

  • 步骤

    • 创建一个类

    • 实现FactoryBean接口

    • 将类配置到ioc容器内

      • 原理:将getObject方法的返回值添加到ioc容器内

2.3bean的作用域

  • singleton

    • 单实例(默认值)

    • 对象什么时候创建?

      • 在ioc容器创建的时候创建
  • prototype

    • 多实例

    • 对象什么时候创建?

      • 在getBean的时候创建
<!--
    bean的作用域
       scope 属性:
        singleton 单实例(默认值)  ★
        prototype  多实例
-->
<bean id="dept01" class="com.atguigu.bean.Department" scope="prototype">
    <property name="id" value="101"/>
    <property name="name" value="财务部"/>
</bean>

2.3 bean的生命周期

3.1 基本的生命周期 对象被创建 属性被赋值 对象可以使用 3.2 添加初始化和销毁的方法 ★ 添加步骤: 在类中创建两个方法 在bean标签内指定哪个是初始化,哪个是销毁 生命周期流程 对象被创建 属性被赋值 初始化方法执行 对象可以使用 ioc容器关闭时,执行销毁方法 3.3 配置后置处理器 配置步骤: 创建一个类 实现接口BeanPostProcessor 重写方法 将这个后置处理器,配到ioc容器内 生命周期流程 对象被创建 属性被赋值 处理器的before方法 初始化方法执行 处理器的after方法 对象可以使用 ioc容器关闭时,执行销毁方法 练习 创建web阶段的三层(Servlet/Service/Dao) 模拟登录操作 单元测试充当客户端

第三章 基于注解的方式管理Bean

3.1 注解版bean的管理

3.1.1 bean对象的创建 ★

  • @Component

  • @Controller

  • @Service

  • @Repository

  • 条件:需要设置注解扫描包(配置文件内设置)

    • <!--条件:注解扫描包
          context:component-scan标签
              base-package属性: 设置一个包名
      -->
      <context:component-scan base-package="com.atguigu.bean"/>
      

3.1.2 bean属性的赋值 ★

  • 自动装配

    /**
         * @Autowired
         *      功能:自动装配
         *      原理:会从ioc容器内寻找类型匹配的对象
         *          ① 找不到   会报错
         *              必须让他找到 ★
         *              找不到就赋值为null,@Autowired(required = false)
         *          ② 找到一个   成功
         *          ③ 找到多个,还会从多个寻找名字相同
         *              能找到   成功
         *              找不到  会报错
         *                  能否从多个中指定一个名字,可以,通过@Qualifier("userDaoImpl02")指定
         *      位置:
         *          属性上,可以没有set方法  ★
         *          set方法上(官方推荐)
         *          构造器上
         * @Resource
         *      来自于JDK
         *          可以和Autowired完成相同的功能
         *          区别:在指定id值的时候,不需要额外添加注解,使用name属性指定
         */
        @Qualifier("userDaoImpl02")
        @Autowired
    //    @Resource
        private UserDao userDao;
    ​
       /* public void setUserDao(UserDao userDao) {
            this.userDao = userDao;
        }
        @Autowired
        public UserServiceImpl(UserDao userDao) {
            this.userDao = userDao;
        }*/
    

@Value装配普通属性

3.1.3 扫描包的排除和包含

<!--注解版管理bean-->
    <!--条件:注解扫描包
        context:component-scan标签
            base-package属性: 设置一个包名
            设置扫描包的排除和包含
    -->
    <context:component-scan base-package="com.atguigu" use-default-filters="false">
        <!--扫描包的排除
            context:exclude-filter标签
                type属性
                    annotation:根据注解的类型排除
                    assignable: 根据接口类型排除
                expression属性
                    注解的全类名
                    接口的全类型
        -->
<!--        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>-->
<!--        <context:exclude-filter type="assignable" expression="com.atguigu.dao.UserDao"/>-->
​
        <!--
                context:include-filter标签
                    type属性
                        annotation:根据注解的类型包含
                        assignable: 根据接口类型包含
                    expression属性
                        注解的全类名
                        接口的全类型
                条件:需要在context:component-scan添加属性use-default-filters设置为false
        -->
        <context:include-filter type="assignable" expression="com.atguigu.dao.UserDao"/>
    </context:component-scan>

3.1.4完全注解开发

/**
 * @Configuration  标记当前类是配置类
 * @ComponentScan  设置扫描包
 */
@ComponentScan(basePackages = {"com.atguigu.dao","com.atguigu.servlet"})
@Configuration
public class SpringConfig {
}
@Test
public void test04(){
    //通过加载配置类创建ioc容器
    ApplicationContext ioc=new AnnotationConfigApplicationContext(SpringConfig.class);
    UserDao userDaoImpl = ioc.getBean("userDaoImpl", UserDao.class);
    System.out.println("userDaoImpl = " + userDaoImpl);
​
    UserServlet userServlet = ioc.getBean("userServlet", UserServlet.class);
    System.out.println("userServlet = " + userServlet);
​
    UserService userServiceImpl = ioc.getBean("userServiceImpl", UserService.class);
    System.out.println("userServiceImpl = " + userServiceImpl);
​
    //获取到Employee对象
    Employee bean = ioc.getBean("emp",Employee.class);
    System.out.println("bean = " + bean);
}

3.1.5 Spring整合junit4

  • 目前:手动创建ioc容器,手动从容器内获取对象

  • 目标:让ioc容器为我们提供对象

  • 步骤:

    • 导入jar包

      • spring-test
    • 在单元测试类上添加两个注解

    • 直接把你想要的对象类型,写在单元测试类的成员变量上

      • 通过自动装配进行注入
/**
 *  @ContextConfiguration
 *      locations属性: 加载xml配置文件
 *      classes属性:   加载配置类
 *  @RunWith(SpringJUnit4ClassRunner.class)
 *      执行器使用SpringJUnit4ClassRunner
 */
@ContextConfiguration(locations = "classpath:applicationContext.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class SpringSecondTest {
​
    //需要UserServlet对象
    @Autowired
    private UserServlet userServlet;
​
    @Test
    public void test01(){
        userServlet.login("admin","");
    }
}

第四章 AOP

4.1 引入案例

4.1.1 需求:实现计算器加减乘除功能

  1. 接口
package com.atguigu.spring.log;
​
public interface Calculator {
    int add(int a , int b);
    int sub(int a , int b);
    int mul(int a , int b);
    int div(int a , int b);
}
​
  1. 实现
package com.atguigu.spring.log;
​
public class CalculatorImpl implements Calculator {
    @Override
    public int add(int a, int b) {
        int result = a + b;
        return result;
    }
​
    @Override
    public int sub(int a, int b) {
        int result = a - b;
        return result;
    }
​
    @Override
    public int mul(int a, int b) {
        int result = a * b;
        return result;
    }
​
    @Override
    public int div(int a, int b) {
        int result = a / b;
        return result;
    }
}
​

4.1.2 功能扩展:在计算结果的前后打印日志

  1. 在每个方法计算结果的前后打印日志
package com.atguigu.spring.log;
​
public class CalculatorImpl2 implements Calculator {
    @Override
    public int add(int a, int b) {
        System.out.println("Logging: The method add begins with ["+a+","+b+"]");
        int result = a + b;
        System.out.println("Logging: The method add returns "+result);
        return result;
    }
​
    @Override
    public int sub(int a, int b) {
        System.out.println("Logging: The method sub begins with ["+a+","+b+"]");
        int result = a - b;
        System.out.println("Logging: The method sub returns "+result);
        return result;
    }
​
    @Override
    public int mul(int a, int b) {
        System.out.println("Logging: The method mul begins with ["+a+","+b+"]");
        int result = a * b;
        System.out.println("Logging: The method mul returns "+result);
        return result;
    }
​
    @Override
    public int div(int a, int b) {
        System.out.println("Logging: The method div begins with ["+a+","+b+"]");
        int result = a / b;
        System.out.println("Logging: The method div returns "+result);
        return result;
    }
}
​
  1. 问题:
  • 代码混乱:越来越多的非业务需求(日志)加入后,原有的业务方法急剧膨胀。每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点。
  • 代码分散: 只是为了满足这个单一需求,就不得不在多个模块(方法)里多次重复相同的日志代码。如果日志需求发生变化,必须修改所有模块。

4.1.3 动态代理

  1. 动态代理的原理

代理设计模式的原理:使用一个代理将原本对象包装起来,然后用该代理对象”取代”原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。

image.png

  1. 动态代理的方式

a) 基于接口实现动态代理: JDK动态代理

b) 基于继承实现动态代理: Cglib、Javassist动态代理

  1. JDK动态代理核心类

image.png

  1. 核心方法

image.png 处理器接口

image.png

4.1.4 使用JDK动态代理实现日志的打印

  1. 创建Java类
package com.atguigu.spring.log;
​
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
​
public class LoggingProxy {
​
    private Object target; //被代理对象
​
    public LoggingProxy(Object target) {
        this.target = target;
    }
​
    public Object getProxy(){
        //获取被代理对象的类加载器
        ClassLoader classLoader = target.getClass().getClassLoader();
        //获取被代理对象实现的接口们
        Class<?>[] interfaces = target.getClass().getInterfaces();
        Object proxy = Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
            /*
                invoke方法中写扩展的代码
                参数说明:
                proxy:代理对象
                method:执行的目标方法
                args:调用目标方法传入的参数
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //获取方法名
                String methodName = method.getName();
                System.out.println("Logging: The method "+methodName+"begins with "+ Arrays.toString(args));
                //执行目标方法
                Object result = method.invoke(target, args);
                System.out.println("Logging: The method "+methodName+" returns "+result);
                return result;
            }
        });
        return proxy;
    }
}
​
  1. 测试
@Test
public void testProxy(){
    Calculator cal = new CalculatorImpl();
    //获取代理对象
    Calculator calculator = (Calculator) new LoggingProxy(cal).getProxy();
    //调用加减乘除的方法
    calculator.add(10,2);
    calculator.sub(10,2);
    calculator.mul(10,2);
    calculator.div(10,2);
}
​

4.2 AOP概述

  1. AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。是一种通过动态代理实现程序功能扩展和统一维护的一种技术。

  2. 利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

  3. AOP编程操作的主要对象是切面(aspect),而切面用于模块化横切关注点(公共功能)。

  4. AOP的好处:

a) 每个事物逻辑位于一个位置,代码不分散,便于维护和升级

b) 业务模块更简洁,只包含核心业务代码

c) AOP图解:

image.png

4.3 AOP术语

  1. 横切关注点

从每个方法中抽取出来的同一类非核心业务。

  1. 切面(Aspect)

封装横切关注点信息的类,每个关注点体现为一个通知方法。

  1. 通知(Advice)

切面必须要完成的各个具体工作

  1. 目标(Target)

被通知的对象

  1. 代理(Proxy)

向目标对象应用通知之后创建的代理对象

  1. 连接点(Joinpoint)

横切关注点在程序代码中的具体位置

  1. 切入点(pointcut)

定位连接点的方式。每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物。如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP可以通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

4.4 AspectJ

  1. 简介

Java社区里最完整最流行的AOP框架。

在Spring2.0以上版本中,可以使用基于AspectJ注解或基于XML配置的AOP。

  1. @Aspect注解

在Spring中声明切面使用@Aspect注解,而且切面也需要交给IOC容器管理,即切面上也需要添加@Component注解。

当在Spring IOC容器中初始化AspectJ切面之后,Spring IOC容器就会为那些与 AspectJ切面相匹配的bean创建代理。

在AspectJ注解中,切面只是一个带有@Aspect注解的Java类,它往往要包含很多通知,通知是标注有某种注解的简单的Java方法。

  1. AspectJ支持5种类型的通知注解:

① @Before:前置通知,在方法执行之前执行

② @After:后置通知,在方法执行之后执行,不管方法是否发生异常

③ @AfterReturning:返回通知,在方法返回结果之后执行

④ @AfterThrowing:异常通知,在方法抛出异常之后执行

⑤ @Around:环绕通知,围绕着方法执行,相当于动态代理的全过程

4.5 在Spring中使用AOP的步骤:

  1. 在Spring核心包的基础上添加以下jar包
<!--spirng-aspects的jar包-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.1</version>
</dependency>

\2) 创建Spring的配置文件,配置自动扫描的包和AspectJ注解支持

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
​
​
    <!--配置自动扫描的包-->
    <context:component-scan base-package="com.atguigu.spring.aop"></context:component-scan>
​
    <!--开启AspectJ注解支持-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
  1. 创建一个类作为切面

a) 在类上添加@Aspect注解将该类标识为切面

b) 在类上添加@Component注解将该类交给IOC容器管理

image.png 4) 在切面中添加通知方法

package com.atguigu.spring.aop;
​
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
​
@Component
@Aspect
public class LoggingAspect {
​
    //前置通知,在方法执行之前执行
    @Before(value = "execution(public int com.atguigu.spring.aop.Calculator.*(int , int ))")
    public void beforeAdvice() {
        System.out.println("在方法执行之前执行!");
    }
}
​
​
  1. 测试
package com.atguigu.spring.test;
​
import com.atguigu.spring.aop.Calculator;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
​
public class AOPTest {
​
    ApplicationContext ioc = new ClassPathXmlApplicationContext("beans-aop.xml");
​
    /*
       测试AOP
    */
    @Test
    public void testAOP(){
        Calculator calculator = (Calculator) ioc.getBean("calculator");
        calculator.add(10,2);
        calculator.sub(10,2);
        calculator.mul(10,2);
        calculator.div(10,2);
    }
}
​

测试结果:在add、sub、mul、div方法执行之前执行切面中的beforeAdvice方法

4.6 AOP细节

4.6.1 切入点表达式

  1. 作用

通过表达式的方式定位一个或多个具体的连接点。 2) 语法格式:

execution([权限修饰符] [返回值类型] [简单类名/全类名] 方法名)

  1. 举例说明:
表达式execution(* com.atguigu.spring.aop.Calculator.*(..))
含义Calculator接口中声明的所有方法。 第一个“ ”代表任意修饰符及任意返回值。 第二个“ ”代表任意方法。 “..”匹配任意数量、任意类型的参数。 若目标类、接口与该切面类在同一个包中可以省略包名。
表达式execution(public * Calculator.*(..))
含义Calculator接口中声明的所有公有方法
表达式execution(public int Calculator.*(..))
含义Calculator接口中声明的返回值为int类型的公有方法
表达式execution(public int Calculator.*(int, ..))
含义第一个参数为int类型的共有方法。 “..” 匹配任意数量、任意类型的参数。
表达式execution(public int Calculator.*(int, int))
含义参数类型为int,int类型的方法
  1. 在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来

| 表达式 | execution (* .add(int,..)) || execution( .sub(int,..)) | | --- | ----------------------------------------------------------- | | 含义 | 任意类中第一个参数为int类型的add方法或sub方法 | | 表达式 | !execution ( *.add(int,..)) | | 含义 | 匹配不是任意类中第一个参数为int类型的add方法 |

4.6.2 连接点细节

切入点表达式通常都会是从宏观上定位一组方法,和具体某个通知的注解结合起来就能够确定对应的连接点。那么就一个具体的连接点而言,我们可能会关心这个连接点的一些具体信息,例如:当前连接点所在方法的方法名、当前传入的参数值等等。这些信息都封装在JoinPoint接口的实例对象中。

image.png

4.6.3 通知细节

  1. 概述
  • 通知即在具体的连接点上要执行的操作。
  • 一个切面可以包括一个或者多个通知。
  • 通知所使用的注解的值往往是切入点表达式。
  1. 前置通知
  • 使用@Before注解标识
  • 在方法执行之前执行的通知。
//前置通知
@Before(value = "execution(public int com.atguigu.spring.aop.Calculator.*(int , int ))")
public void beforeAdvice(JoinPoint joinPoint) {
    //获取方法名
    String methodName = joinPoint.getSignature().getName();
    //获取参数
    Object[] args = joinPoint.getArgs();
    System.out.println("Logging: The method "+methodName+" beginswith "+ Arrays.toString(args));
}
 

\3) 后置通知

n 使用@After注解标识

n 后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的时候。

//后置通知
@After("execution(public int Calculator.*(int , int ))")
public void afterAdvice(JoinPoint joinPoint) {
    //获取方法名
    String methodName = joinPoint.getSignature().getName();
    System.out.println("Logging: The method "+methodName+" ends");
}
​

\4) 返回通知

n 使用@AfterReturning注解标识

n 无论连接点是正常返回还是抛出异常,后置通知都会执行。如果只想在连接点返回的时候记录日志,应使用返回通知代替后置通知。

u 在返回通知中,通过@AfterReturning注解的returning属性获取连接点的返回值。该属性的值即为用来传入返回值的参数名称

u 必须在通知方法的签名中添加一个同名参数。在运行时Spring AOP会通过这个参数传递返回值

u 原始的切点表达式需要出现在pointcut属性中

//返回通知
@AfterReturning(pointcut = "execution(* Calculator.*(int , int ))",returning = "result")
public void returningAdvice(JoinPoint joinPoint , Object result){
    //获取方法名
    String methodName = joinPoint.getSignature().getName();
    //获取参数
    Object[] args = joinPoint.getArgs();
    System.out.println("Logging: The method "+methodName+" returns "+result);
}
}
  1. 异常通知
  • 使用@AfterThrowing注解标识
  • 在异常通知中,通过@AfterThrowing注解的throwing属性获取异常信息。Throwable是所有错误和异常类的顶级父类,所以在异常通知方法可以捕获到任何错误和异常。
  • 如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通知就只在抛出这个类型及其子类的异常时才被执行。
//异常通知
@AfterThrowing(pointcut = "execution(* Calculator.*(..))",throwing = "e")
public void throwingAdvice(JoinPoint joinPoint,Throwable e){
    //获取方法名
    String methodName = joinPoint.getSignature().getName();
    System.out.println("Logging: The method "+methodName+" occurs "+e);
}
​
  1. 环绕通知
  • 通过@Around注解标识
  • 环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。
  • 对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,允许控制何时执行,是否执行连接点。
  • 在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。
  • 注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed()的返回值,否则会出现异常
//环绕通知
@Around("execution(* Calculator.*(..))")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
    //获取方法名
    String methodName = proceedingJoinPoint.getSignature().getName();
    //获取参数
        Object[] args = proceedingJoinPoint.getArgs();
    Object result=null;
    try {
        //前置通知
        System.out.println("★Logging: The method "+methodName+" begins with "+ Arrays.toString(args));
        //执行目标方法
        result = proceedingJoinPoint.proceed();
        //返回通知
        System.out.println("★Logging: The method "+methodName+" returns "+result);
    } catch (Throwable throwable) {
        //异常通知
        System.out.println("★Logging: The method "+methodName+" occurs "+throwable);
        throwable.printStackTrace();
    }finally {
        //后置通知
        System.out.println("★Logging: The method "+methodName+" ends");
    }
    return result;
}
​
​

4.6.4 重用切入点表达式

  1. 在AspectJ切面中,可以通过@Pointcut注解将一个切入点声明成简单的方法。切入点的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的。

  2. 其他通知可以通过方法名称引入该切入点

  3. 切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它们必须被声明为public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中,还必须包含包名。

image.png

4.6.5 设置切面的优先级

  1. 在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。

  2. 切面的优先级可以通过实现Ordered接口或利用@Order注解指定。

  3. 实现Ordered接口,getOrder()方法的返回值越小,优先级越高。

  4. 若使用@Order注解,通过注解的value属性指定优先级,值越小优先级越高。

验证参数的切面类:

image.png 打印日志的切面类

image.png

4.6.6 通过AOP方式实现日志打印的切面类的完整内容

package com.atguigu.spring.aop;
​
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
​
import java.util.Arrays;
​
@Order(value = 2)
@Component
@Aspect
public class LoggingAspect {
​
    //声明切入点表达式
    @Pointcut(value = "execution(* com.atguigu.spring.aop.Calculator.*(..))")
    public void pointCut(){}
​
    //前置通知
    @Before(value = "pointCut()") //引用切入点表达式
    public void beforeAdvice(JoinPoint joinPoint) {
        //获取方法名
        String methodName = joinPoint.getSignature().getName();
        //获取参数
        Object[] args = joinPoint.getArgs();
        System.out.println("Logging: The method "+methodName+" begins with "+ Arrays.toString(args));
    }
​
    //后置通知
    @After("execution(public int Calculator.*(int , int ))")
    public void afterAdvice(JoinPoint joinPoint) {
        //获取方法名
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logging: The method "+methodName+" ends");
    }
    //返回通知
    @AfterReturning(pointcut = "execution(* Calculator.*(int , int ))",returning = "result")
    public void returningAdvice(JoinPoint joinPoint , Object result){
        //获取方法名
        String methodName = joinPoint.getSignature().getName();
        //获取参数
        Object[] args = joinPoint.getArgs();
        System.out.println("Logging: The method "+methodName+" returns "+result);
    }
​
    //异常通知
    @AfterThrowing(pointcut = "execution(* Calculator.*(..))",throwing = "e")
    public void throwingAdvice(JoinPoint joinPoint,Throwable e){
        //获取方法名
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logging: The method "+methodName+" occurs "+e);
    }
​
    //环绕通知
    @Around("execution(* Calculator.*(..))")
    public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
        //获取方法名
        String methodName = proceedingJoinPoint.getSignature().getName();
        //获取参数
        Object[] args = proceedingJoinPoint.getArgs();
        Object result=null;
        try {
            //前置通知
            System.out.println("★Logging: The method "+methodName+" begins with "+ Arrays.toString(args));
            //执行目标方法
            result = proceedingJoinPoint.proceed();
            //返回通知
            System.out.println("★Logging: The method "+methodName+" returns "+result);
        } catch (Throwable throwable) {
            //异常通知
            System.out.println("★Logging: The method "+methodName+" occurs "+throwable);
            throwable.printStackTrace();
        }finally {
            //后置通知
            System.out.println("★Logging: The method "+methodName+" ends");
        }
        return result;
    }
​

4.7 基于XML的方式实现AOP

除了使用AspectJ注解声明切面,Spring也支持在bean配置文件中声明切面。这种声明是通过aop名称空间中的XML元素完成的。

正常情况下,基于注解的声明要优先于基于XML的声明。通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的。由于AspectJ得到越来越多的 AOP框架支持,所以以注解风格编写的切面将会有更多重用的机会。

  1. 配置细节

在bean配置文件中,所有的Spring AOP配置都必须定义在aop:config元素内部。对于每个切面而言,都要创建一个aop:aspect元素来为具体的切面实现引用后端bean实例。

切面bean必须有一个标识符,供aop:aspect元素引用。

image.png

  1. 声明切入点
  1. 切入点使用aop:pointcut元素声明。
  2. 切入点必须定义在aop:aspect元素下,或者直接定义在aop:config元素下
  3. 定义在aop:aspect元素下:只对当前切面有效
  4. 定义在aop:config元素下:对所有切面都有效
  5. 基于XML的AOP配置不允许在切入点表达式中用名称引用其他切入点。

image.png 3) 声明通知

  • 在aop名称空间中,每种通知类型都对应一个特定的XML元素。
  • 通知元素需要使用来引用切入点,或用直接嵌入切入点表达式。
  • method属性指定切面类中通知方法的名称

image.png

  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" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
​
    <!--配置计算器实现类-->
    <bean id="calculator" class="com.atguigu.spring.aop.xml.CalculatorImpl"></bean>
​
    <!--配置切面类-->
    <bean id="loggingAspect" class="com.atguigu.spring.aop.xml.LoggingAspect"></bean>
​
    <!--AOP配置-->
    <aop:config>
        <!--配置切入点表达式-->
        <aop:pointcut id="pointCut"                      expression="execution(* com.atguigu.spring.aop.xml.Calculator.*(..))"/>
        <!--配置切面-->
        <aop:aspect ref="loggingAspect">
            <!--前置通知-->
            <aop:before method="beforeAdvice" pointcut-ref="pointCut"></aop:before>
            <!--返回通知-->
            <aop:after-returning method="returningAdvice" pointcut-ref="pointCut" returning="result"></aop:after-returning>
            <!--异常通知-->
            <aop:after-throwing method="throwingAdvice" pointcut-ref="pointCut" throwing="e"></aop:after-throwing>
            <!--后置通知-->
            <aop:after method="afterAdvice" pointcut-ref="pointCut"></aop:after>
            <!--环绕通知-->
            <aop:around method="aroundAdvice" pointcut-ref="pointCut"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>
​
​