Spring相关知识补充

目录

一、将Bean存储到spring(容器)中

1、使用spring-config的方式将对象存储到spring容器中

2、使用类注解的方式将Bean对象存储到容器中

1️⃣、配置扫描路径(使用注解的方式存对象的前提)

2️⃣、使用五大类注解存储Bean对象

3、使用方法注解注入对象@Bean

3.1、添加@Bean注解的方法的重命名

二、从Spring容器中获取Bean对象

1、通过getBean方法获取Bean对象

2、通过依赖注入的方式获取Bean对象

1.1、通过在spring-config.xml文件中配置实现依赖注入

1>、属性注入(Setter注入)

2> 、构造方法注入

1.2、通过注解的方式实现依赖注入

1>、 属性注入

2>、setter注入

3>、构造方法注入

三、Spring的AOP(面向切面编程)

1、静态代理

2、动态代理

1、JDK代理和CGLIB代理的区别

2、模拟实现动态代理

1、JDK动态代理

2、CGLIB动态代理

3、AOP的组成

1、实现AOP需要添加的依赖和配置

2、切入点表达式


一、将Bean存储到spring(容器)中

1、使用spring-config的方式将对象存储到spring容器中

①首先需要创建一个类,也就是创建一个Bean.

@Data
public class User {
    private Integer id;
    private String name;
    private String password;
}

②在resource目录中,创建一个spring-config.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:content="http://www.springframework.org/schema/context"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>

然后将Bean对象通过spring-config.xml配置文件,注入到Spring中。当然在配置文件中针对一个Bean可以注入多个对象,不过对象id要不同。

<bean id="user" class="com.it.pojo.User"></bean>

这里的id表示的存储到容器中bean对象的名称,class表示bean对象的路径(包名+类名),这里只是进行了声明并没有正真的注入到spring中,只是使用到这个对象中的方法时,才会正真的注入到spring中。

③从容器中获取bean对象

public class UserTest {
   @Test
    public void test(){
        ClassPathXmlApplicationContext ctx = new 
            ClassPathXmlApplicationContext("spring-config.xml");
        User user = ctx.getBean("user", User.class);
       System.out.println(user);
   }
}

2、使用类注解的方式将Bean对象存储到容器中

1️⃣、配置扫描路径(使用注解的方式存对象的前提)

我们需要在spring-config.xml文件当中配置下面的代码。用来配置Bean的扫描根路径。

<content:component-scan base-package="com.it"></content:component-scan>

这里的base-package属性的值,就是配置的扫描路径。只有在这个目录中的所有类才会被扫描是否添加了注解,如果添加了注解,就将这些类存放到IoC容器中。

2️⃣、使用五大类注解存储Bean对象

想要将对象存储到Spring中,有两种注解类型可以实现

  • 类注解:@Controller(控制器),@Service(服务)、@Repository(仓库)、@Component(组件),@Configuration(配置)。

  • 方法注解:@Bean

使用五大类注解将Bean对象注入到Spring容器中。在使用类注解将对象注入到spring容器中,从spring中获取该对象的时候,需要根据Bean对象的名称来获取,这个时候这个对象的名称必须是以小驼峰格式来命名,因为Bean对象名默认为小驼峰形式

@Controller
public class UserController {
    public void hello(){
        System.out.println("hello world");
    }
}
   @Test
   public void test01(){
       ClassPathXmlApplicationContext ctx = new 
           ClassPathXmlApplicationContext("spring-config.xml");
       //这里需要注意,在获取userController对象的使用需要使用小驼峰的命名格式,不然就获取不到这个对象
       UserController userController = ctx.getBean("userController", 
                                                   UserController.class);
       userController.hello();
   }

这里只是用一个类注解来演示一下即可。

3、使用方法注解注入对象@Bean

使用方法注解@Bean需要注意一些事项,我们在使用类注解的时候,只需要将类注解加载类上就可以直接使用依赖查找的方式获取Bean对象,但是方法注解的使用和类注解存在很大的差异。

