Spring

94 阅读15分钟

Spring简绍

什么是Spring?

我们为什么要使用Spring?

Spring可以帮助我们做什么?

image.png

Spring 快速开始

学习Spring所需要的依赖包


<dependencies>
    <!-- spring-context -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.5.RELEASE</version>
    </dependency>
    <!-- spring-core-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>5.0.5.RELEASE</version>
    </dependency>
    <!--spring-beans -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>5.0.5.RELEASE</version>
    </dependency>
    <!-- spring-expression -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-expression</artifactId>
        <version>5.0.5.RELEASE</version>
    </dependency>
    <!-- spring-web -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>5.0.5.RELEASE</version>
    </dependency>
    <!-- spring-web -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.0.5.RELEASE</version>
    </dependency>
    <!-- spring-jdbc -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.0.5.RELEASE</version>
    </dependency>
    <!-- spring-tx -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>5.0.5.RELEASE</version>
    </dependency>
    <!-- spring-aspects -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>5.0.5.RELEASE</version>
    </dependency>
    <!-- spring-aop -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>5.0.5.RELEASE</version>
    </dependency>
    <!-- c3p0 连接池-->
    <dependency>
        <groupId>com.mchange</groupId>
        <artifactId>c3p0</artifactId>
        <version>0.9.2</version>
    </dependency>
    <!-- MySql驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.17</version>
    </dependency>
</dependencies>

正式开始

准备好一个JavaBean,里面的get/set方法请自行创建

public class Person {
    private String lastName;
    private Integer age;
    private String email;
    private String gender;
}

配置spring的配置文件

 <!--
    id:组件的唯一标识
    class:写包的全类名
    property标签:为属性进行赋值
    name:指定为那个属性进行赋值
    value:属性值
-->
    <bean id="person01" class="com.spring.entity.Person">
    <property name="lastName" value="niuxiaoniu"></property>
    <property name="age" value="18"></property>
    <property name="email" value="2691778746@qq.com"></property>
    <property name="gender" value="男"></property>
</bean>

从容器中获取对象

@Test
public void getBean(){
    ApplicationContext context = new ClassPathXmlApplicationContext("springIOC-conf.xml");
    Person person = context.getBean("person", Person.class);
    System.out.println(person);
}

执行结果:

image.png

HelloWorld小总结

1、ApplicationContext(IOC容器的接口)

2、同一个组件在容器中是单实例的

3、组件的创建,在创建容器的时候,相关组件也就跟着创建了,因此组件的创建工作是由组件完成的

4、IOC容器在创建组件对象的时候,会利用setter方法为JavaBean进行赋值

从IOC容器中获取Bean的两种方式

通过Bean的Id进行获取

@Test
public void getBeanById(){
    ApplicationContext context = new ClassPathXmlApplicationContext("springIOC-conf.xml");
    Object person = context.getBean("person");
    System.out.println(person);
}

image.png

通过Bean的class进行获取

@Test
public void getBeanByClass(){
    ApplicationContext context = new ClassPathXmlApplicationContext("springIOC-conf.xml");
    Person person = context.getBean(Person.class);
    System.out.println(person);
}

image.png

如果在容器中有相同类型的Bean会出异常:org.springframework.beans.factory.NoUniqueBeanDefinitionException

为JavaBean赋值的几种方式

通过属性赋值

<bean id="person01" class="com.spring.entity.Person">
   <property name="lastName" value="niuxiaoniu"></property>
   <property name="age" value="18"></property>
   <property name="email" value="2691778746@qq.com"></property>
   <property name="gender" value="男"></property>
</bean>

通过构造器进行赋值

    <bean id="person02" class="com.spring.entity.Person">
        <!--
            constructor-arg标签:使用构造器的方式进行赋值
            name属性:指定属性名
            value属性:指定属性值
        -->
        <constructor-arg name="lastName" value="luxiaolu"></constructor-arg>
        <constructor-arg name="age" value="18"></constructor-arg>
        <constructor-arg name="email" value="1120483855@QQ.com"></constructor-arg>
        <constructor-arg name="gender" value="女"></constructor-arg>
    </bean>

构造器为JavaBean进行赋值小总结:

1、当不使用name属性的时候,则严格按照构造器的顺序进行赋值,如果赋值的顺序不一致,可以使用index实行进行调整

2、当出现构造器重载的时候,可以使用index属性和type属性来确定到底给那个JavaBean属性进行赋值

通过p名称空间进行赋值

名称空间的作用:用来防止xml中的名称空间重复

