当前位置 : 首页 » 文章分类 :  开发  »  Spring-AOP

Spring-AOP

Spring AOP 相关笔记


Spring AOP代理的两种实现方式

AOP实现的关键在于AOP框架自动创建的AOP代理,AOP代理主要分为静态代理和动态代理

  • 静态代理的代表为AspectJ;
  • 而动态代理则以Spring AOP为代表。

AspectJ(静态代理)

AspectJ是静态代理的增强,所谓的静态代理就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强。
它会在编译阶段将Aspect织入Java字节码中, 即修改目标类的.class文件,将相关代码加入其中,运行的时候就是经过增强之后的AOP对象。

Spring AOP(动态代理)

与AspectJ的静态代理不同,Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

Spring AOP中的动态代理主要有两种方式:

  • JDK 动态代理
  • CGLIB 动态代理

JDK动态代理(需要实现接口/基于接口的代理)

JDK 动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类。

CGLib动态代理(不需实现接口/基于子类的代理)

如果目标类没有实现接口,那么 Spring AOP 会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

Spring AOP的实现原理
http://www.importnew.com/24305.html


Spring AOP如何选择代理方式?

spring 官方文档中关于 aop 代理的描述如下:

5.3. AOP Proxies
Spring AOP defaults to using standard JDK dynamic proxies for AOP proxies. This enables any interface (or set of interfaces) to be proxied.
Spring AOP can also use CGLIB proxies. This is necessary to proxy classes rather than interfaces. By default, CGLIB is used if a business object does not implement an interface. As it is good practice to program to interfaces rather than classes, business classes normally implement one or more business interfaces. It is possible to force the use of CGLIB, in those (hopefully rare) cases where you need to advise a method that is not declared on an interface or where you need to pass a proxied object to a method as a concrete type.

5.3. AOP Proxies
https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-introduction-proxies

Spring AOP 默认使用 JDK 动态代理,如果目标类没有实现任何接口则会使用CGLib动态代理

如果要被代理的对象是个实现类(即实现了某个接口),那么 Spring 会使用 JDK 动态代理来完成操作(Spirng默认采用JDK动态代理实现机制);
如果要被代理的对象不是个实现类(即没有实现接口),那么 Spring 会强制使用 CGLib 来实现动态代理。

在 Springboot 中使用过注解配置方式的人会问是否需要在程序主类中增加 @EnableAspectJAutoProxy 来启用,实际并不需要。
看下面关于AOP的默认配置属性,其中 spring.aop.auto 属性默认是开启的,也就是说只要引入了 AOP 依赖后,其实默认已经增加了 @EnableAspectJAutoProxy

# AOP
spring.aop.auto=true # Add @EnableAspectJAutoProxy.
spring.aop.proxy-target-class=false # Whether subclass-based (CGLIB) proxies are to be created (true) as opposed to standard Java interface-based proxies (false).

5.8. Proxying Mechanisms
Spring AOP uses either JDK dynamic proxies or CGLIB to create the proxy for a given target object. JDK dynamic proxies are built into the JDK, whereas CGLIB is a common open-source class definition library (repackaged into spring-core).

If the target object to be proxied implements at least one interface, a JDK dynamic proxy is used. All of the interfaces implemented by the target type are proxied. If the target object does not implement any interfaces, a CGLIB proxy is created.

If you want to force the use of CGLIB proxying (for example, to proxy every method defined for the target object, not only those implemented by its interfaces), you can do so. However, you should consider the following issues:

  • With CGLIB, final methods cannot be advised, as they cannot be overridden in runtime-generated subclasses.
  • As of Spring 4.0, the constructor of your proxied object is NOT called twice anymore, since the CGLIB proxy instance is created through Objenesis. Only if your JVM does not allow for constructor bypassing, you might see double invocations and corresponding debug log entries from Spring’s AOP support.

5.8. Proxying Mechanisms
https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-proxying


JDK动态代理和CGLIB代理性能对比

在1.6和1.7的时候,JDK 动态代理的速度要比 CGLib 动态代理的速度要慢,但是并没有教科书上的10倍差距,在 JDK1.8 的时候,JDK 动态代理的速度已经比 CGLib 动态代理的速度快很多了

JDK 动态代理是面向接口,在创建代理实现类时比 CGLib 要快,创建代理速度快。

CGLib 动态代理是通过字节码底层继承被代理类来实现(如果被代理类被 final 关键字所修饰,那么抱歉会失败),在创建代理这一块没有 JDK 动态代理快,但是运行速度比 JDK 动态代理要快。


Spring 动态代理创建源码

Bean 创建过程中的 AbstractAutowireCapableBeanFactory.doCreateBean() -> initializeBean() 中会应用所有的 Bean 后处理器, 包括初始化前的 applyBeanPostProcessorsBeforeInitialization 和 初始化后的 applyBeanPostProcessorsAfterInitialization

AOP 代理创建是在 Bean 初始化后进行的:

package org.springframework.beans.factory.support;

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
        implements AutowireCapableBeanFactory {
    @Override
    public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
            throws BeansException {

        Object result = existingBean;
        for (BeanPostProcessor processor : getBeanPostProcessors()) {
            Object current = processor.postProcessAfterInitialization(result, beanName);
            if (current == null) {
                return result;
            }
            result = current;
        }
        return result;
    }
}

postProcessAfterInitialization 是所有 Bean 后处理器都要实现的方法,AbstractAutoProxyCreator.postProcessAfterInitialization() 负责创建 AOP 代理:

