SpringAop

背景概念

介绍

  • “横切”的技术,剖解开封装对象的内部,将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为“Aspect”,即切面;简单来说就是将那些与业务无关,却为业务模块所公共调用的逻辑或者责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性性和可维护性;
  • AOP把软件系统分为两部分:核心关注点和横切关注点;比如:日志系统、

编程范式概览

  • 面向过程编程
  • 面向对象编程
  • 函数式编程
  • 事件驱动编程
  • 面向切面编程

解耦分离(关注点分离、Separation of Concerns)

  • 项目角度

水平分离:展示层 ==> 服务层 ==> 持久层

  • 业务角度
垂直分离:模块划分(订单、库存等)
  • 功能角度
切面分离:分离功能性需求与非功能性需求

使用AOP的好处?

  • 集中处理某一关注点/横切逻辑
  • 可以很方便地添加/删除关注点
  • 侵入性少,增强代码可读性及可维护性

场景

  • 权限控制(@PreAuthorize)
  • 缓存控制(@Cacheable)
  • 事务控制(@Transtacional)
  • 异常处理(@AfterThrowing)
  • 审计日志、性能监控、分布式追踪

补充概念

连接点(joinPoint):被拦截到的点,因为spring只支持方法类型的连接点,所以在spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器;


使用方法

注解的方式:@Aspect(切面配置类)

  • @PointCut:用expression表达式,描述在哪些类/方法注入代码
  • @Advice:执行方法的什么时机注入


@PointCut的expression表达式

designators(指示器):

  • 匹配方法:execution()
  • 匹配注解:@Target()、@Within()、@args()、@annotation()
  • 匹配包/类型:within()
  • 匹配对象:this()、bean()、target()
  • 匹配参数:args()

wildcards(通配符)

  •    *     :匹配任意数量的字符
  •    ..    :匹配任意数量的子包、参数
  •    +    :指定类及其子类

operators(运算符)

  • &&:与
  • ||:或
  • !:非

举例:

修饰符为public、返回值任意、包名为com.demo.service、任意类、任意方法、里面的任意参数
  • “execution(public * com.demo.service.*.*(..))”

