Spring AOP从入门到精通

目录

1. AOP的演化过程

1. 代理模式

2. 动态代理

2.1 JDK动态代理

2.2 Cglib动态代理

3. Spring模式

3.1 ProxyFactory

3.2 ProxyFactoryBean

3.3 AbstractAutoProxyCreator

2. Spring AOP抽象

1. 核心术语

1.1 连接点(JoinPoint)

1.2 切点(Pointcut)

1.3 增强(Advice)

1.4 切面(Aspect)

1.5 目标对象(Target)

1.6 代理对象(Proxy)

2. 核心组件

2.1 AutoProxyCreator工作原理

2.2 @EnableAspectJAutoProxy工作原理

3. Spring AOP案例


1. AOP的演化过程

编程中我们经常会遇到这样的场景,有一段通用逻辑横跨多个业务,不能用继承来解决,比如方法耗时、数据库事务、通用日志等等。一个典型的DAO方法调用分为下面3个步骤:

每一次都手工的开启和提交事务,即显得啰嗦,又影响业务的可读性。AOP就十分擅长解决这类问题,它把开启和提交事务的逻辑抽取到Advice当中,在任务DAO上复用。那么AOP到底是怎么实现的呢?

1. 代理模式

熟悉设计模式的人这时候会想到代理模式,通过新增一个代理类实现,负责处理事务的开启和提交。假设我们要处理的是下面的类图,Staff是我们要处理的实体类,StaffDao是接口,定义两个方法employ、paySalary,StaffDaoImpl是实际的DAO实现,StaffDaoProxy是代理类,负责事务开启和提交。

Dao的操作用伪代码实现,StaffDaoImpl和代理类,核心代码如下

public class StaffDaoImpl implements StaffDao {public void employ(Staff p) {System.out.println("employ staff: " + p);}public void paySalary(Staff p) {System.out.println("pay salary :" + p);}
}
public class StaffDaoProxy implements StaffDao {private StaffDao staffDao;public StaffDaoProxy(StaffDao staffDao) {this.staffDao = staffDao;}public void employ(Staff p) {System.out.println("start transaction...");staffDao.employ(p);System.out.println("commit transaction...");}public void paySalary(Staff p) {System.out.println("start transaction...");staffDao.paySalary(p);System.out.println("commit transaction...");}
}

通过如下代码进行测试,StaffDaoImpl只是完成了数据操作,StaffDaoProxy会额外事务开启和提交操作

Staff staff = new Staff("zhangsan",18); StaffDao staffDao = new StaffDaoImpl();  // 直接调用
staffDao.employ(staff);
staffDao.paySalary(staff);StaffDaoProxy proxy = new StaffDaoProxy(staffDao); // 通过代理类使用
proxy.employ(staff);
proxy.paySalary(staff);

2. 动态代理

代理模式确实达成了我们想要的效果,如果每一个需要横切逻辑的类都需要通过代理模式类实现的话,人力成本过高,而且也不便于维护,后续每一次修改都需要改大量的代理类。通过JDK提供的动态代理,或者字节码操作的第三方库能解决这个问题。

2.1 JDK动态代理

我们来看一个通过JDK的Proxy类实现动态代理的示例,首先要定义一个InvocationHandler的实现

public class TransactionInvocationHandler implements InvocationHandler {private StaffDao staffDao;public TransactionInvocationHandler(StaffDao staffDao) {this.staffDao = staffDao;}public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("start transaction...");Object result = method.invoke(staffDao,args);System.out.println("commit transaction...");return result;}
}

接着就可以通过Proxy.newInstance来创建代理类实例,并测试,其中staff和staffDao的实例和代理模式里的创建方式一样,通过proxy.employ和proxy.paySalary调用输出也同之前的案例。

InvocationHandler invocationHandler = new TransactionInvocationHandler(staffDao);
StaffDao proxy = (StaffDao) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{StaffDao.class},invocationHandler);
proxy.employ(staff);
proxy.paySalary(staff);
2.2 Cglib动态代理