package org.springframework.aop.framework.autoproxy;

public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
        implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
    @Override
    public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
        if (bean != null) {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            if (this.earlyProxyReferences.remove(cacheKey) != bean) {
                return wrapIfNecessary(bean, beanName, cacheKey);
            }
        }
        return bean;
    }
}

最终在 wrapIfNecessary() 方法中解析切面后调用 createProxy 完成代理创建:

package org.springframework.aop.framework.autoproxy;

public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
        implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
    protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
            @Nullable Object[] specificInterceptors, TargetSource targetSource) {

        if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
            AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
        }

        // 代理工厂
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.copyFrom(this);

        if (!proxyFactory.isProxyTargetClass()) {
            if (shouldProxyTargetClass(beanClass, beanName)) {
                // 使用 CGLIB 代理
                proxyFactory.setProxyTargetClass(true);
            }
            else {
                // 使用 JDK 动态代理
                evaluateProxyInterfaces(beanClass, proxyFactory);
            }
        }

        // 切面信息
        Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
        proxyFactory.addAdvisors(advisors);
        proxyFactory.setTargetSource(targetSource);
        customizeProxyFactory(proxyFactory);

        proxyFactory.setFrozen(this.freezeProxy);
        if (advisorsPreFiltered()) {
            proxyFactory.setPreFiltered(true);
        }

        // Use original ClassLoader if bean class not locally loaded in overriding class loader
        ClassLoader classLoader = getProxyClassLoader();
        if (classLoader instanceof SmartClassLoader && classLoader != beanClass.getClassLoader()) {
            classLoader = ((SmartClassLoader) classLoader).getOriginalClassLoader();
        }

        // 最终代理在这里创建
        return proxyFactory.getProxy(classLoader);
    }
}    

最终在在 DefaultAopProxyFactory 中创建 cglib 或 jdk动态代理

package org.springframework.aop.framework;

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
    @Override
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        if (!NativeDetector.inNativeImage() &&
                (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass == null) {
                throw new AopConfigException("TargetSource cannot determine target class: " +
                        "Either an interface or a target is required for proxy creation.");
            }
            // proxyTargetClass 开关开启,如果目标对象是一个接口,或者是一个代理,Spring仍然会使用JDK动态代理
            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                return new JdkDynamicAopProxy(config);
            }
            return new ObjenesisCglibAopProxy(config);
        }
        else {
            return new JdkDynamicAopProxy(config);
        }
    }
}

强制使用CGLib代理

当我们需要强制使用 CGLIB 来实现 AOP 的时候,需要配置
spring.aop.proxy-target-class=true

<aop:config proxy-target-class="true">
    <!-- other beans defined here... -->
</aop:config>


@EnableAspectJAutoProxy(proxyTargetClass = true)


@EnableAspectJAutoProxy 注解

注解 @EnableAspectJAutoProxy 源码如下:

package org.springframework.context.annotation;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {

    /**
     * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
     * to standard Java interface-based proxies. The default is {@code false}.
     */
    boolean proxyTargetClass() default false;

    /**
     * Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal}
     * for retrieval via the {@link org.springframework.aop.framework.AopContext} class.
     * Off by default, i.e. no guarantees that {@code AopContext} access will work.
     * @since 4.3.1
     */
    boolean exposeProxy() default false;
}

两个参数:
proxyTargetClass 控制aop的具体实现方式, 为 true 的话使用cglib, 为false的话使用jdk动态代理,当然没有实现接口的肯定都得用cglib, 默认为false
exposeProxy 控制代理的暴露方式, 解决内部调用不能使用代理的场景,默认为false.

引入了 AspectJAutoProxyRegistrar 配置类,核心会将 AnnotationAwareAspectJAutoProxyCreator 这个 Bean 后处理器注册到 Spring,这个后处理器负责 AOP 代理的创建。


exposeProxy 把代理暴露为ThreadLocal对象

public interface UserService {
    public void a();
    public void a();
}

public class UserServiceImpl implements UserService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void a(){
        this.b();
    }

    @Transactional(propagation = Propagation.REQUIRED_NEW)
    public void b(){
        System.out.println("b has been called");
    }
}

Q1:b中的事务会不会生效?
A1:不会,a的事务会生效,b中不会有事务,因为a中调用b属于内部调用,没有通过代理,所以不会有事务产生。

Q2:如果想要b中有事务存在,要如何做?
A2:@EnableAspectJAutoProxy(exposeProxy = true),设置 expose-proxy 属性为 true,将代理暴露出来,使用 AopContext.currentProxy() 获取当前代理,将 this.b() 改为
((UserService)AopContext.currentProxy()).b()


SpringBoot 2.x以后默认使用CGLib动态代理

Spring Boot 中有个自动配置类 AopAutoConfiguration,等价于添加 @EnableAspectJAutoProxy 注解