如果想要使用p名称空间,需要添加上标签头

 xmlns:p="http://www.springframework.org/schema/p"
<bean id="person03" class="com.java.bean.Person"
      p:lastName="luxiaolu"
      p:age="18"
      p:email="3104084881@qq.com"
      p:gender="男"
>
</bean>

为一个Bean赋各种各样的值

在原先的Person类上添加

public class Car {
    private String carName;
    private Integer price;
    private String color;
}
public class Book {
    private String bookName;
    private String author;
}
public class Person {
    private String lastName;
    private Integer age;
    private String email;
    private String gender;

    private Car car;
    private List<Book> books;
    private Map<String,Object> maps;
    private Properties properties;
}

赋值null

<!--想要那个属性为null,那个属性里面添加null标签即可-->
<bean id="person01" class="com.spring.entity.Person">
    <property name="lastName">
        <null></null>
    </property>
</bean>

执行结果

image.png

引用外部的值ref

<bean id="car01" class="com.java.bean.Car">
    <property name="carName" value="宝马BWM"></property>
    <property name="color" value="蓝色"></property>
    <property name="price" value="1000000"></property>
</bean>

<bean id="person02" class="com.java.bean.Person">
    <property name="lastName" value="牛小牛"></property>
    <property name="age" value="18"></property>
    <property name="email" value="2691778746@qq.com"></property>
    <property name="gender" value="男"></property>
    <property name="car" ref="car01"></property>
</bean>

执行结果:

image.png

为List进行赋值

<bean id="person03" class="com.java.bean.Person">
    <property name="lastName" value="牛小牛"></property>
    <property name="age" value="18"></property>
    <property name="email" value="2691778746@qq.com"></property>
    <property name="gender" value="男"></property>
    <property name="car" ref="car01"></property>
    <property name="books">
        <list>
            <bean  class="com.java.bean.Book">
                <property name="bookName" value="斗罗大陆"></property>
                <property name="author" value="三少"></property>
            </bean>
            <bean class="com.java.bean.Book">
                <property name="bookName" value="斗破苍崎"></property>
                <property name="author" value="土豆"></property>
            </bean>
        </list>
    </property>
</bean>

执行结果:

image.png

为Map进行赋值

<bean id="person04" class="com.java.bean.Person">
    <property name="maps">
        <map>
            <!--
                entry标签:一个entry就代表一个键值
            -->
            <entry key="k1" value="张三"></entry>
            <entry key="k2" value="22"></entry>
            <entry key="k3" value-ref="car01"></entry>
            <entry key="k4">
                <bean class="com.java.bean.Book">
                    <property name="bookName" value="斗破苍崎"></property>
                    <property name="author" value="土豆"></property>
                </bean>
            </entry>
        </map>
    </property>
</bean>

执行结果

image.png

为Properties进行赋值

<bean id="person05" class="com.java.bean.Person">
    <property name="properties">
        <props>
            <prop key="username">root</prop>
            <prop key="password">123456</prop>
        </props>
    </property>
</bean>

执行结果:

image.png

util名称空间

xmlns:util="http://www.springframework.org/schema/util"
<bean id="person06" class="com.java.bean.Person">
    <!--引用MyMap名称空间-->
    <property name="maps" ref="MyMap"></property>
    <property name="books" ref="MyList"></property>
</bean>

<!--util名称空间-->
<util:map id="MyMap">
    <entry key="k1" value="张三"></entry>
    <entry key="k2" value="22"></entry>
    <entry key="k3" value-ref="car01"></entry>
    <entry key="k4">
        <bean class="com.java.bean.Book">
            <property name="bookName" value="斗破苍崎"></property>
            <property name="author" value="土豆"></property>
        </bean>
    </entry>
</util:map>

<util:list id="MyList">
    <bean  class="com.java.bean.Book">
        <property name="bookName" value="斗罗大陆"></property>
        <property name="author" value="三少"></property>
    </bean>
    <bean class="com.java.bean.Book">
        <property name="bookName" value="斗破苍崎"></property>
        <property name="author" value="土豆"></property>
    </bean>
</util:list>

执行结果:

image.png

级联属性赋值

<bean id="car" class="com.java.bean.Car">
    <property name="carName" value="BMW7"></property>
    <property name="price" value="1200000"></property>
    <property name="color" value="白色"></property>
</bean>

<!--级联属性赋值-->
<bean id="person07" class="com.java.bean.Person">
    <property name="car" ref="car"></property>
    <property name="car.price" value="999999"></property>
