福莱蒽特:持股5%以上股东质押719.7万股股份 焦点滚动
2023年5月12日,福莱蒽特(605566 SH)公告,近日,杭州福莱蒽特股份有限公司(以下简称“公司”)收到持
Aspect-oriented Programming (AOP) 即面向切面编程。简单来说,AOP 是一种编程范式,允许我们模块化地定义横跨多个对象的行为。AOP 可以帮助我们将应用程序的关注点分离,使得代码更加清晰、易于维护和扩展。
为什么要AOP?大白话:在方法执行前后运行指定代码,比如日志记录、事务开启/提交/回滚等。
AOP可以帮助我们解决在代码中耦合度高的问题,让我们的代码更加模块化和易于维护。具体来说,AOP可以通过在运行时动态地将通用功能(例如日志记录、性能分析、事务管理)应用于多个模块,而无需修改它们的代码。这样可以避免代码重复和嵌套,提高代码的复用性和可维护性。另外,在复杂的业务场景中,多个模块可能需要共享某些共同的功能,而AOP可以让这些功能从模块中抽离出来,以便更好地进行组织和重用。总的来说,AOP可以让我们更好地实现代码的分离和聚合,从而获得更高效、更可靠的代码。
(相关资料图)
伪代码:没有使用AOP前,每个方法都要CV一遍打印方法执行日志大白话:增强原方法的功能,解耦通用功能,透明化静默操作。举例 事务切面切面:增强原方法的功能:原本方法使用的是数据库连接默认的策略自动提交事务的,有了切面能够保证方法内同一事务了;解耦通用功能:但是很多方法都需要做事务的控制,有了切面不需要我们每一个方法都加几行相同的代码;透明化静默操作:方法本身需要知道我怎么开的事务?需要知道我什么时候多打印了日志吗?
@Overridepublic UserPO findByUsername(@AutoTrim String username) { log.info("execute findByUsername by username={}", username); Optional opt = userRepository.findOne((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("username"), username)); Assert.isTrue(opt.isPresent(), "没有找到用户"); UserPO userPO = opt.get(); log.info("execute findByUsername by username={}; return {} ", username, userPO); return userPO;}@Overridepublic UserPO findByEmail(@AutoTrim String email) { log.info("execute findByEmail by email={}", email); Optional opt = userRepository.findOne((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("email"), email)); Assert.isTrue(opt.isPresent(), "没有找到用户"); UserPO userPO = opt.get(); log.info("execute findByEmail by email={}; return {} ", email, userPO); return userPO;}
伪代码:使用AOP后,无需关心方法执行日志的打印@Overridepublic UserPO findByUsername(@AutoTrim String username) { Optional opt = userRepository.findOne((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("username"), username)); Assert.isTrue(opt.isPresent(), "没有找到用户"); return opt.get();}@Overridepublic UserPO findByEmail(@AutoTrim String email) { Optional opt = userRepository.findOne((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("email"), email)); Assert.isTrue(opt.isPresent(), "没有找到用户"); return opt.get();}
此处代码演示 【part1】演示内容:
展示未使用AOP前,使用UserServiceImpl
注入Bean,测试接口查看日志打印;展示使用AOP后,使用UserServiceAopImpl
注入Bean,测试接口查看日志打印;展示切面打印非项目内的类方法执行日志,切换Pointcut
为logPointcut2()
;展示灵活使用配置,控制切面开启日志,开启@ConditionalOnProperty
,修改yml文件;例如:测试环境要开启日志,生产环境要关闭日志,通过配置灵活控制。怎么实现AOP?PS:我们在此暂不考虑基于
XmlApplicationContext
系列;
AOP的实现依赖于多态和动态代理。为了更好的理解,我们可以先举例一个静态代理的类来分析。此处代码演示 【part2】演示内容:
展示打印日志的静态代理,使用UserServiceStaticAopImpl
注入Bean,测试接口查看日志打印;展示多种通知类型的静态代理,使用UserServiceStaticAopAnyImpl
注入Bean,测试接口查看日志打印;AOP的代码结构与核心概念PS:我们在此暂不考虑基于
XmlApplicationContext
系列;
静态代理的AOP结构(简单易理解版):
Aspect切面:指横跨多个类的一个关注点,它与不同类中相似的方法相对应。如保存数据时添加日志,可以新建一个切面配置日志记录逻辑。
大白话:一个模块化的切面程序,也可以理解为是一个实现切面功能的类;用@Aspect定义的Bean Class,或者Spring xml配置里的aop:aspect标签:
...
Join Point连接点:指在应用程序执行过程中的某个特定位置,如方法调用或异常处理等。
Advice可以理解为要切面的对象类型,比如要加切面的目标是构造器,或者一个方法,或者是一个属性的赋值;AspectJ中可以有很多种,详见AspectJ Join Points;SpringAOP中只有一种,就是方法执行(Method execution);
通知:指在切面的某个连接点上执行的代码。通知有许多类型,包括“前置通知”、“后置通知”、“返回通知”、“异常通知”、“环绕通知”,其中“环绕通知”能够完全控制目标方法的执行。
Pointcut切入点:指一个或多个连接点,通常定义在一个正则表达式中,描述哪些方法会被拦截。
其它IntroductionTargetAOP proxyWeaving参考:Join Points and PointcutsSpring 之AOP AspectJ切入点语法详解例:
within(com.supalle.springaop..*.UserServiceAopImpl)
execution(* com.supalle.springaop.*.*(..))
within(com.supalle.springaop.TestController) && @annotation(com.supalle.springaop.Log)
详见:AOP Concepts
Spring AOP常用的方式此处代码演示 【part3】
@Aspect
XmlApplicationContext
的应用需要开启
AnnotationConfigApplicationContext
的应用需要开启@EnableAspectJAutoProxy
;SpringBoot环境下可以不用,在org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
里已经默认启用;
package com.supalle.springaop.aspect;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.Signature;import org.aspectj.lang.annotation.*;import org.aspectj.lang.reflect.MethodSignature;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.core.annotation.Order;import org.springframework.stereotype.Component;import java.util.stream.Collectors;import java.util.stream.IntStream;/** * 日志切面 * * @author supalle * @see {@link https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-pointcuts} */@Slf4j@Aspect@Component//@ConditionalOnProperty(value = "log-execution", havingValue = "true")public class LogAspect { @Pointcut("within(com.supalle.springaop..*.UserServiceAopImpl)") // the pointcut within *AopImpl private void logPointcut() { } @Pointcut("within(com.supalle.springaop..*.UserServiceAopImpl) || within(com.zaxxer.hikari.HikariDataSource)") private void logPointcut2() { } @Before("logPointcut()") public void beforeLog(JoinPoint joinPoint) { log.info("Before Advice 1 execute {}", joinPoint.getSignature().getName()); } // 同一个Aspect内,@Order排序无效,需要靠方法名排序,比如把当前方法改为beforeLog2,就能运行在beforeLog前面 @Before("logPointcut()") public void beforeLog2(JoinPoint joinPoint) { log.info("Before Advice 2 execute {}", joinPoint.getSignature().getName()); } @AfterReturning(value = "logPointcut()", returning = "returnValue") public void afterReturningLog(JoinPoint joinPoint, Object returnValue) { log.info("AfterReturning Advice execute {}; {} ", joinPoint.getSignature().getName(), returnValue); } @AfterThrowing(value = "logPointcut()", throwing = "throwable") public void afterThrowingLog(JoinPoint joinPoint, Throwable throwable) { log.info("AfterThrowing Advice execute {}; {}", joinPoint.getSignature().getName(), throwable.getMessage()); } @After("logPointcut()") public void afterLog(JoinPoint joinPoint) { log.info("After Advice execute {}", joinPoint.getSignature().getName()); } @Around("logPointcut()") public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable { Signature joinPointSignature = joinPoint.getSignature(); String name = joinPointSignature.getName(); Logger logger = LoggerFactory.getLogger(joinPointSignature.getDeclaringTypeName()); Object[] args = joinPoint.getArgs(); String[] argNames = ((MethodSignature) joinPointSignature).getParameterNames(); String argString = ""; if (args != null && args.length > 0) { argString = " by " + IntStream.range(0, args.length) .mapToObj(index -> argNames[index] + "=" + args[index]) .collect(Collectors.joining(" , ")); } logger.info("Around Advice execute {}{}", name, argString); Object returnValue = joinPoint.proceed(); logger.info("Around Advice execute {}{}; return {} ", name, argString, returnValue); return returnValue; }}
XML配置Advisor略
org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#findEligibleAdvisors
中会查找容器中所有的Advisor类型Bean,也属于AutoProxy的支持;
LogBeforeAdvice
:定义一个Advice,加@Component
注册为Beanpackage com.supalle.springaop.advice;import lombok.extern.slf4j.Slf4j;import org.springframework.aop.MethodBeforeAdvice;import org.springframework.stereotype.Component;import java.lang.reflect.Method;/** * org.springframework.aop.framework.adapter.DefaultAdvisorAdapterRegistry的支持 */@Slf4j@Componentpublic class LogBeforeAdvice implements MethodBeforeAdvice { @Override public void before(Method method, Object[] args, Object target) throws Throwable { log.info("LogBeforeAdvice execute {}", method.getName()); }}
LogBeforeAdvisor
:定义一个Advisor,加@Component
注册为Beanpackage com.supalle.springaop.advisor;import com.supalle.springaop.advice.LogBeforeAdvice;import lombok.RequiredArgsConstructor;import org.aopalliance.aop.Advice;import org.springframework.aop.ClassFilter;import org.springframework.aop.MethodMatcher;import org.springframework.aop.Pointcut;import org.springframework.aop.support.AbstractPointcutAdvisor;import org.springframework.stereotype.Component;@Component@RequiredArgsConstructorpublic class LogBeforeAdvisor extends AbstractPointcutAdvisor { private final LogBeforeAdvice advice; @Override public Pointcut getPointcut() { Pointcut pointcut = new Pointcut() { @Override public ClassFilter getClassFilter() { return clazz -> "UserServiceAopImpl".equals(clazz.getSimpleName()); } @Override public MethodMatcher getMethodMatcher() { return MethodMatcher.TRUE; } }; return pointcut; } @Override public Advice getAdvice() { return advice; }}
运行代码后调试接口,看控制台打印输出:
: LogBeforeAdvice execute findByUsername
程序员创建用的不多,可以参考Programmatic Creation of @AspectJ Proxies
进阶篇Spring AOP与动态代理动态代理Spring AOP动态代理是指在程序运行时动态生成代理类的技术,即不需要手工编写代理类的源代码,而是在程序运行期间通过反射等机制动态地生成。动态代理模式可以帮助我们减少重复的代码,提高代码的可维护性和可扩展性。通常情况下,我们都是通过实现接口来创建代理对象,但是如果一个类没有实现任何接口,我们仍然可以通过动态代理来创建代理对象。动态代理模式适用于一些横切关注点(cross-cutting concerns)的处理,例如日志、安全、事务等功能。
在 Java 中,动态代理模式主要有两种实现方式:基于 JDK 动态代理(JDK Dynamic Proxy):JDK 提供了一个 java.lang.reflect.Proxy 类,可以动态地创建实现一组给定接口的代理类。要求被代理对象必须实现至少一个接口,并通过 Proxy 类的静态方法 newProxyInstance 来创建代理对象。基于 CGLIB 动态代理:CGLIB(Code Generation Library) 是一个基于 ASM(Java 字节码操作框架)的高性能字节码生成库,可以在运行时动态生成字节码,并生成对应的代理类。要求被代理对象必须有默认构造函数,并通过 CGLIB 库提供的代理工厂(Enhancer 类)来创建代理对象。总之,动态代理模式可以帮助我们在程序运行期间动态地生成代理类,从而达到增强功能、添加处理逻辑等目的。
Spring AOP 就是动态代理的一种落地实现,底层是通过JDK Proxy和Cglib&ASM策略来进行动态生成代理类;Spring Native应用下实现AOP的话,刚出来的时候我看过一遍,因为无法动态生成字节码,所以是在AOT编译时,给应用中所有的类都生成了一个代理类,当这个类在实际运行时需要动态代理时,则加载这个代理类直接使用。(时过境迁,现在不知道实现方式变了没有)
Spring AOP的结构与核心类AOT(Ahead-of-time )compiler:预先编译器;JIT(Just-In-Time)compiler:即时编译器;
除了上文说的AOP基本概念在Spring中有对应的类外,Spring AOP APIs中还有其它几个核心类,用于具体的AOP实现。
Advised:代理类的顶层接口,包含委托对象和其信息,以及应用的Advisor的集合;Advisor:一个Pointcut
+ Advice
的组合;AopProxy:AOP具体的代理实现,可能是JDK Proxy,也可能是Cglib Proxy;JdkDynamicAopProxyObjenesisCglibAopProxy
org.springframework.aop.framework.DefaultAopProxyFactory#createAopProxy
用UML画出他们的结构图如下:以UserService
为例,Spring AOP生成的代理类大致结构:
仔细思考,上文举例的静态代理实现AOP的代码,是不够健全的,尤其是在Advice
的顺序上;此处代码演示 【part2】:beforeAspects
切面中添加 System.out.println(1/0);使其抛出异常引用Spring官方文档的原文:
Advice: Action taken by an aspect at a particular join point. Different types of advice include "around", "before", and "after" advice. (Advice types are discussed later.) Many AOP frameworks, including Spring, model an advice as an interceptor and maintain a chain of interceptors around the join point.
这句“Many AOP frameworks, including Spring, model an advice as an interceptor and maintain a chain of interceptors around the join point.”翻译过来就是“包括Spring在内的许多AOP框架都将Advice建模为拦截器,并在连接点周围维护拦截器链。”很好,使用类似于javax.servlet.Filter
拦截器的方式,就可以很好的控制Advice
的执行顺序。
详见:
org.springframework.aop.framework.AdvisedSupport#getInterceptorsAndDynamicInterceptionAdvice
时序图大致如下:
Advisor/Advice的执行顺序调用链的顺序,基于Advised中的advisors集合顺序,主要的排序策略如下:
Advisor
的order
值,没有则取Advice
的order
值,还没有则为null
,约等于最低优先级;同一@Aspect
类中定义多个Advice时,先按Around
, Before
, After
, AfterReturning
, AfterThrowing
排序,再按方法名排序;同一个Advice
类实现多个Advice
类型时,按 MethodInterceptor
,BeforeAdvice
,AfterReturningAdvice
,AfterThrowsAdvice
排序;通过AbstractAdvisingBeanPostProcessor
的后置处理器添加的Advisor
,后置处理器可以通过beforeExistingAdvisors
属性控制是否排序在所有已经存在的Advisor
前;Spring官方文档:Advice Ordering排序Advisors:org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator#sortAdvisors
排序同一Aspect中的Advice:org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory#getAdvisorMethods
PS:如果Advisor/Advice是实现了Ordered接口的话,@Order注解是不生效的。
Spring AOP添加Advisor的途径ProxyConfig
下分几个家族,都可以添加Advisor
,创建AOP代理类。
排序:Ordered=Ordered.HIGHEST_PRECEDENCE; // Integer.MIN_VALUE ;方法内写死,继承才可以修改注意:
AsyncAnnotationAdvisor
是在AsyncAnnotationBeanPostProcessor
后置处理器中创建和匹配,beforeExistingAdvisors=true;排序在所有已经存在的Advisor前。
Spring异步执行的AOP支持。
AsyncAnnotationAdvisor & AnnotationAsyncExecutionInterceptor排序:Ordered=Ordered.HIGHEST_PRECEDENCE; // 继承自父类
AsyncExecutionInterceptor
Spring注解@Async
异步执行的AOP支持。
排序:Ordered=Ordered.LOWEST_PRECEDENCE; // 继承自父类
AbstractPointcutAdvisor
,可set;注意:RetryConfiguration
实现IntroductionAdvisor
,排序优先于同order的未实现IntroductionAdvisor
的Advisor。
Spring注解@Retryable
方法失败重试的AOP支持。
排序:Ordered=Ordered.LOWEST_PRECEDENCE; // 继承自父类
AbstractPointcutAdvisor
,可set,默认来自@EnableCaching
的order
属性,也是LOWEST_PRECEDENCE
,可以直接在注解里修改。
Spring注解缓存@EnableCaching
、@Cacheable
等的AOP支持。
排序:Ordered=Ordered.LOWEST_PRECEDENCE; // 继承自父类
AbstractPointcutAdvisor
,可set,默认来自@EnableTransactionManagement
的order
属性,也是LOWEST_PRECEDENCE
,可以直接在注解里修改。
Spring注解事务@Transactional
的AOP支持。
Spring AOP代理类创建的时机,主要是在Bean被引用之前,依靠BeanPostProcessor
来实现;这就要靠ProxyProcessorSupport
家族的AbstractAdvisingBeanPostProcessor
和AbstractAutoProxyCreator
了。
AbstractAdvisingBeanPostProcessor
对单个Advisor
进行AOP切面的添加;AbstractAutoProxyCreator
对所有满足Pointcut
的Bean进行切面的添加;两者大部分情况会在postProcessAfterInitialization()
钩子方法中创建Bean的AOP代理对象,但也有一些特殊情况,比如自定义TargetSource
的时候,会在postProcessBeforeInstantiation()
钩子方法中创建代理对象;还有一种情况是在getEarlyBeanReference()
钩子方法时创建,也会提前创建好AOP代理对象;
详见源码:
org.springframework.aop.framework.AbstractAdvisingBeanPostProcessor#postProcessAfterInitialization
org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessAfterInitialization
另外注释一下:postProcessBeforeInstantiation()
钩子执行时,Bean已经完成了 创建对象、属性填充、依赖注入、初始化方法的执行;
其它自动创建的还有ProxyFactoryBean
、AbstractSingletonProxyFactoryBean
、ScopedProxyFactoryBean
的派生类也会在初始化Bean时自动创建;
Spring IOC容器在扫描所有的BeanDefinition
后,会先按顺序初始化完所有的BeanPostProcessor
,在初始化其它普通的Bean
;而AbstractAdvisingBeanPostProcessor
和AbstractAutoProxyCreator
这两个处理自动AOP的后置处理器排序都是 Ordered=LOWEST_PRECEDENCE
,因此,在这两后置处理器之前初始化的Bean
都无法被他们进行自动AOP代理。
Spring如何防止反复创建AOP代理类首先Customizing Beans by Using a BeanPostProcessorSpring官方提示:BeanPostProcessor instances and AOP auto-proxyingClasses that implement the BeanPostProcessor interface are special and are treated differently by the container. All BeanPostProcessor instances and beans that they directly reference are instantiated on startup, as part of the special startup phase of the ApplicationContext. Next, all BeanPostProcessor instances are registered in a sorted fashion and applied to all further beans in the container. Because AOP auto-proxying is implemented as a BeanPostProcessor itself, neither BeanPostProcessor instances nor the beans they directly reference are eligible for auto-proxying and, thus, do not have aspects woven into them.源码详见:
org.springframework.context.support.PostProcessorRegistrationDelegate#registerBeanPostProcessors
AbstractAutoProxyCreator
中,有三个Map
对象保存AOP代理类的创建信息,分别是 targetSourcedBeans
、advisedBeans
、earlyProxyReferences
,当三者内存在Bean的代理对象时,则不会重复创建;AbstractAdvisingBeanPostProcessor
中会判断 bean instanceof Advised
,如果已经是AOP代理对象,则不会再创建新的代理对象,直接在此代理对象上添加Advisor
;Spring AOP与常说的Spring三级缓存的关系常说的Spring三级缓存指的是org.springframework.beans.factory.support.DefaultSingletonBeanRegistry
类中的三个Map类型的属性,分别是:
Map singletonObjects; // 存放初始化完成的单例BeanMap earlySingletonObjects; // 存放getEarlyBeanReference()钩子创建的早期BeanMap> singletonFactories; // 存放earlySingletonObject的工厂实例
IOC默认开启允许循环引用,但是在依赖注入时,但是Spring并不想把创建AOP代理的行为提前,因此设计了getEarlyBeanReference
钩子和singletonFactories
、earlySingletonObjects
,以支持循环引用。在实例化Bean
后,先创建出earlySingletonObject
的工厂实例,以备后续依赖注入的其它Bean
引用当前Bean
,当真实存在循环依赖时,调用earlySingletonObject
的工厂的方法构建早期Bean
供其注入,并将这个早期Bean
存放到earlySingletonObjects
中,以备循环依赖不止一次;最终在当前Bean初始化完成后,移除早期Bean
,将最终的Bean
放入singletonFactories
中,成为正式暴露的Bean实例;org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
// Eagerly cache singletons to be able to resolve circular references// even when triggered by lifecycle interfaces like BeanFactoryAware.boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));if (earlySingletonExposure) { if (logger.isTraceEnabled()) { logger.trace("Eagerly caching bean "" + beanName + "" to allow for resolving potential circular references"); } addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}
org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
// Eagerly check singleton cache for manually registered singletons.Object sharedInstance = getSingleton(beanName);
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)
@Nullableprotected Object getSingleton(String beanName, boolean allowEarlyReference) { // Quick check for existing instance without full singleton lock Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { synchronized (this.singletonObjects) { // Consistent creation of early reference within full singleton lock singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null) { ObjectFactory> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } } } return singletonObject;}
扩展篇ASM/Cglib动态字节码生成篇幅太长,略
Spring缓存失效与AOP的瓜葛Spring缓存切面主要依赖于AOP的实现,当对象内进行this.doSomething()调用时,因为没有经过代理对象,所以也没有缓存切面的功能;
Spring事务失效与AOP的瓜葛与缓存切面一样,Spring事务切面主要依赖于AOP的实现,当对象内进行this.doSomething()调用时,因为没有经过代理对象,所以也没有事务切面的功能;
Synchronized和@Transactional 使用时,出现的问题因为事务切面会在执行方法之前开启事务,之后再加锁,当锁住的代码执行完成后,在提交事务,因此synchronized代码块执行是在事务之内执行的,可以推断在代码块执行完时,事务还未提交,其他线程进入synchronized代码块后,读取的库存数据不是最新的。解决上面的方法,比较简单的可以在update方法之前加上synchronizedQ在还没有开事务之间就加锁,那么就可以保证线程同步
其他开发语言也可实现AOPJavaScript的Proxy实现AOP思考篇AOP的优点降低了代码的耦合度:AOP可以将横切逻辑与业务逻辑分离,从而避免了代码的重复和冗余,提高了代码的可维护性和可读性。提高了代码的复用性:AOP可以将通用的横切逻辑抽象出来,作为一个切面,从而可以在不同的业务逻辑中重复使用,提高了代码的复用性。提高了系统的可扩展性:AOP可以动态地将横切逻辑织入到业务逻辑中,从而可以方便地添加新的功能,提高了系统的可扩展性。AOP的缺点AOP增加了系统的复杂度:AOP需要额外的配置和代码,增加了系统的复杂度,降低了系统的可读性和可维护性。AOP可能会影响系统的性能:AOP需要在运行时进行额外的处理,可能会影响系统的性能,尤其是在大规模应用中。AOP可能会引入新的问题:AOP可能会引入新的问题,如死锁、并发问题等,需要特别注意。使用AOP的建议不要把应该与业务耦合的代码放到切面中注意切面的顺序注意this调用可能导致AOP失效SpringBoot项目中使用AOP的最佳实践关键词:
2023年5月12日,福莱蒽特(605566 SH)公告,近日,杭州福莱蒽特股份有限公司(以下简称“公司”)收到持
解答:铃声多多如何创建新铃单1、先下载很多铃声,点击打开软件。2、然后点击首页右上角的上传。3、然后点
截至2023年5月12日收盘,气派科技(688216)报收于23 67元,下跌1 46%,换手率1 42%,成交量6173 0手,成交额1480 86万元。
记者11日从中国科学技术大学获悉,该校郭光灿院士团队李传锋教授和项国勇教授与合作者合作,使用单个量子比
1、读作:pí毗部首:比部,部外笔画:5画,总笔画:9画五笔:LXXN,仓颉:WPP,郑码:KIRR电码:3026,区位
1、去西双版纳最佳季节:10月-次年6月。西双版纳地区属于热带雨林气候,夏无酷暑,冬无严寒,四季温暖宜人
从400公里高度看地球是什么感觉?第一次进入中国人自己的空间站心情如何?曾经,作为空间站阶段首次载人飞
西畴县气象台2023年5月11日15时05分发布将暴雨蓝色预警信号升级为暴雨黄色预警信号:过去1小时,我县柏林乡
北京5月11日电伯尔尼消息:据瑞士咨询网(swissinfo)10日报道,据瑞银集团透露,在瑞士信贷与瑞银合并的最初
(记者徐婧杜燕)截至2022年底,北京市医疗卫生机构在册护士总数14 3万人。北京市卫生健康委公众权益保障处处
近日,2023长琴岛·芒果音乐季五一海岛特辑在珠海长琴岛举办,这标志着全国首个无居民海岛开发项目正式对外
5月9日,以“中国品牌走出去”为主题的高质量发展与品牌强国调研座谈会在北京举行,文化和旅游部国际交流与
宇瞳光学近期接受投资者调研时称,车载业务分为车载镜头、激光雷达、抬头显示、智能车灯等业务线条,车载镜
5月11日,中消协发布“五一”消费维权舆情分析报告。报告指出,“五一”期间,住宿酒店普遍价格上涨,酒店
乐居财经严明会5月11日消息,全球床垫龙头泰普尔-丝涟国际集团(TempurSealyInternational)正式以价值40亿
河南日报客户端记者李凤虎河南日报社全媒体记者陈萌萌实习生李若辰 文图为让人民享有更加充实、更为丰富、
2023天津建博会官网(附入口)官网网址:http: www tjjbh-build com 官网入口:点此进入展会简介:波澜壮
1、你好,高压电工证到当地劳动局或者安监局去报名考取。2、具体可以打电话咨询,什么时候报名,需要哪些条
1、联通在信 "是中国联通为联通手机用户推出的无线数据业务,主要提供基于短消息平台的订阅和点播业务。2、
一、全国价格分析宇博智业监测数据显示,2023年05月10日,全国81类杏中,仅有金太阳杏的价格出现了上涨,涨
日前,福建省根治拖欠农民工工资工作领导小组办公室印发《关于开展根治欠薪制度建设争优争先争效行动的通知
2023年4月份,全国居民消费价格同比上涨0 1%
人体春生夏长,秋收冬藏。从立夏开始,接下来的夏三月,是人体“长”的时候。不仅小孩会快速长高,成年人也
巴西批准一项转基因小麦用于商业化种植
36氪获悉,国泰君安最新研报表示,节后锂价延续回暖态势,5月下游企业整体排产向好,另外特斯拉打破市场降