package org.springframework.boot.autoconfigure.aop;
/**
 * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
 * Auto-configuration} for Spring's AOP support. Equivalent to enabling
 * {@link EnableAspectJAutoProxy @EnableAspectJAutoProxy} in your configuration.
 * <p>
 * The configuration will not be activated if {@literal spring.aop.auto=false}. The
 * {@literal proxyTargetClass} attribute will be {@literal true}, by default, but can be
 * overridden by specifying {@literal spring.aop.proxy-target-class=false}.
 *
 * @author Dave Syer
 * @author Josh Long
 * @since 1.0.0
 * @see EnableAspectJAutoProxy
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(Advice.class)
    static class AspectJAutoProxyingConfiguration {

        @Configuration(proxyBeanMethods = false)
        @EnableAspectJAutoProxy(proxyTargetClass = false)
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
        static class JdkDynamicAutoProxyConfiguration {

        }

        @Configuration(proxyBeanMethods = false)
        @EnableAspectJAutoProxy(proxyTargetClass = true)
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
                matchIfMissing = true)
        static class CglibAutoProxyConfiguration {

        }

    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingClass("org.aspectj.weaver.Advice")
    @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
            matchIfMissing = true)
    static class ClassProxyingConfiguration {

        @Bean
        static BeanFactoryPostProcessor forceAutoProxyCreatorToUseClassProxying() {
            return (beanFactory) -> {
                if (beanFactory instanceof BeanDefinitionRegistry) {
                    BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
                    AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
                    AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
                }
            };
        }
    }
}

JDK 动态代理

在 JDK 动态代理机制中,有两个重要的类或接口,一个是 InvocationHandler 接口, 另一个则是 Proxy 类,这一个类和接口是实现我们动态代理所必须用到的。

InvocationHandler 接口

每一个动态代理类都必须要实现 InvocationHandler 这个接口,并且每个代理类的实例都必须关联一个 InvocationHandler 实例,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由 InvocationHandler 这个接口的 invoke 方法来进行调用
InvocationHandler 接口只有一个 invoke 方法:

package java.lang.reflect;

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

proxy 指代我们所代理的那个真实对象
method 指代的是我们所要调用真实对象的某个方法的 Method 对象
args 指代的是调用真实对象某个方法时接受的参数

Proxy 类

Proxy 这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但是我们用的最多的就是 newProxyInstance 这个方法:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
这个方法的作用就是得到一个动态的代理对象,其接收三个参数:

  • loader 一个 ClassLoader 对象,定义了由哪个 ClassLoader 对象来对生成的代理对象进行加载
  • interfaces 一个 Interface 对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
  • h 一个 InvocationHandler 对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个 InvocationHandler 对象上
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
    Objects.requireNonNull(h);

    final Class<?>[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }

    /*
     * Look up or generate the designated proxy class.
     */
    Class<?> cl = getProxyClass0(loader, intfs);

    /*
     * Invoke its constructor with the designated invocation handler.
     */
    try {
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }

        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
        return cons.newInstance(new Object[]{h});
    } catch (IllegalAccessException|InstantiationException e) {
        throw new InternalError(e.toString(), e);
    } catch (InvocationTargetException e) {
        Throwable t = e.getCause();
        if (t instanceof RuntimeException) {
            throw (RuntimeException) t;
        } else {
            throw new InternalError(t.toString(), t);
        }
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString(), e);
    }
}

动态代理实例

1、首先定义一个 ActionInterface 接口
2、Speak 类实现了这个接口,提供了方法 act 的实现
3、MyInvocationHandler 是一个 InvocationHandler,invoke 中在真实方法调用前后进行了增强
4、通过 Proxy.newProxyInstance() 生成代理对象,传入真实对象的类加载器和接口列表,并传入要关联的 invocationHandler

public class JavaDynamicProxyTest {
    // 动作接口
    public interface ActionInterface {
        String act(String word);
    }

    // 说话
    public class Speak implements ActionInterface {
        @Override
        public String act(String word) {
            String res = "Speak: " + word;
            System.out.println(res);
            return res;
        }
    }

    public class MyInvocationHandler implements InvocationHandler {
        private Object object;
        MyInvocationHandler(Object object) {
            this.object = object;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("Before " + method.getName());
            Object result = method.invoke(object, args);
            System.out.println("After " + method.getName());
            return result;
        }
    }

    @Test
    public void testProxy() {
        // 真实对象
        ActionInterface hello = new Speak();
        InvocationHandler invocationHandler = new MyInvocationHandler(hello);

        // 代理对象
        ActionInterface helloProxy = (ActionInterface) Proxy
                .newProxyInstance(hello.getClass().getClassLoader(), hello.getClass().getInterfaces(), invocationHandler);

        String result = helloProxy.act("nihao");
        System.out.println("执行结果:" + result);
    }
}

JAVA动态代理
https://www.jianshu.com/p/9bcac608c714


CGLib 动态代理

cglib: The missing manual - 关于 cglib 非常好的一片文章
http://mydailyjava.blogspot.com/2013/11/cglib-missing-manual.html

Enhancer 类

Enhancer 类是 cglib 动态代理中最常用的一个类,类似 JDK 动态代理中的 Proxy 类。
Enhancer 可以代理没有实现接口的类,相对的,JDK 动态代理的 Proxy 只能代理实现了接口的类。
Enhancer 通过动态创建目标类的子类来实现代理(所以无论目标类是否实现了接口都可以被代理,但不能代理 final 修饰的类),代理后会拦截其中的所有方法调用。

CGLib 动态代理实例

public class SampleClass {
  public String test(String input) {
    return "Hello world!";
  }
}

@Test
public void testFixedValue() throws Exception {
  Enhancer enhancer = new Enhancer();
  enhancer.setSuperclass(SampleClass.class);
  enhancer.setCallback(new FixedValue() {
    @Override
    public Object loadObject() throws Exception {
      return "Hello cglib!";
    }
  });
  SampleClass proxy = (SampleClass) enhancer.create();
  assertEquals("Hello cglib!", proxy.test(null));
}