1. 使用方法注解@Bean,需要方法的返回值是一个对象。
2. 方法注解@Bean不能单独使用,需要和五大类注解搭配使用。
3. 使用方法注解@Bean将对象注入到容器中,获取对象时默认的对象名为方法名。

这里使用User实体类演示,这里不给User类添加五大类注解,以及不在配置文件中声明User类的Bean对象。所以我们是不能直接通过getBean方法来获取到User对象的。

@Data
public class User {
    private Integer id;
    private String name;
    private String dept;
}
@Controller
public class UserController {
    @Bean
    public User test(){
        User user = new User();
        user.setId(101);
        user.setName("张三");
        user.setDept("经理");
        return user;
    }
}
   @Test
   public void test02(){
       ApplicationContext context = new 
           ClassPathXmlApplicationContext("spring-config.xml");
       Object user = context.getBean("test");
       System.out.println(user);
   }

3.1、添加@Bean注解的方法的重命名

使用依赖查找获取使用方法注解@Bean存储的对象时,传入的对象默认情况下为方法名,虽说这样获取没有问题,但是在一个项目中的方法起名时有getXXX获取setXXX,用这种默认的方式获取对象,总是看起来不是很舒服。所以这里可以使用@Bean注解的name属性来设置对象别名

//方式一
@Bean("abc");
//方式二
@Bean(name="abc")
//方式三
@Bean(value="abc")
//方式四:支持指定多个名称
@Bean(name={"abc","ddd","ccc"})
//方式五
@Bean(value={"aabc","ddd","eee"})
//方式六
@Bean({"aaa","bbb"})

这里name和value属性的作用相同。

使用@Bean注解需要注意的细节

1. 给Bean重命名了之后,就不能使用Bean默认的对象名了。
2. 在给Bean重命名时,不能和方法所在的当前类起相同的名字,也就是当前类名的小驼峰形式。否则在获取对象的时候,对象名重复了,就会报错。
3. 对同一类中多个方法返回的对象Bean重命名了相同的名字,程序执行的时候,由上而下,只会将第一个方法返回的Bean存储到Spring中,之后的相同名称的Bean就不会存储到Spring中了,容器会忽略掉。
4. 不同类中的方法在使用了@Bean注解向spring中注入对象,并且重命名起了相同的名字,这个时候容器中只能存储一个同名的Bean对象,可以使用@Order()注解,来指定哪个类里面的方法先执行,那么Spring容器中也就保存了谁。
5.在Spring中方法添加了@Bean注解后,这个方法就不能设置参数,否则执行之后就会报错。

二、从Spring容器中获取Bean对象

在spring框架中,获取容器中的对象(Bean)主要有两种方式:一种是通过applicationContext的getBean方法直接获取,另一种是通过依赖注入(DI)自动获取。

1、通过getBean方法获取Bean对象

@Test
   public void test02(){
       //读取spring上下文,并读取spring-config.xml文件
       ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
       //根据对象名获取Bean对象
       Object user = context.getBean("test");
       //使用Bean对象
       System.out.println(user);
   }

getBean方法有三种不同的用法(参数不同,用法不同)

①根据对象的名称获取Bean对象

//通过对象名来获取Bean对象,他的返回值Object类型,需要我们将其转换为想要的类型
User user = (User)context.getBean("user")

②根据类型来获取Bean对象

User user = context.getBean(User.class)

这种方式获取Bean对象,当Beans中只有一个类的实例是没有问题的,但是当有这个类的多个实例,就会报NoUniqueBeanDefinitionException异常,表示注入的对象不是唯一的。

③根据对象名和类型来获取Bean对象

User user = context.getBean("user",User.class)

这种方式相比于第一种更优雅,不需要强制类型转换。相比第二种也不会出现方式二中的问题。

2、通过依赖注入的方式获取Bean对象

1.1、通过在spring-config.xml文件中配置实现依赖注入
1>、属性注入(Setter注入)

