Spring AOP---java

目录

一、什么是Spring AOP

二、Spring AOP的组成

1.切面(Aspect)

2.连接点(Join Point)

3.切点(Pointcut)

4.通知(Advice)

三、Spring AOP 的实现

1.定义切面和切点

2.定义相关通知 

四、Spring AOP 的实现原理

1.织入:代理的生成时机 

2.动态代理

JDK动态代理模式

3.JDK 和 CGLIB 的区别

五、MyBatis的动态代理 


一、什么是Spring AOP

AOP Aspect Oriented Programming ):面向切面编程,它是一种思想, 它是对某一类事情的集中处 。比如用户登录权限的效验,没学 AOP 之前,我们所有需要判断用户登录的页面(中的方法),都要各自实现或调用用户验证的方法,然而有了 AOP 之后,我们只需要在某一处配置一下,所有需要判断用户登录页面(中的方法)就全部可以实现用户登录验证了,不再需要每个方法中都写相同的用户登录验证了。  AOP 是一种思想,而 Spring AOP 是一个框架,提供了一种对 AOP 思想的实现,它们的关系和 IoC与 DI 类似。

二、Spring AOP的组成

1.切面(Aspect)

切面( Aspect )由切点( Pointcut )和通知( Advice )组成,它既包含了横切逻辑的定义,也包括了连接点的定义。

