zoukankan      html  css  js  c++  java
  • 《spring实战》学习笔记-第四章:面向切面的编程

    本章内容:面向切面编程的基本原理、通过POJO创建切面、使用@AspectJ注解、为AspectJ切面注入依赖。

    4.1 什么是面向切面编程

      下图直观呈现了横切关注点的概念。

      上图展现了一个被划分为模块的典型应用。每个模块的核心功能都是为特定业务领域提供服务,但是这些模块都需要类似的辅助功能,例如安全和事务管理。
      如果要重用通用功能的话,最常见的面向对象技术是继承(inheritance)或委托(delegation)。但是,如果在整个应用中都使用相同的基类,继承往往会导致一个脆弱的对象体系;而使用委托可能需要对委托对象进行复杂的调用。
      切面提供了取代继承和委托的另一种可选方案,而且在很多场景下更清晰简洁。在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。
    4.1.1 定义AOP术语
      描述切面的常用术语有通知(advice)、切点(pointcut)和连接点(join point)。下图展示了这些概念是如何关联在一起的。

    通知(Advice)
      通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之前?之后?之前和之后都调用?还是只在方法抛出异常时调用?
    Spring切面可以应用5种类型的通知:
      前置通知(Before):在目标方法被调用之前调用通知功能;
      后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
      返回通知(After-returning):在目标方法成功执行之后调用通知;
      异常通知(After-throwing):在目标方法抛出异常后调用通知;
      环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
    连接点(Join point)
      连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
    切点(Poincut)
      如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
    切面(Aspect)
      切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。
    引入(Introduction)
      引入允许我们向现有的类添加新方法或属性。
    织入(Weaving)
      织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
      编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
      类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。
      运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。

    4.1.2 Spring对AOP的支持
    Spring提供了4种类型的AOP支持:
    (1)基于代理的经典Spring AOP;
    (2)纯POJO切面;
    (3)@AspectJ注解驱动的切面;
    (4)注入式AspectJ切面(适用于Spring各版本)
      前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。
      但是现在Spring提供了更简洁和干净的面向切面编程方式。引入了简单的声明式AOP和基于注解的AOP之后,Spring经典的AOP看起来就显得非常笨重和过于复杂,直接使用 ProxyFactory Bean会让人感觉厌烦。
      借助Spring的aop命名空间,我们可以将纯POJO转换为切面。实际上,这些POJO只是提供了满足切点条件时所要调用的方法。遗憾的是,这种技术需要XML配置,但这的确是声明式地将对象转换为切面的简便方式。
      Spring借鉴了AspectJ的切面,以提供注解驱动的AOP。本质上,它依然是Spring基于代理的AOP,但是编程模型几乎与编写成熟的AspectJ注解切面完全一致。这种AOP风格的好处在于能够不使用XML来完成功能。
      如果你的AOP需求超过了简单的方法调用(如构造器或属性拦截),那么你需要考虑使用AspectJ来实现切面。在这种情况下,上文所示的第四种类型能够帮助你将值注入到AspectJ驱动的切面中。

    Spring通知是Java编写的

      Spring所创建的通知都是用标准的Java类编写的。这样的话,我们就可以使用与普通Java开发一样的集成开发环境(IDE)来开发切面。而且,定义通知所应用的切点通常会使用注解或在Spring配置文件里采用XML来编写,这两种语法对于Java开发者来说都是相当熟悉的。AspectJ与之相反。虽然AspectJ现在支持基于注解的切面,但AspectJ最初是以Java语言扩展的方式实现的。这种方式有优点也有缺点。通过特有的AOP语言,我们可以获得更强大和细粒度的控制,以及更丰富的AOP工具集,但是我们需要额外学习新的工具和语法。

    Spring在运行时通知对象
      通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。如下图所示,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。

      直到应用需要被代理的bean时,Spring才创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。

    Spring只支持方法级别的连接点
      正如前面所探讨过的,通过使用各种AOP方案可以支持多种连接点模型。因为Spring基于动态代理,所以Spring只支持方法连接点。这与一些其他的AOP框架是不同的,例如AspectJ和JBoss,除了方法切点,它们还提供了字段和构造器接入点。Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而且它不支持构造器连接点,我们就无法在bean创建时应用通知。
      但是方法拦截可以满足绝大部分的需求。如果需要方法拦截之外的连接点拦截功能,那么我们可以利用Aspect来补充Spring AOP的功能。
      对于什么是AOP以及Spring如何支持AOP的,我们现在已经有了一个大致的了解。现在是时候学习如何在Spring中创建切面了,让我们先从Spring的声明式AOP模型开始。

    4.2 通过切点来选择连接点

    4.2.1 编写切点
      为了阐述Spring中的切面,我们需要有个主题来定义切面的切点。为此,我们定义一个Performance接口:

      Performance可以代表任何类型的现场表演,如舞台剧、电影或音乐会。假设我们想编写Performance的perform()方法触发的通知。下图展现了一个切点表达式,这个表达式能够设置当perform()方法执行时触发通知的调用。

      现在假设我们需要配置的切点仅匹配concert包。在此场景下,可以使用within()指示器来限制匹配,如下图所示:

      请注意我们使用了“&&”操作符把execution()和within()指示器连接在一起形成与(and)关系(切点必须匹配所有的指示器)。类似地,我们可以使用“||”操作符来标识或(or)关系,而使用“!”操作符来标识非(not)操作。因为“&”在XML中有特殊含义,所以在Spring的XML配置里面描述切点时,我们可以使用and来代替“&&”。同样,or和not可以分别用来代替“||”和“!”。
    4.2.2 在切点中选择bean
      Spring还引入了一个新的bean()指示器,它允许我们在切点表达式中使用bean的ID来标识bean。bean()使用bean ID或bean名称作为参数来限制切点只匹配特定的bean。

      在某些场景下,限定切点为指定的bean或许很有意义,但我们还可以使用非操作为除了特定ID以外的其他bean应用通知:

      在此场景下,切面的通知会被编织到所有ID不为woodstock的bean中。

    4.3 使用注解创建切面

      我们已经定义了Performance接口,它是切面中切点的目标对象。现在,让我们使用AspecJ注解来定义切面。
    4.3.1 定义切面
      如果一场演出没有观众的话,那不能称之为演出。对不对?从演出的角度来看,观众是非常重要的,但是对演出本身的功能来讲,它并不是核心,这是一个单独的关注点。因此,将观众定义为一个切面,并将其应用到演出上就是较为明智的做法。
    程序清单4.1 Audience类:观看演出的切面

      Audience类使用@AspectJ注解进行了标注。该注解表明Audience不仅仅是一个POJO,还是一个切面。Audience类中的方法都使用注解来定义切面的具体行为。Audience有四个方法,定义了一个观众在观看演出时可能会做的事情。在演出之前,观众要就坐(takeSeats())并将手机调至静音状态(silenceCellPhones())。如果演出很精彩的话,观众应该会鼓掌喝彩(applause())。不过,如果演出没有达到观众预期的话,观众会要求退款(demandRefund())。可以看到,这些方法都使用了通知注解来表明它们应该在什么时候调用。AspectJ提供了五个注解来定义通知,如表4.2所示。

      让我们近距离看一下这个设置给通知注解的切点表达式,我们发现它会在Performance的perform()方法执行时触发。
      相同的切点表达式我们重复了四遍,这可真不是什么光彩的事情。这样的重复让人感觉有些不对劲。如果我们只定义这个切点一次,然后每次需要的时候引用它,那么这会是一个很好的方案。
      幸好,我们完全可以这样做:@Pointcut注解能够在一个@AspectJ切面内定义可重用的切点。接下来的程序清单4.2展现了新的Audience,现在它使用了@Pointcut。
    程序清单4.2 通过@Pointcut注解声明频繁使用的切点表达式

      通过在performance()方法上添加@Pointcut注解,我们实际上扩展了切点表达式语言,这样就可以在任何的切点表达式中使用performance()了,如果不这样做的话,你需要在这些地方使用那个更长的切点表达式。我们现在把所有通知注解中的长表达式都替换成了performance()。
      performance()方法的实际内容并不重要,在这里它实际上应该是空的。其实该方法本身只是一个标识,供@Pointcut注解依附。
      需要注意的是,除了注解和没有实际操作的performance()方法,Audience类依然是一个POJO。我们能够像使用其他的Java类那样调用它的方法,它的方法也能够独立地进行单元测试,这与其他的Java类并没有什么区别。Audience只是一个Java类,只不过它通过注解表明会作为切面使用而已。
    像其他的Java类一样,它可以装配为Spring中的bean:

      如果你就此止步的话,Audience只会是Spring容器中的一个bean。即便使用了AspectJ注解,但它并不会被视为切面,这些注解不会解析,也不会创建将其转换为切面的代理。
      如果你使用JavaConfig的话,可以在配置类的类级别上通过使用EnableAspectJAutoProxy注解启用自动代理功能。程序清单4.3展现了如何在JavaConfig中启用自动代理。
    程序清单4.3 在JavaConfig中启用AspectJ注解的自动代理

      假如你在Spring中要使用XML来装配bean的话,那么需要使用Spring aop命名空间中的<aop:aspectj-autoproxy>元素。下面的XML配置展现了如何完成该功能。

      不管你是使用JavaConfig还是XML,AspectJ自动代理都会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所匹配的bean。在这种情况下,将会为Concertbean创建一个代理,Audience类中的通知方法将会在perform()调用前后执行。
      我们需要记住的是,Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。在本质上,它依然是Spring基于代理的切面。这一点非常重要,因为这意味着尽管使用的是@AspectJ注解,但我们仍然限于代理方法的调用。如果想利AspectJ的所有能力,我们必须在运行时使用AspectJ并且不依赖Spring来创建基于代理的切面。
      到现在为止,我们的切面在定义时,使用了不同的通知方法来实现前置通知和后置通知。但是表4.2还提到了另外的一种通知:环绕通知(around advice)。环绕通知与其他类型的通知有所不同,因此值得花点时间来介绍如何进行编写。
    4.3.2 创建环绕通知
      环绕通知是最为强大的通知类型。它能够让你所编写的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。
      为了阐述环绕通知,我们重写Audience切面。这次,我们使用一个环绕通知来代替之前多个不同的前置通知和后置通知。
    程序清单4.5 使用环绕通知重新实现Audience切面

      在这里,@Around注解表明watchPerformance()方法会作为performance()切点的环绕通知。在这个通知中,观众在演出之前会将手机调至静音并就坐,演出结束后会鼓掌喝彩。像前面一样,如果演出失败的话,观众会要求退款。可以看到,这个通知所达到的效果与之前的前置通知和后置通知是一样的。但是,现在它们位于同一个方法中,不像之前那样分散在四个不同的通知方法里面。
      关于这个新的通知方法,你首先注意到的可能是它接受ProceedingJoinPoint作为参数。这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用ProceedingJoinPoint的proceed()方法。
      需要注意的是,别忘记调用proceed()方法。如果不调这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。有可能这就是你想要的效果,但更多的情况是你希望在某个点上执行被通知的方法。
      有意思的是,你可以不调用proceed()方法,从而阻塞对被通知方法的访问,与之类似,你也可以在通知中对它进行多次调用。要这样做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。
    4.3.3 处理通知中的参数
      到目前为止,我们的切面都很简单,没有任何参数。唯一的例外是我们为环绕通知所编写的watchPerformance()示例方法中使用了ProceedingJoinPoint作为参数。除了环绕通知,我们编写的其他通知不需要关注传递给被通知方法的任意参数。这很正常,因为我们所通知的perform()方法本身没有任何参数。但是,如果切面所通知的方法确实有参数该怎么办呢?切面能访问和使用传递给被通知方法的参数吗?
      为了阐述这个问题,让我们重新看一下2.4.4小节中的BlankDisc样例。play()方法会循环所有的磁道并调用playTrack()方法。但是,我们也可以通过playTrack()方法直接播放某一个磁道中的歌曲。
      假设你想记录每个磁道被播放的次数。一种方法就是修改playTrack()方法,直接在每次调用的时候记录这个数量。但是,记录磁道的播放次数与播放本身是不同的关注点,因此不应该属于playTrack()方法。看起来,这应该是切面要完成的任务。为了记录每个磁道所播放的次数,我们创建了TrackCounter类,它是通知playTrack()方法的一个切面。下面的程序清单展示了这个切面。
    程序清单4.6 使用参数化的通知来记录磁道播放的次数

       像之前所创建的切面一样,这个切面使用@Pointcut注解定义命名的切点,并使用@Before将一个方法声明为前置通知。但是,这里的不同点在于切点还声明了要提供给通知方法的参数。下图将切点表达式进行了分解,以展现参数是在什么地方指定的。

      上图中需要关注的是切点表达式中的args(trackNumber)限定符。它表明传递给playTrack()方法的int类型参数也会传递到通知中去。参数的名称trackNumber也与切点方法签名中的参数相匹配。
      这个参数会传递到通知方法中,这个通知方法是通过@Before注解和命名切点trackPlayed(trackNumber)定义的。切点定义中的参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移。
      现在,我们可以在Spring配置中将BlankDisc和TrackCounter定义为bean,并启用AspectJ自动代理,如程序清单4.7所示。
    程序清单4.7 配置TrackCount记录每个磁道播放的次数

      最后,为了证明它能正常工作,你可以编写如下的简单测试。它会播放几个磁道并通过TrackCounter断言播放的数量。
    程序清单4.8 测试TrackCounter切面

      到目前为止,在我们所使用的切面中,所包装的都是被通知对象的已有方法。但是,方法包装仅仅是切面所能实现的功能之一。让我们看一下如何通过编写切面,为被通知的对象引入全新的功能。
    4.3.4 通过注解引入新功能

    4.4 在XML中声明切面

      在本书前面的内容中,我曾经建立过这样一种原则,那就是基于注解的配置要优于基于Java的配置,基于Java的配置要优于基于XML的配置。但是,如果你需要声明切面,但是又不能为通知类添加注解的时候,那么就必须转向XML配置了。
      在Spring的aop命名空间中,提供了多个元素用来在XML中声明切面,如表4.3所示。

    表4.3 Spring的AOP配置元素能够以非侵入性的方式声明切面

       我们已经看过了<aop:aspectj-autoproxy>元素,它能够自动代理AspectJ注解的通知类。aop命名空间的其他元素能够让我们直接在Spring配置中声明切面,而不需要使用注解

      例如,我们重新看一下Audience类,这一次我们将它所有的AspectJ注解全部移除掉:

      正如你所看到的,Audience类并没有任何特别之处,它就是有几个方法的简单Java类。我们可以像其他类一样把它注册为Spring应用上下文中的bean。
      尽管看起来并没有什么差别,但Audience已经具备了成为AOP通知的所有条件。我们再稍微帮助它一把,它就能够成为预期的通知了。
    4.4.1 声明前置和后置通知
      我们会使用Spring aop命名空间中的一些元素,将没有注解的Audience类转换为切面。下面的程序清单4.9展示了所需要的XML。
    程序清单4.9 通过XML将无注解的Audience声明为切面

      关于Spring AOP配置元素,第一个需要注意的事项是大多数的AOP配置元素必须在<aop:config>元素的上下文内使用。这条规则有几种例外场景,但是把bean声明为一个切面时,我们总是从<aop:config>元素开始配置的。
      在<aop:config>元素内,我们可以声明一个或多个通知器、切面或者切点。在程序清单4.9中,我们使用<aop:aspect>元素声明了一个简单的切面。ref元素引用了一个POJObean,该bean实现了切面的功能——在这里就是audience。ref元素所引用的bean提供了在切面中通知所调用的方法。
      该切面应用了四个不同的通知。两个<aop:before>元素定义了匹配切点的方法执行之前调用前置通知方法—也就是Audience bean的takeSeats()和turnOffCellPhones()方法(由method属性所声明)。<aop:after-returning>元素定义了一个返回(after-returning)通知,在切点所匹配的方法调用之后再调用applaud()方法。同样,<aop:after-throwing>元素定义了异常(after-throwing)通知,如果所匹配的方法执行时抛出任何的异常,都将会调用demandRefund()方法。下图展示了通知逻辑如何织入到业务逻辑中。


      在基于AspectJ注解的通知中,当发现这种类型的重复时,我们使用@Pointcut注解消除了这些重复的内容。而在基于XML的切面声明中,我们需要使用<aop:pointcut>元素。如下的XML展现了如何将通用的切点表达式抽取到一个切点声明中,这样这个声明就能在所有的通知元素中使用了。
    程序清单4.10 使用<aop:pointcut>定义命名切点

    4.4.2 声明环绕通知
      目前Audience的实现工作得非常棒,但是前置通知和后置通知有一些限制。具体来说,如果不使用成员变量存储信息的话,在前置通知和后置通知之间共享信息非常麻烦。
      例如,假设除了进场关闭手机和表演结束后鼓掌,我们还希望观众确保一直关注演出,并报告每个参赛者表演了多长时间。使用前置通知和后置通知实现该功能的唯一方式是在前置通知中记录开始时间并在某个后置通知中报告表演耗费的时间。但这样的话我们必须在一个成员变量中保存开始时间。因为Audience是单例的,如果像这样保存状态的话,将会存在线程安全问题。
      相对于前置通知和后置通知,环绕通知在这点上有明显的优势。使用环绕通知,我们可以完成前置通知和后置通知所实现的相同功能,而且只需要在一个方法中 实现。因为整个通知逻辑是在一个方法内实现的,所以不需要使用成员变量保存状态。
      例如,考虑程序清单4.11中新Audience类的watchPerformance()方法,它没有使用任何的注解。
    程序清单4.11 watchPerformance()方法提供了AOP环绕通知

      在观众切面中,watchPerformance()方法包含了之前四个通知方法的所有功能。不过,所有的功能都放在了这一个方法中,因此这个方法还要负责自身的异常处理。声明环绕通知与声明其他类型的通知并没有太大区别。我们所需要做的仅仅是使用<aop:around>元素。
    程序清单4.12 在XML中使用<aop:around>元素声明环绕通知

      像其他通知的XML元素一样,<aop:around>指定了一个切点和一个通知方法的名字。在这里,我们使用跟之前一样的切点,但是为该切点所设置的method属性值为watchPerformance()方法。
    4.4.3 为通知传递参数
      在4.3.3小节中,我们使用@AspectJ注解创建了一个切面,这个切面能够记录CompactDisc上每个磁道播放的次数。现在,我们使用XML来配置切面,那就看一下如何完成这一相同的任务。
      首先,我们要移除掉TrackCounter上所有的@AspectJ注解。
    程序清单4.13 无注解的TrackCounter1

      去掉@AspectJ注解后,TrackCounter显得有些单薄了。现在,除非显式调用countTrack()方法,否则TrackCounter不会记录磁道播放的数量。但是,借助一点Spring XML配置,我们能够让TrackCounter重新变为切面。
      如下的程序清单展现了完整的Spring配置,在这个配置中声明了TrackCounter bean和BlankDisc bean,并将TrackCounter转化为切面。

      可以看到,我们使用了和前面相同的aop命名空间XML元素,它们会将POJO声明为切面。唯一明显的差别在于切点表达式中包含了一个参数,这个参数会传递到通知方法中。如果你将这个表达式与程序清单4.6中的表达式进行对比会发现它们几乎是相同的。唯一的差别在于这里使用and关键字而不是“&&”(因为在XML中,“&”符号会被解析为实体的开始)。

      我们通过练习已经使用Spring的aop命名空间声明了几个基本的切面,那么现在让我们看一下如何使用aop命名空间声明引入切面。
    4.4.4 通过切面引入新的功能

     4.5 注入AspectJ切面

      虽然Spring AOP能够满足许多应用的切面需求,但是与AspectJ相比,Spring AOP 是一个功能比较弱的AOP解决方案。AspectJ提供了Spring AOP所不能支持的许多类型的切点。

      例如,当我们需要在创建对象时应用通知,构造器切点就非常方便。不像某些其他面向对象语言中的构造器,Java构造器不同于其他的正常方法。这使得Spring基于代理的AOP无法把通知应用于对象的创建过程。
      对于大部分功能来讲,AspectJ切面与Spring是相互独立的。虽然它们可以织入到任意的Java应用中,这也包括了Spring应用,但是在应用AspectJ切面时几乎不会涉及到Spring。但是精心设计且有意义的切面很可能依赖其他类来完成它们的工作。如果在执行通知时,切面依赖于一个或多个类,我们可以在切面内部实例化这些协作的对象。但更好的方式是,我们可以借助Spring的依赖注入把bean装配进AspectJ切面中。

      小结:

      AOP是面向对象编程的一个强大补充。通过AspectJ,我们现在可以把之前分散在应用各处的行为放入可重用的模块中。我们显示地声明在何处如何应用该行为。这有效减少了代码冗余,并让我们的类关注自身的主要功能。

      Spring提供了一个AOP框架,让我们把切面插入到方法执行的周围。现在我们已经学会如何把通知织入前置、后置和环绕方法的调用中,以及为处理异常增加自定义的行为。

    关于在Spring应用中如何使用切面,我们可以有多种选择。通过使用@AspectJ注解和简化的配置命名空间,在Spring中装配通知和切点变得非常简单。

      最后,当Spring AOP不能满足需求时,我们必须转向更为强大的AspectJ。对于这些场景,我们了解了如何使用Spring为AspectJ切面注入依赖。

      此时此刻,我们已经覆盖了Spring框架的基础知识,了解到如何配置Spring容器以及如何为Spring管理的对象应用切面。正如我们所看到的,这些核心技术为创建松散耦合的应用奠定了坚实的基础。

      现在,我们越过这些基础的内容,看一下如何使用Spring构建真实的应用。从下一章开始,首先看到的是如何使用Spring构建Web应用。

      本文代码链接:https://github.com/Gugibv/spring

    -
  • 相关阅读:
    c++Primer再学习(1)
    c++Primer再学习练习Todo
    感悟(一)
    新目标《C++程序设计原理与实践》
    C++Primer再学习(4)
    开篇
    C++Primer再学习(3)
    C++实现的单例模式的解惑
    使用springboot缓存图片
    springboot h2 database
  • 原文地址:https://www.cnblogs.com/gugibv/p/7844297.html
Copyright © 2011-2022 走看看