【微服务】Spring Aop原理深入解析

目录

一、前言

二、aop概述

2.1 什么是AOP

2.2 AOP中的一些概念

2.2.1 aop通知类型

2.3 AOP实现原理

2.3.1 aop中的代理实现

2.4 静态代理与动态代理

2.4.1 静态代理实现

三、 jdk动态代理与cglib代理

3.1 jdk动态代理

3.1.1 jdk代理示例

3.1.2 jdk动态代理模拟实现

3.2 CGLIB 代理

3.2.1 cglib代理代码示例

3.2.2 cglib代理源码模拟实现

3.2.3 cglib代理补充说明

四、spring aop源码探究

4.1 环境准备

4.1.1 引入aop依赖包

4.1.2 自定义aop配置类

4.1.3 测试方法

4.2 spring aop切点表达式

4.2.1 语法结构

4.2.2 常见的切点表达式示例

4.3 spring aop源码分析过程

五、写在文末


一、前言

spring 是一个流行的 Java 企业应用程序开发框架,其中的 aop(面向切面编程)是 spring 框架中一个非常重要的概念。可以说在spring框架以及生态体系下,随处可见aop编程思想的运用,借助这一编程思想,在很多特殊的业务场景下,aop的使用让编码变得易扩展、更优雅、更灵活,同时也能很好的解决通用的业务问题,提升开发效率。本文将详细介绍aop的核心技术和底层实现原理。

二、aop概述

2.1 什么是AOP

AOP,即面向切面编程,AOP是一种编程范式,用于在不修改原始代码的情况下向现有应用程序添加新功能。这种编程方式将应用程序分成许多独立的部分,称为切面。这些切面可以在应用程序的不同位置进行编写和维护,从而提高了应用程序的可重用性和可维护性

AOP是OOP(面向对象编程)的延续,主要用于实现横切关注点,比如:日志记录、性能统计、安全控制、事务处理等方面。简单来说,针对应用程序中那些可以预见的,以及归总为公共的处理业务的场景均可以考虑使用aop的编程实现。

2.2 AOP中的一些概念

为了后续更全面深入的掌握aop的使用,有必要了解下spring aop的相关概念。这些概念能够更好的指导我们在编码中对aop的原理的理解。

切面(Aspect)

AOP核心就是切面,它将多个类的通用行为封装成可重用的模块,该模块含有一组API提供横切功能。比如,一个日志模块可以被称作日志的AOP切面。根据需求的不同,一个应用程序可以有若干切面。在Spring AOP中,切面通过带有@Aspect注解的类实现。

连接点(Join Point)

AOP核心就是切面,它将多个类的通用行为封装成可重用的模块,该模块含有一组API提供横切功能。比如,一个日志模块可以被称作日志的AOP切面。根据需求的不同,一个应用程序可以有若干切面。在Spring AOP中,切面通过带有@Aspect注解的类实现。

连接点(Join Point)

哪些方法需要被AOP增强,这些方法就叫做连接点

通知(Advice)

AOP在特定的切入点上执行的增强处理

常用的通知类型有:

  • before,前置通知 ;
  • after,后置通知 ;
  • afterReturning,返回通知 ;
  • afterThrowing,异常通知 ;
  • around,环绕通知 ;

切入点(Pointcut)

实际真正被增强的方法,称为切入点

2.2.1 aop通知类型

通知(advice)是你希望在横切关注点具体的实现方式,即方法之前触发?执行后触发...,aop中的Advice主要有以下5种类型:

前置通知(Before Advice)

在连接点之前执行的Advice,即方法执行前,使用@Before注解使用这个Advice

返回之后通知(After Retuning Advice)

在连接点正常结束之后执行的Advice,即你的方法正常执行完成之后执行,通过 @AfterReturning注解使用它,前提是你的方法没有抛出异常

抛出(异常)后执行通知(After Throwing Advice)

如果方法执行抛异常的时候,这个Advice就会被执行,通过 @AfterThrowing注解来使用

后置通知(After Advice)

无论连接点通过什么方式退出(方法正常返回或者抛出异常)都会执行在结束后执行这些Advice,通过 @After注解使用

围绕通知(Around Advice)