2.连接点(Join Point

应用执行过程中能够插入切面的一个点,这个点可以是方法调用时,抛出异常时,甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

3.切点(Pointcut

Pointcut 是匹配 Join Point 的谓词。Pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述)来匹配 Join Point,给满足规则的 Join Point 添加 Advice 。切点相当于保存了众多连接点的集合。

4.通知(Advice

切面也是有目标的 —— 它必须完成的工作。在 AOP 术语中, 切面的工作被称之为通知
通知:定义了切面是什么,何时使用,其描述了切面要完成的工作,还解决何时执行这个工作的问题。Spring 切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:
    前置通知 使用 @Before :通知方法会在目标方法调用之前执行。
    后置通知 使用 @After :通知方法会在目标方法返回或者抛出异常后调用。
    返回之后通知 使用 @AfterReturning :通知方法会在目标方法返回后调用。
    抛异常后通知 使用 @AfterThrowing :通知方法会在目标方法抛出异常后调用。
    环绕通知 使用 @Around :通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行     自定义的行为。
切点相当于要增强的方法。

三、Spring AOP 的实现

1.定义切面和切点

import org.aspectj.lang.ProceedingJoinPoint; 
import org.aspectj.lang.annotation.*;
 import org.springframework.stereotype.Component;
 @Aspect // 表明此类为一个切面 
 @Component
 public class UserAspect { 
// 定义切点,这里使用 AspectJ 表达式语法 
  @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") 
  public void pointcut(){ } 
}
其中 pointcut 方法为空方法,它不需要有方法体,此方法名就是起到一个 标识 的作用,标识下面的通知方法具体指的是哪个切点(因为切点可能有很多个)。

2.定义相关通知 

四、Spring AOP 的实现原理

Spring AOP 是构建在 动态代理 基础上,因此 Spring AOP 的支持局限于方法级别的拦截
Spring AOP 支持 JDK Proxy 和 CGLIB 方式实现动态代理。默认情况下,实现了接口的类,使用 AOP 会基于 JDK 生成代理类,没有实现接口的类,会基于 CGLIB 生成代理类。

1.织入:代理的生成时机 

织入 是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
编译期: 切面在目标类编译时被织入。这种方式需要特殊的编译器。 AspectJ 的织入编译器就是以
这种方式织入切面的。
类加载器: 切面在目标类加载到 JVM 时被织入。这种方式需要特殊的类加载器( ClassLoader ,
可以在目标类被引入应用之前增强该目标类的字节码。 AspectJ5 的加载时织入( load-time
weaving. LTW )就支持以这种方式织入切面。
运行期: 切面在应用运行的某一时刻被织入。一般情况下,在织入切面时, AOP 容器会为目标对象
动态创建一个代理对象。 SpringAOP就是以这种方式织入切面的

2.动态代理

此种实现在设计模式上称为动态代理模式,在实现的技术手段上,都是在 class 代码运行期 ,动态的织入字节码。
我们学习 Spring 框架中的 AOP ,主要基于两种方式: JDK CGLIB 的方式。这两种方式的代理目标都是被代理类中的方法,在运行期,动态的织入字节码生成代理类。
CGLIB Java 中的动态代理框架,主要作用就是根据目标类和方法,动态生成代理类。Java中的动态代理框架,几乎都是依赖字节码框架(如 ASM Javassist 等)实现的。字节码框架是直接操作 class 字节码的框架。可以加载已有的 class 字节码文件信息,修改部分信息,或动态生成一个 class

JDK动态代理模式

这里的对象必须实现一个接口

代码展示:

接口:

public interface Executable {
    void execute();
}

被代理的类:

public class SayHelloCommand implements Executable {
    @Override
    public void execute() {
        System.out.println("SayHelloCommand.execute(): 你好世界");
    }
}

代理对象的生成:

@Slf4j
@Configuration
public class AppConfig {
    static public class ExecutableProxy implements InvocationHandler {
        private final SayHelloCommand command = new SayHelloCommand();

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            log.info("调用方法前: {}", method);
            Object returnValue = method.invoke(command, args);
            log.info("调用方法后: {}", returnValue);

            return returnValue;
        }
    }

    @Bean
    public Executable executable() {
        return (Executable) Proxy.newProxyInstance(
                Executable.class.getClassLoader(),
                new Class[] { Executable.class },
                new ExecutableProxy()
        );
    }
}

程序主界面:

@Component
public class CommandLine implements CommandLineRunner {
    private final Executable executable;

    @Autowired
    public CommandLine(Executable executable) {
        // 实际上注入的已经是被代理过的对象了
        this.executable = executable;
    }

    @Override
    public void run(String... args) throws Exception {
        // SpringBoot 启动的时候会去调用
        System.out.println("CommandLine.run() 被调用了");
        executable.execute();
    }
}

网上找了一份比较接近的:

JDK 实现时,先通过实现 InvocationHandler 接口创建方法调用处理器,再通过 Proxy 来创建代理类。

3.JDK CGLIB 的区别

1. JDK 实现,要求被代理类必须实现接口,之后是通过 InvocationHandler Proxy ,在运行时动态的在内存中生成了代理类对象,该代理对象是通过实现同样的接口实现(类似静态代理接口实现的方式),只是该代理类是在运行期时,动态的织入统一的业务逻辑字节码来完成。
2. CGLIB 实现,被代理类可以不实现接口,是通过继承被代理类,在运行时动态的生成代理类对象。

五、MyBatis的动态代理 

这里只是演示一个大体的流程:

定义一个注解类:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQL {
    String value();
}

接口:

public interface UserDao {
    @SQL("select uid, username, password from users where uid = ?")
    Map<String, Object> selectOneByUid(int uid);

    @SQL("select uid, username, password from users order by uid limit 1 offset ?")
    Map<String, Object> selectOneByOffset(int offset);
}

注册代理对象:

@Configuration
public class AppConfig {
    public static class DaoProxy implements InvocationHandler {
        private final DataSource dataSource;

        public DaoProxy() {
            MysqlDataSource m = new MysqlDataSource();
            m.setUrl("jdbc:mysql://127.0.0.1:3306/java?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai");
            m.setUser("root");
            m.setPassword("123456");
            dataSource = m;
        }

        @Override
        @SneakyThrows
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println(method);
            SQL annotation = method.getAnnotation(SQL.class);
            String sql = annotation.value();
            System.out.println(sql);

            // 可以真实的去做查询,而不需要有被代理对象
            try (Connection c = dataSource.getConnection()) {
                try (PreparedStatement ps = c.prepareStatement(sql)) {
                    // 我们知道参数一定是 int 类型
                    int uid = (Integer) args[0];

                    ps.setInt(1, uid);

                    System.out.println(ps);

                    try (ResultSet rs = ps.executeQuery()) {
                        if (!rs.next()) {
                            return null;
                        }

                        LinkedHashMap<String, Object> map = new LinkedHashMap<>();
                        map.put("uid", rs.getInt("uid"));
                        map.put("username", rs.getString("username"));
                        map.put("password", rs.getString("password"));

                        return map;
                    }
                }
            }
        }
    }

    @Bean
    public UserDao userDao() {
        return (UserDao) Proxy.newProxyInstance(
                UserDao.class.getClassLoader(),
                new Class[] { UserDao.class },
                new DaoProxy()
        );
    }
}

主界面:


@Component
public class CommandLine implements CommandLineRunner {
    private final UserDao userDao;

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

    @Override
    public void run(String... args) throws Exception {
        Map<String, Object> map = userDao.selectOneByOffset(1);
        if (map != null) {
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                String column = entry.getKey();
                Object value = entry.getValue();
                System.out.println(column + " = " + value);
            }
        } else {
            System.out.println("null");
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值