spring-config.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:content="http://www.springframework.org/schema/context"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">  
    
    <!--定义被依赖的Bean-->
    <bean id="userDao" class="com.it.dao.impl.userDaoImpl"></bean>
	<!--定义依赖注入的Bean-->
    <bean id="userService" class="com.it.service.impl.UserServiceImpl">
        <!--使用property元素进行setter注入-->
        <property name="userDaoImpl" ref="userDao"></property>
    </bean>
</beans>

如果一个类的Bean对象依赖于其他的类的Bean对象,那么使用属性赋值的方式,将依赖的对象通过赋值的方式给到当前类的Bean对象。这里使用ref属性进行复制。

Bean对象的普通属性赋值,使用下面这种方式

    <bean id="user" class="com.it.pojo.User">
        <constructor-arg name="id" value="1"></constructor-arg>
        <constructor-arg name="name" value="张三"></constructor-arg>
        <constructor-arg name="password" value="123"></constructor-arg>
    </bean>

UserDaoImpl类

@Data
public class userDaoImpl  implements UserDao {
    public void print() {
        System.out.println("userDaoImpl");
    }
}

UserServiceImpl类

@Data
public class UserServiceImpl implements UserService {
    private UserDao userDaoImpl;
    public void print(){
        System.out.println("userServiceImpl");
    }
}

测试代码

public class UserTest {
    @Test
    public void test(){
        ClassPathXmlApplicationContext ctx = new 
            ClassPathXmlApplicationContext("beans.xml");
        UserServiceImpl userService = 
            ctx.getBean("userService",UserServiceImpl.class);
        userService.print();
    }
2> 、构造方法注入

构造注入是通过Bean的构造函数来注入依赖项。在Spring-config.xml中,可以使用<constructor-arg>元素来指定构造函数的参数。

spring-config.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:content="http://www.springframework.org/schema/context"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
           http://www.springframework.org/schema/beans/spring-beans.xsd 
		   http://www.springframework.org/schema/context
		   https://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/aop 
           https://www.springframework.org/schema/aop/spring-aop.xsd">
    
	<!--被依赖的Bean对象-->
    <bean id="userMapperImpl" class="com.it.mapper.impl.UserMapperImpl"></bean>
	<!--使用构造注入,将依赖对象注入到userServiceImpl对象中-->
    <bean id="userServiceImpl" class="com.it.service.impl.UserServiceImpl" >
        <!-- 使用constructor-arg标签来指定构造方法的参数,当只有一个参数的时候可以直接使用ref属性来指定传入构造方法中的参数对象 -->
        <constructor-arg ref="userMapperImpl"></constructor-arg>
    </bean>
</beans>

被依赖的对象的类

public class UserMapperImpl implements UserMapper {
    public void print() {
        System.out.println("hello world");
    }
}

依赖注入的类

public class UserServiceImpl implements UserService {
    private UserMapper userMapperImpl;
	//构造方法
    public UserServiceImpl(UserMapper userMapperImpl){
        this.userMapperImpl = userMapperImpl;
    }

    public void print() {
        userMapperImpl.print();
    }
}

如果构造方法中存在多个形参,可以使用这种方式

<!--使用构造注入,将依赖对象注入到userServiceImpl对象中-->
    <bean id="userServiceImpl" class="com.it.service.impl.UserServiceImpl" >
        <!--通过参数的位置(索引)来注入-->
        <constructor-arg index="0" ref="userMapperImpl"></constructor-arg>
        <constructor-arg index="1" ref="userMapperImpl"></constructor-arg>
    </bean>
1.2、通过注解的方式实现依赖注入
1>、 属性注入

属性注入只需要在选哟注入对象的属性上添加@Autowired或者@Resource注解即可。

1️⃣容器中同类型的对象只有一个:直接将获取到的对象注入到当前属性中。

@Repository
public class UserMapperImpl implements UserMapper {
    public void print() {
        System.out.println("hello world");
    }
}

将容器中的UserMapperImpl类的Bean对象注入到UserServiceImpl类的Bean对象中

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapperImpl;
    public void print() {
        userMapperImpl.print();
    }
}