围绕连接点执行的Advice,方法执行前可以拦截参数,方法执行后可以拿到执行结果,属于几种通知中比较灵活的一种,通过@Around注解使用

2.3 AOP实现原理

Spring AOP 的实现原理是基于动态代理字节码操作的。具体来说,

  • 在编译阶段, Spring 会使用 AspectJ 编译器将切面代码编译成字节码文件;
  • 在运行阶段, Spring 会使用 Java 动态代理或 CGLIB 代理生成代理类,这些代理类会在目标对象方法执行前后插入切面代码,从而实现AOP增强的功能;

Spring AOP 主要使用两种代理方式:JDK动态代理和 CGLIB 代理。如果目标对象实现了至少一个接口,则框架使用JDK动态代理;否则,使用 CGLIB 代理。

2.3.1 aop中的代理实现

基于接口情况下,使用JDK动态代理

JDK动态代理,创建一个接口实现类的代理对象,该接口实现类的代理对象会调用该接口的真实实现,并且在代理对象中调用真实的实现类的方法进行增强

没有接口情况下,使用 CGLIB 动态代理

CGLIB动态代理,创建一个类子类的代理对象,该子类的代理对象会去调用父类中的方法,并且在子类代理对象调用其父类方法后做增强

2.4 静态代理与动态代理

很多同学在理解aop的代理时,很容易对静态代理与动态代理这个概念搞混,顾名思义,静态代理,简单理解就是代理的类或方法定义已经提前定义好了,在需要使用的地方直接调用即可,而动态代理,关键是理解动态这个概念,从程序的角度来说,就是在程序运行过程中,动态生成了目标对象的代理对象。

2.4.1 静态代理实现

定义一个接口

public interface LoginService {void login(String userId);
}

接口实现

public class LoginServiceImpl implements LoginService {@Overridepublic void login(String userId) {System.out.println("登录成功,userId:"+userId);}
}

实现了LoginService 接口的代理实现类

public class ProxyLoginService implements LoginService {private LoginServiceImpl loginServiceImpl;public ProxyLoginService(LoginServiceImpl loginServiceImpl){this.loginServiceImpl=loginServiceImpl;}@Overridepublic void login(String userId) {System.out.println("执行登录之前操作");loginServiceImpl.login("user");System.out.println("执行登录方法后操作");}
}

测试方法