</bean>

执行结果

image.png

Bean之间的依赖关系以及创建顺序

<bean id="person" class="com.java.bean.Person"></bean>
<bean id="car" class="com.java.bean.Car"></bean>
<bean id="book" class="com.java.bean.Book"></bean>

测试创建顺序的代码

@Test
public void getBeanContext() {
    ApplicationContext context = new ClassPathXmlApplicationContext("springIOC-conf-02.xml");
}

执行结果:

可以看到执行的结果顺序和在配置文件里面配置的顺序一样,在这个配置文件里面,谁配置考前,谁就先加载。

image.png

Bean的作用域

<!--
 scope="singleton"    单例的    ,默认
 scope="prototype"    多实例的
-->
<bean id="person01" class="com.java.bean.Person" scope="prototype">
    <property name="lastName">
        <null></null>
    </property>
    <property name="age" value="18"></property>
    <property name="email" value="2691778746@qq.com"></property>
    <property name="gender" value="男"></property>
</bean>
@Test
public void getBeanProtype(){
    ApplicationContext context = new ClassPathXmlApplicationContext("springIOC-conf-02.xml");
    Person person01 = context.getBean("person01", Person.class);
    Person person02 = context.getBean("person01", Person.class);
    System.out.println(person01 == person02);
}

执行结果: image.png

Bean作用域小总结:

1、单实例的singleton(默认)

​ 1.1、在容器启动完成的时候就已经创建好了对象保存在容器中了

​ 1.2、在任何时候获取都是之前创建好的那个对象

2、prototype多实例的

​ 2.1、容器启动默认不会去创建Bean

​ 2.2、获取的对象的时候创建Bean

​ 2.3、在每一次获取都会创建新的对象

静态工厂和实例工厂

静态工厂

/**
 * 静态工厂
 */
public class CoffieStaticFactory {

    public static Coffie getCoffir(String brand){
        System.out.println("静态工厂正在做冲咖啡。。。。。。。。");
        Coffie coffie = new Coffie();
        coffie.setBrand(brand);
        coffie.setPrice(100);
        coffie.setSweet("不甜");
        return coffie;
    }
}

配置xml

    <!--
    class:指定静态工厂的全类名
    factory-method:指定那个是工厂方法
    constructor-arg:为方法传参数
-->
    <bean id="coffieFactory" class="com.java.factory.CoffieStaticFactory" factory-method="getCoffir">
        <constructor-arg name="brand" value="拿铁咖啡"></constructor-arg>
    </bean>

实例工厂

/**
 * 实例工厂
 */
public class CoffieFactory {
    public Coffie getCoffir(String brand){
        System.out.println("实例工厂正在做冲咖啡。。。。。。。。");
        Coffie coffie = new Coffie();
        coffie.setBrand(brand);
        coffie.setPrice(100);
        coffie.setSweet("不甜");
        return coffie;
    }
}

配置

   <bean id="coffieFactory" class="com.java.factory.CoffieFactory"> </bean>

    <!--
    factory-bean 指定使用那个工厂
    factory-method 指定使用工厂的那个方法
-->
    <bean id="coffie" class="com.java.bean.Coffie" factory-bean="coffieFactory" factory-method="getInstanceCoffir">
        <constructor-arg name="brand" value="雀巢咖啡"></constructor-arg>
    </bean>

元素获取

/*实例工厂*/
@Test
public void getBeanInstanceFactory(){
    ApplicationContext context = new ClassPathXmlApplicationContext("springIOC-conf-03.xml");
    Coffie coffie = context.getBean("coffie", Coffie.class);
    System.out.println(coffie);
}

执行结果

image.png

实现FactoryBean的工厂

public class MyFactoryBean implements FactoryBean<Book> {

    /**
     *工厂方法
     */
    public Book getObject() throws Exception {
        System.out.println("调用了getObject......");
        Book book = new Book("龙族", "无名氏");
        return book;
    }

    /**
     *返回创建的对象类型
     * spring会自动调用这个方法来确认创建的对象是什么类型
     */
    public Class<?> getObjectType() {
        return Book.class;
    }

    /**
     * 是否是单例?
     *      true   单例
     *      false 不是单例
     */
    public boolean isSingleton() {
        return false;
    }
}

将这个工厂类注册到xml中

   <bean id="book" class="com.spring.factory.MyFactoryBean"></bean>

