从零开始学Spring-Ioc与AOP


1.Ioc注解式开发

1.1 注解回顾

注解的存在主要是为了简化XML的配置,Spring倡导全注解开发

注解定义的回顾

@Target(value = {ElementType.METHOD,ElementType.ANNOTATION_TYPE})
//target元注解,标准注解的注解,告诉你这个注解能在哪里出现
//使用某个注解,如果属性名为value,那么value可以省略掉
//使用某个注解,如果属性值为数组,并且数组只有一个元素,那么大括号可以省略掉
//@Retention(RetentionPolicy.RUNTIME)//保持性策略:运行时存在,标注注解最终保留在哪,最终可以被反射机制所识别
//@Retention(RetentionPolicy.CLASS),保留在CLASS文件中,最终无法被反射所识别
@Retention(RetentionPolicy.SOURCE)//最终只在JAVA源文件中存在,一经编译成为class文件则注解就不存在了
public @interface Component {
    String[] values();
}

1.2 反射与注解

@Component(values = "userBean")
public class User {}
//通过反射机制读取注解
Class<?> clazz = Class.forName("com.orm.domain.User");
//判断类上面是否有注解
if (clazz.isAnnotationPresent(Component.class)) {
    //如果有,获取类上的注解
    Component annotation = clazz.getAnnotation(Component.class);
    //如果想获取值?
    String[] values = annotation.values();
    System.out.println(values[0]);
}

1.3 组件扫描原理

用户指定你一个包名,然后你的扫描程序就去扫描这个包下的所有的类,一旦有这个类,你就把这个类造出来,放到容器里面去

public static void main(String[] args) throws Exception {
    Map<String,Object> map = new HashMap<>();
    String packageName = "com.orm.domain";
    //然后问题就是你怎么去扫这个包下的所有的类?
    //1.将包名变成路径
    //在正则表达式,要表达普通的.字符,那么就需要用/.
    String packagePath = packageName.replaceAll("\\.", "/");
    //这个packagePath,是在类路径下的
    URL resource = ClassLoader.getSystemClassLoader().getResource(packagePath);
    assert resource != null;
    String path = resource.getPath();
    //拿到绝对路径后,获取绝对路径下的所有文件
    File file = new File(path);
    List<String> classNames = new ArrayList<>();
    if(file.isDirectory()){
        //拿到所有子文件
        File[] files = file.listFiles();
        assert files != null;
        Arrays.stream(files).forEach(file1 ->{
            String[] split = file1.getName().split("\\.");
            classNames.add(split[0]);
        });
    }
    System.out.println(classNames);
    //然后通过反射机制
    for (String className : classNames) {
        Class<?> clazz = Class.forName(packageName + "." + className);
        if (clazz.isAnnotationPresent(Component.class)) {
            Component annotation = clazz.getAnnotation(Component.class);
            map.put(annotation.values()[0],clazz.newInstance());
        }
    }
    for (Map.Entry<String, Object> stringObjectEntry : map.entrySet()) {
        System.out.println(stringObjectEntry.getKey()+","+stringObjectEntry.getValue());
    }
}

1.4 声明bean的注解

声明Bean的注解

  • @Component:通用
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {

	/**
	 * The value may indicate a suggestion for a logical component name,
	 * to be turned into a Spring bean in case of an autodetected component.
	 * @return the suggested component name, if any (or empty String otherwise)
	 */
	String value() default "";

}
  • @Controller:View
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {

	/**
	 * The value may indicate a suggestion for a logical component name,
	 * to be turned into a Spring bean in case of an autodetected component.
	 * @return the suggested component name, if any (or empty String otherwise)
	 */
	@AliasFor(annotation = Component.class)//别名
	String value() default "";

}
  • @Service:Service
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {

	/**
	 * The value may indicate a suggestion for a logical component name,
	 * to be turned into a Spring bean in case of an autodetected component.
	 * @return the suggested component name, if any (or empty String otherwise)
	 */
	@AliasFor(annotation = Component.class)
	String value() default "";

}
  • @Repository:DAO
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {

	/**
	 * The value may indicate a suggestion for a logical component name,
	 * to be turned into a Spring bean in case of an autodetected component.
	 * @return the suggested component name, if any (or empty String otherwise)
	 */
	@AliasFor(annotation = Component.class)
	String value() default "";

}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface AliasFor {
    @AliasFor("attribute")
    String value() default "";

    @AliasFor("value")
    String attribute() default "";

    Class<? extends Annotation> annotation() default Annotation.class;
}

