Spring IOC & AOP

IOC容器

概念

IOC,全程Inversion of Control(控制反转)
通过控制反转(创建对象的权限交给框架,所以叫反转)创建的对象被称为Spring Bean,这个Bean和用new创建出来的对象是没有任何区别的。

官方解释:Spring 通过 IoC 容器来管理所有 Java 对象的实例化和初始化,控制对象与对象之间的依赖关系。
举个例子:有一杯水,杯子相当于IOC容器,杯子里面的水相当于Bean对象,这个杯子从开始装水,到水被喝完结束这过程,就相当于容器控制对象的生命周期(从对象被创建,到最后被销毁的过程)

控制反转

  • 控制反转是一种思想而非技术。

  • 控制反转是为了降低程序耦合度,提高程序扩展力。

  • 控制反转,反转的是什么?

    • 将对象的创建权利交出去,交给第三方容器负责。
    • 将对象和对象之间关系的维护权交出去,交给第三方容器负责。
  • 控制反转这种思想如何实现呢?

    • DI(Dependency Injection):依赖注入

IOC过程说明
Spring IOC & AOP_第1张图片

依赖注入

DI(Dependency Injection):依赖注入,依赖注入实现了控制反转的思想。

依赖注入:

  • 指Spring创建对象的过程中,将对象依赖属性(XML)通过配置进行注入

依赖注入常见的实现方式包括两种:

  • 第一种:set注入
  • 第二种:构造注入

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

Bean管理说的是:Bean对象的创建,以及Bean对象中属性的赋值(或者叫做Bean对象之间关系的维护)。

依赖注入DI在Spring中的实现方式

Spring 的 IoC 容器就是 IoC思想的一个落地的产品实现。IoC容器中管理的组件也叫做 bean。在创建 bean 之前,首先需要创建IoC 容器。Spring 提供了IoC 容器的两种实现方式:

①BeanFactory

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

②ApplicationContext

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

③ApplicationContext接口 的主要实现类

Spring IOC & AOP_第2张图片

类型名 简介
ClassPathXmlApplicationContext 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象
FileSystemXmlApplicationContext 通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象
ConfigurableApplicationContext ApplicationContext 的子接口,包含一些扩展方法 refresh() 和 close() ,让 ApplicationContext 具有启动、关闭和刷新上下文的能力。
WebApplicationContext 专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。

基于XML管理Spring Bean管理

一般项目里很少用这种XML管理Bean的,这里就不演示了,想看的可以转战
尚硅谷Spring6

基于注解管理Spring Bean管理

何为注解?注解是代码中的一种特殊标记

开启组件扫描过程

用注解创建Spring Bean的过程
(没有用SpringBoot,默认只用Spring,如果用SpringBoot就可以用@ComponentScan等等来指定扫描路径)

Spring 默认不使用注解装配 Bean,因此我们需要在 Spring 的 XML 配置中,通过 context:component-scan 元素开启 Spring Beans的自动扫描功能。开启此功能后,Spring 会自动从扫描指定的包(base-package 属性设置)及其子包下的所有类,如果类上使用了 @Component 注解,就将该类装配到容器中。

看一下XML配置文件
com.atguigu.spring6这包下的文件就会被扫描,只要扫描到 @Component 注解,就将该类装配到容器中


<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
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd">
    
    <context:component-scan base-package="com.atguigu.spring6">context:component-scan>
beans>

注意:在使用 context:component-scan 元素开启自动扫描功能前,首先需要在 XML 配置的一级标签 中添加 context 相关的约束。

情况一:最基本的扫描方式
和上面的例子一样,当前路径下全扫

<context:component-scan base-package="com.atguigu.spring6">
context:component-scan>

情况二:指定要排除的组件
可以排除某些不想被扫描的类

<context:component-scan base-package="com.atguigu.spring6">
    
    
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        
context:component-scan>

情况三:仅扫描指定组件

<context:component-scan base-package="com.atguigu" use-default-filters="false">
    
    
    
    
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
	
context:component-scan>

使用注解定义 Bean

Spring 提供了以下多个注解,这些注解可以直接标注在 Java 类上,将它们定义成 Spring Bean。

功能其实都一样,比如说我的controller类叫XXXcontroller,并不是非要用@Controller来标记,用@Service等其他注解也一样可以完成目标,这里用不同名字的注解是为了更好的区分类功能。