获取工厂中的内容

    @Test
    public void getFactoryBean() throws Exception {
        ApplicationContext context = new ClassPathXmlApplicationContext("springIOC-conf-4.xml");
        Object book = context.getBean("book");
        System.out.println(book);
    }

执行结果

image.png

FactoryBean工厂类小总结:

1、使用FactoryBean工厂类的时候 IOC容器在启动的时候不会创建实例

2、FactoryBean 进行获取的时候才会创建对象

创建具有声明周期方法的Bean

在Book类中定义两个方法,初始化方法、和销毁方法

    public void initMethod(){
        System.out.println("Book类初始化");
    }

    public void distoryMethod(){
        System.out.println("Book类销毁了");
    }
<!--
    init-method  规定初始化方法
    destroy-method 规定销毁方法
-->
<bean id="book" class="com.java.bean.Book" init-method="initMethod" destroy-method="distoryMethod">
    
</bean>
/*携带声明周期的Bean*/
@Test
public void aroundBean(){
    ApplicationContext context = new ClassPathXmlApplicationContext("springIOC-conf-03.xml");
    Book book = context.getBean("book", Book.class);
    System.out.println(book);
    ((ClassPathXmlApplicationContext) context).destroy();
}

执行结果:

image.png

后置处理器

后置处理器接口BeanPostProcessor,可以在Bean的初始化前后调用方法

首先实现BeanPostProcessor接口

public class MyBeanPostProcessor implements BeanPostProcessor {
    //Bean初始化之前进行调用
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println(beanName+"BeforeInitialization .........");
        return bean;

    }
    //Bean初始化之后进行调用
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println(bean + "AfterInitialization");
        return bean;
    }
}

将实现了BeanPostProcessor接口的类注册到Spring中

<bean id="MyBeanPostProcessor" class="com.java.factory.MyBeanPostProcessor"></bean>

执行结果:

image.png

后置处理器小总结:

后置处理器的执行顺序:启动容器(构造器) -------> 后置处理器的Before方法 -------> 初始化方法 -------> 后置处理器的After方法 ------> 销毁方法

无论有没有初始化方法后置处理器都会进行工作

spring管理连接池

需要驱动jar包

<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

写配置,c3p0链接配置

<!--引用外部属性文件-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/elm?characterEncoding=utf8&amp;useSSL=false&amp;serverTimezone=UTC"></property>
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
</bean>

获取链接

@Test
public void getSpringUrlPool() throws SQLException {
    ApplicationContext context = new ClassPathXmlApplicationContext("springIOC-conf-04.xml");
    DataSource dataSource = context.getBean("dataSource", DataSource.class);
    System.out.println(dataSource.getConnection());
}

执行测试:

image.png

引用外部属性文件

创建配置文件db.properties

user=root
password=root
jdbcUrl=jdbc:mysql://localhost:3306/jdbc_tx?characterEncoding=utf8&useSSL=false&serverTimezone=UTC
driverClass=com.mysql.cj.jdbc.Driver

读取配置文件里面的内容

        <!--
        context:property-placeholder 加载外部的配置文件,
        location属性:要加载外部配置文件的位置
	    使用${} 将外部的properties文件中的内容进行取出来
        -->
        <context:property-placeholder location="classpath:db.properties"></context:property-placeholder>
        <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
                <property name="user" value="${user}"></property>
                <property name="password" value="${password}"></property>
                <property name="jdbcUrl" value="${jdbcUrl}"></property>
                <property name="driverClass" value="${driverClass}"></property>
        </bean>

测试代码:

@Test
public void getSpringUrlPool() throws SQLException {
    ApplicationContext context = new ClassPathXmlApplicationContext("springIOC-conf-04.xml");
    DataSource dataSource = context.getBean("dataSource", DataSource.class);
    System.out.println(dataSource.getConnection());
}

image.png

基于xml的自动装配

<!--基于xml的自动装配-->
<bean id="car1" class="com.spring.entity.Car">
        <property name="carName" value="BWM"></property>
        <property name="color" value="白色"></property>
</bean>
<bean id="car" class="com.spring.entity.Car">
        <property name="carName" value="大众"></property>
        <property name="price" value="1200000"></property>
        <property name="color" value="白色"></property>
</bean>