1.5 Spring注解的使用

配置扫描文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:c="http://www.springframework.org/schema/c"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!--给Spring框架指定要扫描哪些包-->
    <context:component-scan base-package="com.orm.domain"/>

</beans>

在Bean上配置

然后测试程序即可

ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("Spring.xml");
Student studentBean = classPathXmlApplicationContext.getBean("StudentBean", Student.class);
System.out.println(studentBean); 

默认名称是你的类名的首字母小写

如果是多个包怎么办?

  • 在配置文件中指定多个包,用逗号隔开
  • 指定多个包的共同父包
<!--给Spring框架指定要扫描哪些包-->
<context:component-scan base-package="com.orm.domain,com.orm.dao"/>
<!--给Spring框架指定要扫描哪些包-->
<context:component-scan base-package="com.orm"/>

1.6 选择性实例化Bean

假设某个包下有很多bean,有Component,Controller…现在只允许Controller参与Spring管理,其他的不实例化

<!--给Spring框架指定要扫描哪些包-->
<context:component-scan base-package="com.orm.domain">
    <!--它不参与扫描了-->
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Component"/>
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
</context:component-scan>

1.7 负责注入的注解

注入:让对象和对象之间产生关系、关联

@Value:当属性的类型是简单类型的时候,用此属性进行注入,用来代替property,前提是这个对象的实例将会交给IOC容器管理,这时候@Value注解才会生效

@Component(value = "StudentBean")
public class Student {
    private String id;
    @Value("张三")
    private String name;
    private Teacher tutor;
    private Set set;
}
@Value("张三")
public void setName(String name) {
    this.name = name;
}

@Value注解也可以用在方法上,如上的set方法

同时还可以用在构造方法上

public Student(String id, @Value("张三") String name, Teacher tutor) {
    this.id = id;
    this.name = name;
    this.tutor = tutor;
}

@AutoWired与@Qualifier

@AutoWired注解可以用来注入非简单类型,自动装配就是它,单独使用@Autowired是默认根据类型进行装配的

基于XML的自动装配有

  • 根据名称进行自动装配
  • 根据类型进行自动装配
<bean id = "UserService" class = "com.domain.UserService" autowire="byName>
    <bean id = "UserService" class = "com.domain.UserService" autowire="byType">

这个注解可以用在哪?

  • 构造方法
  • 方法上
  • 形参上
  • 属性上
  • 注解上

其作用是根据类型进行自动装配,如果接口有多个实现类的,那么@Autowired就无法实现自动装配了

这是因为它无法知道你实际上想要的是哪个实现类

如果想要解决以上的问题,只能根据名字进行装配

方法是使用@AutoWired@Qualifier联合使用,可以根据名字进行装配

@Qualifier("")中指定你的BeanId

当你没有在属性上加@AutoWired的时候,也可以在setXxxx()上加,也可以实现自动注入

同时,也可以在构造方法上指向,也可以实现自动注入,也可以在构造方法上的参数上执行

满足以下条件可以省略掉@Autowired

  • 属性名和构造方法的参数名一致
  • 构造方法中的参数只有一个,只有一个构造方法的时候,注解可以省略

@Resource

@Resouce注解可以完成非简单类型的注入,与@Autowired注解有什么区别?

  • @Resource是JDK扩展包中的一部分,该注解是标准的注解
  • @Autowired是Spring框架所特别提供的
  • @Resource注解默认根据名称装配byName,没有指定name的时候,使用属性名作为name,通过name找不到的话会自动启动通过类型byType进行装配
  • @Autowired注解默认根据类型装配byType,如果想要根据名称进行装配,需要配合@Qualifier注解一起使用
  • @Resource注解用在属性上,setter方法上
  • @Autowired注解用在属性上,setter方法上,构造方法上,构造方法的参数上