注解 说明
@Component 该注解用于描述 Spring 中的 Bean,它是一个泛化的概念,仅仅表示容器中的一个组件(Bean),并且可以作用在应用的任何层次,例如 Service 层、Dao 层等。 使用时只需将该注解标注在相应类上即可。
@Repository 该注解用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
@Service 该注解通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
@Controller 该注解通常作用在控制层(如SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。

注入注解@Autowired和@Resource

实验一:@Autowired注入

单独使用@Autowired注解,默认根据类型装配。【默认是byType】
看下Autowired源码

package org.springframework.beans.factory.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
    boolean required() default true;
}

源码中有两处需要注意:

  • 第一处:该注解可以标注在哪里?
    @Target的元注解就标记了注解的位置

    • ElementType.CONSTRUCTOR 构造方法上
    • ElementType.METHOD 方法上
    • ElementType.PARAMETER 形参上
    • ElementType.FIELD 属性上
    • ElementType.ANNOTATION_TYPE 注解上
  • 第二处:该注解有一个required属性,默认值是true,表示在注入的时候要求被注入的Bean必须是存在的,如果不存在则报错。如果required属性设置为false,表示注入的Bean存在或者不存在都没关系,存在的话就注入,不存在的话,也不报错。

@Autowired的几种注入方式

这几个场景都是说明注解可以用的地方

①场景一:属性注入(用的最多)

创建UserDao接口(假装有数据库)
PS:Dao层和那个Mapper层是一个东西

package com.atguigu.spring6.dao;

public interface UserDao {

    public void print();
}

创建UserDaoImpl实现

package com.atguigu.spring6.dao.impl;

import com.atguigu.spring6.dao.UserDao;
import org.springframework.stereotype.Repository;

@Repository
public class UserDaoImpl implements UserDao {

    @Override
    public void print() {
        System.out.println("Dao/Mapper层执行结束");
    }
}

创建UserService接口

package com.atguigu.spring6.service;

public interface UserService {

    public void out();
}

创建UserServiceImpl实现类

package com.atguigu.spring6.service.impl;

import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    @Override
    public void out() {
        userDao.print();
        System.out.println("Service层执行结束");
    }
}

创建UserController类

package com.atguigu.spring6.controller;

import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

    @Autowired
    private UserService userService;

    public void out() {
        userService.out();
        System.out.println("Controller层执行结束。");
    }

}

测试一

package com.atguigu.spring6.bean;

import com.atguigu.spring6.controller.UserController;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class UserTest {

    private Logger logger = LoggerFactory.getLogger(UserTest.class);

    @Test
    public void testAnnotation(){
  		//从配置文件中读取Bean配置信息
        ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
        //获取Bean
        UserController userController = context.getBean("userController", UserController.class);
        //调用方法
        userController.out();
        logger.info("执行成功");
    }
}

测试结果:

所有调用的方法均可以正常执行
Spring IOC & AOP_第3张图片

以上构造方法和setter方法都没有提供,经过测试,仍然可以注入成功。

②场景二:set注入

修改UserServiceImpl类

package com.atguigu.spring6.service.impl;

import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public void out() {
        userDao.print();
        System.out.println("Service层执行结束");
    }
}

修改UserController类

package com.atguigu.spring6.controller;

import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

    private UserService userService;
	//这种就相当于把整个Service注入了,那么userService就可以直接调用
    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void out() {
        userService.out();
        System.out.println("Controller层执行结束。");
    }

}

测试:成功调用

③场景三:构造方法注入

修改UserServiceImpl类

package com.atguigu.spring6.service.impl;

import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    @Autowired
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public void out() {
        userDao.print();
        System.out.println("Service层执行结束");
    }
}

修改UserController类

package com.atguigu.spring6.controller;

import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

    private UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    public void out() {
        userService.out();
        System.out.println("Controller层执行结束。");
    }

}

测试:成功调用

④场景四:形参上注入

修改UserServiceImpl类

package com.atguigu.spring6.service.impl;

import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

    private UserDao userDao;
	//直接把注解加入到参数上面,就可以自动注入,后面就可以调用了
    public UserServiceImpl(@Autowired UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public void out() {
        userDao.print();
        System.out.println("Service层执行结束");
    }
}

修改UserController类

package com.atguigu.spring6.controller;

import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

    private UserService userService;

    public UserController(@Autowired UserService userService) {
        this.userService = userService;
    }

    public void out() {
        userService.out();
        System.out.println("Controller层执行结束。");
    }

}

测试:成功调用

⑤场景五:只有一个构造函数,无注解(可以省略)

修改UserServiceImpl类

package com.atguigu.spring6.service.impl;

import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

    // @Autowired 这个注解在只有一个有参构造是可以省略!
    private UserDao userDao;
	//唯一的一个有参构造,但凡多一个都注入不了
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public void out() {
        userDao.print();
        System.out.println("Service层执行结束");
    }
}

测试通过