JDK动态代理存在的一个问题是,生成代理对象必须基于接口,如果一个类没有实现接口就无法创建代理。好在还有大量的第三方库支持无接口的类生成动态代理。我们来看一个cglib的示例。和JDK代理类似,首先我们要实现一个回调类,不过cglib里回调类接口是MethodInterceptor。

import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;import java.lang.reflect.Method;public class TransactionMethodInterceptor implements MethodInterceptor {private StaffDao staffDao;public TransactionMethodInterceptor(StaffDao staffDao) {this.staffDao = staffDao;}public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {System.out.println("start transaction...");Object result = method.invoke(staffDao, args);System.out.println("commit transaction...");return result;}
}

紧接着使用Enhancer类创建代理类对象,并使用测试,也能得到同样的测试结果。

TransactionMethodInterceptor interceptor = new TransactionMethodInterceptor(staffDao);
Enhancer enhancer = new Enhancer();
enhancer.setCallback(interceptor);
enhancer.setSuperclass(StaffDao.class);
StaffDao proxy = (StaffDao) enhancer.create();

3. Spring模式

动态代理解决代理模式带来的部分问题,我不需要再为每个类手动创建子类,所有横切逻辑都集中到InvocationHandler或MethodInterceptor中了。但是每次使用的时候都需要手工创建代理对象,还是比较麻烦。Spring基于自己的能力对这个问题做了简化

3.1 ProxyFactory

Spring提供了ProxyFactory来简化代理对象的创建,频闭通过JDK或Cglib创建代理对象的差异(涉及类如下图),除此以外并没有其他改善。

同样需要实现一个回调类,实现aopaliance提供的MethodInterceptor接口,具体代码如下

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;public class SpringTransactionMethodInterceptor implements MethodInterceptor {@Overridepublic Object invoke(MethodInvocation invocation) throws Throwable {System.out.println("start transaction...");Object result = invocation.proceed();System.out.println("commit transaction...");return result;}
}

然后使用ProxyFactory来创建代理对象,并进行测试,测试结果同之前的示例

ProxyFactory factory = new ProxyFactory();
factory.setTarget(staffDao);
factory.addAdvice(new SpringTransactionMethodInterceptor());
StaffDao proxy = (StaffDao) factory.getProxy();Staff staff = new Staff("zhangsan", 18);
proxy.employ(staff);
proxy.paySalary(staff);
3.2 ProxyFactoryBean

到目前为止我们依然需要手动创建代理类对象,通过Spring本身的FactoryBean机制,Spring AOP为我们提供了ProxyFactoryBean实现。首先通过xml配置Bean实现

<bean id="staffDao" class="com.lws.designPattern.StaffDaoImpl"/>
<bean id="springTransaction" class="com.lws.spring.SpringTransactionMethodInterceptor"/>
<bean id="staffDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean"><property name="target" ref="staffDao"/><property name="interceptorNames"><list><value>springTransaction</value></list></property>
</bean>

接着通过创建ApplicationContext,获取Bean实例,测试代码如下

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
StaffDao proxy = context.getBean("staffDaoProxy", StaffDao.class);
proxy.employ(staff);
proxy.paySalary(staff);
3.3 AbstractAutoProxyCreator

ProxyFactoryBean已经基本可以使用了,只是每一个Bean要做代理时都需要手工配置一个ProxyFactoryBean,使用起来还是略显繁琐。Spring提供了AbstractAutoProxyCreator来完成AOP代理的自动创建。这里我们以BeanNameAutoProxyCreator为例,看看AutoProxyCreator是如何使用,它是怎么工作的。同样先基于xml配置BeanNameAutoProxyFactory,使用相同interceptorNames的所有Bean都可以通过这一个配置自动创建代理,而且staffDao不需要和之前那样创建staffDao、staffDaoProxy两个Bean。

<bean id="staffDao" class="com.lws.designPattern.StaffDaoImpl"/>
<bean id="springTransaction" class="com.lws.spring.SpringTransactionMethodInterceptor"/><bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"><property name="beanNames"><list><value>staffDao</value></list></property><property name="interceptorNames" value="springTransaction"/>
</bean>