public static void main(String[] args) {//创建电影院(静态代理)ProxyLoginService proxyLoginService = new ProxyLoginService(new LoginServiceImpl());proxyLoginService.login("user1");}

静态代理优点

  • 使得真实角色处理的业务更加纯粹,不再去关注一些公共的事情 ;
  • 公共的业务由代理来完成---实现业务的分工;
  • 公共业务发生扩展时变得更加集中和方便 ;

缺点:

  • 每一个代理类都必须实现一遍真实实现类(也就是realMovie)的接口,如果接口增加方法,则代理类也必须跟着修改;
  • 每一个代理类对应一个真实实现类(委托类),如果真实实现(委托类)非常多,则静态代理类就非常臃肿,难以胜任;

三、 jdk动态代理与cglib代理

3.1 jdk动态代理

与静态代理不同,动态代理是根据代理对象,动态创建代理类。这样就可以避免静态代理中代理类接口过多的问题。jdk动态代理是通过反射来实现的,借助Java自带的java.lang.reflect.Proxy,通过固定的规则生成。

jdk动态代理的使用步骤如下:

  • 创建一个需要动态代理的接口,即Movie接口;
  • 创建一个需要动态代理接口的真实实现,即RealMovie类;
  • 创建一个动态代理处理器,实现InvocationHandler接口,并重写invoke方法去增强真实实现中的目标方法;
  • 在测试类中,生成动态代理的对象;

3.1.1 jdk代理示例

自定义一个类,实现InvocationHandler接口,并重写里面的invoke方法

public class ProxyInvocationHandler implements InvocationHandler {//需要动态代理接口的真实实现类private Object object;//通过构造方法去给需要动态代理接口的真实实现类赋值public ProxyInvocationHandler(Object object) {this.object = object;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//方法增强System.out.println("方法执行前操作");//object是真实实现,args是调用方法的参数//当代理对象调用真实实现的方法,那么这里就会将真实实现和方法参数传递过去,去调用真实实现的方法method.invoke(object,args);//方法增强System.out.println("方法执行后操作");return null;}
}

编写测试类,使用自定义的handler生成代理对象并调用目标方法

public class DynamicProxyTest {public static void main(String[] args) {//需要动态代理接口的真实实现LoginServiceImpl realMovie = new LoginServiceImpl();//动态代理处理类ProxyInvocationHandler handler = new ProxyInvocationHandler(realMovie);//获取动态代理对象//第一个参数:真实实现(被代理对象)的类加载器//第二个参数:真实实现类(被代理对象)它所实现的所有接口的数组//第三个参数:动态代理处理器LoginService loginService = (LoginService) Proxy.newProxyInstance(realMovie.getClass().getClassLoader(),realMovie.getClass().getInterfaces(),handler);loginService.login("user");}}

执行结果

3.1.2 jdk动态代理模拟实现

由于jdk的代理是在运行期间动态产生字节码,所以很难看到运行期间代理产生的源码,我们可以模拟其原理进行一个简单的实现,代码如下:

定义一个接口,作为被代理的对象,实现类也一并贴出

    interface MinuService {int addMinu();void reduceMinu();}//目标对象实现类static class Target implements MinuService {@Overridepublic int addMinu() {System.out.println("tager do addMinu");return 1;}@Overridepublic void reduceMinu() {System.out.println("tager do reduceMinu");}}

定义一个handler,handler的作用可理解为作为代理对象中实际执行方法调用的一个入口

    interface MyInvocationHandler {Object invoke(Object proxy,Method method, Object[] args);}

定义代理类,代理类是核心实现,在代理类中主要做的事情如下:

  • 引用自定义handler;
  • 引用代理类中待执行的目标方法(该方法其实是运行期间动态生成的);
  • 调用handler中的invoke反射调用目标方法,并得到执行结果;
import java.lang.reflect.Method;public class MyProxy implements JdkInner.MinuService {private JdkInner.MyInvocationHandler handler;public MyProxy(JdkInner.MyInvocationHandler handler) {this.handler = handler;}static Method addMinu;static Method reduceMinu;static {try {addMinu = JdkInner.MinuService.class.getMethod("addMinu");reduceMinu = JdkInner.MinuService.class.getMethod("reduceMinu");} catch (NoSuchMethodException e) {e.printStackTrace();}}@Overridepublic int addMinu() {Object invoke = null;invoke = handler.invoke(this, addMinu, new Object[0]);return Integer.parseInt(invoke.toString());}@Overridepublic void reduceMinu() {handler.invoke(this, reduceMinu, new Object[0]);}
}

编写测试类,有没有发现这个写法和上述使用jdk的动态代理很像

 public static void main(String[] args) {MinuService minuService = new MyProxy(new MyInvocationHandler() {@Overridepublic Object invoke(Object proxy,Method method, Object[] args) {System.out.println("before  invoke method");Object result = null;try {result = method.invoke(new Target(), args);System.out.println("result :" + result);} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}return result;}});System.out.println(minuService.getClass());minuService.addMinu();}

如果你想查看jdk动态代理时产生的源码,可以考虑使用arthars进行反编译查看

3.2 CGLIB 代理

CGLIB 代理是一个基于字节码操作的代理方式,它是一个强大的、高性能、高质量的 Code 生成类库,可以在运行期扩展 Java 类与实现 Java 接口,可以为没有实现接口的类创建代理对象。CGLIB 代理会在运行时生成一个目标对象的子类,并覆盖其中的方法,以实现AOP的功能。下面是 CGLIB 代理的实现代码:

public class CglibAopProxy implements AopProxy {private final AdvisedSupport advised;public CglibAopProxy(AdvisedSupport advised) {this.advised = advised;}@Overridepublic Object getProxy() {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(advised.getTargetSource().getTargetClass());enhancer.setCallback(new DynamicAdvisedInterceptor(advised));return enhancer.create();}private static class DynamicAdvisedInterceptor implements MethodInterceptor {private final AdvisedSupport advised;public DynamicAdvisedInterceptor(AdvisedSupport advised) {this.advised = advised;}@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {MethodInvocation methodInvocation = new CglibMethodInvocation(advised.getTargetSource().getTarget(),method,args,proxy,advised.getMethodInterceptor(),advised.getTargetSource().getTargetClass());return methodInvocation.proceed();}}
}

3.2.1 cglib代理代码示例

下面是一段使用cglib进行代理的代码

import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;import java.lang.reflect.Method;public class CglibTest {static class Target {public void get() {System.out.println("target get");}}public static void main(String[] args) {Target target = new Target();//拿到代理的类Target proxyTarget = (Target)Enhancer.create(Target.class, new MethodInterceptor() {@Overridepublic Object intercept(Object proxyClass, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {System.out.println("before....");Object result = method.invoke(target, args);System.out.println("after...");return result;}});//执行代理类的方法proxyTarget.get();}}

结果看起来和使用jdk代理类似

与jdk代理不同的是,jdk的代理仅能针对接口代理,而cglib生成的代理对象是一个子类,所以需要注意,使用cglib进行代理的时候,父类不能是final的,并且目标类中的代理方法也不能是final的。

3.2.2 cglib代理源码模拟实现

上面的案例了解了如何使用cglib进行代理以及代码的实现,下面来模拟一下其底层源码的实现

定义一个目标类

public class CglibTarget {public void add(){System.out.println("add()");}public void add(int num){System.out.println("add() :" + num);}public void reduce(int count){System.out.println("reduce() :" + count);}
}

定义代理类

通过上面的介绍了解到,cglib的代理对象是通过生成目标对象的子类实现的,所以代理类需要继承目标类

import org.springframework.cglib.proxy.MethodInterceptor;import java.lang.reflect.Method;public class CglibProxy extends CglibTarget {private MethodInterceptor methodInterceptor;public void setMethodInterceptor(MethodInterceptor methodInterceptor) {this.methodInterceptor = methodInterceptor;}static Method add_1;static Method add_2;static {try {add_1 = CglibTarget.class.getMethod("add");add_2 = CglibTarget.class.getMethod("add",int.class);} catch (NoSuchMethodException e) {e.printStackTrace();}}@Overridepublic void add() {try {methodInterceptor.intercept(this,add_1,new Object[0],null);} catch (Throwable throwable) {throwable.printStackTrace();}}public void add(int num){try {methodInterceptor.intercept(this,add_2,new Object[]{num},null);} catch (Throwable throwable) {throwable.printStackTrace();}}public void reduce(int count){super.reduce(count);}}

测类类

import com.congge.aop.cglib.CglibProxy;
import com.congge.aop.cglib.CglibTarget;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;import java.lang.reflect.Method;public class CglibTest {public static void main(String[] args) {CglibProxy proxy = new CglibProxy();CglibTarget target = new CglibTarget();proxy.setMethodInterceptor(new MethodInterceptor() {@Overridepublic Object intercept(Object targetObj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {System.out.println("before handle");Object result = method.invoke(target, args);System.out.println("after handle");return result;}});proxy.add(10);}
}

运行代码,得到如下效果

3.2.3 cglib代理补充说明

在使用cglib代码编码实现中,注意到在intercept方法参数中有一个Method的参数,这个参数是做什么用的呢?

不妨将代码修改成下面这样

public static void main(String[] args) {Target target = new Target();//拿到代理的类Target proxyTarget = (Target)Enhancer.create(Target.class, new MethodInterceptor() {@Overridepublic Object intercept(Object proxyClass, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {System.out.println("before....");//Object result = method.invoke(target, args);Object result = methodProxy.invoke(target, args);System.out.println("after...");return result;}});//执行代理类的方法proxyTarget.get();}

再次执行,发现仍然可以得到正确的结果,为什么会这样呢?

cglib代理底层通过这种方式为调用者提供了多一种选择,当选择使用methodProxy的invoke方法时,将不反射,而是退化为直接使用原始目标对象去调用方法,某些情况下,可以获得比反射更高的性能。

四、spring aop源码探究

spring底层在aop的代理上是怎么处理的呢,接下来让我们通过源码一探究竟。

4.1 环境准备

4.1.1 引入aop依赖包

只需要引入aspectjweaver即可

        <dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.8.3</version></dependency>

4.1.2 自定义aop配置类

定义一个切面的配置类,这里扫描的是某个包路径下的所有方法,并且使用了环绕通知,在通知方法里,输出了方法实际执行耗时

@Component
@Aspect
public class AspectConfig {@Pointcut("execution(* com.congge.service.aop.*.*(..)))")private void pointcut(){}@Around("pointcut()")public Object around(ProceedingJoinPoint point) throws Throwable {long start = System.currentTimeMillis();Object proceed = point.proceed();long end = System.currentTimeMillis();System.out.println("消耗时间:" + (end-start));return proceed;}}

4.1.3 测试方法

编写测试方法,验证上述aop切面通知是否生效

@ComponentScan("com.congge.service.aop")
@EnableAspectJAutoProxy
public class SpringApp {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringApp.class);UserService userService = context.getBean(UserService.class);userService.getUser();}}

运行之后效果如下,说明aop的环绕通知配置生效了

4.2 spring aop切点表达式

在实际开发中,aop编程的一般步骤是,定义切面类,然后定义具体的切点表达式,然后再在具体的通知方法上面引入切点表达式即可。下面列举几种常用的几种自定义切点表达式的写法。

4.2.1 语法结构

例1:对扫描包中的某个类的方法增强

execution(* com.xxx.xxxClass.方法名(..))

例2:对扫描包中的某个类的所有方法增强

 execution(* com.xxx.xxxClass.* (..))

例3:对扫描包中所有类,类中所有方法增强

 execution(* com.xxx.service.*.* (..))

例4:所有带有某个注解的方法或类

@annotation(com.xxx.annotation.xxxAnnotation)

上面举的例子都是去切入具体的某个方法、类,也可以切到某个包下所有的方法,也可以去切某包下带有某注解的方法等等。  

4.2.2 常见的切点表达式示例

下面列举常用的几种切点表达式示例,便于后续的参考

 所有方法执行

execution(public * *(..))

名以"XXX"开头的所有方法执行

execution(* XXX*(..))

XXX接口中所有方法执行

 execution(* com.xxx.XXX.*(..))

service包下所有方法执行

 execution(* com.xxx.service..*.*(..))

service包下的所有连接点

 (仅在Spring AOP中执行方法)

within(com.xxx.service..*)

代理实现XXX接口的任何连接点

 (仅在Spring AOP中执行方法)

this(com.xxx.service.XXXService)

所有带有@XXX注解的方法或类

@annotation(com.xxx.annotation.XXX)

4.3 spring aop源码分析过程

通过断点,在真正执行userService.getUser()之前,我们可以看到,userService已经是一个代理对象,而且不难看出,这是一个jdk动态代理产生的对象(userService接口存在实现类)

于是可以断定,在真正执行目标方法的时候,是代理对象在帮我们执行了,那么代理对象在哪里产生的呢?代理对象是什么时候产生的呢?这就是接下来需要搞清的重点所在。

从getBean入手,一路往下走,通过getBean方法,最终获取到了代理对象

public <T> T getBean(Class<T> requiredType) throws BeansException {this.assertBeanFactoryActive();return this.getBeanFactory().getBean(requiredType);}

通过getBean方法走到下面这个doGetBean方法里,在这个方法中,注意有一个关键的位置:getSingleton,很多同学在看spring源码的时候发现调用栈非常深,经常陷入一种不知道从何看起的状态,这里说过小技巧,你只需要关注你期望得到的目标即可。比如在doGetBean这里,为了快速获取到userService,可以在此处设置条件断点,如下:

以上述getSingleton为例来说,通过这个方法,可能会得到代理对象,需不需要进去看呢,可以先走一步,如果得到的是代理对象,那么说明getSingleton这个方法中是产生代理对象的地方,按照这个思路,我们走一步看看,发现此刻得到的对象就是一个代理对象,说明确实是在getSingleton这个方法中产生了代理对象;

以getSingleton继续深入,进入该方法之后发现,从singletonObjects中拿到的就是代理对象了,这里不禁冒出一个问题来了,singletonObjects中是什么时候将代理对象放进去的呢?

这里不得不说另一个源码的调试和阅读技巧,就是直接看debug中的调用栈,从下面的调用栈可以发现,上一步singletonObjects中存储的这个代理对象就是在其中的某一步产生并放到容器中的;

 这样一来,至少可以说明,产生代理对象的时机在当前这个getSingleton之前,从spring的bean的生命周期来看,要经历解析,创建,初始化,实例化等一些步骤,所以可以将目光定位到创建的过程,即createBean阶段,于是从getBean方法入手作为突破点即可,再次断点进入,来到getBean方法中

通过getBean一路来到getSingleton方法中,该方法即创建初始的userService的核心代码,而且第一次进入的时候发现userService的bean对象还未创建出来,一直来到该方法的如下位置,利用上面的调试技巧发现就是在这个singletonFactory.getObject()方法得到了代理对象

继续进入这个singletonFactory.getObject()方法,就来到创建bean的方法中,即createBean

沿着该方法继续往下走,当走到doCreateBean这里时,发现这个方法执行结束后就成了代理对象

继续进入doCreateBean方法中,当走完initializeBean方法之后,发现就产生了代理对象

那么initializeBean里面发生了什么呢?可以断定这个方法的某个地方最终产生了代理对象,于是来到下面这个方法

进入到applyBeanPostProcessorsAfterInitialization方法中

来到postProcessAfterInitialization里面之后,沿着wrapIfNecessary继续进入

 

历经千辛万苦,最终来到这里,这个方法就是最终产生代理对象的地方,核心创建代理对象的代码就是下面这段

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {return bean;} else if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {return bean;} else if (!this.isInfrastructureClass(bean.getClass()) && !this.shouldSkip(bean.getClass(), beanName)) {Object[] specificInterceptors = this.getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, (TargetSource)null);if (specificInterceptors != DO_NOT_PROXY) {this.advisedBeans.put(cacheKey, Boolean.TRUE);Object proxy = this.createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));this.proxyTypes.put(cacheKey, proxy.getClass());return proxy;} else {this.advisedBeans.put(cacheKey, Boolean.FALSE);return bean;}} else {this.advisedBeans.put(cacheKey, Boolean.FALSE);return bean;}}

 我们重点关注方法中createProxy这个方法即可