当有参数的构造方法只有一个时,@Autowired注解可以省略。

说明:有多个构造方法时呢?大家可以测试(再添加一个无参构造函数),测试报错

⑥场景六:@Autowired注解和@Qualifier注解联合

@Autowired注解是默认byType进行注入的
@Qualifier注解是默认根据名称进行注入的
添加dao层实现

package com.atguigu.spring6.dao.impl;

import com.atguigu.spring6.dao.UserDao;
import org.springframework.stereotype.Repository;

@Repository
public class UserDaoRedisImpl implements UserDao {

    @Override
    public void print() {
        System.out.println("Redis Dao层执行结束");
    }
}

此时UserDao 就会有两个实现类(之前还创建了一个UserDao的实现类,加上这个就两个)。因为是根据类型注入的,装配的过程就会出现两个对应UserDao的对象。

测试:测试异常

错误信息中说:不能装配,UserDao这个Bean的数量等于2

怎么解决这个问题呢?当然要byName,根据名称进行装配了。

修改UserServiceImpl类
用Qualifier指定bean(实现类)的名字,默认首字母小写

package com.atguigu.spring6.service.impl;

import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
	//先类型,类型注入不了就按指定名字
    @Autowired
    @Qualifier("userDaoImpl") // 指定bean(实现类)的名字,默认首字母小写
    private UserDao userDao;

    @Override
    public void out() {
        userDao.print();
        System.out.println("Service层执行结束");
    }
}

总结

  • @Autowired注解可以出现在:属性上、构造方法上、构造方法的参数上、setter方法上。
  • 当带参数的构造方法只有一个,@Autowired注解可以省略。()
  • @Autowired注解默认根据类型注入。如果要根据名称注入的话,需要配合@Qualifier注解一起使用。

实验二:@Resource注入

@Resource注解也可以完成属性注入。那它和@Autowired注解有什么区别?

  • @Resource注解是JDK扩展包中的,也就是说属于JDK的一部分。所以该注解是标准注解,更加具有通用性。
  • @Autowired注解是Spring框架自己的,而非JDK的。
  • @Resource注解默认根据名称装配byName,未指定name时,使用属性名作为name。通过name找不到的话会自动启动通过类型byType装配。
  • @Autowired注解默认根据类型装配byType,如果想根据名称装配,需要配合@Qualifier注解一起用。
  • @Resource注解用在属性上、setter方法上。
  • @Autowired注解用在属性上、setter方法上、构造方法上、构造方法参数上。

源码这里就不详细展开了
Spring IOC & AOP_第4张图片

Resource注入的两种情况

我们知道Resource会根据名字进行注入,那么这个名字在制定了名字时,也就是@Resource(value="名称")时,就会按照这个指定的名字注入。
如果没有指定这个名字,就会按照被注入类的属性名称(不是类里的属性,是哪个myUserDao的名称)来注入。
不指定时:
Spring IOC & AOP_第5张图片
**指定名称时:**按照已经设定好的属性来注入
Spring IOC & AOP_第6张图片

几种注入方式

①场景一:根据name注入

修改UserDaoImpl类

package com.atguigu.spring6.dao.impl;

import com.atguigu.spring6.dao.UserDao;
import org.springframework.stereotype.Repository;

@Repository("myUserDao")
public class UserDaoImpl implements UserDao {

    @Override
    public void print() {
        System.out.println("Dao层执行结束");
    }
}

修改UserServiceImpl类

package com.atguigu.spring6.service.impl;

import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

    @Resource(name = "myUserDao")
    private UserDao myUserDao;

    @Override
    public void out() {
        myUserDao.print();
        System.out.println("Service层执行结束");
    }
}

测试通过,这种是正常的根据设定好的名称去注入

②场景二:name未知注入

修改UserDaoImpl类,我这里定义好了名字,但是注入的位置没有标记

package com.atguigu.spring6.dao.impl;

import com.atguigu.spring6.dao.UserDao;
import org.springframework.stereotype.Repository;

@Repository("myUserDao")
public class UserDaoImpl implements UserDao {

    @Override
    public void print() {
        System.out.println("Dao层执行结束");
    }
}

修改UserServiceImpl类

package com.atguigu.spring6.service.impl;

import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserDao myUserDao;

    @Override
    public void out() {
        myUserDao.print();
        System.out.println("Service层执行结束");
    }
}

测试通过

当@Resource注解使用时没有指定name的时候,还是根据name进行查找,这个name是属性名。

③场景三 其他情况

修改UserServiceImpl类,userDao1属性名不存在

package com.atguigu.spring6.service.impl;