2️⃣容器中同类型的对象存在多个:根据属性的名字来进行匹配

通过下面的例子来来了解容器中存在多个同类型对象,在获取对象时没有给属性指定注入那个对象,程序会出现错误。

@Data
public class User {
    private Integer id;
    private String name;
    private String dept;
}

@Component
public class UserBean {
    @Bean("user1")
    public User addBean(){
        User user = new User();
        user.setId(1);
        user.setName("张三");
        user.setDept("经理");
        return user;
    }

    @Bean("user2")
    public User addBean2(){
        User user = new User();
        user.setId(2);
        user.setName("李四");
        user.setDept("职员");
        return user;
    }
}
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private User user;
    public void print() {
        System.out.println(user.toString());
    }
}
 @Test
    public void test04(){
        ApplicationContext context = new 
           ClassPathXmlApplicationContext("spring-config.xml");
         UserMapperImpl userMapperImpl = 
             context.getBean("userMapperImpl", UserMapperImpl.class);
        userMapperImpl.print();
    }

这个时候由于我们使用@Bean存入了多个User类的对象,在执行这个程序的测试代码就会出现错误。

①这个时候就需要我们在注入User类的对象的时候,在UserServiceImpl类中,让属性名与存入容器时的对象名一致即可。

②使用@Autowired注解和@Qualifier搭配使用,根据@Qualifier注解的参数筛选对象

③也可以使用@Resource注解,这个注解的参数中存在name属性,可以通过name的值设置为这些对象中的一个即可。

2>、setter注入

使用setter注入,在setXXX方法上添加一个@Autowired或者@Resource注解即可,当然setter注入也存在和属性注入一样的问题,解决方法也和上述一样

@Repository
public class UserMapperImpl implements UserMapper {
    public void print() {
        System.out.println("你好世界");
    }
}

@Service
public class UserServiceImpl implements UserService {
    private UserMapper userMapperImpl;

    @Autowired
    public void setUserMapperImpl(UserMapper userMapperImpl){
        this.userMapperImpl = userMapperImpl;
    }
    public void print() {
        userMapperImpl.print();
    }
}
3>、构造方法注入

使用构造方法注入,标准的写法在构造方法上添加一个@Autowired注解或者在构造方法上不加注解也可以实现注入效果,当然构造注入也存在和属性注入一样的容器中相同类型的对象个数问题。解决方法和上述一样。但是构造方法不支持使用@Resource注解。

1️⃣标准写法,添加注解

2️⃣不太标准的写法,构造方法上不加注解,这种写法是当前类中只有一个构造方法的时候可以使用,但是有多个构造方法的时候,构造方法上的@Autowired注解是不可以省略的。

三、Spring的AOP(面向切面编程)

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

AOP的底层是通过动态代理来实现的。Spring采用JDK Proxy和CGLib动态代理。

1、静态代理

静态代理需要我们在程序运行之前,手动的为每个类或者接口的实现类实现代理类,这样操作起来非常的麻烦,当我们的类没增添一个新的方法,代理类中相应的也要为新方法编写增强方法。

接口类型

public interface UserService2 {
    public void save();
    public int update(User user);
    public int test(int a,int b);
}

接口的实现类

@Service
public class UserServiceImpl implements UserService2 {

    public void save() {
        System.out.println("UserServiceImpl中的save方法");
    }

    public int update(User user) {
        System.out.println("UserServiceImpl中的update方法");
        return 0;
    }

    public int test(int a, int b) {
        System.out.println("UserServiceImpl中的test方法");
        return 0;
    }
}
public class UserService2Impl implements UserService2 {
    public void save() {
        System.out.println("userService2中的save方法");
    }

    public int update(User user) {
        System.out.println("userService2中的update方法");
        return 0;
    }