修饰符为public、返回值任意、包名为com.demo.service及其子包、任意类、任意方法、里面的任意参数
  • “execution(public * com.demo.service..*.*(..)

@Advice注解

  • @Before:前置通知
  • @After(finally):后置通知,方法执行完之后
  • @AfterReturning:返回通知,成功执行之后
  • @AfterThrowing:异常通知,抛出异常之后
  • @Around:环绕通知

代码演示

思路:在service包及其子包的以Impl结束的类,类里面有insert开头的方法、delete开头的方法。利用AOP在前面判断权限、而不直接调用实现方法
举例:在service.impl.ProductServiceImpl里面的insert()、delete()进行切入。

目录结构


代码实现:

Pojo的实体类、使用@Accessors(chain = true)可以进行链式编程
package com.example.springaop.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

/**
 * @author SHshuo
 * @data 2021/10/25--9:33
 */
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class Product {

    private Long id;
    private String name;
}
接口
package com.example.springaop.service;

import com.example.springaop.pojo.Product;

/**
 * @author SHshuo
 * @data 2021/10/25--9:35
 */
public interface ProductService {

    void insert(Product product);
    void delete(Long id);
}
实现接口的具体方法
package com.example.springaop.service.Impl;

import com.example.springaop.pojo.Product;
import org.springframework.stereotype.Service;

/**
 * @author SHshuo
 * @data 2021/10/25--9:36
 */
@Service
public class ProductServiceImpl implements com.example.springaop.service.ProductService {

//    @Autowired
//    private CheckAccess checkAccess;

    @Override
    public void insert(Product product) {
//        checkAccess.checkAccess();
        System.out.println("insert Product");
    }

    @Override
    public void delete(Long id) {
//        checkAccess.checkAccess();
        System.out.println("delete Product");
    }
}
进行判断的方法类
package com.example.springaop.service.Impl;

import com.example.springaop.security.CurrentUserHolder;
import org.springframework.stereotype.Component;

/**
 * @author SHshuo
 * @data 2021/10/25--9:51
 */
@Component
public class CheckAccess {

    public void checkAccess(){
        String user = CurrentUserHolder.get();
        if(!"admin".equals(user)){
            System.out.println("operation not allow");
        }else{
            System.out.println("operation allow");
        }
    }
}
使用ThreadLocal将对象共有资源
我感觉其实也可以注入@Autowired private Product product,但是这样实体类就需要添加@Component注解添加到IOC中,就违背了封装性的原则、增加了耦合。
package com.example.springaop.security;

/**
 * @author SHshuo
 * @data 2021/10/25--9:47
 * 将对象暴露出来、共有资源
 */
public class CurrentUserHolder {
    private static final ThreadLocal<String> holder = new ThreadLocal<>();

    public static String get(){
        return holder.get() == null ? "unknown" : holder.get();
    }

    public static void set(String user){
        holder.set(user);
    }
}
使用AOP进行切入:先将类声明为@Aspect
package com.example.springaop.security;

import com.example.springaop.service.Impl.CheckAccess;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author SHshuo
 * @data 2021/10/25--10:08
 * 声明Aop
 */

@Component
@Aspect
public class SecurityAspect {

    @Autowired
    private CheckAccess checkAccess;

//    service目录下、包含子目录。。后缀为Impl的类里面已insert开头的方法的任意参数
    @Pointcut("execution(* com.example.springaop.service..*Impl.insert*(..))")
    public void insert(){

    }

    //    service目录下、包含子目录。。后缀为Impl的类里面已delete开头的方法的任意参数
    @Pointcut("execution(* com.example.springaop.service..*Impl.delete*(..))")
    public void delete(){

    }

    @Before("insert() || delete()")
    public void check(){
        checkAccess.checkAccess();
    }
}
使用测试单元测试
package com.example.springaop;

import com.example.springaop.pojo.Product;
import com.example.springaop.security.CurrentUserHolder;
import com.example.springaop.service.ProductService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringAopApplicationTests {

    @Autowired
    private ProductService productService;

    @Test()
    public void deleteTest() {
        try {
//            创建共有对象
            CurrentUserHolder.set("admin");
//            创建对象
            Product product = new Product();
            product.setId(2L).setName("admin");
//            调用方法
            productService.insert(product);
            productService.delete(1L);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}

原理

Spring AOP 代理的生成过程:

  1. 通过ProxyFactoryBean(FactoryBean接口的实现)来去配置相应的代理对象的信息 
  2. 在获取ProxyFactoryBean实例时,本质上并未获取到ProxyFactoryBean的对象,而是获取到由ProxyFactoryBean所返回的那个对象实例(getObject方法)
  3. 在整个ProxyFactoryBean实例的构建与缓存的过程中,其流程与普通bean的对象完全一致
  4. 差别在于,当创建完成ProxyFactoryBean对象后,Spring会判断当前所创建的对象是否是一个FactoryBean实例;如果不是,那么Spring就直接将其返回
  5. 如果是,Spring会根据我们在配置信息中所指定的各种元素,如目标对象是否实现了接口以及Advisor等信息,使用动态代理或是CGLIB等方式来为目标对象创建相应的代理对象
  6. 当相应的代理对象创建完毕后,Spring就会通过ProxyFactoryBean的getObject方法将所创建的代理对象返回
  7. 对象返回到调用端,它本质上是一个代理对象,可以代理对目标对象的访问与调用,这个代理对象对用户来说,就好像是一个目标对象一样
  8. 客户在使用代理对象时,可以正常调用目标对象的方法,同时在执行过程中,会根据我们在配置文件中所配置的信息来在调用前后执行额外的附加逻辑


织入时机

  • 编译期(AspectJ)
  • 类加载时(AspectJ5 + )
  • 运行时(SpringAOP)

运行时织入实现

通过代理对象实现、两种实现方式:静态代理、动态代理

静态代理

  • 缺点:
              100个target需要委托(represents)100个proxy。但是proxy执行方法前后基本一致,就会产生大量的冗余。
               同时接口每多增加一个方法,对应的proxy代理类就需要重写一次,建立与对应的方法的委托

动态代理:
  • proxy代理类不需要一个一个手动的增加新的委托,基本不需要更改。利用反射实现,给我的感觉就是动态的与真实的类里面所有方法进行委托
  • JDK代理:基于接口的代理实现
  • Cglib代理:基于继承的代理实现

JDK动态代理:

要点:

  • 通过proxy类动态生成代理类(Proxy.newProxyInstance)
  • 实现接口,实现织入的逻辑(invocationHandler)

目录结构:


代码实现:

接口:
package com.example.jdkproxy.pojo;

/**
 * @author SHshuo
 * @data 2021/10/31--14:56
 */
public interface Subject {
    void request();
    void newRequest();
}
真实的类:
package com.example.jdkproxy.pojo;

/**
 * @author SHshuo
 * @data 2021/10/31--14:44
 */
public class RealSubject implements Subject{

    @Override
    public void request(){
        System.out.println("hshuo");
    }

    @Override
    public void newRequest() {
        System.out.println("new Hshuo");
    }
}
代理类:method.invoke(realSubject, args)
package com.example.jdkproxy.proxy;

import com.example.jdkproxy.pojo.RealSubject;
import org.springframework.beans.factory.annotation.Autowired;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * @author SHshuo
 * @data 2021/10/31--14:43
 * 相当于Aspect、jdk动态代理。不需要实现subject接口、再委托对应的方法了
 * method.invoke()感觉是直接与对应的真实类联系起来、而不需要每次都委托具体的方法
 */
public class JdkProxy implements InvocationHandler {

    @Autowired
    private RealSubject realSubject;

//    传入参数
    public JdkProxy (RealSubject realSubject){
        this.realSubject = realSubject;
    }

//    动态反射方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        try {
            result = method.invoke(realSubject, args);
        }catch (Exception e){
            System.out.println("e:" + e.getMessage());
            throw e;
        }finally {
            System.out.println("反射结束");
        }
        return result;
    }
}
Client调用:Proxy.newProxyInstance
package com.example.jdkproxy.controller;

import com.example.jdkproxy.pojo.RealSubject;
import com.example.jdkproxy.pojo.Subject;
import com.example.jdkproxy.proxy.JdkProxy;

import java.lang.reflect.Proxy;

/**
 * @author SHshuo
 * @data 2021/10/31--14:44
 * 使用Proxy.newProxyInstance反射创建对象
 */
public class ClentController {
    public static void main(String[] args) {
        Subject subject = (Subject) Proxy.newProxyInstance(ClentController.class.getClassLoader(),
                new Class[]{Subject.class}, new JdkProxy(new RealSubject()));

        subject.request();
        subject.newRequest();
    }
}

Cglib动态代理:

要点:
  • 调用enhancer.create()
  • 织入实现MethodInterceptor里面的intercept
  • 给我感觉的区别:将代理类实现的代码部分移动到client设置

目录结构:


代码实现:

真实的类与接口与JDK代理代码一致
接口:
package com.example.cglibproxy.service;

/**
 * @author SHshuo
 * @data 2021/10/31--16:01
 */
public interface Subject {

    void request();
    void newRequest();
}
真实的类
package com.example.cglibproxy.service.Impl;

import com.example.cglibproxy.service.Subject;

/**
 * @author SHshuo
 * @data 2021/10/31--16:02
 * 真实对象
 */
public class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("hshuo");
    }

    @Override
    public void newRequest() {
        System.out.println("new Hshuo");
    }
}
代理类:methodProxy.invokeSuper(o, objects)
package com.example.cglibproxy.proxy;

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