import com.atguigu.spring6.dao.UserDao;
import com.atguigu.spring6.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserDao userDao1;

    @Override
    public void out() {
        userDao1.print();
        System.out.println("Service层执行结束");
    }
}

测试异常

根据异常信息得知:显然当通过name找不到的时候,自然会启动byType进行注入,以上的错误是因为UserDao接口下有两个实现类导致的。所以根据类型注入就会报错。

@Resource的set注入可以自行测试

总结:

@Resource注解:默认byName(指注解上的那个value名字)注入,没有指定name时把属性名当做name,根据name找不到时,才会byType注入。byType注入时,某种类型的Bean只能有一个。

手写IOC

我们都知道,Spring框架的IOC是基于Java反射机制实现的,下面我们先回顾一下java反射。

回顾反射

反射(上)

新建一个Car类用于反射测试

public class Car {

    private String name;

    private int age;

    private String color;
	
	...一大堆setter getter constructor 这里不赘述了

    private void run() {
        System.out.println("私有方法-run.....");
    }
}

要测试的内容:

  • 用反射获取class对象的多种方式
  • 获取构造方法
  • 通过反射获取类中属性
  • 获取方法
通过反射获取class对象的几种方法
public class TestCar {
    //获取Class对象的多种方式
    @Test
    public void test01() throws Exception {
        // 1 类名.class
        Class class1=Car.class;
        // 2 对象.getClass()
        Car car = new Car();
        Class class2 = car.getClass();
        // 3 Class.forName("类的全路径")
        //因为有可能路径不对获取不到,所以要捕获或者抛出异常
        Class class3 = Class.forName("com.cc.reflect.Car");
        //把刚刚获取的类实例化
        Car o=(Car) class3.getDeclaredConstructor().newInstance();
		//输出类的信息
        System.out.println(class1.toString());
        System.out.println(class2.toString());
        System.out.println(class3.toString());
    }
}

打印输出,可以看到,直接获取到了类的路径信息
Spring IOC & AOP_第7张图片

反射(中)

通过反射获取class对象的构造方法

这里有个条件,把Car的其中一个构造方法改成私有的,原本是公开的
Spring IOC & AOP_第8张图片

package com.cc.reflect;

import org.junit.jupiter.api.Test;
import java.lang.reflect.Constructor;

public class TestCar {
    //获取构造方法
    @Test
    public void test02() throws Exception {
        //获取class文件
    	//这里的car有三个属性,并且已经写好了构造、setter、getter
        Class clazz = Car.class;
        //获取所有构造方法(public的),放在数组里(这个数组因为是遍历公有方法的,所以这个数组只能发现并存放一个共有的构造)
        Constructor[] constructors=clazz.getConstructors();
        for (Constructor c: constructors) {
            System.out.println("构造方法名称"+c.getName()+ "参数个数"+c.getParameterCount());
        }
		
		//获取所有构造方法(public和private的),放在数组里(这个数组因为是遍历公有+私有方法的,所以这个数组发现并存放所有的构造(无论是共有还是私有))
		//PS:Declared声称的
        Constructor[] constructors1=clazz.getDeclaredConstructors();
        for (Constructor c: constructors1) {
            System.out.println("全部构造方法名称"+c.getName()+ "参数个数"+c.getParameterCount());
        }
    }
}

运行测试
Spring IOC & AOP_第9张图片

指定有参构造创建对象
  • public的构造方法
    @Test
    public void test03() throws Exception {
    	//获取class文件
        Class clazz = Car.class;
        //指定有参数构造创建对象
        //1 构造public
        Constructor c1 = clazz.getConstructor(String.class, int.class, String.class);
        Car car1 = (Car)c1.newInstance("夏利", 10, "红色");
        System.out.println(car1.toString());
    }

Spring IOC & AOP_第10张图片

  • private的构造方法
    @Test
    public void test03() throws Exception {
        //获取class文件
        Class clazz = Car.class;
        //2 构造private
        Constructor c2 = clazz.getDeclaredConstructor(String.class, int.class, String.class);
        //默认是访问不了私有方法的,把这个选项打开才能访问到,实例化的方法和之前一样
        c2.setAccessible(true);
        Car car2 = (Car)c2.newInstance("捷达", 20, "蓝色");
        System.out.println(car2);
    }

Spring IOC & AOP_第11张图片

反射(下)

在类里新增两个方法,一个public一个private
Spring IOC & AOP_第12张图片

通过反射获取属性

主要是先获取field属性数组,遍历,匹配,设置可进入,set值。