<!--
    autowire="default"          默认的属性只default,不自动装配
    autowire="no"               不自动装配
    autowire="byName"           根据名称进行自动装配
        根据属性名作为id去容器中找到相应的组件,然后对他进行赋值,如果找不到则装配Null
    autowire="byType"           根据类型进行自动装配
        根据属性的类型去容器中找到这个组件。就相当于ioc.getBean(属性类型.class)如果找到则正常赋值
        如果找不到赋值为null
        如果容器中有多个一样类型的组件,此时在按照类型去查找的话,会出异常uniqueException
    autowire="constructor"      根据构造器进行自动装配
        1、先按照有参构造器参数的类型进行装配,没有就直接装配null
        2、如果找到了多个相同的类型,参数的名作为id继续进行装配,找到就装配,没找到装配null

-->
<bean id="person" class="com.spring.entity.Person" autowire="constructor"></bean>

测试代码:

@Test
public void getBeanXML(){
    ApplicationContext context = new ClassPathXmlApplicationContext("springIOC-conf-05.xml");
    Object person = context.getBean("person", Person.class);
    System.out.println(person);
}

执行结果:

image.png

通过注解的方式将组件加入到容器

创建三个类 分别是: BookDAOBookServiceBookController

@Controller
public class BookController {}
@Repository
public class BookDAO {}
@Service
public class BookService {}

配置spring配置文件

<!--通过包扫描的方式加载JavaBean-->
<context:component-scan base-package="com.java"></context:component-scan>

测试代码:

@Test
public void getBean(){
    ApplicationContext context = new ClassPathXmlApplicationContext("springIOC-conf-06.xml");
    Object bookService = context.getBean("bookService");
    System.out.println(bookService);
}

执行结果:

image.png

如果使用注解加载默认他的id就是类名首字母小写,作用域默认是单例的

如果想要改变这个组件的默认名称需要在注解的小括号内部写上指定的名字

image.png

如果想要改变这个组件默认的作用域可以使用@Scope这个注解,默认是单例的

image.png

Spring中相关的注解

@Controller 通常标注在控制层

@Service 通常标注在Service层

@Component 通常标注在普通的组件上面

@Repository 通常标注在DAO层

以上四个注解都可以将一个组件交给Spring容器管理,他们的底层归根结底就是一个@Component注解

如果想要使用者四个注解,需要添加包扫描,扫描添加了其中四个任意一个注解的组件,然后交给Spring容器进行管理,扫描的规则是,把基础包和基础包下面的子包一块扫描。

如果想要使用注解的方式,一定要导入Aop的依赖包,我这里使用的是maven所以它事先帮助我们集成好了

如果想要使用注解的方式一定要记得去导AOP的jar包

指定扫描包的时候包含或者不包含的类

<!--自动扫描包-->
<context:component-scan base-package="com.spring">
        <!--
        exclude-filter 扫描的时候排除一些不必要的组件
        type属性:指定排除的规则
        type="annotation"              根据注解进行排除
        type="assignable"              根据类进行排除
        type="aspectj"                 使用Aspectj表达式进行排除
        type="custom"                  自定义排除规则
        type="regex"                   根据正则经行排除
        -->
        <!--<context:exclude-filter type="assignable" expression="com.spring.dao.BookDao"></context:exclude-filter>-->
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

自动装配

自动装配案例:

Controller

@Controller()
public class BookController {
    @Autowired
    private BookService bookService;
    public void doGet(){
        bookService.saveBook();
    }
}

Service

@Service
public class BookService {
    @Autowired
    private BookDAO bookDao;
    public void saveBook(){
        System.out.println("BookService 正在调用 bookDao的save方法........");
        bookDao.saveBook();
    }
}

DAO

@Repository
public class BookDAO {
    public void saveBook(){
        System.out.println("图书已经保存了......");
    }
}

Test

@Test
public void getController(){
    ApplicationContext context = new ClassPathXmlApplicationContext("springIOC-conf-06.xml");
    BookController bookController = context.getBean("bookController", BookController.class);
    bookController.doGet();
}

执行结果:

image.png

@Autowired匹配原理

@Autowired 是先按照类型然后从容器中去找,找到就装配上,找不到扔出异常

@Autowired 找到多个相同的类型,下一步按照变量名作为id继续进行匹配,匹配上就装配,如果没有匹配上就继续抛异常

在这不得不认识一个新的注解@Qualifier("id名"),这个注解的作用是指定一个名作为id,让spring别使用变量名作为id,找到就装配,找不到就抛异常

如果在方法上添加@Qualifier注解,这个方法也会在bean创建的时候自动运行,在方法上的每一个参数都会自动注入值

@Autowired 和 @Resource 这都是自动注入的注解

其中@Autowired是spring框架中的,离开Spring框架,就不可以继续使用了。而@Resource 注解是java的标准注解,它有很强的扩展性

