spring事务传播之NESTED

下面是官方的解释

/**
 * Execute within a nested transaction if a current transaction exists,
 * behave like PROPAGATION_REQUIRED else. There is no analogous feature in EJB.
 * 

Note: Actual creation of a nested transaction will only work on specific * transaction managers. Out of the box, this only applies to the JDBC * DataSourceTransactionManager when working on a JDBC 3.0 driver. * Some JTA providers might support nested transactions as well. * @see org.springframework.jdbc.datasource.DataSourceTransactionManager */ NESTED(TransactionDefinition.PROPAGATION_NESTED);

如果事务存在则在一个嵌套的事务中执行,如果没有则像PROPAGATION_REQUIRED表现形式一样,支持嵌套事务的前提条件
*JDBC3+
* 使用DataSourceTransactionManager 事务管理器

实例

spring配置文件


<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-4.2.xsd 
        http://www.springframework.org/schema/tx 
        http://www.springframework.org/schema/tx/spring-tx-4.2.xsd
        http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">
    
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
        destroy-method="close">
        <property name="driverClass" value="${jdbc.driver}" />
        <property name="jdbcUrl" value="${jdbc.url}" />
        <property name="user" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />

        
        <property name="checkoutTimeout" value="30000" />
        
        <property name="idleConnectionTestPeriod" value="30" />
        
        <property name="maxIdleTime" value="30" />
        
        <property name="initialPoolSize" value="10" />
        <property name="minPoolSize" value="10" />
        <property name="maxPoolSize" value="20" />
        
        <property name="acquireIncrement" value="5" />
    bean>

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource" />
    bean>

    
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    bean>

    
    <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
beans>

测试代码

