基础

Spring快速开始

IOC

接下来我们要用Spring去实现上面我们解决的耦合问题,快速入门来体会一下Spring的感觉

1、准备持久层和业务层

package com.howling.SpringDemo1.Dao;

/**
 * 持久层接口
 */
public interface AccountDao {
    void saveAccount();
}
package com.howling.SpringDemo1.Dao;

/**
 * 持久层
 */
public class AccountDaoImpl implements AccountDao {
    public void saveAccount() {
        System.out.println("持久化层接口");
    }
}
package com.howling.SpringDemo1.Service;

/**
 * 业务层接口
 */
public interface AccountService {
    void saveAccount();
}
package com.howling.SpringDemo1.Service;

import com.howling.SpringDemo1.Dao.AccountDao;
import com.howling.SpringDemo1.Dao.AccountDaoImpl;

/**
 * 业务层
 */
public class AccountServiceImpl implements AccountService {

    private AccountDao accountDao = new AccountDaoImpl();

    public void saveAccount() {
        System.out.println("业务层调用持久化层");
        accountDao.saveAccount();
    }
}

2、导入Spring的依赖

<!--设置Spring-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>

3、准备bean对象的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">

    <!--把对象的创建交给spring来管理
        id:配置文件中的key值,与在讲工厂模式的时候的key值相同
        class:配置文件中的value值,是全限定类名,与在讲工厂模式的时候的value值相同
    -->
    <bean id="accountService" class="com.howling.SpringDemo1.Service.AccountServiceImpl"></bean>
    <bean id="accountDao" class="com.howling.SpringDemo1.Dao.AccountDaoImpl"></bean>

</beans>

4、使用Spring获取bean对象

  • 改造Service
package com.howling.SpringDemo1.Service;

import com.howling.SpringDemo1.Dao.AccountDao;
import com.howling.SpringDemo1.Dao.AccountDaoImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * 业务层
 */
public class AccountServiceImpl implements AccountService {

    private static AccountDao accountDao = null;

    static {
        //获取bean.xml
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");

        //根据key获取对象
        accountDao = applicationContext.getBean("accountDao", AccountDao.class);
    }

    public void saveAccount() {
        System.out.println("业务层调用持久化层");
        accountDao.saveAccount();
    }
}
  • 改造Controller
package com.howling.SpringDemo1;


import com.howling.SpringDemo1.Service.AccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * 控制层
 */
public class SpringDemo1 {
    public static void main(String[] args) {

        //获取bean.xml文件
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");

        //根据key获取value,如果不指定Class则需要强制转换
        AccountService accountService = (AccountService) applicationContext.getBean("accountService");

        accountService.saveAccount();

    }
}

5、查看结果

上面的快速开始就是Spring的IOC,控制反转,其实在作用上感觉非常像我们刚才写的单例工厂,不用怀疑,它就是。

学习Spring一定要把单例工厂弄个门清,至少要会手写单例工厂,让别人看看你的Spring学会了。


Application和三种构造方法

下面我们来分析一下ApplicationContext

上面是ApplicationContext的体系结构,ApplicationContext是一个接口

1、我们可以看到他继承了BeanFactory

BeanFactory是Spring容器的顶层接口,他下面的实现接口有很多,其中我们常常使用的是ApplicationContext

2、ApplicationContext有三个比较常用的实现类

  • ClassPathXmlApplicationContext:基于Xml的配置,刚才已经演示过了
  • FileSystemXmlApplicationContext:也是基于Xml的配置
  • AnnotationConfigApplicationContext:基于注解的配置

好的类和方法的命名让人一看就大体知道这个是干嘛的,比如

ClassPathXmlApplicationContext:根据ClassPath(类路径)的xml创建

FileSystemXmlApplicationContext:根据文件的xml来创建,但是必须要有访问权限

AnnotationConfigApplicationContext:基于注解配置来创建


BeanFactory和Application的两个加载策略

1、BeanFactory:是延迟加载,也就是说什么时候根据id加载了对象什么时候真正创建对象

这个其实想一下我们没有完成单例的工厂模式就很好理解了

所以这个模式下其实不是单例模式,每次调用工厂都会产出一个新的单例

BeanFactory主要是面向Spring本身

2、ApplicationContext:立即加载

这个就是我们改造完成之后的单例工厂了

每次调用工厂都是返回相同的实例

ApplicationContext主要是面向开发者

其实我推荐使用ApplicationContext,因为这个继承了BeanFactory和其他的接口,功能相对于BeanFactory更加强大。

但是Spring是一个非常牛逼的框架,他会根据你的配置进行更改。


Spring的XML配置详解

Spring的Bean细节

Spring中bean.xml属性

  • id:唯一标识
  • name:名字,可以指定多个名字,使用逗号,分号,空格分隔
  • class:映射的类,要用全类名
  • scope:作用范围
    • singleton:单例(默认值)
    • prototype:多例(常用)
    • request:作用于web应用的请求范围
    • session:作用于web应用的会话范围
    • global-session:作用于集群环境的会话范围(全局会话范围),当不是集群环境时他是session
  • factory-method:可以让我们自己写bean的工厂,指定我们的方法来创建,有两种形式
    • 静态方法
    • 普通方法,需要借助factory-bean来使用
  • factory-bean:指定工厂,说明这个类是工厂类

5、init-method:初始化要执行的方法

6、destory-method:销毁时要执行的方法

<bean id="" class="" factory-bean="" factory-method="" init-method="" destroy-method="" scope=""></bean>

这里只列出了部分,还有其他的后面都会一一讲到

三种创建Bean对象的方式

在上面的xml属性我们讲过了,其中可以看到,还有两种创建对象的属性:指定工厂和指定静态工厂

那么现在我们就有三种创建bean对象的方式了,在这里总结一下

1、根据默认的构造函数创建:默认使用。(这里简略写一下,反正前面都已经写过了)

  • 编写类
/**
 * 持久层
 */
public class AccountDaoImpl implements AccountDao {
    public void saveAccount() {
        System.out.println("持久化层接口");
    }
}
  • 指定spring的配置文件:bean.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="accountService" class="com.howling.SpringDemo1.Service.AccountServiceImpl"></bean>
    <bean id="accountDao" class="com.howling.SpringDemo1.Dao.AccountDaoImpl"></bean>

</beans>
  • Spring创建bean对象
/**
 * 业务层
 */
public class AccountServiceImpl implements AccountService {

    private static AccountDao accountDao = null;

    static {
        //获取bean.xml
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");

        //根据key获取对象
        accountDao = applicationContext.getBean("accountDao", AccountDao.class);
    }

    public void saveAccount() {
        System.out.println("业务层调用持久化层");
        accountDao.saveAccount();
    }
}

使用这种方式创建的bean对象会根据默认的构造函数进行创建,Spring直接通过反射给你创建一个

2、指定静态工厂创建

  • 指定静态工厂
package com.howling.SpringDemo2.factory;

import com.howling.SpringDemo2.Dao.AccountDaoImpl;
import com.howling.SpringDemo2.Service.AccountServiceImpl;

public class BeanFactory {
    public static AccountServiceImpl getAccountService() {
        return new AccountServiceImpl();
    }