IOC小总结

1、IOC是一个容器,可以帮助我们管理所有的组件

2、依赖注入(DI) @Autowried可以帮助我们进行自动的赋值

3、如果某个组件想要使用Spring的强大功能,就必须将这个组件注册到Spring容器中

4、@Autowried在进行自动装配的时候也是从容器中获取负责规则的bean

5、容器说到底就是哟个map用来存储各个组件

源码调试的一个思路:

​ 从HelloWorld开始,将类上面每一步关键地方打上断点。进去方法看看做了那些操作。如果看不懂先翻译方法名,如果在看不懂,就放行,看看这个方法做了什么操作,还可以看方法的注释,看控制台观察变量的变化

AOP

AOP 概述

AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。 指的就是在程序运行的期间将某段代码动态的切入到指定方法的指定位置进行运行的编程方式,称之为面向切面编程。

场景案例

计算器运行的时候进行日志记录。

创建一个接口

public interface Calculator {
    public int add(int i,int j);
    public int sub(int i,int j);
    public int div(int i,int j);
    public int mul(int i,int j);
}

实现这个接口

public class CalculatorImpl implements Calculator {
    @Override
    public int add(int i, int j) {
        System.out.println("[add] 方法开始了,他使用的参数是["+i+","+j+"]");
        int result = i + j;
        System.out.println("[add] 方法运行完成,他的计算结果是:" + result);
        return result;
    }

    @Override
    public int sub(int i, int j) {
        System.out.println("[sub] 方法开始了,他使用的参数是["+i+","+j+"]");
        int result = i - j;
        System.out.println("[sub] 方法运行完成,他的计算结果是:" + result);
        return result;
    }

    @Override
    public int div(int i, int j) {
        System.out.println("[div] 方法开始了,他使用的参数是["+i+","+j+"]");
        int result = i/j;
        System.out.println("[div] 方法运行完成,他的计算结果是:" + result);
        return result;
    }

    @Override
    public int mul(int i, int j) {
        System.out.println("[mul] 方法开始了,他使用的参数是["+i+","+j+"]");
        int result = i * j;
        System.out.println("[mul] 方法运行完成,他的计算结果是:" + result);
        return result;
    }
}

测试代码:

public class AopTest {
    @Test
    public void test(){
        Calculator calculator = new CalculatorImpl();
        calculator.add(1,2);
    }
}

执行结果;

image.png

这个方式肯定是不推荐的,如果要修改代码维护的话是相当滴麻烦

使用动态代理

/**
 * 代理对象的类
 */
public class CalculatorProxy {
    /**
     *
     * @param calculator  被代理的对象
     * @return
     */
    public static Calculator getProxy(Calculator calculator) {

        /**
         *     Object proxy    代理对象,给JDK使用
         *     Method method   当前将要执行的目标对象方法
         *     Object[] args   这个方法调用时候外界传入的参数
          */
        InvocationHandler h = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object result = null;
                try {
                    System.out.println("【"+method.getName()+"】方法开始执行了,使用的参数列表是" + "[" + Arrays.asList(args) + "]");
                    result = method.invoke(calculator, args);
                    System.out.println("【"+method.getName() +"】方法执行完成,计算的结果是:" + result);
                }catch (Exception e){
                    System.out.println("【"+method.getName()+"】方法执行出现了异常,异常信息是:" + e.getMessage());
                }finally {
                    System.out.println("【"+method.getName()+"】方法执行完成");
                }
                return result;
            }
        };
        //这个接口实现了那些类
        Class<?>[] interfaces = calculator.getClass().getInterfaces();
        //类加载器
        ClassLoader loader = calculator.getClass().getClassLoader();
        Object proxy = Proxy.newProxyInstance(loader, interfaces, h);
        return (Calculator) proxy;
    }
}

动态代理测试:

@Test
public void proxyTest(){
    Calculator calculator = new CalculatorImpl();
    //使用代理对象;代理对象进行加减乘除
    Calculator proxy = CalculatorProxy.getProxy(calculator);
    //proxy.add(2,1);
    //proxy.sub(2,1);

    proxy.div(1,0);
}

执行的结果:

image.png

动态代理的缺点:

1、写起来比较繁琐

2、Jdk默认的动静态代理,如果目标对象没有实现任何的接口,是没有办法创建代理对象的。

AOP的几个相关术语

image.png

1、横切关注点

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

2、切面(Aspect)

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