    public int test(int a, int b) {
        System.out.println("userService2中的test方法");
        return 0;
    }

接口的代理类

public class StaticProxy implements UserService2{
    //这里使用接口,是为了适应这个接口所有的实现类
    UserService2 dto;
    //通过构造方法可以将该接口的不同实现类赋值给dto
    public StaticProxy(UserService2 dto){
        this.dto = dto;
    }
    //在这个方法中就可以为具体的实现类的方法实现增强
    public void save() {
        System.out.println("save方法执行之前。。。。。。。。。");
        dto.save();
        System.out.println("save方法执行之后。。。。。。。。。");

    }

    public int update(User user) {
        System.out.println("update方法执行之前。。。。。。。。。");
        dto.update(user);
        System.out.println("update方法执行之后。。。。。。。。。");
        return 0;
    }

    public int test(int a, int b) {
        System.out.println("test方法执行之前。。。。。。。。。");
        dto.test(1,2);
        System.out.println("test方法执行之后。。。。。。。。。");
        return 0;
    }
}

测试类

public class ProxyTest {
    @Test
    public void test(){
        StaticProxy staticProxy = new StaticProxy(new UserServiceImpl());
        staticProxy.save();
    }
}

❓看到这里存在一些小问题为什么staticProxy类,需要继承接口,不继承接口,根据类中的代码,也能够为传输的实现类实现静态代理?

主要作用是为了让代理类的方法签名严格遵循接口定义,防止运行是发生错误。

2、动态代理

当想要给实现了某个接口的类中的方法,加一些额外的处理,比如日志,事务等。可以给这个类创建一个代理,也就是创建一个新的类,这个类不仅包含原来类方法的功能,而且还在原来的基础上添加了额外处理的新方法,这个代理类并不是定义好的,而是在程序运行时动态生成的,具有解耦意义,灵活、扩展性强。

Java中,动态代理通常使用Java.lang.reflect.Proxy类和Java.lang.reflect.InvocationHandler接口来实现。

1、JDK代理和CGLIB代理的区别
  1. 接口要求:JDK动态代理只能对实现了接口的类生成代理;而CGLIB代理可以为没有实现接口的类生成代理,是通过继承被代理类,在运行时动态的生成代理对象。

  2. 生成方式:JDK代理使用Java的反射机制来完成代理对象,而CGLIB代理使用CGLIB库生成代理对象,通过修改目标类的字节码来实现。所以该类不能被final修饰。

2、模拟实现动态代理
1、JDK动态代理

1️⃣第一种实现方式

测试类

这里是通过实现InvocationHandler接口的匿名内部类来实现UserServiceImpl类的动态代理。

 @Test
    public void test02(){
       UserServiceImpl service = new UserServiceImpl();
       //第一个参数是类加载器,这里使用当前测试类的类加载器
       //第二个参数是被代理对象实现的接口数组,也就是被代理对象实现了那些接口,还有一种写法是new Class<?>[]{UserServie.class},直接指定代理对象将要实现那些接口。
       //第三个参数是InvocationHandler的实现,他会在每个方法调用时被调用
       UserService2 proxy=(UserService2) Proxy.newProxyInstance(ProxyTest.class.getClassLoader(),                                                         service.getClass().getInterfaces(), new InvocationHandler() {
            //InvocationHandler的invoke方法会在代理对象的方法被调用时执行
           //这里的Object[] args代表的意思是一个对象数组,包含调用代理对象方法时传递的参数
          public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
              System.out.println("方法执行之前------");
              //这里的args表示的是调用被代理对象的方法时传递的参数数组。
              Object res = method.invoke(service, args);
              System.out.println("方法执行之后******");
              return res;
          }
       });
        proxy.save();
    }

这种写法是JDK动态代理的一种实现方式,但是这种方式不够灵活,只能指定给一种对象实现动态代理。

2️⃣第二种实现方式

编写一个JDK动态代理的工具类,不论那个类想要通过JDK动态代理实现代理对象,都可以使用这个工具类