跳过中间的步骤,最终来到createAopProxy方法中,在这个方法中,展示了如何创建代理对象,以及采用哪种方式创建代理对象,即使用jdk动态代理呢?还是cglib方式呢?

从这段代码不难看出,这里做了一个判断,如果目标对象是接口,将采用jdk动态代理,如果目标对象是类,则使用cglib的代理,由于在前面的代码中目标对象是普通的类,所以将会产生一个cglib的代理对象

补充说明:  

在上面判断使用哪种代理方式时,有一个很重要的参数,即判断下面的这个参数的布尔值,通过源码点击进去发现,默认情况下该参数初始值为false

config.isProxyTargetClass()

该参数的意义在于,开发者可以通过强制指定这个参数的值,从而改变代理的方式强制使用cglib,如何修改呢,只需要将下面的注解中改为true即可

五、写在文末

本文通过较大的篇幅全面而深度的总结了aop代理相关的技术点,并且深入到源码层面解读了spring aop的完整过程,aop不仅是spring编程中的重要内容,也是日常开发中运用比较广泛的技术点,有必要对其做深入的理解和掌握。本篇到此结束,感谢观看。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/282968.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

设计模式——策略模式

引言 策略模式是一种行为设计模式&#xff0c; 它能让你定义一系列算法&#xff0c; 并将每种算法分别放入独立的类中&#xff0c; 以使算法的对象能够相互替换。 问题 一天&#xff0c; 你打算为游客们创建一款导游程序。 该程序的核心功能是提供美观的地图&#xff0c; 以…