3、通知(Advice)

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

4、目标(Target)

被通知的对象

5、代理(Proxy)

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

6、连接点(Joinpoint)

横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置。例如:类某个方法调用前、调用后、方法捕获到异常后等。 在应用程序中可以使用横纵两个坐标来定位一个具体的连接点:

7、切入点

我们真正要执行日志记录的地方

AOP的使用

1、导包


<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.0.5.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.0.5.RELEASE</version>
</dependency>


<dependency>
    <groupId>net.sourceforge.cglib</groupId>
    <artifactId>com.springsource.net.sf.cglib</artifactId>
    <version>2.2.0</version>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>com.springsource.org.aspectj.weaver</artifactId>
    <version>1.6.4.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.aopalliance</groupId>
    <artifactId>com.springsource.org.aopalliance</artifactId>
    <version>1.0.0</version>
</dependency>

2、写配置

首先需要将目标类和切面方法加入到IOC容器中去

需要在切面类上添加@Aspect注解

写切入点表达式

/*切面类*/
@Aspect
@Component
public class LogUtils {

    @Before("execution(public int com.aop.impl.CalculatorImpl.*(int, int))")
    public static void logStart(){
        System.out.println("[xxx] 方法开始了,他使用的参数是[xxx]");
    }
    @AfterReturning("execution(public int com.aop.impl.CalculatorImpl.*(int,int))")
    public static void logReturn(){
        System.out.println("[xxx] 方法执行完成,执行的结果是[xxx]");
    }
    @AfterThrowing("execution(public int com.aop.impl.CalculatorImpl.*(int,int))")
    public static void logException(){
        System.out.println("[xxx] 方法出现了异常,异常信息是[xxx]");
    }
    @After("execution(public int com.aop.impl.CalculatorImpl.*(int,int))")
    public static void logEnd(){
        System.out.println("[xxx] 方法执行结束,他使用的参数是[xxx]");
    }
}
目标类
@Component
public class CalculatorImpl implements Calculator {
    @Override
    public int add(int i, int j) {
        int result = i + j;
        return result;
    }

    @Override
    public int sub(int i, int j) {
        int result = i - j;
        return result;
    }

    @Override
    public int div(int i, int j) {
        int result = i/j;
        return result;
    }

    @Override
    public int mul(int i, int j) {
        int result = i * j;
        return result;
    }
}

3、扫描包 开启基于注解的AOP功能

<!--包扫描-->
<context:component-scan base-package="aop"></context:component-scan>
<!--开启基于注解的AOP功能-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

4、执行测试:

@Test
public void proxyTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Config-AOP.xml");
    Calculator calculator = context.getBean(Calculator.class);
    int add = calculator.add(1, 2);
    System.out.println(add);
    System.out.println(calculator);
    System.out.println(calculator.getClass());
}

执行的结果:

可以看到这个接口实际上是他的代理对象

image.png

不使用接口进行创建

@Component
public class CalculatorImpl {
    
    public int add(int i, int j) {
        int result = i + j;
        return result;
    }

   
    public int sub(int i, int j) {
        int result = i - j;
        return result;
    }

    
    public int div(int i, int j) {
        int result = i/j;
        return result;
    }

    
    public int mul(int i, int j) {
        int result = i * j;
        return result;
    }
}

测试方法:

@Test
public void proxyTestNoInter(){
    ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Config-AOP.xml");
    CalculatorImpl bean = context.getBean(CalculatorImpl.class);
    System.out.println(bean);
    System.out.println(bean.getClass());
    bean.div(1,0);
}

执行结果:

image.png

通知注解:

@AfterThrowing      异常通知
@AfterReturning     返回通知
@After              后置通知
@Before             前置通知
@Around             环绕通知

切入点通配符

细节3 切入点通配符
*:
    作用一:匹配一个或者多个字符
    作用二:匹配任意一个参数
    作用三:匹配一层目录
    注意:这个 * 不能用来修饰权限

..:
    作用一:匹配任意多个参数,任意类型参数
    作用二:匹配任意多层路径

通知方法的执行顺序

    正常执行:前置通知(@Before)-----》后置通知(@After)-----》正常返回AfterReturning
    正常执行:前置通知(@Before)-----》后置通知(@After)-----》正常返回AfterThrowing

完善

可以发现上面的AOP虽然实现了动态切入,但是不知道调用的是那个方法、不知道使用的参数是谁、以及返回值和异常信息,接下来完善AOP