public class LogProxy implements InvocationHandler {
    //表示可以传递任何的对象
    private Object target;
    public LogProxy(Object target){
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("日志记录了......方法开始执行了");
        Object result = method.invoke(target,args);
        System.out.println("日志记录了,,,方法执行结束了");
        return result;
    }

    public Object getProxy(){
        //第一个参数表示代理类的类加载器
        //第二个参数是一个接口数组,表示被代理对象实现的接口,也就是代理对象将实现的接口
        //表示当前对象
        return  Proxy.newProxyInstance(LogProxy.class.getClassLoader(),target.getClass().getInterfaces(),this);                              
    }

}

测试类

    @Test
    public void test03(){
        UserService2 service2 = new UserService2Impl();
        //getProxy方法的所用时创建并返回一个动态代理对象
        UserService2 proxy = (UserService2)new LogProxy(service2).getProxy();
        proxy.save();
    }

❌上面这种测试方法是正确的,但是我们在测试的时候这样编写测试代码就会报ClassCastException异常。

//这段代码是一个错误展示
@Test
public void test03(){
    UserService2Impl service2 = new UserService2Impl();
    UserService2Impl proxy = (UserService2Impl)new LogProxy(service2).getProxy();
    proxy.save();
}

我们将Proxy.newProxyInstance返回的代理对象强制转换为UserService2Impl类型,会导致ClassCastException,这是因为Proxy.newProxyInstance创建的代理对象实现了target对象所实现的接口(在这里也就说实现了UserService接口,可以将其强转为UserService),而不是UserService2Impl类本身。

2、CGLIB动态代理

1️⃣第一种实现方式,通过创建类并继承MethodInterceptor接口的方式。

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;
//MethodInterceptor接口是CGLIB提供的,用于拦截对目标对象的方法调用
public class UserServiceHandler_CGLIB implements MethodInterceptor {
   private Object target;
   public UserServiceHandler_CGLIB(Object target){
       this.target = target;
   }

    //参数Object o 表示代理对象的实例
    //参数Method method被调用的方法的Method对象
    //Object[] objects 调用方法是传入的参数数组
    //MethodProxy methodProxy:CGLIB提供的MethodProxy实例,用于调用原始方法。使用他比直接使用Java反射更高效
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("方法增强,方法执行之前");
        //通过Java反射机制调用target对象的method方法,并传入objects参数数组。
        Object res = method.invoke(target,objects);
        System.out.println("方法增强,方法执行之后");
        return res;
    }

    /**
     * 用于生成代理对象
     * @return
     */
    public Object getProxyInstance(){
        //由CGLIB提供,用于动态创建类的子类
        Enhancer enhancer = new Enhancer();
        //指定Enhancer要增强的目标类的父类,这里直接使用target.getClass方法获取目标对象的类
        enhancer.setSuperclass(target.getClass());
        //设置回调为当前UserServiceHandler_CGLIB实例。当通过CGLIB生成的代理对象调用任何方法时,
        // 都会调用UserServiceHandler_CGLIB的intercept方法
        enhancer.setCallback(this);
        //调用enhancer.create方法生成代理对象,并将其返回。这个代理对象会在调用其任何方法时,通过
        //UserServiceHandler_CGLIB的intercept方法进行拦截和处理
        return enhancer.create();
    }
}

测试类 

 @Test
    public void test05(){
        UserServiceImpl service = new UserServiceImpl();
        UserServiceImpl proxy =(UserServiceImpl) new UserServiceHandler_CGLIB(service).getProxyInstance();
        proxy.save();
    }