package org.bear.bookstore.test.propagation.jdbc;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class Nested {
    @Autowired JdbcTemplate jdbcTemplate;

    @Transactional(
            propagation=Propagation.REQUIRED,
            rollbackFor={Exception.class}
            )
    public void a(){
        jdbcTemplate.execute("insert into custom(address,cusname,email,phone,sex) values('北京市,朝阳区,十里河村','xxx','[email protected]','15555555555','1')");
        int x = 1/0;
        System.out.println(x);
    }

    @Transactional(
            propagation=Propagation.NESTED,
            rollbackFor={Exception.class}
            )
    public void b(){
        jdbcTemplate.execute("insert into custom(address,cusname,email,phone,sex) values('北京市,朝阳区,十里河村','xxx','[email protected]','15555555555','0')");

        int x = 1/0;
        System.out.println(x);
    }

}
package org.bear.bookstore.test.propagation.jdbc;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class Nested2 {
    @Autowired Nested nested;
    @Autowired JdbcTemplate jdbcTemplate;

    @Transactional(
            propagation=Propagation.REQUIRED,
            rollbackFor={Exception.class}
            )
    public void a(){
        /**
         * 如果aa()抛出异常,b未出现异常,则a会回滚b的结果
         */
        aa();
        try {
            /**
             * 调用b,捕获异常,异常不会冒泡,aa()执行成功
             * 也就是nested的用法,在一个类中这么调用没有效果,如调用aaa()
             */
            nested.b();

            /**
             * 如果调用a则会出现如下异常,
             * org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
                    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:724)
                    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:504)
                    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:292)
                    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
                    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
                    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655)
                    at org.bear.bookstore.test.propagation.jdbc.Nested2$$EnhancerBySpringCGLIB$$567ca65b.a()
                    at org.bear.bookstore.test.propagation.JdbcTransactionPropagationTest.NestedTest(JdbcTransactionPropagationTest.java:25)
                    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
                    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
                    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
                    at java.lang.reflect.Method.invoke(Unknown Source)
                    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
                    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
                    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
                    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
                    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
                    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
                    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
                    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
                    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
                    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
                    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
                    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
                    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
                    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
                    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
                    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
                    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
                    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
                    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
                    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
                    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
                    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
                    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678)
                    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
                    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
             */
            //nested.a();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Transactional(
            propagation=Propagation.REQUIRED,
            rollbackFor={Exception.class}
            )
    public void aa(){
        jdbcTemplate.execute("insert into custom(address,cusname,email,phone,sex) values('北京市,朝阳区,十里河村','xxx','[email protected]','15555555555','1')");
        /*int x = 1/0;
        System.out.println(x);*/
    }


    /**
     * 这样的结果是
     *  如果bbb是propagation=Propagation.NESTED,则两个custom都保存成功
     *  如果bbb是propagation=Propagation.REQUIRED,则两个custom都保存成功
     */
    @Transactional(
            propagation=Propagation.REQUIRED,
            rollbackFor={Exception.class}
            )
    public void aaa(){
        jdbcTemplate.execute("insert into custom(address,cusname,email,phone,sex) values('北京市,朝阳区,十里河村','xxx','[email protected]','15555555555','1')");
        try {
            bbb();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    @Transactional(
            //propagation=Propagation.NESTED,
            propagation=Propagation.REQUIRED,
            rollbackFor={Exception.class}
            )
    public void bbb(){
        jdbcTemplate.execute("insert into custom(address,cusname,email,phone,sex) values('北京市,朝阳区,十里河村','xxx','[email protected]','15555555555','1')");
        int x = 1/0;
        System.out.println(x);
    }

}
package org.bear.bookstore.test.propagation;

import org.bear.bookstore.test.propagation.jdbc.Nested2;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
        locations={"file:src/test/resources/spring-app-jdbc.xml","file:src/test/resources/spring-jdbc.xml"}
        )
public class JdbcTransactionPropagationTest {
    /**
     * 运行在datasourcemanager中
     */
    @Autowired Nested2 Nested;
    /**
     * PROPAGATION_NESTED 
     *  
     */
    @Test
    public void NestedTest(){
        Nested.a();
        //Nested.aaa();
    }
}

实验分析

咱们看堆栈信息

Thread [main] (Suspended (breakpoint at line 186 in DataSourceTransactionManager))  
    DataSourceTransactionManager.doGetTransaction() line: 186   
    DataSourceTransactionManager(AbstractPlatformTransactionManager).getTransaction(TransactionDefinition) line: 337    
    TransactionInterceptor(TransactionAspectSupport).createTransactionIfNecessary(PlatformTransactionManager, TransactionAttribute, String) line: 447   
    TransactionInterceptor(TransactionAspectSupport).invokeWithinTransaction(Method, Class, InvocationCallback) line: 277    
    TransactionInterceptor.invoke(MethodInvocation) line: 96    
    CglibAopProxy$CglibMethodInvocation(ReflectiveMethodInvocation).proceed() line: 179    
    CglibAopProxy$DynamicAdvisedInterceptor.intercept(Object, Method, Object[], MethodProxy) line: 655 
    Nested$$EnhancerBySpringCGLIB$$cb68a86.b() line: not available  
    Nested2.a() line: 28    
    Nested2$$FastClassBySpringCGLIB$$7a769380.invoke(int, Object, Object[]) line: not available  
    MethodProxy.invoke(Object, Object[]) line: 204  
    CglibAopProxy$CglibMethodInvocation.invokeJoinpoint() line: 720    
    CglibAopProxy$CglibMethodInvocation(ReflectiveMethodInvocation).proceed() line: 157    
    TransactionInterceptor$1.proceedWithInvocation() line: 99  
    TransactionInterceptor(TransactionAspectSupport).invokeWithinTransaction(Method, Class, InvocationCallback) line: 282    
    TransactionInterceptor.invoke(MethodInvocation) line: 96    
    CglibAopProxy$CglibMethodInvocation(ReflectiveMethodInvocation).proceed() line: 179    
    CglibAopProxy$DynamicAdvisedInterceptor.intercept(Object, Method, Object[], MethodProxy) line: 655 
    Nested2$$EnhancerBySpringCGLIB$$d54acf0.a() line: not available 
    JdbcTransactionPropagationTest.NestedTest() line: 25    
    NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]  
    NativeMethodAccessorImpl.invoke(Object, Object[]) line: not available   
    DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: not available   
    Method.invoke(Object, Object...) line: not available    
    FrameworkMethod$1.runReflectiveCall() line: 50 
    FrameworkMethod$1(ReflectiveCallable).run() line: 12   
    FrameworkMethod.invokeExplosively(Object, Object...) line: 47   
    InvokeMethod.evaluate() line: 17    
    RunBeforeTestMethodCallbacks.evaluate() line: 75    
    RunAfterTestMethodCallbacks.evaluate() line: 86 
    SpringRepeat.evaluate() line: 84    
    SpringJUnit4ClassRunner(ParentRunner).runLeaf(Statement, Description, RunNotifier) line: 325 
    SpringJUnit4ClassRunner.runChild(FrameworkMethod, RunNotifier) line: 252    
    SpringJUnit4ClassRunner.runChild(Object, RunNotifier) line: 94  
    ParentRunner$3.run() line: 290 
    ParentRunner$1.schedule(Runnable) line: 71 
    SpringJUnit4ClassRunner(ParentRunner).runChildren(RunNotifier) line: 288 
    ParentRunner.access$000(ParentRunner, RunNotifier) line: 58 
    ParentRunner$2.evaluate() line: 268    
    RunBeforeTestClassCallbacks.evaluate() line: 61 
    RunAfterTestClassCallbacks.evaluate() line: 70  
    SpringJUnit4ClassRunner(ParentRunner).run(RunNotifier) line: 363 
    SpringJUnit4ClassRunner.run(RunNotifier) line: 191  
    JUnit4TestReference.run(TestExecution) line: 86 
    TestExecution.run(ITestReference[]) line: 38    
    RemoteTestRunner.runTests(String[], String, TestExecution) line: 459    
    RemoteTestRunner.runTests(TestExecution) line: 678  
    RemoteTestRunner.run() line: 382    
    RemoteTestRunner.main(String[]) line: 192   

具体分析流程

重点日志打印信息
2016-11-23 11:03:26.652 [main] DEBUG org.springframework.jdbc.core.JdbcTemplate at 430 - Executing SQL statement [insert into custom(address,cusname,email,phone,sex) values('北京市,朝阳区,十里河村','xxx','[email protected]','15555555555','1')]
2016-11-23 11:04:09.012 [main] DEBUG o.s.jdbc.datasource.DataSourceTransactionManager at 450 - Creating nested transaction with name [org.bear.bookstore.test.propagation.jdbc.Nested.b]
2016-11-23 11:04:09.102 [main] DEBUG org.springframework.jdbc.core.JdbcTemplate at 430 - Executing SQL statement [insert into custom(address,cusname,email,phone,sex) values('北京市,朝阳区,十里河村','xxx','[email protected]','15555555555','0')]
2016-11-23 11:04:09.104 [main] DEBUG o.s.jdbc.datasource.DataSourceTransactionManager at 845 - Rolling back transaction to savepoint
java.lang.ArithmeticException: / by zero
    at org.bear.bookstore.test.propagation.jdbc.Nested.b(Nested.java:30)
    at org.bear.bookstore.test.propagation.jdbc.Nested$$FastClassBySpringCGLIB$$c1e2caf2.invoke()
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:720)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655)
    at org.bear.bookstore.test.propagation.jdbc.Nested$$EnhancerBySpringCGLIB$$cb68a86.b()
    at org.bear.bookstore.test.propagation.jdbc.Nested2.a(Nested2.java:28)
    at org.bear.bookstore.test.propagation.jdbc.Nested2$$FastClassBySpringCGLIB$$7a769380.invoke()
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:720)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655)
    at org.bear.bookstore.test.propagation.jdbc.Nested2$$EnhancerBySpringCGLIB$$d54acf0.a()
    at org.bear.bookstore.test.propagation.JdbcTransactionPropagationTest.NestedTest(JdbcTransactionPropagationTest.java:25)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)2016-11-23 11:04:09.102 [main] DEBUG org.springframework.jdbc.core.JdbcTemplate at 430 - Executing SQL statement [insert into custom(address,cusname,email,phone,sex) values('北京市,朝阳区,十里河村','xxx','[email protected]','15555555555','0')]