@Aspect
@Component
public class LogUtils {
   @Before("execution(public int com.aop.impl.CalculatorImpl.*(int, int))")
    public static void logStart(JoinPoint joinPoint){
       Object[] objects = joinPoint.getArgs();
       Signature signature = joinPoint.getSignature();
       String methodName = signature.getName();
       System.out.println("["+methodName+"] 方法开始了,他使用的参数是["+objects+"]");
    }

    @AfterReturning(value = "execution(public int com.aop.impl.CalculatorImpl.*(int,int))",returning = "result")
    public static void logReturn(JoinPoint joinPoint,Object result){
        Object[] objects = joinPoint.getArgs();
        Signature signature = joinPoint.getSignature();
        String methodName = signature.getName();
        System.out.println("["+methodName+"] 方法执行完成,执行的结果是["+result+"]");
    }
    @AfterThrowing(value = "execution(public int com.aop.impl.CalculatorImpl.*(int,int))",throwing = "e")
    public static void logException(JoinPoint joinPoint,Exception e){

        Signature signature = joinPoint.getSignature();
        String methodName = signature.getName();
        System.out.println("["+methodName+"] 方法出现了异常,异常信息是["+e.getMessage()+"]");
    }

    @After("execution(public int com.aop.impl.CalculatorImpl.*(int,int))")
    public static void logEnd(JoinPoint joinPoint){
        Object[] objects = joinPoint.getArgs();
        Signature signature = joinPoint.getSignature();
        String methodName = signature.getName();
        System.out.println("["+methodName+"] 方法执行结束,他使用的参数是["+objects+"]");
    }
}

执行测试;

image.png

抽取可以重复使用的切入点表达式

 如何抽取可重用的切入点表达式
     1、随便声明一个没有使用的返回void的空方法
     2、给方法上标注@Pointcut注解
@Aspect
@Component
public class LogUtils {

    /**
     * 抽取可重用的切入点表达式
     * 1、随便声明一个没有使用的返回void的空方法
     * 2、给方法上标注@Pointcut注解
     */
    @Pointcut("execution(public int com.aop.impl.CalculatorImpl.*(int, int))")
    public void myPoint(){}

   @Before("myPoint()")
    public static void logStart(JoinPoint joinPoint){
       Object[] objects = joinPoint.getArgs();
       Signature signature = joinPoint.getSignature();
       String methodName = signature.getName();
       System.out.println("["+methodName+"] 方法开始了,他使用的参数是["+objects+"]");
    }

    @AfterReturning(value = "myPoint()",returning = "result")
    public static void logReturn(JoinPoint joinPoint,Object result){
        Object[] objects = joinPoint.getArgs();
        Signature signature = joinPoint.getSignature();
        String methodName = signature.getName();
        System.out.println("["+methodName+"] 方法执行完成,执行的结果是["+result+"]");
    }
    @AfterThrowing(value = "myPoint()",throwing = "e")
    public static void logException(JoinPoint joinPoint,Exception e){

        Signature signature = joinPoint.getSignature();
        String methodName = signature.getName();
        System.out.println("["+methodName+"] 方法出现了异常,异常信息是["+e.getMessage()+"]");
    }

    @After("myPoint()")
    public static void logEnd(JoinPoint joinPoint){
        Object[] objects = joinPoint.getArgs();
        Signature signature = joinPoint.getSignature();
        String methodName = signature.getName();
        System.out.println("["+methodName+"] 方法执行结束,他使用的参数是["+objects+"]");
    }
 }

环绕通知

@Pointcut("execution(public int com.aop.impl.CalculatorImpl.*(int, int))")
public void myPoint(){}

@Around("myPoint()")
public Object myRound(ProceedingJoinPoint pjp) throws Throwable {
    String name = pjp.getSignature().getName();
    Object[] args = pjp.getArgs();
    Object proceed = null;
    try{
        //利用反射获取到目标方法
        System.out.println("[环绕前置通知]["+ name+"方法开始]");
        proceed = pjp.proceed(args);
        System.out.println("[环绕返回通知]["+name+"方法返回] ,返回值"+proceed);
    }catch (Exception e){
        System.out.println("[环绕异常通知]["+name+"]方法出现异常,异常信息:" + e);
    }finally {
        System.out.println("[环绕后置通知]["+name+"]方法结束");
    }
    return  proceed;
}

执行结果

image.png

AOP的应用场景

1、AOP加日志保存到数据库

2、AOP做权限认证

3、AOP做安全检查

4、AOP做事务的控制

事务控制

待更新.....