2️⃣第二种实现方式:通过MethodInterceptor接口的匿名内部类的方式来实现。

  @Test
    public void test04(){
        //创建要被代理的对象
        final UserService2Impl service = new UserService2Impl();
        //使用这个类,为被代理对象创建代理对象
        Enhancer enhancer = new Enhancer();
        //被代理对象的父类类型
        enhancer.setSuperclass(service.getClass());
        //回调函数,Enhancer创建的代理对象在调用方法之前都要经过intercept方法
        enhancer.setCallback(new MethodInterceptor() {
            //这里通过匿名内部类的方式对MethodInterceptor接口中的intercept方法进行了重写
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                System.out.println("方法增强,方法执行之前");
                Object result = method.invoke(service, objects);
                System.out.println("方法增强,方法执行之后");
                return result;
            }
        });
        //创建代理对象
        UserService2 proxy =(UserService2) enhancer.create();
        //执行代理对象的方法
        proxy.save();
    }
}

3、AOP的组成

  • 切面:是由切点和通知组成,它既包含了横切逻辑的定义,也包含了连接点的定义。简单来说,切面就是当前AOP功能的类型,比如当前AOP是用户登录和鉴权的功能,那么它就是一个切面。

  • 切点:他的作用就是提供一组规则用来匹配连接点的。

  • 通知:切面中必须完成的工作,在AOP术语中,切面的工作被称之为通知。简单来说,当控制器方法(controller层方法)被拦截之后,触发执行的具体方法就是通知。

  • 连接点:是指在应用程序执行过程中可以被拦截的特定点。换句话说,连接点是在程序执行过程中的某个特定位置,它可以是方法调用,方法执行、异常抛出等。连接点是AOP中的基本单位,它代表了一个可以被切面拦截的位置,允许在这些位置插入额外的逻辑。切面可以通过连接点来捕获并应用他们的横切关注点。

  • 织入:将切面逻辑动态的插入到目标对象的方法执行流程中,实现了代码的解耦和复用。

1️⃣普通类

@Repository
public class EmpMapperImpl{
    public void save() {
        System.out.println("empMapper中实现save");
    }

    public void delete(int id) {
        System.out.println("empMapper中实现delete");
    }

    public int update() {
        return 1;
    }
}

2️⃣切面类

@Component  //交给spring管理
@Aspect //定义切面类
public class AspectProxy {

    //* com.it.mapper.impl.EmpMapperImpl.save()切入点表达式
    //前置通知
    @Before(value = "execution(* com.it.mapper.impl.EmpMapperImpl.*(..))")
    public void before(){
        System.out.println("前置通知...");
    }
    
     //后置通知
    @After(value = "execution(* com.it.mapper.impl.EmpMapperImpl.*(..))")
    public void after(){
        System.out.println("后置通知...");
    }

    //环绕通知
    @Around(value = "execution(* com.it.mapper.impl.EmpMapperImpl.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕之前...");
        Object result = joinPoint.proceed();
        System.out.println("环绕之后...");
        return result;
    }

    //返回通知
    @AfterReturning(value = "execution(* com.it.mapper.impl.EmpMapperImpl.*(..))")
    public void afterReturn(){
        System.out.println("返回通知...");
    }

    //抛出异常通知
    @AfterThrowing(value = "execution(* com.it.mapper.impl.EmpMapperImpl.*(..))")
    public void afterThrow(){
        System.out.println("抛出异常通知...");
    }
}

3️⃣测试类

public class AspectTest {
    @Test
    public void test() {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
        EmpMapperImpl mapper = ctx.getBean(EmpMapperImpl.class);
        mapper.save();
    }

从上面的代码中我们可以看到

🍂连接点:在EmpMapperImpl类中,每一个方法调用都可以视为一个连接点。这一位置,无论我们是否打算对这些方法进行增强,他们都存在与程序中,并可能在某个时间点被执行。

🍂切点:我们决定只对save方法进行增强,一遍在每次保存员工信息是记录一些日志信息。为了实现这一点,我们需要在切面类中定义一个切点,该切点通过切入点表达式来指定只配置EmpMapperImpl类中的Save方法

🍂织入:在EmpMapperImpl类中想要每个方法在执行之前/之后,记录一个日志,通过AOP的方式,我们可以在不修改源代码的方式,将记录日志的逻辑织入到EmpMapperImpl的save方法中。

🍁链接点匹配到切入点表达式,那么这个连接点就是切入点。

🍃织入过程