通过 Enhancer#create 创建了代理类实例,如果不想立即创建实例,还可以先调用 Enhancer#createClass 创建代理类的 Class 类型,之后再动态创建实例。

注意上面 SampleClass 类中的所有方法都被替换为 FixedValue 固定值拦截器返回的 Hello cglib! 字符串,包括 Object 类的 toString/equals/hashCode 等方法,虽然方法签名都不一致也会被强制替换,所以调用会直接报错。

final 方法比如 Object#getClass 不会被拦截,调用返回的类名是类似 SampleClass$$EnhancerByCGLIB$$ce31c2dd,这是 cglib 随机生成的类名。


CGLib 代理的类必须有默认构造器

CGLib 代理的类必须有默认构造器
下面代码执行会报错: java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given
原因是内部类 SampleClass 虽然显式添加了无参构造器,但编译器还是会默认添加一个类型为外部类的参数,这也是为什么内部类可以任意访问外部类成员,所以内部类的真实构造器是 EnhancerTest$SampleClass(CGlibTest)
解决方法可以将内部类改为 static 的,静态内部类不需要依赖于外部类,所以构造器中也不会被编译器给塞入外部类的引用,有真正的无参构造器。

public class EnhancerTest {

    @Test
    public void testFixedValue() throws Exception {
        SampleClass sss = new SampleClass();
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(SampleClass.class);
        enhancer.setCallback(new FixedValue() {
            @Override
            public Object loadObject() throws Exception {
                return "Hello cglib!";
            }
        });
        SampleClass proxy = (SampleClass) enhancer.create();
        assertEquals("Hello cglib!", proxy.test(null));
    }

    class SampleClass {
        public SampleClass() {
        }

        public String test(String input) {
            return "Hello world!";
        }
    }
}

查看CGLib或JDK动态代理生成的代码

代码中

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\class");  //该设置用于输出cglib动态代理产生的类
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");   //该设置用于输出jdk动态代理产生的类

其中 DebuggingClassWriter.DEBUG_LOCATION_PROPERTY 的值是 cglib.debugLocation, 貌似只要这个变量有值就会把生成的动态代理class保存下来。

或者直接在启动参数中
-Dserver.port=8080 -Dcglib.debugLocation="." -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
然后启动 spring 项目即可。

输出cglib以及jdk动态代理产生的class文件
https://blog.csdn.net/john1337/article/details/76439507

比如看看自动注入的 commentService 代理信息

public class CommentV1Controller {
    @Autowired
    CommentService commentService;

    public CommentBean createComment(CreateCommentRequest request) throws Exception {
        proxyInfo(commentService);
        CommentBean commentBean = commentService.createComment(request);
        return commentBean;
    }

    // 学习用,输出代理相关信息
    public void proxyInfo(Object object) {
        //是否是JDK动态代理
        System.out.println("isJdkDynamicProxy => " + AopUtils.isJdkDynamicProxy(object));
        //是否是CGLIB代理
        System.out.println("isCglibProxy => " + AopUtils.isCglibProxy(object));
        //代理类的类型
        System.out.println("proxyClass => " + object.getClass());
        //代理类的父类的类型
        System.out.println("parentClass => " + object.getClass().getSuperclass());
        //代理类的父类实现的接口
        System.out.println("parentClass's interfaces => " + Arrays.asList(object.getClass().getSuperclass().getInterfaces()));
        //代理类实现的接口
        System.out.println("proxyClass's interfaces => " + Arrays.asList(object.getClass().getInterfaces()));
        //代理对象
        System.out.println("proxy => " + object);
        //目标对象
        System.out.println("target => " + AopProxyUtils.getSingletonTarget(object));
        //代理对象和目标对象是不是同一个
        System.out.println("proxy == target => " + (object == AopProxyUtils.getSingletonTarget(object)));
        //目标类的类型
        System.out.println("targetClass => " + AopProxyUtils.getSingletonTarget(object).getClass());
        //目标类实现的接口
        System.out
                .println("targetClass's interfaces => " + Arrays.asList(AopProxyUtils.getSingletonTarget(object).getClass().getInterfaces()));
    }
}

proxyInfo 输出如下

isJdkDynamicProxy => false
isCglibProxy => true
proxyClass => class com.masikkk.blog.service.CommentService$$EnhancerBySpringCGLIB$$d0976ebc
parentClass => class com.masikkk.blog.service.CommentService
parentClass's interfaces => []
proxyClass's interfaces => [interface org.springframework.aop.SpringProxy, interface org.springframework.aop.framework.Advised, interface org.springframework.cglib.proxy.Factory]
proxy => com.masikkk.blog.service.CommentService@52af6b5a
target => com.masikkk.blog.service.CommentService@52af6b5a
proxy == target => false
targetClass => class com.masikkk.blog.service.CommentService
targetClass's interfaces => []

启动spring 后,动态代理代码会保存到 项目同目录下的 com.masikkk.blog.service 目录中,每次启动生成的类名都不一样,有不同的序号后缀
com.masikkk.blog.service.CommentService$$EnhancerBySpringCGLIB$$d0976ebc 类的源码片段如下,主要看看我调用的 commentService.createComment 方法的代理

package com.masikkk.blog.service;

public class CommentService$$EnhancerBySpringCGLIB$$cb9a0ead extends CommentService implements SpringProxy, Advised, Factory {