接着只需要正常的获取staffDao的Bean实例,并调用测试即可

Staff staff = new Staff("zhangsan", 18);
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
StaffDao proxy = context.getBean("staffDao", StaffDao.class);
proxy.employ(staff);
proxy.paySalary(staff);

应该承认BeanNameAutoProxyCreator的使用已经比较简单了,对使用相同interceptorNames的AOP场景,只需要把Bean实例名称添加到beanNames就行了。再进一步简化就是通过AnnotationAwareAspectJAutoProxyCreator来实现了,它会自动判断Bean是否匹配AspectJ表达式,对符号条件的Bean完成代理。后面的代码中我们会进行源码级解释,现在我们先来讲讲Spring对AOP做的抽象,以及它实现AOP的核心组件。

2. Spring AOP抽象

1. 核心术语

1.1 连接点(JoinPoint)

连接点是程序中客观存在的特定位置,比如类初始化前、类初始化后、方法执行前、方法执行后等等。Spring AOP中只支持方法的连接点。连接到有两组信息组成,一个是程序的执行点,比如哪个类的哪个方法;一个是相对位置,比如方法执行前、方法返回后等等。Spring使用切点(Pointcut)表示执行点,用增强(Advice)表示相对位置。

1.2 切点(Pointcut)

连接点是程序中客观存在的事务,比如一个程序内有2个方法,方法执行前、方法执行后、抛出异常时,连接点个数就已经确定了,如果只看这3类相对位置的话,连接点就是6个。切点用来表示我们关心的连接点。Spring中使用Pointcut类表示切点。下图是Pointcut类的定义,它有两个成员变量,分别是ClassFilter实例、MethodMatcher实例。通过如下的UML图中的方法定义可以看到,ClassFilter用来表示哪些类是满足当前切点要求,MethodMatcher用来确定哪些方法满足当前切点要求。

Spring内部的核心实现类有下面这些,后面我们会挑几个看一下如何使用。

1.3 增强(Advice)

在介绍连接点的时候我们提供到过,连接点相对位置是通过增强(Advice)来表示的,此外Advice还包含相对位置要执行的代码。下图是Spring提供的Advice的核心类。我们日常使用的较多的就是在图的中心位置的4个类:

  1. MethodBeforeAdvice,方法执行前增强
  2. MethodInterceptor,方法环绕增强
  3. AfterReturningAdvice,方法返回后增强
  4. ThrowsAdvice,方法抛异常是的正常,这里需要特别注意ThrowsAdvice是一个标记接口,Spring通过反射调用,查看类JavaDoc有描述对方法签名的限制

1.4 切面(Aspect)

从之前的定义中可知,Pointcut和Advice已经完整的定义了在哪里执行什么增强逻辑。切面有切点(Pointcut)和增强(引介)组成,即包括横切逻辑的定义,也包括连接点的定义。

1.5 目标对象(Target)

要进行增强逻辑的目标类,在我们前面的例子里,StaffDao类的实例就是目标对象。

1.6 代理对象(Proxy)

对目标对象进行增强后生成的对象,就称为代理对象,这个对象已经包含增强的执行逻辑。

2. 核心组件

这张图里给出了Spring AOP中的核心流程涉及的组件,包括怎么定义切点(Pointcut),增强(Advice),并且通过组合Pointcut和Advice获得Advisor的定义。拿到Advisor后,我们可以使用ProxyFactory将Advisor和目标对象(target)组合,生成代对象。Spring默认支持两种生成代理对象的实现,一种是基于JDK的实现,JdkDynamicAopProxy,一种是基于Cglib的实现,ObjenesisCglibAopProxy。

每一个组件都有大量的实现类,这里不进行展开。接下来我们对几个关键点做详细的说明

2.1 AutoProxyCreator工作原理