@Test
    public void test04() throws Exception {
        //获取class文件
        Class clazz = Car.class;
        //通过无参构造创建car对象
        Car car = (Car)clazz.getDeclaredConstructor().newInstance();
        //获取属性,属性集合是个数组
        Field[] fields=clazz.getDeclaredFields();
        //遍历这个数组获取属性,操作属性
        for (Field field : fields) {
            //当匹配到name属性时,进入操作
            if (field.getName().equals("name")){
                //将私有属性允许操作
                field.setAccessible(true);
                //设置值(car对象的name属性设置为 五菱宏光)
                field.set(car,"五菱宏光");
            }
        }
        System.out.println(car.toString());
    }
通过反射获取类内方法
  • 获取所有public方法
    @Test
    public void test05(){
        //创建对象
        Car car = new Car("奔驰",10,"黑色");
        Class clazz = car.getClass();
        //1.获取类内部的public方法
        //和上面的属性大同小异,也是通过用数组接收类内部的所有方法,组成一个Method数组
        Method[] methods=clazz.getMethods();
        for (Method m:methods){
            System.out.println("所有public方法的名字"+m.getName());
        }
    }
  • 在获取的基础上执行某个public方法
    反射里面的执行一般用invoke方法
    public Object invoke(Object obj, Object... args)Spring IOC & AOP_第13张图片
    @Test
    public void test05() throws Exception {
        //创建对象
        Car car = new Car("奔驰",10,"黑色");
        Class clazz = car.getClass();
        //1.获取类内部的public方法
        //和上面的属性大同小异,也是通过用数组接收类内部的所有方法,组成一个Method数组
        Method[] methods=clazz.getMethods();
        for (Method m:methods){
            //System.out.println("所有public方法的名字"+m.getName());
            if (m.getName().equals("test1")){
                //遍历到名字为test1的方法时执行
                m.invoke(car);
            }
        }
    }

执行结果:
Spring IOC & AOP_第14张图片

  • 获取所有private方法
    方法和上面一样类似,就是改一下调用的方法
    @Test
    public void test06() throws Exception {
        //创建对象
        Car car = new Car("奔驰",10,"黑色");
        Class clazz = car.getClass();
        //1.获取类内部的public方法
        //也是通过用数组接收类内部的所有方法,组成一个Method数组,通过getDeclaredMethods获取
        Method[] methods=clazz.getDeclaredMethods();
        for (Method m:methods){
            System.out.println("所有public+private方法的名字"+m.getName());
        }
    }

输出结果:输出所有私有和公有方法

  • 在获取的基础上执行某个private方法
    @Test
    public void test06() throws Exception {
        //创建对象
        Car car = new Car("奔驰",10,"黑色");
        Class clazz = car.getClass();
        //1.获取类内部的public方法
        //也是通过用数组接收类内部的所有方法,组成一个Method数组,通过getDeclaredMethods获取
        Method[] methods=clazz.getDeclaredMethods();
        for (Method m:methods){
            if (m.getName().equals("test2")){
                //遍历到名字为test2的私有方法时执行
                m.setAccessible(true);
                m.invoke(car);
            }
        }
    }

执行结果:
Spring IOC & AOP_第15张图片

手写IOC

先空着,占个位,以后再补

AOP

计算器的例子

声明计算器接口Calculator,包含加减乘除的抽象方法

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

创建对应实现类

public class CalculatorImpl implements Calculator {
    
    @Override
    public int add(int i, int j) {
    
        int result = i + j;
    
        System.out.println("方法内部 result = " + result);
    
        return result;
    }
    .....很多接口方法实现
}

此时来了一个需求,我要在输出的时候打日志log
Spring IOC & AOP_第16张图片
如果一行一行代码写,就会变成这样

public class CalculatorLogImpl implements Calculator {
    
    @Override
    public int add(int i, int j) {
    
        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
    
        int result = i + j;
    
        System.out.println("方法内部 result = " + result);
    
        System.out.println("[日志] add 方法结束了,结果是:" + result);
    
        return result;
    }
    ......省略很多相似的实现方法,但是都是带了很多log
}

打个比方我要是有一天需要改每一个日志的输出方法,那么就会非常难搞,每一个都得这么改,过于复杂。

引出的问题

①现有代码缺陷**

针对带日志功能的实现类,我们发现有如下缺陷:

  • 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
  • 附加功能分散在各个业务功能方法中,不利于统一维护

②解决思路

解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。

③困难

解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。

现有解决方案-代理模式

代理模式概念

①介绍
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
Spring IOC & AOP_第17张图片
使用代理后:
相当于把目标对象用代理对象包裹起来
Spring IOC & AOP_第18张图片
②生活中的代理

  • 广告商找大明星拍广告需要经过经纪人
  • 合作伙伴找大老板谈合作要约见面时间需要经过秘书
  • 房产中介是买卖双方的代理