    public static AccountDaoImpl getAccountDao() {
        return new AccountDaoImpl();
    }
}
  • 指定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="accountDao" class="com.howling.SpringDemo2.factory.BeanFactory" 
          factory-method="getAccountDao"></bean>
    
    <bean id="accountService" class="com.howling.SpringDemo2.factory.BeanFactory" 
          factory-method="getAccountService"></bean>

</beans>

除了这两个地方,其他都不用动

3、指定工厂方法创建

  • 指定工厂
package com.howling.SpringDemo3.factory;

import com.howling.SpringDemo3.Dao.AccountDaoImpl;
import com.howling.SpringDemo3.Service.AccountServiceImpl;

public class BeanFactory {
    public AccountServiceImpl getAccountService() {
        return new AccountServiceImpl();
    }

    public AccountDaoImpl getAccountDao() {
        return new AccountDaoImpl();
    }
}
  • 指定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="beanFactory" class="com.howling.SpringDemo3.factory.BeanFactory"></bean>

    <!--指定工厂和工厂中的方法-->
    <bean id="accountDao" factory-bean="beanFactory" factory-method="getAccountDao"></bean>
    <bean id="accountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>

</beans>

现在可能有人要问了:这不是还是new出了对象了么,不还是耦合了么

原因在于:第二种和第三种的方式是解决jar包的方法的。jar包中的代码是.class文件不能更改,所以只能new

也就是说,要是你想通过Spring来获得jar包中的某个方法就可以使用第二种和第三种方式。

我们自己写的话直接使用第一种方式即可

作用范围

作用范围在bean.xml属性中也已经讲过了,简单来说就是配置一个scope属性即可

之前我们讲过BeanFactory和ApplicationContext的区别,说这两个一个是多例一个是单例,但是可以根据配置具体更改

这就是我们说过的那个配置

scope:作用范围

  • singleton:单例(默认值)
  • prototype:多例(常用)
  • request:作用于web应用的请求范围
  • session:作用于web应用的会话范围
  • global-session:作用于集群环境的会话范围(全局会话范围),当不是集群环境时他是session

生命周期

友情提示,在看生命周期的时候,多想想我们之前写过的两个工厂,然后结合理解

多例对象的生命周期

出生:当使用对象时,对象创建

活着:当使用的时候活着

死亡:GC垃圾回收,Spring不管

单例对象的生命周期

出生:容器创建时立刻出生

活着:只要容器还在就一直活着

死亡:容器销毁,对象死亡

也就是说,单例对象的生命周期和容器的生命周期是完全一致的,只不过我们一般发现不了,因为main函数执行完成之后内存直接就释放了,也就是说容器还没来得及调用销毁方法就已经被释放了内存

但是我们想要手动关闭也是可以的,Spring中提供了关闭的方法,但是有个注意点

我们一般使用的接口是ApplicationContext,但是Application中并没有关闭的方法,只有它的子类才有,所以假如想要调用关闭的方法,使用多态是不可以的,比如下面这种就不可以

ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");

在传统的Java中,我们对于Bean的管理也就是new一个出来,然后用完等待GC,但是Spring中要考虑的显然不止这些东西,它的加载顺序还是比较复杂的

1、Bean的实例化:Spring启动,扫描需要被Spring管理的Bean,进行Bean的实例化

2、依赖注入:Bean实例化之后,对Bean的引入和值注入到Bean的属性中

3、注入Aware:Spring会检测是否实现了xxxAware接口,并将对应的Aware接口注入给Bean

4、BeanPostProcessor:经过上面的几个步骤,bean对象已经构造,但是在这个情况下还没有被使用

接口BeanPostProcessor下面有两个函数

  • postProcessBeforeInitialzation:bean对象传进来,早于Initialzation,所以叫做前置处理,所有的Aware接口在这一步注入
  • postProcessAfterInitialzation:bean对象传进来,晚于Initialzation,所以叫后置处理

5、InitializingBean与init-method:前置处理之后会执行这个,可以在这一步添加一些代码的逻辑,但是它不会把bean对象传进来,所以不能处理对象本身

6、DisposableBean和destroy-method:可以在bean销毁前执行逻辑,但是bean不会传递进来

Spring依赖注入

依赖注入概述

什么叫做依赖注入

依赖注入:Dependency Injection

依赖注入的意思是:在当前类中需要用到其他类的对象,像这种依赖关系以后我们就都交给Spring去管理了,这种依赖关系的维护就叫做依赖注入

什么数据能够进行依赖注入

能够注入的数据分类三类

1、所有的基本类型和String

2、复杂类型(集合)

3、其他bean类型(在配置文件或者注解中配置过的bean)

注入的方式

依赖注入的方式有三种

1、构造函数

2、set方法

3、注解

经常要变化的数据是不适合注入的

构造函数注入

标签

构造函数的标签:<constructor-arg></constructor-arg>,其中有以下几个属性

  • type:要注入的类型
  • index:要注入数据的索引位置(构造函数上)
  • name:要注入数据的名称呢过
  • value:要注入数据的值
  • ref:要注入数据的bean引用

构造函数注入的快速起步:基本类型的注入

1、写一个类,里面包含有参数的构造函数

package com.howling.ConstructorInjection.service;

public interface AccountService {
    void saveAccount();
}
package com.howling.ConstructorInjection.service;

public class AccountServiceImpl implements AccountService {

    private String name;
    private Integer age;

    /**
     * 使用构造函数注入
     *
     * @param name name是String类型
     * @param age  age是Integer类型
     */
    public AccountServiceImpl(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public void saveAccount() {
        System.out.println(this.age + "岁的" + this.name + "saveAccount...");
    }
}

2、编写Spring的bean.xml

  • ConstructorInjectionDemo1.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">


    <!-- 1、首先使用一个bean对象指定对应的类,这个类初始化的时候需要带有参数 -->
    <bean id="accountService" class="com.howling.ConstructorInjection.service.AccountServiceImpl">
        <!-- 2、constructor-arg:构造函数注入方式
            name:要注入的数据名称是什么
            value:要制定基本类型和String类型的数据,可以自动转换,比如这个18会转为Integer
         -->
        <constructor-arg name="name" value="张三"></constructor-arg>
        <constructor-arg name="age" value="18"></constructor-arg>
    </bean>

</beans>

3、进行测试

package com.howling.ConstructorInjection;

import com.howling.ConstructorInjection.service.AccountServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringDemo1 {
    public static void main(String[] args) {

        ApplicationContext context = new ClassPathXmlApplicationContext("ConstructorInjectionDemo1.xml");

        AccountServiceImpl accountService = context.getBean("accountService", AccountServiceImpl.class);

        accountService.saveAccount();
    }
}

bean类型的注入

如果一个类A要依赖另一个类B,那么B首先要作为一个Spring管理的bean对象,然后才能被A引入,当然A也要成为一个bean对象

下面我们来做一个bean类型的注入

1、改造上面的类

package com.howling.ConstructorInjection.service;

import java.util.Date;

public class AccountServiceImpl implements AccountService {

    private String name;
    private Integer age;

    /**
     * 一个Date类,作为bean对象来注入
     */
    private Date birthday;

    public AccountServiceImpl(String name, Integer age, Date birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }

    public void saveAccount() {
        System.out.println(this.age + "岁的" + this.name + "saveAccount..." + "要过他" + birthday + "的生日");
    }
}

2、做一个bean对象

  • 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对象,Spring会使用反射初始化一个出来-->
    <bean id="time" class="java.util.Date"></bean>