2016-11-23 11:04:09.111 [main] DEBUG o.s.jdbc.datasource.DataSourceTransactionManager at 759 - Initiating transaction commit

    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
2016-11-23 11:04:09.104 [main] DEBUG o.s.jdbc.datasource.DataSourceTransactionManager at 845 - Rolling back transaction to savepoint

从堆栈信息和打印信息我们可以清楚的得到以下信息:
测试类方法调用
JdbcTransactionPropagationTest.NestedTest() line: 25
CBLIB字节码增强我的service类
Nested2$$EnhancerBySpringCGLIB$$d54acf0.a() line: not available
事务拦截器拦截注解了事务的方法
Nested2.a() line: 28    
Nested2$$FastClassBySpringCGLIB$$7a769380.invoke(int, Object, Object[]) line: not available  
MethodProxy.invoke(Object, Object[]) line: 204  
CglibAopProxy$CglibMethodInvocation.invokeJoinpoint() line: 720    
CglibAopProxy$CglibMethodInvocation(ReflectiveMethodInvocation).proceed() line: 157    
TransactionInterceptor$1.proceedWithInvocation() line: 99  
TransactionInterceptor(TransactionAspectSupport).invokeWithinTransaction(Method, Class, InvocationCallback) line: 282    
TransactionInterceptor.invoke(MethodInvocation) line: 96    
CglibAopProxy$CglibMethodInvocation(ReflectiveMethodInvocation).proceed() line: 179    
CglibAopProxy$DynamicAdvisedInterceptor.intercept(Object, Method, Object[], MethodProxy) line: 655 

发现调用了另一个service的声明了NESTED的方法,创建savepoint,走新的代理拦截流程
Nested$$EnhancerBySpringCGLIB$$cb68a86.b() line: not available

现在我们解释一些问题了
为什么b的执行失败也不会影响aa方法的插入操作,因为b失败后事务,会回滚到我们保存的savepoint,(当然咱们必须捕获异常进行处理)然后继续往下执行,也就是执行a方法的其它流程
为什么aa的执行失败会影响b,理由同上

总结

不闻不若闻之,闻之不若见之,见之不若知之,知之不若行之。
不断的学习,不断的交流,不断的实践

你可能感兴趣的:(杂记)