③相关术语

  • 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
  • 目标:被代理“套用”了非核心逻辑代码的类、对象、方法。

静态代理

创建静态代理类(代理类是另外的类,不是在原有类上操作的):
这也就是相当于解耦了,唯一的区别是不用动原来的核心代码,但是本质上相当于copy出来了一份,在copy的基础上进行修改增强。

public class CalculatorStaticProxy implements Calculator {
    
    // 将被代理的目标对象声明为成员变量
    private Calculator target;
    
    public CalculatorStaticProxy(Calculator target) {
        this.target = target;
    }
    
    @Override
    public int add(int i, int j) {
    
        // 附加功能由代理类中的代理方法来实现
        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
    
        // 通过目标对象来实现核心业务逻辑
        int addResult = target.add(i, j);
    
        System.out.println("[日志] add 方法结束了,结果是:" + addResult);
    
        return addResult;
    }
}

静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。

提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。

动态代理

我们创建一个代理类,来帮助我们在操作前和操作后输出日志
Spring IOC & AOP_第19张图片
生产代理对象的工厂类:
源码,看一下就行

public class ProxyFactory {

    private Object target;

    public ProxyFactory(Object target) {
        this.target = target;
    }

    public Object getProxy(){

        /**
         * newProxyInstance():创建一个代理实例
         * 其中有三个参数:
         * 1、classLoader:加载动态生成的代理类的类加载器
         * 2、interfaces:目标对象实现的所有接口的class对象所组成的数组
         * 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法
         */
        ClassLoader classLoader = target.getClass().getClassLoader();
        Class<?>[] interfaces = target.getClass().getInterfaces();
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                /**
                 * 几个参数的含义
                 * proxy:代理对象
                 * method:代理对象需要实现的方法,即其中需要重写的方法
                 * args:method所对应方法的参数
                 */
                Object result = null;
                try {
                    System.out.println("[动态代理][日志] "+method.getName()+",参数:"+ Arrays.toString(args));
                    result = method.invoke(target, args);
                    System.out.println("[动态代理][日志] "+method.getName()+",结果:"+ result);
                } catch (Exception e) {
                    e.printStackTrace();
                    System.out.println("[动态代理][日志] "+method.getName()+",异常:"+e.getMessage());
                } finally {
                    System.out.println("[动态代理][日志] "+method.getName()+",方法执行完毕");
                }
                return result;
            }
        };

        return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
    }
}

AOP概念以及相关术语

AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

相关术语

①横切关注点

分散在每个各个模块中解决同一样的问题,如用户验证、日志管理、事务处理、数据缓存都属于横切关注点。

从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。

这个概念不是语法层面的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
Spring IOC & AOP_第20张图片

②通知(增强)

增强,通俗说,就是你想要增强的功能,比如 安全,事务,日志等。