import java.lang.reflect.Method;

/**
 * @author SHshuo
 * @data 2021/10/31--16:03
 * 基于MethodInterceptor实现
 */
public class CglibProxy implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        Object result = null;
        try {
            result = methodProxy.invokeSuper(o, objects);
        }catch (Exception e){
            System.out.println("e:" + e.getMessage());
            throw e;
        }finally {
            System.out.println("反射结束");
        }
        return result;
    }
}
Client调用:enhancer.create()
package com.example.cglibproxy.controller;

import com.example.cglibproxy.proxy.CglibProxy;
import com.example.cglibproxy.service.Impl.RealSubject;
import com.example.cglibproxy.service.Subject;
import org.springframework.cglib.proxy.Enhancer;

/**
 * @author SHshuo
 * @data 2021/10/31--16:01
 * Enhancer调用
 */
public class ClientController {
    public static void main(String[] args) {
//        相当于proxy.newProxyInstance()
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(RealSubject.class);
        enhancer.setCallback(new CglibProxy());
        Subject subject = (Subject) enhancer.create();

//        调用
        subject.request();
        subject.newRequest();
    }
}

Cglib与JDK代理的区别:

  • JDK代理只能针对有接口(InvocationHandler)的类的接口方法进行动态代理
  • Cglib基于继承来实现代理,无法对static(方法+类)、final类进行代理
  • JDK、Cglib无法对private方法进行代理