    <bean id="accountService" class="com.howling.ConstructorInjection.service.AccountServiceImpl">
        <constructor-arg name="name" value="张三"></constructor-arg>
        <constructor-arg name="age" value="18"></constructor-arg>

        <!-- name还是指定类中的值,但是bean对象不能指定value,要使用引用的方式 -->
        <constructor-arg name="birthday" ref="time"></constructor-arg>
    </bean>

</beans>

3、测试

可以看到,创建了一个Date对象

复杂类型的注入

复杂类型有这么几种

1、数组

2、集合

  • List
  • Map

3、Properties

所以我们根据数据的结构,分了两部分:

1、array和list

2、map和props

下面进行案例

1、改造类

package com.howling.ConstructorInjection.service;

import java.util.*;

public class AccountServiceImpl implements AccountService {

    private String[] strings;
    private List<String> list;
    private Map<String, Object> map;
    private Properties properties;

    public AccountServiceImpl(String[] strings, List<String> list, Map<String, Object> map, Properties properties) {
        this.strings = strings;
        this.list = list;
        this.map = map;
        this.properties = properties;
    }

    @Override
    public void saveAccount() {
        Arrays.stream(strings).forEach(System.out::print);

        System.out.println();
        list.forEach(System.out::print);

        System.out.println();
        map.keySet().forEach(System.out::print);

        System.out.println();
        properties.keySet().forEach(System.out::print);
    }
}

2、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="accountService" class="com.howling.ConstructorInjection.service.AccountServiceImpl">

        <!--参数1:数组:array-value-->
        <constructor-arg index="0">
            <array>
                <value>a1</value>
                <value>a2</value>
                <value>a3</value>
            </array>
        </constructor-arg>

        <!--参数2:列表:list-value-->
        <constructor-arg index="1">
            <list>
                <value>l1</value>
                <value>l2</value>
                <value>l3</value>
            </list>
        </constructor-arg>

        <!--参数3:map:map-entry(key-value)-->
        <constructor-arg index="2">
            <map>
                <entry key="m1" value="m1"/>
                <entry key="m2" value="m2"/>
                <entry key="m3" value="m3"/>
            </map>
        </constructor-arg>

        <!--参数4:properties-->
        <constructor-arg index="3">
            <props>
                <prop key="p1">1</prop>
                <prop key="p2">2</prop>
                <prop key="p3">3</prop>
            </props>
        </constructor-arg>
    </bean>

</beans>

3、测试

package com.howling.ConstructorInjection;

import com.howling.ConstructorInjection.service.AccountServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringDemo1 {
    public static void main(String[] args) {

        ApplicationContext context = new ClassPathXmlApplicationContext("ConstructorInjectionDemo1.xml");

        AccountServiceImpl accountService = context.getBean("accountService", AccountServiceImpl.class);

        accountService.saveAccount();
    }
}

set注入

Set注入概述

简单来说,就是将构造方法的注入转换为了set方法注入(不需要get方法)

标签

Set方法的标签是<property></property>,主要有这么几个属性

1、name:从set方法中得到的名字,比如:setName–>Name–>name

2、value:值

3、ref:注入数据的bean引用

没错,set注入直接就是property,而且set注入是最常用的一种注入方式

实例

1、代码改造

package com.howling.ConstructorInjection.service;

import java.util.*;

public class AccountServiceImpl implements AccountService {

    private String name;
    private Integer age;
    private Date birthday;
    private String[] strings;
    private List<String> list;
    private Map<String, Object> map;
    private Properties properties;

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

    public void setAge(Integer age) {
        this.age = age;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public void setStrings(String[] strings) {
        this.strings = strings;
    }

    public void setList(List<String> list) {
        this.list = list;
    }

    public void setMap(Map<String, Object> map) {
        this.map = map;
    }

    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    @Override
    public void saveAccount() {
        Arrays.stream(strings).forEach(System.out::print);

        System.out.println();
        list.forEach(System.out::print);

        System.out.println();
        map.keySet().forEach(System.out::print);

        System.out.println();
        properties.keySet().forEach(System.out::print);
    }
}

2、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="time" class="java.util.Date"></bean>

    <bean id="accountService" class="com.howling.ConstructorInjection.service.AccountServiceImpl">
        <!-- 1、基础类型和字符串 -->
        <property name="name" value="张三"></property>
        <property name="age" value="18"></property>

        <!--bean类型-->
        <property name="birthday" ref="time"></property>

        <!--复杂类型-->
        <property name="strings">
            <array>
                <value>1</value>
                <value>2</value>
                <value>3</value>
            </array>
        </property>

        <property name="list">
            <list>
                <value>1</value>
                <value>2</value>
                <value>3</value>
            </list>
        </property>

        <property name="map">
            <map>
                <entry key="1" value="1"></entry>
                <entry key="2" value="2"></entry>
                <entry key="3" value="3"></entry>
            </map>
        </property>

        <property name="properties">
            <props>
                <prop key="1">1</prop>
                <prop key="2">2</prop>
                <prop key="3">3</prop>
            </props>
        </property>
    </bean>

</beans>

3、测试

package com.howling.ConstructorInjection;

import com.howling.ConstructorInjection.service.AccountServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringDemo1 {
    public static void main(String[] args) {

        ApplicationContext context = new ClassPathXmlApplicationContext("ConstructorInjectionDemo1.xml");

        AccountServiceImpl accountService = context.getBean("accountService", AccountServiceImpl.class);

        accountService.saveAccount();
    }
}

Spring的注解详解

注解概述

首先明确一件事情:注解的方式和xml的方式本质上没有什么区别

注解按照作用分类

  • 用于创建对象
  • 改变作用范围
  • 和生命周期相关
  • 用于注入数据

环境准备

首先我们需要告诉Spring:我要用注解。

回想一下我们学习的JavaWeb,我们使用注解的使用是首先在web.xml上填写使用注解的命名空间,在Spring中也是这么干,那么我们的xml需要换成下面这个

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
    
    <context:component-scan base-package="com.howling"></context:component-scan>
</beans>

1、我们需要beans的命名空间

2、component-scan中的base-package告诉了Spring在创建容器要扫描com.howling下面的包

3、当扫描包的时候就会发现包中的类上的注解


创建对象的注解

注解说明

1、@Component:组件

2、@Controller:一般用于控制层(表现层)

3、@Service:一般用于业务层

4、@Repository:一般用于持久层

说明一下,其实上面这四个的作用都是一样的,但是分开命名就是让我们程序员开发的时候比较好理解,但是其实用啥都一样

属性说明

注解肯定要有属性,属性名就是value,属性值是唯一的,用于找到这个类

1、当不写value时,默认value是类的小驼峰形式

package com.howling.Annotation.service;

import org.springframework.stereotype.Component;

@Component
public class AccountServiceImpl implements AccountService {
    public void saveAccount() {
        System.out.println("bean的创建");
    }
}

这里的value就是accountServiceImpl

2、当自定义value时,就是你自己定义的值

package com.howling.Annotation.service;

import org.springframework.stereotype.Component;

@Component(value = "accountService")
public class AccountServiceImpl implements AccountService {
    public void saveAccount() {
        System.out.println("bean的创建");
    }
}
package com.howling.Annotation.service;

import org.springframework.stereotype.Component;

@Component("accountService")
public class AccountServiceImpl implements AccountService {
    public void saveAccount() {
        System.out.println("bean的创建");
    }
}

因为只有一个value赋值,所以value写不写都行

例子

package com.howling.Annotation.service;

import org.springframework.stereotype.Component;

@Component
public class AccountServiceImpl implements AccountService {
    public void saveAccount() {
        System.out.println("bean的创建");
    }
}
package com.howling.Annotation;

import com.howling.Annotation.service.AccountServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringDemo1 {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("SpringApplication.xml");

        AccountServiceImpl accountServiceImpl = context.getBean("accountServiceImpl", AccountServiceImpl.class);

        accountServiceImpl.saveAccount();
    }
}

作用范围

注解

@Scope:指定bean的作用范围

属性

value:指定范围的取值