@Target({TYPE, FIELD, METHOD})
@Retention(RUNTIME)
public @interface Resource {
    String name() default "";
    String lookup() default "";
    Class<?> type() default java.lang.Object.class;
    enum AuthenticationType {
            CONTAINER,
            APPLICATION
    }
    AuthenticationType authenticationType() default AuthenticationType.CONTAINER;
    boolean shareable() default true;
    String mappedName() default "";
    String description() default "";
}
//配置文件
@Configuration
@ComponentScan({"com.client","com.orm"})//指明扫描哪个包
public class SpringConfig { }
@Test
public void testNoXml(){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);

}

2. GoF之代理模式

2.1 对代理模式的理解

生活场景1:牛村的牛二看上了隔壁村小花,牛二不好意思直接找小花,于是牛二找来了媒婆王妈妈。这里面就有一个非常典型的代理模式。牛二不能和小花直接对接,只能找一个中间人。其中王妈妈是代理类,牛二是目标类。王妈妈代替牛二和小花先见个面。(现实生活中的婚介所)【在程序中,对象A和对象B无法直接交互时。】

生活场景2:你刚到北京,要租房子,可以自己找,也可以找链家帮你找。其中链家是代理类,你是目标类。你们两个都有共同的行为:找房子。不过链家除了满足你找房子,另外会收取一些费用的。(现实生活中的房产中介)【在程序中,功能需要增强时。】

西游记场景:八戒和高小姐的故事。八戒要强抢民女高翠兰。悟空得知此事之后怎么做的?悟空幻化成高小姐的模样。代替高小姐与八戒会面。其中八戒是客户端程序。悟空是代理类。高小姐是目标类。那天夜里,在八戒眼里,眼前的就是高小姐,对于八戒来说,他是不知道眼前的高小姐是悟空幻化的,在他内心里这就是高小姐。所以悟空代替高小姐和八戒亲了嘴儿。这是非常典型的代理模式实现的保护机制。代理模式中有一个非常重要的特点:对于客户端程序来说,使用代理对象时就像在使用目标对象一样。【在程序中,目标需要被保护时】

业务场景:系统中有A、B、C三个模块,使用这些模块的前提是需要用户登录,也就是说在A模块中要编写判断登录的代码,B模块中也要编写,C模块中还要编写,这些判断登录的代码反复出现,显然代码没有得到复用,可以为A、B、C三个模块提供一个代理,在代理当中写一次登录判断即可。代理的逻辑是:请求来了之后,判断用户是否登录了,如果已经登录了,则执行对应的目标,如果没有登录则跳转到登录页面。【在程序中,目标不但受到保护,并且代码也得到了复用。】

在Java程序中代理模式的作用

  • 当一个对象需要收到保护的时候,可以考虑使用代理对象去完成某个行为
  • 需要给某个对象的功能进行功能增强的时候,可以考虑找一个代理进行增强
  • A对象和B对象无法直接交互的时候,可以使用代理模式进行解决

代理模式是GOF23种设计模式之一,属于结构型的设计模式

代理模式的作用是:为其他对象提供一种代理以控制对这个对象的访问,在某些情况下,一个客户不想或者不能直接引用一个对象,这时候可以通过一个称之为对代理的第三者来实现简介引用,代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不应该看到的内容和服务或者添加客户所需要的额外服务,通过引入一个新的对象来实现对真实对象的操作或者将新的对象作为真实对象的一个替身,这种实现机制就是代理模式,通过引入代理对象来间接访问一个对象,这就是代理模式的模式动机。

代理模式的三大角色:目标对象(演员),代理对象(替身演员),目标对象和代理对象的公共接口(演员和替身演员有相同的行为和动作),客户端是无法察觉你的上面的动作的,它只会觉得是在使用目标对象,在使用代理对象的时候就像在使用目标对象

2.2 静态代理

如果现在要统计每个方法的执行耗时,执行方案如下:

  • 硬编码:在每个方法中都添加统计执行耗时的代码

做了功能扩展的时候修改了源代码,破坏了OCP原则,代码没有得到复用

  • 编写子类,编写业务类的子类,让子类继承业务类,对每个业务方法进行重写
package com.powernode.mall.service.impl;
public class OrderServiceImplSub extends OrderServiceImpl{
    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        super.generate();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void detail() {
        long begin = System.currentTimeMillis();
        super.detail();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        super.modify();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }
}