每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。

  • 前置通知:在被代理的目标方法执行
  • 返回通知:在被代理的目标方法成功结束后执行(正常结束
  • 异常通知:在被代理的目标方法异常结束后执行(抛出异常
  • 后置通知:在被代理的目标方法最终结束后执行(最后执行
  • 环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
    Spring IOC & AOP_第21张图片
③切面

封装通知方法的类。

Spring IOC & AOP_第22张图片

④目标

被代理的目标对象。

⑤代理

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

⑥连接点

这也是一个纯逻辑概念,不是语法定义的。

把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。通俗说,就是spring允许你使用通知的地方
Spring IOC & AOP_第23张图片

⑦切入点

定位连接点的方式。

每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。

如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。

Spring 的 AOP 技术可以通过切入点定位到特定的连接点。通俗说,要实际去增强的方法

切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

作用
  • 简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。

  • 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了。

基于注解AOP

前期说明

Spring IOC & AOP_第24张图片
本质上:动态代理的底层是静态代理,在配置好动态代理类之后,系统按照你的配置要求,对目标类生成静态代理类,执行的时候就去自动执行增强过的动态代理类了
Spring IOC & AOP_第25张图片

  • 动态代理分为JDK动态代理和cglib动态代理
  • 当目标类有接口的情况使用JDK动态代理和cglib动态代理,没有接口时只能使用cglib动态代理
  • JDK动态代理动态生成的代理类会在com.sun.proxy包下,类名为$proxy1,和目标类实现相同的接口
  • cglib动态代理动态生成的代理类会和目标在在相同的包下,会继承目标类
  • 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
  • cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
  • AspectJ:是AOP思想的一种实现。本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。

前期准备

引入AOP的依赖
创建一个计算器类的接口以及对应实现类

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

实现类:

@Component
public class CalculatorImpl implements Calculator {
    
    @Override
    public int add(int i, int j) {
    
        int result = i + j;
    
        System.out.println("方法内部 result = " + result);
    
        return result;
    }
    
   ....省略其他代码
}

配置文件

首先要在xml配置文件中开启组件扫描
在Spring的配置文件中配置:
创建一个bean.xml文件,并写入相关配置

    
    <context:component-scan base-package="com.cc.annotationAop">context:component-scan>
    
    <aop:aspectj-autoproxy />

切入点表达式语法

Spring IOC & AOP_第26张图片

看一下切入点表达式的构成

切入点表达式:
execution(访问修饰符 增强方法返回类型 方法所在类全类名.方法名(方法参数))

Spring IOC & AOP_第27张图片
来点细节:

  • 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限

  • 在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。

    • 例如:*.Hello匹配com.Hello,不匹配com.atguigu.Hello
  • 在包名的部分,使用“*…”表示包名任意、包的层次深度任意

  • 在类名的部分,类名部分整体用*号代替,表示类名任意

  • 在类名的部分,可以使用*号代替类名的一部分

    • 例如:*Service匹配所有名称以Service结尾的类或接口
  • 在方法名部分,可以使用*号表示方法名任意

  • 在方法名部分,可以使用*号代替方法名的一部分

    • 例如:*Operation匹配所有方法名以Operation结尾的方法
  • 在方法参数列表部分,使用(…)表示参数列表任意

  • 在方法参数列表部分,使用(int,…)表示参数列表以一个int类型的参数开头

  • 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的

    • 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
  • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符

    • 例如:execution(public int Service.(…, int)) 正确
      例如:execution(
      int *…Service.(…, int)) 错误

@Before()和@After()

以前置通知和后置通知为例,对方法进行增强。

// @Aspect表示这个类是一个切面类
@Aspect
// @Component注解保证这个切面类能够放入IOC容器
@Component
public class LogAspect {

    // 设置切入点和通知类型
    // 切入点表达式:execution(访问修饰符 增强方法返回类型 方法所在类全类名.方法名(方法参数))
    // 通知类型:
    // 前置@Before(value = "切入点表达式配置切入点"),这里增强了CalculatorImpl的add方法的任意参数
    @Before(value = "execution(public int com.cc.annotationAop.CalculatorImpl.add(..))")
    public void beforeMethod(JoinPoint joinPoint){
        //可以根据JoinPoint获取对象信息(方法名,参数等等)
        String methodName = joinPoint.getSignature().getName();
        String args = Arrays.toString(joinPoint.getArgs());
        System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
    }
    // 后置@After(),这里增强了CalculatorImpl的任意方法的任意参数
    @After("execution(* com.cc.annotationAop.CalculatorImpl.*(..))")
    public void afterMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->后置通知,方法名:"+methodName);
    }
}

测试一下:
注意:AOP的情况下,手动创建对象是没办法增强的

public class testAop {
    @Test
    public void testAdd(){
        //xml方式获取对象(注意,对象必须由框架创建,不能手动new,手动new就不执行AOP了)
        ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        Calculator calculator = ac.getBean( Calculator.class);
        calculator.add(1, 2);
    }
}

测试通过,可以发现,按照预想结果输出,JoinPoint信息也能输出
Spring IOC & AOP_第28张图片
以上就是两种通知类型,还有许多其他的通知形式

@AfterReturning()

和@After()差不太多,主要是多了一个方法返回值
如果和@After()同时存在,那么先执行@AfterReturning()的增强内容,再执行@After()的增强内容,优先级略高
必须方法正常执行结束以后,有返回值,才会触发AfterReturning
如果方法都被异常中断了,没有返回值,那么返回值结果也就没有意义了,所以触发的条件就是方法正常结束
Spring IOC & AOP_第29张图片

	// 返回@AfterReturning() 在被代理的目标方法成功结束后执行,可以获取到目标方法的执行结果
    // 注意,returning的返回值是增强方法结果的返回值,对应的属性名字要和传入的参数名字保持一致
    //此处如果多个属性的话,value就要标注了
    @AfterReturning(value = "execution(* com.cc.annotationAop.CalculatorImpl.*(..))",returning = "result")
    public void afterReturningMethod(JoinPoint joinPoint,Object result){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result);
    }

执行测试
Spring IOC & AOP_第30张图片

@AfterThrowing()

在被代理的目标方法异常结束后执行,可以获取异常信息

    // 异常@AfterThrowing()
    // 目标方法执行,在被代理的目标方法抛出异常后执行,可以获取到目标方法抛出的异常信息
    // 注意:注解里throwing属性的名字,要与传入参数的名称保持一致
    @AfterThrowing(value = "execution(* com.cc.annotationAop.CalculatorImpl.*(..))",throwing = "ex")
    public void afterThrowingMethod(JoinPoint joinPoint,Throwable ex){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->异常通知,方法名:" + methodName + ",结果:" + ex);
    }

在对应的类中加入手动异常
Spring IOC & AOP_第31张图片
执行测试
Spring IOC & AOP_第32张图片

@Around()

(删掉刚刚手动异常的内容)
环绕通知就是在之前增强的基础上,再包一层,其他增强也会执行
注意,这里如果想执行方法,就不能用JoinPoint了,JoinPoint只能获取方法信息,无法促使方法执行,这里改用ProceedingJoinPoint对象,才可以执行方法
环绕通知也有和AfterReturning一样的返回值,可以操作返回值

    // 环绕@Around()
    // 在方法的执行前后都会执行,只有execution执行点一个参数
    // 注意,这里如果想执行方法,就不能用JoinPoint了,JoinPoint只能获取方法信息,无法促使方法执行
    // 这里改用ProceedingJoinPoint对象,可以执行方法
    // 环绕通知也可以有AfterReturning的返回值,可以操作返回值
    @Around("execution(* com.cc.annotationAop.CalculatorImpl.*(..))")
    public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint){
        Object result = null;
        try {
            System.out.println("环绕通知-->目标对象方法执行之前");
            //目标对象(连接点)方法的执行
            result = proceedingJoinPoint.proceed();
            System.out.println("环绕通知-->目标对象方法返回值之后");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("环绕通知-->目标对象方法出现异常时");
        } finally {
            System.out.println("环绕通知-->目标对象方法执行完毕");
        }
        return result;
    }