BeanNameAutoProxyCreator继承自AbstractAutoProxyCreator,实现了Bean生命周期里的InstantiationAwareBeanFactoryProcessor、BeanPostProcessor,覆写的是两个方法: postProcessBeforeInstantiation、postProcessAfterInitialization,核心流程图如下:

对于定义了customTargetSource的AbstractAutoProxyCreator,Bean不会走正常的实例化流程,在postProcessBeforeInstantiation就通过TargetSource实例化并创建代理返回。日常开发中我们极少使用TargetSource,TargetSource适用于aop时期望目标对象(target)支持池化或者热替换的场景,更多TargetSource信息可以阅读Spring官方文档中关于Using TargetSources章节。

BeanNameAutoProxy主要覆写了AbstractAutoProxyCreator以下几个方法:

  1. isSupportedBeanName,基于配置的beanNames判断当前bean是否支持自动创建代理对象
  2. getAdvicesAndAdvisorsForBean,支持的时候返回空数组,不支持返回null

BeanNameAutoProxyCreator的Advice和Advisor对象的注入主要时通过interceptorNames做为bean名称从容器中查找的,具体实现看AbstractorAutoProxyCreator的buildAdvisors实现

AnnotationAwareAspectJAutoProxyCreator基于AbstractAutoProxyCreator实现,不适用interceptorNames,核心逻辑在于查找bean匹配的Advisor,通过findCandidateAdvisors查找候选Advisor。

以下是AbstractAutoProxyCreator的骨干代码,实现类基于下面的模板修改部分逻辑。

    @Overridepublic Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {...TargetSource targetSource = getCustomTargetSource(beanClass, beanName);                // 自定义customTargetSourceCreator的Beanif (targetSource != null) {if (StringUtils.hasLength(beanName)) {this.targetSourcedBeans.add(beanName);}Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource); // 通过自定义TargetSource直接返回BeanObject proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);this.proxyTypes.put(cacheKey, proxy.getClass());return proxy;}...}@Overridepublic Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {if (bean != null) {Object cacheKey = getCacheKey(bean.getClass(), beanName);if (this.earlyBeanReferences.remove(cacheKey) != bean) {return wrapIfNecessary(bean, beanName, cacheKey);}}return bean;}
2.2 @EnableAspectJAutoProxy工作原理

EnableAspectJAutoProxy通过在自身注解@Import引入ImportBeanDefinitionRegistrar实现,ImportBeanDefinitionRegistrar接口允许用户通过代码注册自己的BeanDefinition。

@Import({AspectJAutoProxyRegistrar.class})
public @interface EnableAspectJAutoProxy 

AspectJAutoProxyRegistrar的核心代码如下:

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);AnnotationAttributes enableAspectJAutoProxy = AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);...}
}

进一步往AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary下面追踪,我们发现自动注册了AnnotationAwareAspectJAutoProxyCreator,结合上面我们对AutoProxyCreator的了解,整个基于注解的配置AOP的流程就通顺了。

3. Spring AOP案例

基于上面对原理的探究,我们能理解怎么从Advice、Pointcut开始创建Advisor,怎么利用ProxyFactoryBean创建代理,通过AutoProxyCreator让Spring自动创建代理,Spring对所有这些元素的抽象,以及相互之间如何协同。然而在显示工作中,仅仅知道这些是不够的,我们不可能每次都从最原始的MethodBeforceAdvice来创建我们的Advice,不可能从ClassFilter、MethodMatcher开始定义Pointcut,这样的话使用成本太高了,Spring提供了大量的内置类型,降低我们使用Spring AOP的成本。下一篇中我们会专注提供实战案例,包括但不限于如下主题

1.使用Advice

2.使用Advisor

3.使用BeanNameAutoProxyCreator

4.使用AnnotationAwareAspectJAutoProxyCreator

5.使用aop名称空间

6.使用@Aspect注解

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

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

相关文章

C# Solidworks二次开发:三种获取SW设计结构树的方法-第二讲