违反了里式替换原则,重写了父类的方法,暴力地破坏了父类方法中的代码,同时用了耦合度高的继承关系。

相同的代码也要写很多遍,没有实现代码的复用

  • 采用代理模式解决,方案如下:

service提供一个代理类,让这个代理类去实现公共的接口,这样做:

保证客户端能够正常使用服务,屏蔽了底层的实现细节

package com.powernode.mall.service;

/**
 * @author 动力节点
 * @version 1.0
 * @className OrderServiceProxy
 * @since 1.0
 **/
public class OrderServiceProxy implements OrderService{ // 代理对象

    // 目标对象,将目标对象作为代理对象的属性,关联/聚合关系,比继承的关系的耦合度要低
    // 写一个公共接口,耦合度低
    private OrderService orderService;

    // 通过构造方法将目标对象传递给代理对象
    public OrderServiceProxy(OrderService orderService) {
        this.orderService = orderService;
    }

    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.generate();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void detail() {
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.detail();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.modify();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }
}

使用代理模式的客户端代码

// 创建目标对象
OrderService target = new OrderServiceImpl();
// 创建代理对象
OrderService proxy = new OrderServiceProxy(target);
// 调用代理对象的代理方法
proxy.generate();
proxy.modify();
proxy.detail();

这种方式是符合OCP原则的,采用的是关联关系has a,所以程序的耦合度低,所以这种方案是被推荐的

但是这种方法依然会产生类爆炸的问题

此时应该使用动态代理来解决,只需要将相对固定的代码写入到内存中(字节码生成技术),而不需要开发者再去维护这些类,同时一套代码也可以一直复用。

2.3 动态代理概述

在程序的运行阶段,在内存中动态生成代理类,被称为动态代理,目的是为了减少代理类的数量,解决代码复用的问题

其本质是生成字节码并且存储在内存中,供客户端程序调用Test

在内存中动态生成类的技术包括有

  • JDK动态代理技术:只能够代理接口
  • CGLIB:CodeGenerationLibrary,在运行期间扩展Java类与实现Java接口,既可以代理接口,也可以代理类,底层是通过继承的方式实现的,性能比JDK动态代理要好,底层有一个小而快的字节码处理框架ASM
  • Javassist动态代理技术

2.4 JDK动态代理

//创建目标对象
TestDAOImpl testDAOImpl = new TestDAOImpl();
//创建代理对象,JDK做这件事情newProxyInstance():新建代理对象实例,执行这个方法就会在底层创建代理对象
//3个参数
//类加载器
//代理类要实现的接口
//调用处理器
Object proxyObj = Proxy.newProxyInstance();
//使用方法

其中关键是三个参数

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
  • newProxyInstance():在底层动态生成类,并且生成了类之后创建了对象
  • 类加载器:作用是在你生成了class字节码文件后,通过这个类加载器加载到JVM中。并且JDK要求目标类的类加载器和代理类的类加载器使用同一个
  • 代理类要实现的接口:代理类和目标类要实现同一个接口或者同一些接口,必须要告诉JDK,你这个代理类要实现哪些接口
  • 调用处理器(InvocationHandler h):主要用来告诉JDK,你想要实现什么样的增强代码,你要把这个代码告诉JDK后,它才能处理,既然是接口,就要写接口的实现类

使用方法如下:

public class TimeInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return null;
    }
}

invoke()

因为一个类实现接口就必须实现接口中的方法

JDK已经提前将调用invoke()方法的相关代码写好了,你只需要补充代码即可,调用方是JDK

当代理对象调用代理方法的时候,注册到InvocationHandler调用处理器当中的invoke()方法被调用,但是在这一步,目标对象的方法没有被调用,需要知道,如果要做增强,目标对象的目标方法需要执行。

下面解析一下invoke()方法的三个参数

  • proxy(Object):代理对象的引用,这个参数使用较少
  • method:目标对象上的目标方法,要执行的目标方法就是它
  • args[]:目标方法上的实参