  • singleton:单例,默认
  • prototype:多例

例子

package com.howling.Annotation.service;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope("prototype")
public class AccountServiceImpl implements AccountService {
    public void saveAccount() {
        System.out.println("bean的创建");
    }
}

生命周期

注解

  • @PostConstruct:初始化要执行的方法
  • @PreDestory:销毁前要执行的方法

1、这俩都用在方法上

2、不能用多态来测试销毁方法,因为ApplicationContext没有这个方法,子类才有

3、多例模式下GC回收,close也没用

4、详情查看Spring的生命周期

package com.howling.Annotation.service;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Component
public class AccountServiceImpl implements AccountService {
    public void saveAccount() {
        System.out.println("bean的创建");
    }

    @PostConstruct
    void init(){
        System.out.println("init");
    }

    @PreDestroy
    void destory(){
        System.out.println("destory");
    }
}
package com.howling.Annotation;

import com.howling.Annotation.service.AccountServiceImpl;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringDemo1 {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("SpringApplication.xml");

        AccountServiceImpl accountServiceImpl = context.getBean("accountServiceImpl", AccountServiceImpl.class);

        accountServiceImpl.saveAccount();

        context.close();
    }
}

注入数据

注解分类

  • @Autowried:先按照类型匹配,然后按照bean的id注入
  • @Qualifier:给类成员注入和@Autowried一起使用,给方法参数注入可以单独使用
  • @Resource:根据bean的id注入,不是Spring的注解
  • @Value:注入基本类型和String

@Autowried

这个注解会首先按照类型匹配,假如只有一个匹配的类型就按照那个类型注入,假如有多个匹配的类型就按照名称注入

而且@Autowried会调用的是无参构造,所以是这样的:类型–>名称–>无参构造

而且假如只有有参构造没有无参构造,它也没法调用,因为没有值给它调用

下面来进行情景一:只有一个继承类

package com.howling.Annotation.dao;

import org.springframework.stereotype.Repository;

@Repository
public class AccountDaoImpl implements AccountDao{

    public void saveAccount() {
        System.out.println("继承1");
    }
}
package com.howling.Annotation.service;


import com.howling.Annotation.dao.AccountDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao = null;

    public void saveAccount() {
        System.out.println("bean的创建");
    }
}

情景二:多个继承类

package com.howling.Annotation.dao;

import org.springframework.stereotype.Repository;

@Repository
public class AccountDaoImpl implements AccountDao{

    public void saveAccount() {
        System.out.println("继承1");
    }
}
package com.howling.Annotation.dao;

import org.springframework.stereotype.Repository;

@Repository
public class AccountDaoImpl2 implements AccountDao {
    public void saveAccount() {
        System.out.println("继承2");
    }
}
package com.howling.Annotation.service;


import com.howling.Annotation.dao.AccountDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao = null;

    public void saveAccount() {
        System.out.println("bean的创建");
    }
}

accountDao是名字,@Autowried也会根据这个名字去找对应的类,但是很遗憾找不到,所以注入不进去

在这里要注意一件事情,两个类都要去交给Spring管理,也就是说都要加上注解,要不然白写

情景三:有相同的bean名字

package com.howling.Annotation.dao;

import org.springframework.stereotype.Repository;

@Repository
public class AccountDaoImpl implements AccountDao{

    public void saveAccount() {
        System.out.println("继承1");
    }
}
package com.howling.Annotation.dao;

import org.springframework.stereotype.Repository;

@Repository
public class AccountDaoImpl2 implements AccountDao {
    public void saveAccount() {
        System.out.println("继承2");
    }
}
package com.howling.Annotation.service;


import com.howling.Annotation.dao.AccountDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDaoImpl = null;

    public void saveAccount() {
        System.out.println("bean的创建");
    }
}

@Qualifier

  • 给类成员注入的使用要和@Autowried配合使用,目的是为了消除歧义
  • 给方法参数注入可以单独使用,这里不讲,不好举例子

给类成员注入

我们之前看过@Autowried在多个继承类下要去找和属性名一致的bean的id,但是加入找不到就会报错

这个时候我们可以和@Qualifier配合,@Qualifier用于指定你要指定的bean的id

package com.howling.Annotation.service;


import com.howling.Annotation.dao.AccountDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class AccountServiceImpl implements AccountService {

    @Autowired
    @Qualifier("accountDaoImpl")
    private AccountDao accountDao = null;

    public void saveAccount() {
        accountDao.saveAccount();
    }
}

这个时候假如有多个值,直接会找accountDaoImpl,即使有一个叫做accountDao的也会找accountDaoImpl

@Resource

@Resource其实就是@Autowried和@Qualifier的结合体,他直接根据bean的id注入,所以bean的id是必须写的

package com.howling.Annotation.service;


import com.howling.Annotation.dao.AccountDao;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Component
public class AccountServiceImpl implements AccountService {

    @Resource(name = "accountDaoImpl")
    private AccountDao accountDaoImpl2 = null;

    public void saveAccount() {
        accountDaoImpl2.saveAccount();
    }
}

@Value

@Value注解可以给属性赋值,有几种方式:

1、直接赋值

2、使用SpEL表达式赋值

3、读取配置文件赋值

下面依次来讲解(下面以基本类型来演示,但其实你完全可以使用复杂类型)

直接赋值

package com.howling.Annotation.service;


import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class AccountServiceImpl implements AccountService {

    @Value("18")
    private Integer age;

    public void saveAccount() {
        System.out.println(age);
    }
}

注意,这种方式可以直接写在属性上,有set方法也可以卸载set方法上

使用SpEL表达式赋值

package com.howling.Annotation.domain;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class User {

    private String username;

    @Value("张三")
    public void setUsername(String username) {
        this.username = username;
    }

    public String getUsername() {
        return username;
    }
}
package com.howling.Annotation.service;


import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class AccountServiceImpl implements AccountService {

    @Value("#{user.username}")
    private String username;

    public void saveAccount() {
        System.out.println(username);
    }
}

通过SpEL表达式来进行其他类的读取,但是注意要有get方法,它是这样的user.username–>user.getUserName()

通过get方法来读取

读取配置文件赋值

1、首先需要在Spring的配置文件上加上一句话

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 表示读取classpath下面的application.properties文件 -->
    <context:property-placeholder location="classpath:application.properties">
    </context:property-placeholder>
    

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

</beans>

关于classpath在编译前后的对应关系我有一篇闲谈

2、application.properties文件

address=Asia/Shanghai

3、编写bean

package com.howling.Annotation.service;


import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class AccountServiceImpl implements AccountService {

    @Value("${address}")
    private String address;

    public void saveAccount() {
        System.out.println(address);
    }
}