Spring如何创建AOP代理类

  • 由AopProxyFactory根据AdvisedSupport对象的配置类决定;
  • 默认的策略是如果目标类是接口,则使用JDK代理;否则使用Cglib代理。

hshuo的面试之路 文章被收录于专栏

作者目标是找到一份Java后端方向的工作 此专栏用来记录从Bilibili、书本、其他优质博客上面学习的内容 用于巩固、总结内容 主要包含Docker、Dubbo、Java基础、JUC、Maven、MySQL、Redis、SpringBoot、SpringCloud、数据结构、杂文、算法、计算机网络、操作系统、设计模式等相关内容

全部评论

相关推荐

秋招进行到现在终于能写总结了。完全没想到战线会拉这么长,过程会如此狼狈,不过更应该怪自己太菜了。好在所有的运气都用在了最后,也是有个去处。背景:双2本硕科班,无竞赛,本科一段研究所实习,硕士一段大厂暑期实习但无转正。技术栈是C++&nbsp;&amp;&nbsp;Golang,实习是客户端音视频(而且是鸿蒙端开发),简历两个C++项目一个Golang项目。主要投递岗位:后端,cpp软开,游戏服务端,测开,以及一些不拘泥于Java的岗位。从8月起总共投递123家公司,笔试数不清了,约面大约30家。offer/oc/意向:友塔游戏(第一个offer,面试体验很好,就是给钱好少南瑞继保(计算机科班点击就送(限男生),不...
乡土丁真真:佬很厉害,羡慕~虽然我还没有到校招的时候,也想讲一下自己的看法:我觉得不是CPP的问题,佬的背书双2,技术栈加了GO,有两段实习。投了123,面了30.拿到11个offer。这个数据已经很耀眼了。这不也是CPP带来的吗?当然也不止是CPP。至少来说在这个方向努力过的也会有好的结果和选择。同等学历和项目选java就会有更好的吗?我个人持疑问态度。当然CPP在方向选择上确实让人头大,但是我觉得能上岸,至于最后做什么方向,在我看来并不重要。至于CPP特殊,有岗位方向的随机性,java不是不挑方向,只是没得选而已。也希望自己以后校招的时候能offer满满
点赞 评论 收藏
分享
shtdbb_:还不错,没有让你做了笔试再挂你
点赞 评论 收藏
分享
威猛的小饼干正在背八股:挂到根本不想整理
点赞 评论 收藏
分享
评论
3
收藏
分享
牛客网
牛客企业服务