执行测试
Spring IOC & AOP_第33张图片

通知类型总结

  • 前置通知:在被代理的目标方法执行
  • 返回通知:在被代理的目标方法成功结束后执行(正常结束
  • 异常通知:在被代理的目标方法异常结束后执行(抛出异常
  • 后置通知:在被代理的目标方法最终结束后执行(最后执行
  • 环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

各种通知的执行顺序:

  • Spring版本5.3.x以前:
    • 前置通知
    • 目标操作
    • 后置通知
    • 返回通知或异常通知
  • Spring版本5.3.x以后:
    • 前置通知
    • 目标操作
    • 返回通知或异常通知
    • 后置通知

重(chong)用切入点表达式

表达式可以复用,要不然每个都写一遍太麻烦,而且不好维护

注意:如果不是在同一个切面(切面类)使用的话,比如:A切面类里定义好的重用表达式,在B切面类使用,就要在路径前加上包名来区分

在重用表达式定义的类里面使用,就直接用方法名即可
如果不在重用表达式定义的类里面使用,需要包名+类名+方法名

①重用切入点表达式声明

@Pointcut("execution(* com.cc.annotationAop.CalculatorImpl.add(..))")
public void pointCut(){}

②在同一个切面(类)中使用

@Before("pointCut()")
public void beforeMethod(JoinPoint joinPoint){
    String methodName = joinPoint.getSignature().getName();
    String args = Arrays.toString(joinPoint.getArgs());
    System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}

③在不同切面(类)中使用

	//@Before("引用全路径.类名.表达式定义()")
    @Before("com.cc.annotationAop.LogAspect.pointCut()")
    public void beforeMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        String args = Arrays.toString(joinPoint.getArgs());
        System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
    }

切面优先级

相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序

  • 优先级高的切面:外面
  • 优先级低的切面:里面

使用@Order注解可以控制切面的优先级:

  • @Order(较小的数):优先级高
  • @Order(较大的数):优先级低

Spring IOC & AOP_第34张图片

基于XML的AOP

前期准备参考注解的AOP
具体实现的形式在bean.xml里面进行

<context:component-scan base-package="com.atguigu.aop.xml">context:component-scan>

<aop:config>
    
    <aop:aspect ref="loggerAspect">
        <aop:pointcut id="pointCut" 
                   expression="execution(* com.atguigu.aop.xml.CalculatorImpl.*(..))"/>
        <aop:before method="beforeMethod" pointcut-ref="pointCut">aop:before>
        <aop:after method="afterMethod" pointcut-ref="pointCut">aop:after>
        <aop:after-returning method="afterReturningMethod" returning="result" pointcut-ref="pointCut">aop:after-returning>
        <aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut-ref="pointCut">aop:after-throwing>
        <aop:around method="aroundMethod" pointcut-ref="pointCut">aop:around>
    aop:aspect>
aop:config>

现在项目上用的不多,就不详细展开了

你可能感兴趣的:(笔记,spring,java,后端)