其他注解

注解概述

1、@Configuration:指定配置类,主要作用是完全摆脱xml

2、@ComponentScan:指定创建容器时要扫描的包,在前面已经讲过了

3、@Bean:放在方法上,作用是将方法的返回值作为bean对象存到IOC容器里

4、@Import:在一个配置类中引入其他的配置类

5、@PropertySource:用于指定properties文件的路径,路径是编译后的classpath下的

6、@PropertySources:下面有多个@PropertySource

@Configuration

我们之前使用注解都是首先在配置文件上声明:我要用注解了,然后采用的注解。但是这种方式始终离不开xml注解。

我们在一开始讲过的创建IOC容器的时候说过,ApplicationContext由三个实现方法,我们之前一直用的是xml的方法,现在我们要换为注解的方法:new AnnotationConfigApplicationContext(配置类.class),使用这个方法就可以完全脱离xml了。

但是使用这个方法的配置就需要一个配置类,我们的配置类声明的注解为:@Configuration

下面来测试一下

1、编写配置类

package com.howling.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * @Configuration:加到类上,说明这个类是配置类
 * @ComponentScan("com.howling"):指定注解要扫描的包,和xml中指定的是一个道理
 */
@Configuration
@ComponentScan("com.howling")
public class SpringConfiguration {

}

2、编写Service

package com.howling.servlet;

import org.springframework.stereotype.Service;

@Service
public class AccountServiceImpl implements AccountService {
    public void saveAccount() {
        System.out.println("saveAccount");
    }
}

3、测试

package com.howling;

import com.howling.config.SpringConfiguration;
import com.howling.servlet.AccountServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SpringDemo4 {
    public static void main(String[] args) {
        ApplicationContext context = 
            new AnnotationConfigApplicationContext(SpringConfiguration.class);

        AccountServiceImpl accountServiceImpl = 
            context.getBean("accountServiceImpl", AccountServiceImpl.class);

        accountServiceImpl.saveAccount();
    }
}

4、其他说明

我们在controller中new AnnotationConfigApplicationContext(SpringConfiguration.class);

这一步已经将SpringConfiguration这个类的class字节码加载进去了,所以其实在这个类上不写@Configuration也是可以的

但是其他的情况是需要写的,比如配置类A中引用了配置类B,然后在创建的时候引入了配置类A。这个时候虽然引用的是配置类A,但是和上面不同,这里必须写上注解。

顺便说明一下,A调用B是有可能的,只需要在A中的注解改动一下:@Configuration(“要扫描的包”,”配置类2”)

为了我们的规范使用,建议不管怎么样都要加上@Configuration这个注解,但是看到没有加上注解也不要奇怪


@Import

在刚才,我们在最后说了配置类A调用配置类B是可以的,但是@Configuration(“要扫描的包”,”配置类2”),像这种方式是非常不好用的,我们有一种更好的方式,就是使用注解@Import(字节码)

使用这种方式,我们就可以在配置类A中引入配置类B,将其整合到一个配置中

package com.howling.config;

import org.springframework.context.annotation.Configuration;

@Configuration
public class ConfigurationB {
}
package com.howling.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * @Configuration:加到类上,说明这个类是配置类
 * @ComponentScan("com.howling"):指定注解要扫描的包,和xml中指定的是一个道理
 */
@Configuration
@ComponentScan("com.howling")
@Import(ConfigurationB.class)
public class SpringConfiguration {

}

@PropertySource

关于这个注解,其实前面我们已经提了一嘴,就是可以指定properties文件的路径,只需要指定classpath下面的properties文件即可

关于classpath虽然说了很多遍但是还要再说一遍,在项目编译后java和resources目录下的文件都会到classes下面,这个classes就是我们说的classpath

举个例子

1、application.properties

address=Asia/Shanghai

2、配置类

package com.howling.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@PropertySource("classpath:application.properties")
public class ConfigurationB {
}
package com.howling.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;


@Configuration
@ComponentScan("com.howling")
@Import(ConfigurationB.class)
public class SpringConfiguration {

}

3、service

package com.howling.servlet;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class AccountServiceImpl implements AccountService {

    @Value("${address}")
    private String address;

    public void saveAccount() {
        System.out.println(address);
    }
}

4、演示

package com.howling;

import com.howling.config.SpringConfiguration;
import com.howling.servlet.AccountServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SpringDemo4 {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);

        AccountServiceImpl accountServiceImpl = context.getBean("accountServiceImpl", AccountServiceImpl.class);

        accountServiceImpl.saveAccount();
    }
}

Spring整合Junit

Junit这么厉害是因为Junit集成了main方法,Junit不管我们是否采用了什么框架,他只会执行@Test注解上的内容

所以问题来了,Junit不知道我们使用了Spring,没法注入,所以我们需要想个办法,能让Junit在单元测试的时候能够注入

我们使用的方式是替换掉Junit中内置的main方法,这样就可以实现注入了

1、导入环境

        <!--替换Junit的main方法让Spring能够在单元测试中注入-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <!-- Junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
        </dependency>

当使用Spring5.x的时候,Junit必须在4.12及以上,否则会报错

2、使用注解@RunWith(SpringJunit4ClassRunner.class)替换掉main方法

3、告知Spring运行器,Spring的IOC是基于xml还是基于注解,并且说明位置

  • locations:指定xml所在目录,加上classpath关键字表示在类路径下
  • classes:指定注解类所在的位置

4、测试

package howling;

import com.howling.servlet.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = com.howling.config.SpringConfiguration.class)
//@ContextConfiguration(locations = "classpath:SpringConfiguration.xml"):使用xml时
public class TestJunit {
    @Autowired
    private AccountService accountService;
    
    @Test
    public void account(){
        accountService.saveAccount();
    }
}

AOP

动态代理

动态代理概述

代理:在生活中,我们遇到了很多代理,比如买电脑买手机,找售后都是找代理。代理去代理厂家的产品,厂家卖的多,代理也抽成。

在Java中也有类似的概念,回想一下反射的内容,我们就很容易理解

动态代理:和代理差不多,不过更加高级一些

1、字节码随用随创建,随用随加载

2、不修改源码的基础上对方法继承增强

3、可以分类为:基于接口的动态代理,基于子类的动态代理

基于接口的动态代理

基于接口的动态代理,由JDK提供,需要Proxy类,这里是回顾前面讲过的反射的内容,更加具体的去看反射

1、首先我们来写厂家

package com.howling.proxy;

/**
 * 这里是厂家的接口,有一个方法是销售的方法
 */
public interface Producer {

    public void saleProducer(float money);
}
package com.howling.proxy;

/**
 * 厂家的实现
 */
public class ProducerImpl implements Producer {

    /**
     * 厂家进行销售
     *
     * @param money
     */
    public void saleProducer(float money) {
        System.out.println("厂家赚得:" + money + "元");
    }
}

2、我们来写代理和测试

package com.howling.proxy;

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

/**
 * 模拟消费者进行消费
 */