  public final CommentBean createComment(CreateCommentRequest var1) throws Exception {
    try {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (this.CGLIB$CALLBACK_0 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        return var10000 != null ? (CommentBean)var10000.intercept(this, CGLIB$createComment$0$Method, new Object[]{var1}, CGLIB$createComment$0$Proxy) : super.createComment(var1);
    } catch (Error | Exception | RuntimeException var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
    }
  }
}

可以看到, 代理类继承了目标类,重写了同名方法,里面具体的看不懂。


实践

spring 自定义注解annotation+aspect 环绕通知配置对dubbo的consumer监控报警
https://blog.csdn.net/ChiChengIT/article/details/50570161

Spring 中使用@Aspect 控制自定义注解(自定义注解和AOP实现自动写日志)
https://blog.csdn.net/jmdonghao/article/details/78880899

Spring AOP 切面实现消息发送

自定义注解

package com.masikkk.annotation;

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

/**
 * User Profile Change 消息发送
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface UserProfileMessage {
}

切面拦截注解后进行增强

1、通过 proceedingJoinPoint.proceed() 执行被拦截方法,无法修改方法参数。
2、想修改方法参数,需要使用 proceedingJoinPoint.proceed(Object[] args) 来执行被拦截方法

//获取方法参数值数组
Object[] args = proceedingJoinPoint.getArgs();
// 修改 args
proceedingJoinPoint.proceed(args)
package com.nio.uds.user.aspect;

import com.masikkk.common.json.JSONUtils;
import com.masikkk.common.monitorconfig.MonitorUtils;
import com.masikkk.config.UserProfileContextHolder;
import com.masikkk.producer.UserProfileProducer;
import java.lang.reflect.Method;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
@Order(20)
public class UserProfileMessageAspect {
    @Autowired
    private UserProfileProducer userProfileProducer;

    @Pointcut("@annotation(com.nio.uds.user.annotation.UserProfileMessage)")
    public void userProfileMessagePointcut() {
    }

    @Around("userProfileMessagePointcut()")
    public Object sendUserProfileMessage(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();

        // 清除之前的消息上下文
        log.info("User profile message aspect in {}, args {}", MonitorUtils.getMethodShortName(method), JSONUtils.writeValue(proceedingJoinPoint.getArgs()));
        UserProfileContextHolder.clean();

        Object result = proceedingJoinPoint.proceed();

        // 发送User Profile Change kafka消息
        log.info("User profile message aspect, result {}", JSONUtils.writeValue(result));
        userProfileProducer.sendUserProfileChangeMessage();

        return result;
    }
}

Spring AOP代理不生效问题

此类问题及解决方法见笔记 Spring-Transaction 中的 @Transactional 注解事务不生效。


同一注解嵌套多次会被切面多次拦截吗?会

有如下代码, methodA() 中调用 methodB() ,两个方法都被自定义注解 @MyAnnotatiion 注解,切面 MyAspect 拦截 @MyAnnotatiion 注解的方法做增强。

@Service
public class ServiceA {
  @Autowired
  private ServiceB serviceB;

  @MyAnnotatiion
  public void methodA() {
    serviceB.methodB();
  }
}

@Service
public class ServiceB {
  @MyAnnotatiion
  public void methodB() {
    ...
  }
}

@Aspect
@Component
public class MyAspect {
  @Around("@annotation(com.masikkk.annotation.MyAnnotatiion)")
  public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
    ...
  }
}

问:执行过程中会两次进入 aroundAdvice() 方法吗?即,会被切面 MyAspect 拦截两次吗?为什么?
会拦截两次,多次嵌套就会拦截多次,由于 Spring 切面是通过代理实现的,只要不违背代理生效的规则(private方法不行,类内调用不行)就会多次被切面拦截


如何代理没实现接口的final类

假如一个 final 类没有实现接口,如何给他创建代理?
CGLIB 是通过继承原类的方式来实现动态代理的,如果原类是 final 的,是无法被 cglib 代理的。

解决的思路是 实现一个自己的类加载器 classloader, 加载 final 后用 字节码修改工具 ASM 把 final 关键字去掉,但不建议这样做,很危险,容易出错。

徒手撸一个Mock框架(二)——如何创建final类的代理
https://www.jianshu.com/p/fe85e8fd248c


工具

AopContext

currentProxy() 返回当前AOP代理对象

注意:调用 currentProxy() 方法时, @EnableAspectJAutoProxy(exposeProxy=true) 必须设置为true,否则会抛出 IllegalStateException异常

源码

package org.springframework.aop.framework;

import org.springframework.core.NamedThreadLocal;
import org.springframework.lang.Nullable;

public final class AopContext {
    private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<>("Current AOP proxy");

    private AopContext() {
    }

    public static Object currentProxy() throws IllegalStateException {
        Object proxy = currentProxy.get();
        if (proxy == null) {
            throw new IllegalStateException(
                    "Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.");
        }
        return proxy;
    }

    @Nullable
    static Object setCurrentProxy(@Nullable Object proxy) {
        Object old = currentProxy.get();
        if (proxy != null) {
            currentProxy.set(proxy);
        }
        else {
            currentProxy.remove();
        }
        return old;
    }
}

AopUtils

isJdkDynamicProxy() 是否JDK动态代理

isCglibProxy() 是否CGLib动态代理

源码:

package org.springframework.aop.support;

public abstract class AopUtils {
  /**
   * Check whether the given object is a JDK dynamic proxy.
   * <p>This method goes beyond the implementation of
   * {@link Proxy#isProxyClass(Class)} by additionally checking if the
   * given object is an instance of {@link SpringProxy}.
   * @param object the object to check
   * @see java.lang.reflect.Proxy#isProxyClass
   */
  public static boolean isJdkDynamicProxy(Object object) {
    return (object instanceof SpringProxy && Proxy.isProxyClass(object.getClass()));
  }