[python]用python获取EXCEL文件内容并保存到DBC

目录 关键词平台说明背景所需库实现过程方法1.1.安装相关库2.代码实现 关键词 python、excel、DBC、openpyxl 平台说明 项目Valuepython版本3.6 背景 在搭建自动化测试平台的时候经常会提取DBC文件中的信息并保存为excel或者其他文件格式&#xff0c;用于自动化测试。本文…

用GitBook制作自己的网页版电子书

用GitBook制作自己的网页版电子书 前言 几年前阅读过其他人用GitBook创建的文档&#xff0c;可以直接在浏览器中打开&#xff0c;页面干净整洁&#xff0c;非常清爽&#xff0c;至今印象深刻。 GitBook非常适合用来为个人或团队制作文档&#xff0c;对于我这种偶尔写博客的人…

【Hadoop面试】HDFS读写流程

HDFS&#xff08;Hadoop Distributed File System&#xff09;是GFS的开源实现。 HDFS架构 HDFS是一个典型的主/备&#xff08;Master/Slave&#xff09;架构的分布式系统&#xff0c;由一个名字节点Namenode(Master) 多个数据节点Datanode(Slave)组成。其中Namenode提供元数…

Java架构师系统架构内部维度分析

目录 1 导语2.1 安全性维度概述2.2 流程安全性2.3 架构安全性2.4 安全维度总结3 伸缩性维度概述和场景思路3.1 无状态应用弹性伸缩3.2 阿里云Knative弹性伸缩3.3 有状态应用弹性伸缩3.4 伸缩性维度总结想学习架构师构建流程请跳转:Java架构师系统架构设计 1 导语