public class Client {
    public static void main(String[] args) {

        //必须是final修饰才可以被代理
        final ProducerImpl producerImpl = new ProducerImpl();

        //动态代理,也就是我们模拟的代理商
        Producer producer = (Producer) Proxy.newProxyInstance(producerImpl.getClass().getClassLoader(), producerImpl.getClass().getInterfaces(), new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                //假如当前执行的方法是saleProducer
                if ("saleProducer".equals(method.getName())) {
                    float money = (Float) args[0];//读取参数值

                    //代理商进行抽成
                    money *= 0.8;

                    System.out.println("代理商抽两成的利润");

                    //返回代理商的方法
                    return method.invoke(producerImpl, money);
                }

                return method.invoke(method,args);
            }
        });

        //消费者去代理商那里进行购买,花了一万块
        producer.saleProducer(10000f);
    }
}

3、下面来讲一下几个参数

Proxy.newProxyInstance(ClassLoader,Class[],InvocationHandler)

1、类加载器,被代理的类的类加载器

2、字节码数组,这个的作用是让代理对象和被代理的对象有相同的方法,所以需要被代理对象的父级接口

3、提供增强代码,使用InvocationHandler(proxy,method,args)

1、proxy:当前被代理对象的引用,也就是InvocationHandler本身

2、method:当前方法的名称

3、args:当前执行的方法中的参数数组

基于子类的动态代理

基于子类的动态代理,我们需要一个外部的包CGLIB,他是一个功能强,性能高的代码生成包。

主要是为没有实现接口的类实现代理,算是对JDK代理的一个很好的补充。

1、依赖

        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.1_3</version>
        </dependency>

2、编写生产者,这里就不需要写接口了

package com.howling.cglib;

/**
 * 生产者
 */
public class Producer {

    /**
     * 实现销售方法
     * @param money
     */
    public void saleProducer(float money){
        System.out.println("销售"+money+"...");
    }
}

3、编写消费者

package com.howling.cglib;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * 模拟消费者
 */
public class Client {


    public static void main(String[] args) {


        final Producer producer = new Producer();

        Producer proxyProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                if ("saleProducer".equals(method.getName())){
                    float money = (Float) objects[0];
                    return method.invoke(producer,money*0.8f);
                }
                return null;
            }
        });

        proxyProducer.saleProducer(10000f);

    }

}

4、结果

AOP概述

1、AOP:面向切面编程,通过预编译和运行期动态代理实现程序功能统一维护的技术

2、AOP是OOP(面向对象)的延伸,是软件开发的一个热点

3、AOP是Spring中的一个重要内容,是函数式编程的一种衍生模型

4、利用AOP可以对业务逻辑的各个部分进行隔离,从而使业务逻辑的各个部分之间的耦合度降低,提高程序的可重用性

优势:减少重复代码,提高开发效率,维护方便


AOP有以下术语

1、连接点:JoinPoint:所谓连接点是那些被拦截到的点,在Spring中是方法,因为Spring只支持方法类型的连接点

也就是我们所说的要增强的方法

2、切入点:Pointcut:所为切入点是指我们要对哪些连接点进行拦截的定义

在事务中被增强的方法就叫做切入点

注意连接点和切入点的区别,连接点指的就是原来定义的方法,切入点指的是我们已经明确进行拦截的方法

3、通知:Active:拦截到连接点之后要做的事情

通知也就是指的增强,通知分为:前置通知,后置通知,异常通知,最终通知

现在想一下我们在JDK动态代理中的的invoke方法

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

     //假如当前执行的方法是saleProducer
     if ("saleProducer".equals(method.getName())) {
           float money = (Float) args[0];//读取参数值

           //代理商进行抽成
           money *= 0.8;

           System.out.println("代理商抽两成的利润");

           //返回代理商的方法
           return method.invoke(producerImpl, money);
     }

     return method.invoke(method,args);
}

method.invoke(producerImpl, money)这段代码也就是我们最终要执行的方法,也就是我们的切入点,看好这个切入点

1、在切入点之前的代码的都是前置通知

2、在切入点之后的都是后置通知

3、出现异常,被捕获的都叫做异常通知

4、不论如何都要执行的就是最终通知

5、整个invoke方法就是环绕通知


如图:

4、引介:Introduction:引介是一种特殊的通知,在不改变类代码的前提下,引介可以在运行期为类动态地添加一些方法或者成员变量

5、目标对象:Target:代理的目标对象

6、织入:Weaving:指把增强应用到目标对象来创建新的代理对象的过程,Spring使用动态代理,而AspectJ使用编译器和类装载器

7、代理:Proxy:一个类被织入之后,就会产生一个结果代理类

8、切面:Aspect:切入点+通知=切面


AOP其实非常简单,我们只需要

1、编写核心业务代码

2、抽取公共代码,制作为通知

3、在配置文件中声明切入点和通知的关系(切面)

基于XML的AOP配置

快速起步

1、pom.xml

        <!--Spring-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>

        <!--用于解析切入点表达式-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.7</version>
        </dependency>

2、service

package com.howling.aop.service;

/**
 * 对账户的操作
 */
public interface AccountService {

    /**
     * 模拟保存操作
     */
    void saveAccount();

    /**
     * 模拟更新操作
     *
     * @param i
     */
    void updateAccount(int i);

    /**
     * 模拟删除操作
     *
     * @return
     */
    int deleteAccount();
}
package com.howling.aop.service;

public class AccountServiceImpl implements AccountService {
    public void saveAccount() {
        System.out.println("用户已保存");
    }

    public void updateAccount(int i) {
        System.out.println("用户已更新" + i);
    }

    public int deleteAccount() {
        System.out.println("用户已删除");
        return 0;
    }
}

3、通知

package com.howling.aop.utils;

/**
 * 模拟日志记录的工具类,一会就会将此方法进行织入
 */
public class Logger {

    public void printLogger() {
        System.out.println("日志已经开始记录...");
    }
}

4、xml配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--注意,上面是新的规范-->

    <!-- 首先将我们的Target交给Spring管理 -->
    <bean id="accountService" class="com.howling.aop.service.AccountServiceImpl"></bean>


    <!--将Logger也交给Spring管理-->
    <bean id="logger" class="com.howling.aop.utils.Logger"></bean>

    <!--配置AOP-->
    <aop:config>
        <!--配置切面,引用通知方法-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置通知的方式,指定被增强的方法-->
            <aop:before method="printLogger" pointcut="execution(public void com.howling.aop.service.AccountServiceImpl.saveAccount())">
            </aop:before>
        </aop:aspect>
    </aop:config>

    <!--我们需要使用AspectJ来解析切入点表达式,所以我们需要AspectJ的支持-->
    <aop:aspectj-autoproxy  proxy-target-class="true"/>
</beans>

5、测试

package com.howling.aop;

import com.howling.aop.service.AccountServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AOPTest {
    public static void main(String[] args) {


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


        AccountServiceImpl accountServiceImpl = context.getBean("accountService", AccountServiceImpl.class);

        accountServiceImpl.saveAccount();

    }
}

切入点表达式

之前我们在快速起步的时候曾经见过切入点表达式

<aop:config>
    <aop:aspect id="logAdvice" ref="logger">
        <aop:before method="printLogger" pointcut="execution(public void com.howling.aop.service.AccountServiceImpl.saveAccount())">
        </aop:before>
    </aop:aspect>
</aop:config>

pointcut="execution(public void com.howling.aop.service.AccountServiceImpl.saveAccount())"

准确的说这并不是切入点表达式,只是指定了这个方法的访问修饰符,返回值,全类名,参数等等

这样的写法在开发中肯定是不可以的,接下来就是切入点表达式