public class TimeInvocationHandler implements InvocationHandler {
    //invoke方法执行过程中,使用method来调用目标对象的目标方法
    private Object target;//目标对象
    public TimeInvocationHandler(Object target){
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //这个地方就是为了编写增强的代码的
        System.out.println("invoke1");
        //调用目标对象上的目标方法
        //方法四要素:对象,方法,参数,返回值
        Object retVal = method.invoke(target, args);
        System.out.println("invoke2");
        return null;
    }
}

关于返回值

如果按照上面的,就拿不到返回值,一定要把目标对象的目标方法的返回值给返回

到这里,我们梳理一下JDK的动态代理的底层实现

首先JDK的动态代理会在运行期间生成一个动态代理类$Proxy0,它继承了Proxy类,同时根据反射机制,获取到被代理类中的所实现的接口,然后这个$Proxy0也会去实现这些接口

因此在这种情况下,由于Java不支持多继承,因为其本身继承了Proxy的,因此它不能够去extends其他类,而只能够implements其他接口,那么在继承这个Proxy的时候,作用其实就是获取这个InvocationHandler这个类,那么从使用功能的角度来讲,将继承改成组合,也完全是可以的,这样做的话就可以代理类了。

那么从类的角度上分析完之后,我们分析它的执行机制是怎么样的

  • 首先第一步,当我们使用newProxyInstance的时候,这时候就会创建对应的代理类,然后使用对应的代理类中的方法,那么这个方法怎么来的呢?
  • 就是通过我们所写的Handler中invoke方法来调用
  • 解释一下,为什么我们只重写了一个方法,但是所有代理的方法都变成了invoke了?
  • 这是因为反射机制中提供了method,在执行方法的时候,可以解析获取调用方法的信息,从而可以在方法体内部精确地调用原方法。

这时候还有一个小问题,就是为什么要继承InvocationHandler呢?继承了之后它是怎么操作的呢?

代理类继承Proxy类,其主要目的还是为了传递InvocationHandler,所以的话,它是一种设计的上的思路,仅仅只是为了复用之前设计的类结构,它在产生那个$Proxy0的时候,会将Handler也给它传递过去,最终就可以用了

同时这种设计的好处就是能够减少类的初始化开销,因为注入对象也是需要开销的

同时继承自Proxy的关键特征,能够很好地说明某一个类是经过动态代理产生的

动态代理工具类

public class ProxyUtils {
    public static Object newProxyInstance(Object target){
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new TimeInvocationHandler(target));
    }
}

2.5 CGLIB动态代理

CGLIB既可以代理接口也可以代理类,它的底层是采用继承的方式实现的,所以被代理的目标类是不能用final修饰符的

所谓底层采用继承,也就是说底层是生成一个目标类的子类,来提供代理的,因此既可以代理类,也可以代理接口

代理类:增强被代理类的功能

代理接口:动态接口接口的实现类

@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    System.out.println("invoke1");
    Object ret = methodProxy.invokeSuper(o, objects);
    System.out.println("invoke2");
    return ret;
}
//CGLib
//创建字节码增强器对象,核心对象
Enhancer enhancer = new Enhancer();
//告诉CGlib父类是谁?
enhancer.setSuperclass(Entry.class);
//设置回调函数
//在CGLIB中不是InvocationHandler接口,而是方法拦截器接口MethodInterceptor
enhancer.setCallback(new TimeMethodInterceptor());
//创建代理对象
//在内存中生成Entry的子类
//创建代理对象
Entry entry = (Entry) enhancer.create();
System.out.println(entry.getKey());

3. 面向切面编程AOP

3.1 深入理解切面编程

IoC使得软件的组织松耦合,而AOP能够让你捕捉系统中经常使用的功能,并将其转化为组件

AOP(Aspect Oriented Programming):面向切面编程

AOP是对OOP的补充和延伸,AOP的底层实现就是动态代理

Spring的AOP所实现的动态代理技术是:JDK的动态代理+CGLIB的动态代理技术,Spring在这两种动态代理中灵活切换,如果是代理接口,那么默认使用的是JDK的动态代理,如果需要代理类而且这个类没有实现接口,就会切换使用CGLIB

切面:在业务流程中和你的业务逻辑不挂钩的通用代码

  • 事务管理
  • 日志模块
  • 安全管理