  /**
   * Check whether the given object is a CGLIB proxy.
   * <p>This method goes beyond the implementation of
   * {@link ClassUtils#isCglibProxy(Object)} by additionally checking if
   * the given object is an instance of {@link SpringProxy}.
   * @param object the object to check
   * @see ClassUtils#isCglibProxy(Object)
   */
  public static boolean isCglibProxy(Object object) {
      return (object instanceof SpringProxy && ClassUtils.isCglibProxy(object));
  }
}

【面试】我是如何在面试别人Spring事务时“套路”对方的
https://mp.weixin.qq.com/s/rHlfKmughNeI8-gafcW8VQ


AspectJ 切点

@annotation()注解方法

表示标注了指定注解的目标类方法
例如 @annotation(org.springframework.transaction.annotation.Transactional) 表示标注了 @Transactional 的方法

@target()注解类

所有标注了指定注解的类
@target(org.springframework.stereotype.Service) 表示所有标注了 @Service 的类的所有方法

@within()注解类及子类

匹配标注了指定注解的类及其所有子类
如 @within(org.springframework.stereotype.Service) ,给Horseman加上 @Service 标注,则Horseman和Elephantman(Elephantman extends Horseman) 的所有方法都匹配

@args()注解参数

匹配方法的参数是否加了注定注解
如 @args(org.springframework.stereotype.Service) 表示有且仅有一个标注了 @Service 的参数的方法

within()匹配类

通过类名指定切点
如 with(examples.chap03.Horseman) 表示Horseman的所有方法

target()类及子类

通过类名指定,同时包含所有子类
如 target(examples.chap03.Horseman) 且Elephantman extends Horseman,则两个类的所有方法都匹配

args()方法参数

通过目标类方法的参数类型指定切点
例如 args(String) 表示有且仅有一个String型参数的方法

this()代理类

大部分时候和target()相同,区别是this是在运行时生成代理类后,才判断代理类与指定的对象类型是否匹配

循序渐进之Spring AOP(6) - 使用@Aspect注解
https://www.cnblogs.com/sa-dan/p/6837219.html


execution()方法执行

execution函数用于匹配方法执行的连接点,语法为:
execution(方法修饰符(可选) 返回类型 方法名 参数 异常模式(可选))

切点表达式

参数部分允许使用通配符:
*匹配任意字符,但只能匹配一个元素
.. 匹配任意字符,可以匹配任意多个元素,表示类时,必须和*联合使用
+必须跟在类名后面,如Horseman+,表示类本身和继承或扩展指定类的所有类

所以示例* chop(..)解读为:
方法修饰符 无
返回类型 *匹配任意数量字符,表示返回类型不限
方法名 chop表示匹配名称为chop的方法
参数 (..)表示匹配任意数量和类型的输入参数
异常模式 不限

更多示例:
void chop(String,int)
匹配目标类任意修饰符方法、返回void、方法名chop、带有一个String和一个int型参数的方法

public void chop(*)
匹配目标类public修饰、返回void、方法名chop、带有一个任意类型参数的方法

public String *o*(..)
匹配目标类public修饰、返回String类型、方法名中带有一个o字符、带有任意数量任意类型参数的方法

public void *o*(String,..)
匹配目标类public修饰、返回void、方法名中带有一个o字符、带有任意数量任意类型参数,但第一个参数必须有且为String型的方法

也可以指定类:
public void examples.chap03.Horseman.*(..)
匹配Horseman的public修饰、返回void、不限方法名、带有任意数量任意类型参数的方法

public void examples.chap03.*man.*(..)
匹配以man结尾的类中public修饰、返回void、不限方法名、带有任意数量任意类型参数的方法

指定包:
public void examples.chap03.*.chop(..)
匹配examples.chap03包下所有类中public修饰、返回void、方法名chop、带有任意数量任意类型参数的方法

public void examples..*.chop(..)
匹配examples.包下和所有子包中的类中public修饰、返回void、方法名chop、带有任意数量任意类型参数的方法

execution(* com.masikkk.dao..*.*(..))
匹配 com.masikkk.dao 包下 任意类名、任意方法名、任意参数、任意返回值的方法,即所有 dao 方法

循序渐进之Spring AOP(6) - 使用@Aspect注解
https://www.cnblogs.com/sa-dan/p/6837219.html

AspectJ中的其他切点函数

@AspectJ 除上表中所列的函数外,还有call()、initialization()、 preinitialization()、 staticinitialization()、 get()、 set()、handler()、 adviceexecution()、 withincode()、 cflow()、 cflowbelow()、 if()、 @this()以及@withincode()等函数,这些函数在Spring中不能使用,否则会抛出IllegalArgumentException 异常。在不特别声明的情况下,本书中所讲@AspectJ函数均指表 1中所列的函数。

spring 自定义注解annotation+aspect 环绕通知配置对dubbo的consumer监控报警
https://blog.csdn.net/ChiChengIT/article/details/50570161


逻辑运算符

切点表达式可由多个切点函数通过逻辑运算组成

&&
与操作,求交集,也可以写成and
例如 execution(* chop(..)) && target(Horseman) 表示Horseman及其子类的chop方法

||
或操作,求并集,也可以写成or
例如 execution(* chop(..)) || args(String) 表示名称为chop的方法或者有一个String型参数的方法