今天这篇文章是接上一篇文章的&#xff0c;主要讲述的是获取SW设计结构树节点的第二种方法。 这个方法获取节点的逻辑是先获取最顶层节点&#xff0c;然后再通过获取顶层节点的子节点一层一层的把所有节点都找出来&#xff0c;也就是需要递归。想要用这个方法就要了解下面几个…

tomcat环境搭建

镜像下载地址&#xff1a;https://mirror.tuna.tsinghua.edu.cn/apache/tomcat/ 配置环境变量 添加系统变量 编辑Path 测试 dos窗口运行startup启动tomcat 访问http://localhost:8080/

【每日一题】—— D. Divide and Equalize(Codeforces Round 903 (Div. 3))(数学、数论)

&#x1f30f;博客主页&#xff1a;PH_modest的博客主页 &#x1f6a9;当前专栏&#xff1a;每日一题 &#x1f48c;其他专栏&#xff1a; &#x1f534; 每日反刍 &#x1f7e1; C跬步积累 &#x1f7e2; C语言跬步积累 &#x1f308;座右铭&#xff1a;广积粮&#xff0c;缓称…

Linux操作系统 中的用户管理,也就是关于用户的相关的操作与理解

目录 1 概念2 用户管理的类型3、模板目录4 查看历史命令5 相关文件6 创建用户useradd7 命令passwd 存储及shadow命令下的用户密码8 usermod命令&#xff08;修改属性&#xff09;9 密码设置10 删除命令userdel11 用户组的操作12 用户权限13 创建一个用户组&#xff0c;并且将这…

ORA-600 kcbzib_kcrsds_1一键恢复

一个19c库由于某种原因redo损坏强制打开库报ORA-600 kcbzib_kcrsds_1错误 SQL> startup mount pfile?/database/pfile.txt; ORACLE instance started. Total System Global Area 859830696 bytes Fixed Size 9034152 bytes Variable Size 5…

我们一起聊一聊JWT的那些事

我们一起聊一聊JWT的那些事 一切美好&#xff0c;如期而至… 什么是JWT JWT&#xff0c;全称为 JSON Web Token&#xff0c;是一种用于在网络上安全地传递信息的开放标准&#xff08;RFC 7519&#xff09;。JWT 是一种紧凑且独立的方式&#xff0c;用于在各方之间以 JSON 对象…

javaTCP协议实现一对一聊天

我们首先要完成服务端&#xff0c;不然出错&#xff0c;运行也要先运行服务端&#xff0c;如果不先连接服务端&#xff0c;就不监听&#xff0c;那客户端不知道连接谁 服务端 import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.Actio…

消费1000返1500元,买了4罐奶粉倒赚商家2000元?商家亏吗?

大家好&#xff0c;我是微三云胡佳东&#xff0c;一个深耕私域电商模式玩法的互联网人&#xff01;&#xff01; 在当前的全球经济环境下&#xff0c;消费增值的概念正逐渐受到广泛的关注。这一模式的崛起&#xff0c;不仅仅是一种商业模式的创新&#xff0c;更代表着我们对经…

[⑦ADRV902x]: JESD204学习笔记

前言 JESD204B/C基于SERDES&#xff08;SERialization/DESerialization&#xff09;技术&#xff0c;也就是串化和解串&#xff0c;在发送端将多位并行的数据转换为1 bit的串行数据&#xff0c;在接收端将串行数据恢复成原始的并行数据。 在JESD204接口出现以前&#xff0c;数…

TOC清洁验证棉签:擦拭取样清洁验证专用的强力助手!

由PP聚丙烯杆聚酯纤维布头构成的TOC清洁验证棉签&#xff0c;用于清洁验证协议的表面采样取样&#xff0c;对难以到达的区域或狭小空间的精密清洗等。 棉签杆使用聚丙烯制作,杆上的缺口能使棉签头在较小的操作及污染下放进小瓶中,具有较好的化学相容性。 有缺口的分离式手柄允许…

class036 二叉树高频题目-上-不含树型dp【算法】