DSP捕获输入简单笔记

之前使用stm32的大概原理是&#xff1a; 输入引脚输入一个脉冲&#xff0c;捕获1开始极性捕获&#xff0c;捕获的是从启动捕获功能开始计数&#xff0c;捕获的是当前的计数值&#xff1b; 例如一个脉冲&#xff0c;捕获1捕获上升沿&#xff0c;捕获2捕获下降沿&#xff1b;而两…

vue2入门

vue2官方文档&#xff1a;安装 — Vue.js 1、安装 新建"vue"文件夹——>新建vue1.html 直接用<script>标签引入vue&#xff1a; <script src"https://cdn.jsdelivr.net/npm/vue2.7.14/dist/vue.js"></script> tips: CDN:一个网络…

机器学习项目精选 第一期:超完整数据科学资料合集

大噶吼&#xff0c;不说废话&#xff0c;分享一波我最近看过并觉得非常硬核的资源&#xff0c;包括Python、机器学习、深度学习、大模型等等。 1、超完整数据科学资料合集 地址&#xff1a;https://github.com/krishnaik06/The-Grand-Complete-Data-Science-Materials Pytho…

女生想通过培训转行软件测试类可行吗?

首先&#xff0c;女生转行IT行业做软件测试是可以的&#xff0c;因为软件测试岗&#xff0c;尤其是其中的功能性测试岗&#xff0c;入行门槛并不高&#xff0c;有很多女生在做&#xff0c;且我个人认为还蛮适合女生的&#xff0c;因为女生相对来说更细心&#xff0c;文档能力也…

Linux:时间显示(函数介绍)

文章目录 1、sleep&#xff1a;延迟函数2、time/localtime3、示例&#xff1a;sleep time localtime4、Linux时间调整 1、sleep&#xff1a;延迟函数 函数原型&#xff1a;unsigned int sleep(unsigned int seconds); 功 能&#xff1a;延时 参 数&#xff1a;seconds:秒&am…

高频面试:Spring 如何解决循环依赖?(荣耀典藏版)

目录 一、 基础知识 1.1 什么是循环依赖 &#xff1f; 1.2 三级缓存 1.3 原理执行流程 二、 源码解读 2.1 代码入口 2.2 第一层 2.3 第二层 2.4 第三层 2.5 返回第二层 2.6 返回第一层 三、 原理深度解读 3.1 什么要有 3 级缓存 &#xff1f; 3.2 能干掉第 2 级缓…

C#结合JavaScript实现多文件上传

目录 需求 引入 关键代码 操作界面 ​JavaScript包程序 服务端 ashx 程序 服务端上传后处理程序 小结 需求 在许多应用场景里&#xff0c;多文件上传是一项比较实用的功能。实际应用中&#xff0c;多文件上传可以考虑如下需求&#xff1a; 1、对上传文件的类型、大小…