  1. 当Spring容器启动时,他会识别并注册所有的切面、切点和通知
  2. 当应用程序中的某个组件(可能时服务层、控制器层和其他任何层)调用EmpMapperImpl的方法时,Spring Aop框架会拦截这个调用
  3. 框架会检查是否存在与这个调用相匹配的切点。在上述例子中,save方法的调用切入点表达式相匹配。
  4. 一但找到匹配的切点,框架就会执行与该切点相关联的通知,记录日志。
  5. 完成通知的执行后,框架会继续执行原始的save方法。

❗❗❗ 当然我们在定义切入点表达式的时候也可以通过这个方式来实现

package com.it.mapper.proxy;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component  //交给spring管理
@Aspect //定义切面类
public class UserAspectProxy {

    //通过一个没有方法体的方法定义一个切入点表达式
    @Pointcut(value = "execution(* com.it.mapper.impl.UserMapperImpl.*(..))")
    public void pointCut() {
    }

    //其他的方法去调用这个方法即可
    //前置通知
    @Before("pointCut()")
    public void before(){
        System.out.println("前置通知...");
    }

    //后置通知
    @After("pointCut()")
    public void after(){
        System.out.println("后置通知...");
    }

    //环绕通知
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕之前...");
        Object result = joinPoint.proceed();
        System.out.println("环绕之后...");
        return result;
    }

    //返回通知
    @AfterReturning("pointCut()")
    public void afterReturn(){
        System.out.println("返回通知...");
    }

    //抛出异常通知
    @AfterThrowing("pointCut()")
    public void afterThrow(){
        System.out.println("抛出异常通知...");
    }
}

🍂通知的执行顺序

①环绕通知最先被执行

②其次是前置通知

③在是目标方法

④再是后置通知

⑤最后所有的方法执行完成之后,环绕通知方法才会执行完成。

1、实现AOP需要添加的依赖和配置

pom.xml文件

<dependencies>
        <!-- spring-aop 提供了面向切面编程的支持,它允许你定义横切关注点 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.3.5</version>
        </dependency>
        
        <!-- aspectjtools 提供了编译和织入方面的工具,它允许你以声明的方式定义切面 -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjtools</artifactId>
            <version>1.9.5</version>
        </dependency>
        
        <!-- aoplliance是一个标准化的AOP API,它定义了一组用于面向切面编程的接口和类-->
        <dependency>
            <groupId>aopalliance</groupId>
            <artifactId>aopalliance</artifactId>
            <version>1.0</version>
        </dependency>
        
        <!-- aspectjweaver 是Aspectj的一部分,它通过了在运行时动态织入方面的能力-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.0</version>
        </dependency>
    </dependencies>

beans.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:content="http://www.springframework.org/schema/context"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    
	<!-- 添加扫描路径 -->
    <content:component-scan base-package="com.it"/>
    <!-- 用于在Spring应用程序中启动对AspectJ注解的支持,告诉Spring框架自动检测那些使用了AspectJ注解的类(如@Aspect、@Before、@After等) -->
    <aop:aspectj-autoproxy/>
</beans>

2、切入点表达式

上述的切入点表达式表达的意思是:拦截UserMapperImpl类中所有方法其参数为任意参数并且返回值为任意类型的返回值。

  • execution:表示的意思为执行,执行的是后面()中的规则。

  • ..:表示不定式传参。

🍂常见的表达式示例

  • execution(* .com.it.controller.UserController.*(..)):配置UserController类中所有的方法。

  • execution(* .com.it.controller.UserController+.*(..)):匹配该类的子类包括该类的所有方法。

  • execution(* .com.it.controller.* . *(..)):匹配com.it.controller包下的所有类的所有方法。

  • execution(* .com.it.controller..* .*(..)):匹配com.it.controller包下,子孙包下所有类的所有方法

  • execution(* addUser(String,int)):匹配addUser方法,其第一个参数类型是String,第二个参数类型是int.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值