!
非操作,求反集,也可以写成not
例如 execution(* chop(..)) and !args(String) 表示名称为chop的方法但是不能是只有一个String型参数的方法

循序渐进之Spring AOP(6) - 使用@Aspect注解
https://www.cnblogs.com/sa-dan/p/6837219.html


@Pointcut 切点表达式重用

如果几种切面方法用的切面表达式都是相同的,此时没必要在每个切面通知方法上面标识重复的切面表达式,可以选择重用切面表达式。
使用 @Pointcut 来声明一个切入点表达式
定义一个方法, 用于声明切入点表达式. 一般地, 该方法中再不需要添入其他的代码. 后面的其他通知直接使用方法名来引用当前的切入点表达式。

例如:

@Aspect
@Component
public class LogProxy {

    /*定义一个方法, 用于声明切入点表达式. 一般地, 该方法中再不需要添入其他的代码.
    使用 @Pointcut 来声明切入点表达式.
    后面的其他通知直接使用方法名来引用当前的切入点表达式. */
    @Pointcut("execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))")
    public void performance(){}

    @Before("performance()")
    public void beforMethod(JoinPoint point){
        String methodName = point.getSignature().getName();
        System.out.println("LogProxy切面>目标方法为" + methodName);
    }

    @After("performance()")
    public void afterMethod(JoinPoint point){
        String methodName = point.getSignature().getName();
        List<Object> args = Arrays.asList(point.getArgs());
        System.out.println("调用后连接点方法为:" + methodName + ",参数为:" + args);
    }

    @AfterReturning(value="performance()", returning="result")
    public void afterReturning(JoinPoint point, Object result){
        String methodName = point.getSignature().getName();
        List<Object> args = Arrays.asList(point.getArgs());
        System.out.println("连接点方法为:" + methodName + ",参数为:" + args + ",目标方法执行结果为:" + result);
    }

    @AfterThrowing(value="performance()", throwing="ex")
    public void afterReturning(JoinPoint point, NullPointerException ex){
        String methodName = point.getSignature().getName();
        List<Object> args = Arrays.asList(point.getArgs());
        System.out.println("连接点方法为:" + methodName + ",参数为:" + args + ",异常为:" + ex);
    }
}

如果另一个切面和LogProxy切面在同一个包下面,如果想重用LogProxy中的切面表达式,可以用下面的方式:

@Component
@Aspect
public class ThirdMethod {

//  @Before("execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))")
    /*重用LogProxy切面中的切面表达式*/
    @Before("LogProxy.performance()")
    public void getInfo(JoinPoint joint){
        String methodName = joint.getSignature().getName();
        System.out.println("ThirdMethod切面>目标方法" + methodName);
    }
}

如果LogProxy和ThirdMethod 两个切面不在同一个包下面,可以用包全名进行限制。例如ThirdMethod 在B包下面,而LogProxy在A包下面,在ThirdMethod 切面中若想复用LogProxy中的切面表达式,可以如下设置:

@Before("A.LogProxy.performance()")

另外,命令切点时可以加作用域修饰符,例如:

package com.yyq.aspectJAdvanced;
import org.aspectj.lang.annotation.Pointcut;
public class TestNamePointcut {
    //通过注解方法inPackage()对该切点进行命名,方法可视域修饰符为private,表明该命名切点只能在本切面类中使用
    @Pointcut("within(com.yyq.aspectJAdvaned.*)")
    private void inPackage(){}
    @Pointcut("execution(* greetTo(..))")
    protected void greetTo(){}
    @Pointcut("inPackage() and greetTo()")
    public void inPkgGreetTo(){}
}

切点方法修饰符为private表明该命名切点只能在本切面类中使用
@Pointcut——切点表达式重用
https://blog.csdn.net/u010502101/article/details/78838086


AspectJ 切面

@Order(int) 指定切面顺序

@Order 标记定义了组件的加载顺序。

@Order 标记从spring 2.0出现,但是在spring 4.0之前, @Order 标记只支持 AspectJ 的切面排序。spring 4.0对 @Order 做了增强,它开始支持对装载在诸如Lists和Arrays容器中的自动包装(auto-wired)组件的排序。

在spring内部,对基于spring xml的应用,spring使用OrderComparator类来实现排序。对基于注解的应用,spring采用AnnotationAwareOrderComparator来实现排序。

@Order 标记定义如下:

@Retention(value=RUNTIME)
@Target(value={TYPE,METHOD,FIELD})
@Documented
public @interface Order

这个标记包含一个value属性。属性接受整形值。如:1,2 等等。值越小拥有越高的优先级。

AOP切面的执行顺序

增强织入的顺序
一个连接点可以同时匹配多个切点,切点对应的增强在连接点上的织入顺序的安排主要有以下3种情况:
1)如果增强在同一个切面类中声明,则依照增强在切面类中定义的顺序进行织入;
2)如何增强位于不同的切面类中,且这些切面类都实现了org.springframework.core.Order接口,则由接口方法的顺序号决定(顺序号小的先织入);
3)如果增强位于不同的切面类中,且这些切面类没有实现org.springframework.core.Order接口,织入的顺序是不确定的。

order 值越小,在 AOP 的 chain 中越靠前,越先执行。
如果是环绕切面,执行完目标方法后,order 值越小的越后执行。
如下图所示:


spring @Order 标记
https://www.cnblogs.com/lzmrex/p/6944961.html

Spring AOP @AspectJ 进阶
http://www.cnblogs.com/yangyquin/p/5582911.html