1、访问修饰符可以省略

void com.howling.aop.service.AccountServiceImpl.saveAccount()

2、所有的返回值可以使用星号来表示

* com.howling.aop.service.AccountServiceImpl.saveAccount()

3、包名可以使用*来代替,但是有几级包就要写几个*

* *.*.*.*.AccountServiceImpl.saveAccount()

4、如果有多个子包,可以使用..代替当前包和子包

* *..AccountServiceImpl.saveAccount()

5、类名可以使用*来代替

* *..*.saveAccount()

6、方法名可以使用*来代替

* *..*.*

7、基本类型的参数可以直接写名称,引用类型需要全类名

* *..*.*(int,java.lang.String)

8、参数可以使用*来代替,但是必须要有参数才能够使用*

* *..*.*(*)

9、参数可以使用..来代替有参数或者无参数,参数可以为任意类型

* *..*.*(..)

所以最终可以简化为

* *..*.*(..)

注意:我们在开发过程中不建议使用全通配的表示方式,建议是切换到业务层底下再使用通配

* com.howling.aop.service.*.*(..)

四种常用的通知类型

  • 前置通知
  • 后置通知
  • 异常通知
  • 最终通知

  • Logger
package com.bean.utils;

/**
 * 模拟用于记录日志的工具类,里面提供了公共代码
 */
public class Logger {

    /**
     * 前置通知
     */
    public void beforePrintLog(){
        System.out.println("Logger类中的前置通知方法开始记录日志了。。。。。。");
    }

    /**
     * 后置通知通知
     */
    public void afterReturningPrintLog(){
        System.out.println("Logger类中的后置通知开始记录日志了。。。。。。");
    }

    /**
     * 异常通知
     */
    public void afterThrowsPrintLog(){
        System.out.println("Logger类中的异常通知方法开始记录日志了。。。。。。");
    }

    /**
     * 最终通知
     */
    public void afterPrintLog(){
        System.out.println("Logger类中的最终通知方法开始记录日志了。。。。。。");
    }

}
  • bean.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
  <!--注意,上面是新的规范-->

    <!--配置spring的IOC,把service对象配置进来
        我们想要对service方法进行增强,使service中执行任意一个方法前都执行一个日志
    -->
    <bean id="accountService" class="com.bean.service.impl.AccountServiceImpl"></bean>


    <!--我们有这个通知类,通知类就是记录日志,我们也交给spring-->
    <bean id="logger" class="com.bean.utils.Logger"></bean>

    <!--配置AOP-->
    <aop:config>
        <!--配置切面,引用通知方法-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置前置通知-->
            <aop:before method="beforePrintLog" pointcut="execution(* com.bean.service.impl.*.*(..))"></aop:before>

            <!--配置后置通知-->
            <aop:after-returning method="afterReturningPrintLog" pointcut="execution(* com.bean.service.impl.*.*(..))"></aop:after-returning>

            <!--&lt;!&ndash;异常通知&ndash;&gt;-->
            <aop:after-throwing method="afterThrowsPrintLog" pointcut="execution(* com.bean.service.impl.*.*(..))"></aop:after-throwing>

            <!--&lt;!&ndash;最终通知&ndash;&gt;-->
            <aop:after method="afterPrintLog" pointcut="execution(* com.bean.service.impl.*.*(..))"></aop:after>
        </aop:aspect>
    </aop:config>


</beans>
  • AOPTest
package com.bean.test;

import com.bean.service.IAccountService;
import com.bean.service.impl.AccountServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AOPTest {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");

        IAccountService accountService = (IAccountService) context.getBean("accountService");

        accountService.saveAccount();
    }
}

使用标签配置切入点表达式

<aop:pointcut></<aop:pointcut>

  • id
  • expression

此标签可以写在<aop:aspect></aop:aspect>里面,那么只能在这里面使用,再来一个切面要重新配

所以我们把它挪到外面挪到外面之后可能发现会报错,所以注意这个东西必须在aop:aspect之前,因为这是约束。

不然就会报错,一定要注意


  • bean.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
  <!--注意,上面是新的规范-->

    <!--配置spring的IOC,把service对象配置进来
        我们想要对service方法进行增强,使service中执行任意一个方法前都执行一个日志
    -->
    <bean id="accountService" class="com.bean.service.impl.AccountServiceImpl"></bean>


    <!--我们有这个通知类,通知类就是记录日志,我们也交给spring-->
    <bean id="logger" class="com.bean.utils.Logger"></bean>

    <!--配置AOP-->
    <aop:config>
        <!--配置切面表达式-->
        <aop:pointcut id="pointCut" expression="execution(* com.bean.service.impl.*.*(..))"></aop:pointcut>
        <!--配置切面,引用通知方法-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置前置通知,引用切面表达式-->
            <aop:before method="beforePrintLog" pointcut-ref="pointCut" ></aop:before>

            <!--配置后置通知,引用切面表达式-->
            <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pointCut"></aop:after-returning>

            <!--异常通知,引用切面表达式;-->
            <aop:after-throwing method="afterThrowsPrintLog" pointcut-ref="pointCut"></aop:after-throwing>

            <!--最终通知,引用切面表达式-->
            <aop:after method="afterPrintLog" pointcut-ref="pointCut"></aop:after>
        </aop:aspect>
    </aop:config>


</beans>

Spring中的环绕通知

  • 环绕通知

这个是我们前面讲的基于动态代理的通知

  • 我们发现环绕通知就是这整个方法
  • 里面包含着
    • 前置通知
    • 方法调用
    • 后置通知
    • 异常通知
    • 最终通知

所以环绕通知在Spring中的地位非同一般

首先我们需要注意几件事:

  1. 既然环绕通知包含了这些东西,那么也就代表着在spring中可以在环绕通知中配置其他的通知
  2. 既然上面的动态代理图片中有明确的方法调用,所以在环绕通知中也应该进行方法调用,要不然就不会进行方法执行

掌握了以上几件事,我们开始敲代码

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


    <bean id="accountService" class="com.bean.service.impl.AccountServiceImpl"></bean>


    <bean id="logger" class="com.bean.utils.Logger"></bean>

    <aop:config>

        <aop:pointcut id="pointCut" expression="execution(* com.bean.service.impl.*.*(..))"></aop:pointcut>

        <aop:aspect id="logAdvice" ref="logger">
            <!--只配置了一个环绕通知,环绕通知的标签就是<aop:around></aop:around>-->
            <aop:around method="aroudnPrintLog" pointcut-ref="pointCut"></aop:around>
        </aop:aspect>
    </aop:config>


</beans>
  • Logger
package com.bean.utils;

import org.aspectj.lang.ProceedingJoinPoint;

/**
 * 模拟用于记录日志的工具类,里面提供了公共代码
 */
public class Logger {

    /*
    * 我们刚才在事项里面说到
    *   1. 环绕通知必须要进行方法调用,否则方法不会执行
    *   2. 观看之前我们写的基于动态代理执行的方法,我们也可以进行其他四种通知的调用
    *   3. 我们也要有返回值
    *
    *    既然要进行方法调用,就要有参数
    *       ProceedingJoinPoint就是参数,用于获取方法
    *           - proceed():参数下面有一个方法proceed(),这个就相当于明确调用切入点方法
    *           - getArgs():用于获取切入点点方法的参数
    * */
    public Object aroudnPrintLog(ProceedingJoinPoint proceedingJoinPoint){

        Object returnValue = null;
        try {
            System.out.println("这叫前置通知");
            returnValue = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());   //这叫切入点方法调用
            System.out.println("这叫后置通知");
        } catch (Throwable throwable) {//注意这里使用的是Throwable,因为Exception拦不住它
            System.out.println("这叫异常通知");
            throwable.printStackTrace();
        }finally {
            System.out.println("这叫最终通知");
        }
        return returnValue;
    }
}