class036 二叉树高频题目-上-不含树型dp code1 102. 二叉树的层序遍历 // 二叉树的层序遍历 // 测试链接 : https://leetcode.cn/problems/binary-tree-level-order-traversal/ code1 普通bfs code2 一次操作一层 package class036;import java.util.ArrayList; import java…

全自动影像仪图像自动匹配对焦,测量一致性好

全自动影像仪使用自动边缘提取、自动匹配、自动对焦、测量合成等先进的机器视觉和人工智能技术&#xff0c;能实现高精度的测量与自动对焦。这些技术提高测量的连贯性和准确性&#xff0c;进而提升生产效率与产品质量。 此外&#xff0c;全自动影像仪还具有多种功能&#xff0…

【二叉树】

文章目录 树形结构注意要点细分概念树在生活中的应用 二叉树什么是二叉树二叉树特点&#xff1a;两种特殊的二叉树二叉树的性质二叉树性质的练习二叉树的存储二叉树的遍历前序遍历中序遍历后序遍历遍历练习 树形结构 树是一种非线性的数据结构&#xff0c;它具有以下的特点&am…

LeetCode 1038. 从二叉搜索树到更大和树:(反)中序遍历

【LetMeFly】1038.从二叉搜索树到更大和树&#xff1a;&#xff08;反&#xff09;中序遍历 力扣题目链接&#xff1a;https://leetcode.cn/problems/binary-search-tree-to-greater-sum-tree/ 给定一个二叉搜索树 root (BST)&#xff0c;请将它的每个节点的值替换成树中大于…

k8s中的Pod网络;Service网络;网络插件Calico

Pod网络&#xff1b;Service网络&#xff1b;网络插件Calico Pod网络 在K8S集群里&#xff0c;多个节点上的Pod相互通信&#xff0c;要通过网络插件来完成&#xff0c;比如Calico网络插件。 使用kubeadm初始化K8S集群时&#xff0c;有指定一个参数–pod-network-cidr10.18.0…

如何让软文更具画面感,媒介盒子分享

写软文这种带有销售性质的文案时&#xff0c;总说要有画面感&#xff0c;要有想象空间。只有针对目标用户的感受的设计&#xff0c;要了解用户想的是什么&#xff0c;要用可视化的描述来影响用户的感受&#xff0c;今天媒介盒子就和大家分享&#xff1a;如何让软文更具画面感。…

2023-12-05 LeetCode每日一题(到达首都的最少油耗)

2023-12-05每日一题 一、题目编号 2477. 到达首都的最少油耗二、题目链接 点击跳转到题目位置 三、题目描述 给你一棵 n 个节点的树&#xff08;一个无向、连通、无环图&#xff09;&#xff0c;每个节点表示一个城市&#xff0c;编号从 0 到 n - 1 &#xff0c;且恰好有 …

Ubuntu之Sim2Real环境配置(坑居多)

不要一上来就复制哦,因为很多下面的步骤让我走了很多弯路,如果可能的话,我会重新整理再发出来 前提: 参考教程 Docs 创建工作空间(不用跟着操作,无用) 1.创建sim2real server container 1.尝试创建sim2real_server容器 后来使用 docker ps -a 发现我已经有sim2real_se…

机器学习 类别特征编码:Category Encoders 库的使用

✅作者简介&#xff1a;人工智能专业本科在读&#xff0c;喜欢计算机与编程&#xff0c;写博客记录自己的学习历程。 &#x1f34e;个人主页&#xff1a;小嗷犬的个人主页 &#x1f34a;个人网站&#xff1a;小嗷犬的技术小站 &#x1f96d;个人信条&#xff1a;为天地立心&…

202350读书笔记|《再别康桥:徐志摩诗选》——微风起,清芬酝藉,不减荼

202350读书笔记|《再别康桥&#xff1a;徐志摩诗选》——微风起&#xff0c;清芬酝藉&#xff0c;不减荼 《再别康桥&#xff1a;徐志摩诗选》我觉得有时候诗人是很狂热的&#xff0c;上头的感觉。 有几首很喜欢&#xff0c;节选如下&#xff1a; 偶然 我是天空里的一片云&…