如果在每一个业务的处理过程中,都掺杂这些交叉业务的话,问题:

  • 交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复用,修改也很麻烦
  • 程序员无法专注于业务逻辑的编写,在编写核心业务的时候同时还需要处理这些交叉的业务

对AOP的理解:将与核心业务无关的代码(交叉业务)独立的抽取出来,形成一个独立的组件,然后以横向交叉的方式应用到业务流程当中的过程就叫做AOP

交叉业务就是一些通用的逻辑代码,例如说日志啊,安全啊这些

3.2 面向切面编程的术语

  • 连接点JoinPoint:在程序的整个执行流程中,可以组织切入切面的位置,方法的执行前后,异常抛出之后等位置,它描述的是位置
  • 切点PointCut:在程序执行流程中,真正织入切面的方法(一个切点对应多个连接点),描述的是方法,应用切面的具体的方法叫做切点,就是具体需要增强的方法
  • 通知Advice:通知又叫做增强,就是你具体要织入的代码,通知描述的是代码
    • 包括有前置通知:放到目标方法之前
    • 后置通知:放到目标方法之后
    • 环绕通知:前也有,后也有
    • 异常通知:捕捉到异常
    • 最终通知:finally语句块中的
  • 切面Aspect:切点+通知就是切面
  • 织入Weaving:把通知应用到目标对象上的过程
  • 代理对象Proxy:一个目标对象被织入通知后产生的新对象
  • 目标对象Target:被织入通知的对象

3.3 切点表达式

切点表达式用来定义通知在哪些需要增强的方法上进行切入

execution([访问控制权限修饰符] 返回值类型[全限定类名]方法名(形式参数列表)[异常])

访问控制权限修饰符:可选项,没写的话就是4个权限都包括,写public就表示只包括公开的方法

返回值类型:必填项,*表示返回任意类型

全限定类名:可选项,两个.代表当前包以及子包下的所有类,省略时表示所有的类

方法名:*表示所有方法,set*表示所有的set方法

形式参数列表:必填项,()表示没有参数,(…)表示参数类型和个数随意,(*)只有一个参数的方法

4. SpringAOP

4.1 实现概述

Spring对AOP的实现包括以下三种方式

  • Spring框架结合AspectJ框架实现的基于注解方式的AOP
  • Spring框架结合AspcetJ框架实现的基于XML文件方式的AOP
  • Spring框架自身实现的AOP,基于XML配置方式

4.2 实现环境

导入如下依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>

然后在xml文件中引入相关的命名空间

xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"

4.3 基于AspectJ的AOP注解式开发

package com.orm.service.impl;
@Service("studentService")
public class StudentServiceImpl implements StudentService{
    private StudentDAO studentDAO;

    public StudentServiceImpl(){
        System.out.println("执行了构造方法");
    }

    public StudentServiceImpl(StudentDAO studentDAO){
        System.out.println("执行了构造方法");
        this.studentDAO  = studentDAO;
    }

    private static final Logger logger = LoggerFactory.getLogger(StudentService.class);
    @Override
    public void saveStudent(Student student) {
        logger.info("执行了保存对象的方法....");
        //studentDAO.addStudent(student);
    }