AspectJ 切面增强方式

增强的方式:
@Before:前置通知, 在方法执行之前执行。标识一个前置增强方法,相当于BeforeAdvice的功能
@AfterReturning:后置增强,相当于AfterReturningAdvice,方法正常退出时执行
@AfterThrowing:异常通知, 在方法抛出异常之后。异常抛出增强,相当于ThrowsAdvice
@After:无论方法以何种方式结束,都会执行(类似于finally)。final增强,不管是抛出异常或者正常退出都会执行
@Around:环绕执行。环绕增强,相当于MethodInterceptor
@DeclareParents: 引介增强,相当于IntroductionInterceptor

@AfterReturning 正常退出通知

当连接点方法成功执行后,返回通知方法才会执行,如果连接点方法出现异常,则返回通知方法不执行。

例如:

@Aspect
@Component
public class LogProxy {

    /*通过returning属性指定连接点方法返回的结果放置在result变量中,在返回通知方法中可以从result变量中获取连接点方法的返回结果了。*/
    @AfterReturning(value="execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))",
                    returning="result")
    public void afterReturning(JoinPoint point, Object result){
        String methodName = point.getSignature().getName();
        List<Object> args = Arrays.asList(point.getArgs());
        System.out.println("连接点方法为:" + methodName + ",参数为:" + args + ",目标方法执行结果为:" + result);
    }
}

@AfterThrowing 异常通知

异常通知方法只在连接点方法出现异常后才会执行,否则不执行。在异常通知方法中可以获取连接点方法出现的异常。在切面类中异常通知方法,示例如下:

/*通过throwing属性指定连接点方法出现异常信息存储在ex变量中,在异常通知方法中就可以从ex变量中获取异常信息了*/
@AfterThrowing(value="execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))",
            throwing="ex")
public void afterReturning(JoinPoint point, Exception ex){
    String methodName = point.getSignature().getName();
    List<Object> args = Arrays.asList(point.getArgs());
    System.out.println("连接点方法为:" + methodName + ",参数为:" + args + ",异常为:" + ex);
}

上面的例子中,异常类型设置的是Exception,表示捕获连接点方法的所有异常信息,也可以指定捕获指定类型的信息,例如:

@AfterThrowing(value="execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))",
            throwing="ex")
/*只捕获连接点方法中的NullPointerException 类型的异常信息*/
public void afterReturning(JoinPoint point, NullPointerException ex){
    String methodName = point.getSignature().getName();
    List<Object> args = Arrays.asList(point.getArgs());
    System.out.println("连接点方法为:" + methodName + ",参数为:" + args + ",异常为:" + ex);
}

@Around 环绕通知

当定义一个Around增强处理方法时,该方法的第一个形参必须是 ProceedingJoinPoint 类型,在增强处理方法体内,调用ProceedingJoinPoint的proceed方法才会执行目标方法——这就是@Around增强处理可以完全控制目标方法执行时机、如何执行的关键;如果程序没有调用ProceedingJoinPoint的proceed方法,则目标方法不会执行。

调用ProceedingJoinPoint的proceed方法时,还可以传入一个Object[ ]对象,该数组中的值将被传入目标方法作为实参。如果传入的Object[ ]数组长度与目标方法所需要的参数个数不相等,或者Object[ ]数组元素与目标方法所需参数的类型不匹配,程序就会出现异常。

且环绕通知必须有返回值, 返回值即为目标方法的返回值。例如:

@Around("execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))")
public Object aroundMethod(ProceedingJoinPoint pdj){
    /*result为连接点的放回结果*/
    Object result = null;
    String methodName = pdj.getSignature().getName();

    /*前置通知方法*/
    System.out.println("前置通知方法>目标方法名:" + methodName + ",参数为:" + Arrays.asList(pdj.getArgs()));

    /*执行目标方法*/
    try {
        result = pdj.proceed();
        /*返回通知方法*/
        System.out.println("返回通知方法>目标方法名" + methodName + ",返回结果为:" + result);
    } catch (Throwable e) {
        /*异常通知方法*/
        System.out.println("异常通知方法>目标方法名" + methodName + ",异常为:" + e);
    }

    /*后置通知*/
    System.out.println("后置通知方法>目标方法名" + methodName);
    return result;
}
}

注意:不可以在执行目标方法时在定义result变量:

……
/*执行目标方法*/
try {
Object result = pdj.proceed();
……
} catch (Throwable e) {
……
}
……
return result;

这种方法是行不通的,在Object result = pdj.proceed();中,如果pdj.proceed()执行失败,就会被try …catch捕获到异常,就不会执行定义result变量那一步了,即Object result不会执行,所以在return result;就会出现错误。

AspectJ 切面注解中五种通知注解:@Before、@After、@AfterRunning、@AfterThrowing、@Around
https://blog.csdn.net/u010502101/article/details/78823056

基于Annotation的Spring AOP: @Around
https://blog.csdn.net/confirmAname/article/details/9735975

spring 自定义注解annotation+aspect 环绕通知配置对dubbo的consumer监控报警
https://blog.csdn.net/ChiChengIT/article/details/50570161

循序渐进之Spring AOP(6) - 使用@Aspect注解
https://www.cnblogs.com/sa-dan/p/6837219.html


上一篇 Spring-Transaction

下一篇 Consul

阅读
评论
9.5k
阅读预计43分钟
创建日期 2019-06-12
修改日期 2023-02-16
类别
目录

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论