基于注解的AOP配置

快速起步

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

2、加入bean对象并且配置注解

  • service
package com.bean.service.impl;

import com.bean.service.IAccountService;
import org.springframework.stereotype.Service;

@Service
public class AccountServiceImpl implements IAccountService {
    public void saveAccount() {
        System.out.println("保存方法执行了");
    }

    public void updateAccount(int i) {
        System.out.println("更新方法执行了"+i);
    }

    public int deleteAccount() {
        System.out.println("删除方法执行了");
        return 0;
    }
}
  • Logger
package com.bean.utils;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * 模拟用于记录日志的工具类,里面提供了公共代码
 */
@Component//加入bean
@Aspect//注意,这里制定了这个Logger类是一个切面类
public class Logger {


    @Pointcut("execution(* com.bean.service.impl.*.*(..))")//配置切面表达式,id就是方法名称
    public void AspectPointCut(){};

    /**
     * 前置通知
     */
    @Before("AspectPointCut()")//指定前置通知,注意看好包,配置了切面表达式,注意一定要加括号
    public void beforePrintLog(){
        System.out.println("Logger类中的前置通知方法开始记录日志了。。。。。。");
    }

    /**
     * 后置通知通知
     */
    @AfterReturning("AspectPointCut()")//指定后置通知,配置切面表达式,注意加括号
    public void afterReturningPrintLog(){
        System.out.println("Logger类中的后置通知开始记录日志了。。。。。。");
    }

    /**
     * 异常通知
     */
    @AfterThrowing("AspectPointCut()")//指定异常通知,配置切面表达式,注意加括号
    public void afterThrowsPrintLog(){
        System.out.println("Logger类中的异常通知方法开始记录日志了。。。。。。");
    }

    /**
     * 最终通知
     */
    @After("AspectPointCut()")//指定最终通知,配置切面表达式,注意加括号
    public void afterPrintLog(){
        System.out.println("Logger类中的最终通知方法开始记录日志了。。。。。。");
    }


    //环绕通知
    @Around("AspectPointCut()")//指定环绕通知,配置切面表达式,注意加括号
    public Object aroudnPrintLog(ProceedingJoinPoint proceedingJoinPoint){
        Object returnValue = null;
        try {
            System.out.println("这叫前置通知");
            returnValue = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());   //这叫切入点方法调用
            System.out.println("这叫后置通知");
        } catch (Throwable throwable) {//注意这里使用的是Throwable,因为Exception拦不住它
            System.out.println("这叫异常通知");
            throwable.printStackTrace();
        }finally {
            System.out.println("这叫最终通知");
        }
        return returnValue;
    }

}
  • AOPTest
package com.bean.test;

import com.bean.service.IAccountService;
import com.bean.service.impl.AccountServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AOPTest {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");

        IAccountService accountService  = (IAccountService) context.getBean("accountServiceImpl");

        accountService.saveAccount();
    }
}

AOP注解中的问题,实际开发中应该怎么做

其实虽然注解好用,但是我不得不告诉你,Spring在完全使用注解方式执行AOP的时候会出现问题,就是顺序调用问题,比如下面的

Logger类中的前置通知方法开始记录日志了。。。。。。
保存方法执行了
Logger类中的最终通知方法开始记录日志了。。。。。。
Logger类中的后置通知开始记录日志了。。。。。。
  • 这是不使用环绕通知的时候进行的测试,代码写的没有问题,但是调用顺序出错了,其实是Spring有问题
  • 调用顺序为:前置通知–>最终通知–>后置通知
  • 所以假如使用注解,可能会出现问题

所以在这个方法下,采用半注解半代码的形式来配置(注解环绕通知)或者使用xml的形式,便可以避免调用顺序出错的问题


使用纯注解

1、首先看一下前面的不使用xml的时候使用的配置类

2、然后在配置类上加上一个@EnableAspectJAutoProxy来配置好切面类

3、下面是一个例子

  • java.config.SpringConfiguration
package config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan("com.bean")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}
  • java.com.bean.service.impl.AccountServiceImpl
package com.bean.service.impl;

import com.bean.service.IAccountService;
import org.springframework.stereotype.Service;

@Service
public class AccountServiceImpl implements IAccountService {
    public void saveAccount() {
        System.out.println("保存方法执行了");
    }

    public void updateAccount(int i) {
        System.out.println("更新方法执行了"+i);
    }

    public int deleteAccount() {
        System.out.println("删除方法执行了");
        return 0;
    }
}
  • java.com.bean.utils.Logger
package com.bean.utils;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * 模拟用于记录日志的工具类,里面提供了公共代码
 */
@Component//加入bean
@Aspect//注意,这里制定了这个Logger类是一个切面类
public class Logger {


    @Pointcut("execution(* com.bean.service.impl.*.*(..))")//配置切面表达式,id就是方法名称
    public void AspectPointCut(){};

    /**
     * 前置通知
     */
    @Before("AspectPointCut()")//指定前置通知,注意看好包,配置了切面表达式,注意一定要加括号
    public void beforePrintLog(){
        System.out.println("Logger类中的前置通知方法开始记录日志了。。。。。。");
    }

    /**
     * 后置通知通知
     */
    @AfterReturning("AspectPointCut()")//指定后置通知,配置切面表达式,注意加括号
    public void afterReturningPrintLog(){
        System.out.println("Logger类中的后置通知开始记录日志了。。。。。。");
    }

    /**
     * 异常通知
     */
    @AfterThrowing("AspectPointCut()")//指定异常通知,配置切面表达式,注意加括号
    public void afterThrowsPrintLog(){
        System.out.println("Logger类中的异常通知方法开始记录日志了。。。。。。");
    }

    /**
     * 最终通知
     */
    @After("AspectPointCut()")//指定最终通知,配置切面表达式,注意加括号
    public void afterPrintLog(){
        System.out.println("Logger类中的最终通知方法开始记录日志了。。。。。。");
    }


    //环绕通知
    @Around("AspectPointCut()")//指定环绕通知,配置切面表达式,注意加括号
    public Object aroudnPrintLog(ProceedingJoinPoint proceedingJoinPoint){
        Object returnValue = null;
        try {
            System.out.println("这叫前置通知");
            returnValue = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());   //这叫切入点方法调用
            System.out.println("这叫后置通知");
        } catch (Throwable throwable) {//注意这里使用的是Throwable,因为Exception拦不住它
            System.out.println("这叫异常通知");
            throwable.printStackTrace();
        }finally {
            System.out.println("这叫最终通知");
        }
        return returnValue;
    }

}
  • test.com.bean.test.AOPTest
package com.bean.test;

import com.bean.service.IAccountService;
import com.bean.service.impl.AccountServiceImpl;
import config.SpringConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AOPTest {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);

        IAccountService accountService  = (IAccountService) context.getBean("accountServiceImpl");

        accountService.saveAccount();
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。