    //定义set方法,这样符合DAO规范,底层是依赖于拼装set方法来invoke对应的方法的.
    public void setStudentDAO(StudentDAO studentDAO) {
        System.out.println("装配了DAO对象"+studentDAO.toString());
        this.studentDAO = studentDAO;
    }
}
@Component("logAspect")
@Aspect
public class LogAspect {
    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
    //通知以方法的形式出现,前置通知
    //execution(修饰符 返回值类型 方法名(形式参数列表))
    @Before("execution(* com.orm.service.StudentService.*(..))")//前置通知,标注的方法就是一个前置的通知,要标注上切点表达式
    public void beforeAdvice(){
        log.info("学生服务启动");
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:c="http://www.springframework.org/schema/c"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!--给Spring框架指定要扫描哪些包-->
    <context:component-scan base-package="com.orm"/>
    <!--开启自动代理-->
    <!--检查Aspect注解-->
    <!--spring容器在扫描类的时候,检查这个类上是否有@Aspect注解,如果有,就给这个类生成代理对象-->
    <!--当proxy-target-class为true表示强制使用cglib的动态代理-->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

4.4 通知类型

  • 前缀通知:使用@Before,目标方法执行之前的通知
  • 后置通知:使用@AfterReturning,目标方法执行之后的通知
  • 环绕通知:使用@Around,目标方法之前添加通知,同时目标方法执行之前添加通知
  • 异常通知:使用@AfterThrowing发生异常之后所添加的通知
  • 最终通知:使用@After放在finally语句块中的通知
@Component("logAspect")
@Aspect
public class LogAspect {
    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
    //通知以方法的形式出现,前置通知
    //execution(修饰符 返回值类型 方法名(形式参数列表))
    @Before("execution(* com.orm.service.StudentService.*(..))")//前置通知,标注的方法就是一个前置的通知,要标注上切点表达式
    public void beforeAdvice(){
        log.info("学生服务启动");
    }
    @AfterReturning("execution(* com.orm.service.StudentService.*(..))")
    public void afterAdvice(){
        log.info("后置通知");
    }
    @Around("execution(* com.orm.service.StudentService.*(..))")
    public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("前环绕通知");
        Object proceed = proceedingJoinPoint.proceed();//执行目标
        log.info("后环绕通知");
        return proceed;
    }
    @AfterThrowing("execution(* com.orm.service.StudentService.*(..))")
    public void throwingAdvice(){
        log.info("异常通知");
    }
    @After("execution(* com.orm.service.StudentService.*(..))")
    public void afterAdviceFinally(){
        log.info("最终通知");
    }
}

要特别注意这个环绕通知的写法,它需要用户来指定你的目标方法的位置

切面的先后顺序

当业务流程中有多个切面的时候,比如说有的切面负责控制事务,有的进行的是安全控制,如果多个切面的话,可以使用@Order注解来标识切面类,为@Order注解的value指定一个整数型的数字,数字越小,优先级越高

通用切点

public class LogAspect {
    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
    @Pointcut("execution(* com.orm.service.StudentService.*(..))")
    public void genericPointCut(){

    }
    //通知以方法的形式出现,前置通知
    //execution(修饰符 返回值类型 方法名(形式参数列表))
    @Before("genericPointCut()")//前置通知,标注的方法就是一个前置的通知,要标注上切点表达式
    public void beforeAdvice(){
        log.info("学生服务启动");
    }
    @AfterReturning("genericPointCut()")
    public void afterAdvice(){
        log.info("后置通知");
    }
    @Around("genericPointCut()")
    public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("前环绕通知");
        Object proceed = proceedingJoinPoint.proceed();//执行目标
        log.info("后环绕通知");
        return proceed;
    }
    @AfterThrowing("genericPointCut()")
    public void throwingAdvice(){
        log.info("异常通知");
    }
    @After("genericPointCut()")
    public void afterAdviceFinally(){
        log.info("最终通知");
    }
}

配置类

//配置文件
@Configuration
@ComponentScan({"com.client","com.orm"})//指明扫描哪个包
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class SpringConfig { }

4.5 基于XML的开发

<bean id="timeAspect" class="com.orm.aspects.LogAspect"></bean>
<aop:config>
    <!--切点表达式-->
    <aop:pointcut id="myPointCut" expression="execution(* com.orm.service..*(..))"/>
    <aop:aspect ref="timeAspect">
        <!--切面:通知+切点-->
        <aop:around method="aroundAdvice" pointcut-ref="myPointCut"/>
    </aop:aspect>
</aop:config>

4.6 AOP的实际案例:事务处理

项目中对于事务的控制是必须的,在一个业务流程当中,可能需要多条DML语句来共同完成,为了保证数据的安全,这多条DML语句要么同时成功,要么同时失败,这就需要添加事务控制的代码

package com.powernode.spring6.biz;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
// 事务切面类
public class TransactionAspect {
    
    @Around("execution(* com.powernode.spring6.biz..*(..))")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
        try {
            System.out.println("开启事务");
            // 执行目标
            proceedingJoinPoint.proceed();
            System.out.println("提交事务");
        } catch (Throwable e) {
            System.out.println("回滚事务");
        }
    }
}

文章作者: 穿山甲
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 穿山甲 !
  目录