Spring事務(wù)的淺析

1. 事務(wù)的使用


Spring中的事務(wù)有以下幾種使用方式

  • 編程式事務(wù)敬尺;
  • 使用XML配置聲明式事務(wù)害幅;
  • 使用注解配置聲明式事務(wù)。

在實際應(yīng)用中榴徐,很少通過編程來進(jìn)行事務(wù)管理,一般多使用聲明式事務(wù)匀归,而隨著注解在Spring 中流行開來坑资,所以使用注解配置聲明式事務(wù)會較多

1.1. 編程式事務(wù)

使用TransactionTemplate進(jìn)行編程式事務(wù)的開發(fā)穆端,實際開發(fā)中很少使用了袱贮。

@Service
public class UserService {
    
    @Autowired
    private UserDao userDao;
    @Autowired
    private TransactionTemplate transactionTemplate;

    /**
     * 編程式事務(wù)
     */
    public void insertUser(boolean hasException){
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                userDao.insert();
                System.out.println("...");
                if (hasException) {
                    int i = 10 / 0;
                }
            }
        });
    }
}

1.2. 使用XML配置聲明式事務(wù)

使用XML進(jìn)行聲明式事務(wù)的配置又可以分為兩種:

  1. 使用原始的TransactionProxyFactoryBean
  2. 基于aop/tx命名空間的配置体啰。

其中TransactionProxyFactoryBean 還是Spring 1.0時代的遠(yuǎn)古產(chǎn)物字柠,它需要給每個需要使用事務(wù)的類進(jìn)行單獨的配置,比較繁瑣狡赐。我們只需知道有這么個事就可以窑业。

下面看看基于aop/tx命名空間的配置

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
     http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">

    <import resource="classpath:applicationContext-dao.xml"/>

    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
          p:dataSource-ref="dataSource"/>

    <aop:config>
        <aop:pointcut id="serviceMethod"
                      expression="execution(* io.zgc.spring.features.tx.usetype.xml.UserService.*(..))"/>
        <aop:advisor pointcut-ref="serviceMethod"
                     advice-ref="txAdvice"/>
    </aop:config>

    <tx:advice id="txAdvice">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="insert*" rollback-for="Exception"/>
            <tx:method name="update*"/>
        </tx:attributes>
    </tx:advice>
</beans>

可以看到事務(wù)的配置是基于AOP的配置。

1.3. 使用注解配置聲明式事務(wù)

我們可以在需要開啟事務(wù)的地方使用注解@Transactional 枕屉,該注解可以使用在類或方法上常柄。有兩種方式可以使@Transactional注解生效

  1. 使用xml配置文件的應(yīng)用中,需要在配置文件中添加下列配置:
<bean id="txManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
          p:dataSource-ref="dataSource"/>

<tx:annotation-driven transaction-manager="txManager" proxy-target-class="true"/>
  1. 使用配置類的應(yīng)用中搀擂,使用@EnableTransactionManagement開啟事務(wù)管理
@EnableTransactionManagement
@ComponentScan("io.zgc.spring.features.tx")
@Configuration
public class TxConfig {

2. 事務(wù)的傳播行為


當(dāng)事務(wù)方法被另外一個事務(wù)方法調(diào)用時西潘,必須指定事務(wù)應(yīng)該如何傳播。例如:方法可以繼續(xù)在現(xiàn)有事務(wù)中運行哨颂,也可以開啟一個新事務(wù)喷市。

事務(wù)的傳播行為可以通過事務(wù)的傳播屬性來指定,Spring中定義了7種傳播行為威恼。

  • REQUIRED:如果有事務(wù)在運行品姓,當(dāng)前的方法就使用這個事務(wù)寝并,在這個事務(wù)中運行。否則腹备,就開啟一個新的事務(wù)衬潦,并在新的事務(wù)中運行。
  • REQUIRES_NEW:當(dāng)前的方法必須開啟新的事務(wù)植酥,并在新的事務(wù)中運行镀岛,如果調(diào)用的方法有事務(wù)在運行,就將它先掛起友驮。
  • SUPPORTS:如果有事務(wù)在運行漂羊,當(dāng)前的方法就在這個事務(wù)內(nèi)運行,否則它可以不運行在事務(wù)中卸留。
  • NOT_SUPPORTS:當(dāng)前的方法不應(yīng)該運行在事務(wù)中拨与,如果當(dāng)前有運行的事務(wù),就將它掛起艾猜。
  • MANDATORY:當(dāng)前方法運行在調(diào)用方法的事務(wù)中,如果不存在運行中的事務(wù)就拋出異常捻悯。
  • NEVER:當(dāng)前的方法不應(yīng)該運行在事務(wù)中匆赃,如果有運行的事務(wù),就拋出異常今缚。
  • NESTED:如果有事務(wù)運行算柳,當(dāng)前的方法就應(yīng)該在這個事務(wù)的嵌套事務(wù)內(nèi)運行,否則就啟動一個新的事務(wù)姓言,并在自己的事務(wù)內(nèi)運行瞬项。

常用的傳播行為:REQUIREDREQUIRES_NEW何荚,其中Spring默認(rèn)的傳播行為為:REQUIRED囱淋。

@Service
public class UserService {
    
    @Autowired
    private UserDao userDao;
    @Autowired
    private UserService self;
    
    @Transactional
    public void insertUser(boolean hasException){
        userDao.insert();
        System.out.println("...");
        if (hasException) {
            int i = 10 / 0;
        }
    }

    /**
     * 一個用戶都不會插入,事務(wù)的傳播行為是REQUIRED
     * 兩個self.insertUser方法共用insetBatchUserWithRequred方法開啟的事務(wù)
     * 
     * 事務(wù)最終由于self.insertUser(true);執(zhí)行拋出異常餐塘,而回滾
     */
    @Transactional
    public void insetBatchUserWithRequred() {
        self.insertUser(false);

        self.insertUser(true);
    }

    /**
     * 會插入1個用戶妥衣,因為事務(wù)的傳播行為為REQUIRES_NEW
     * self.insertUserRn(false); 開啟單獨的事務(wù),并成功執(zhí)行
     * self.insertUserRn(true); 開啟單獨的事務(wù)戒傻,執(zhí)行失敗
     *
     * insetBatchUserWithRequredNew的事務(wù)不會受self.insertUserRn(true)開啟的事務(wù)影響
     */
    @Transactional
    public void insetBatchUserWithRequredNew() {
        self.insertUserRn(false);

        self.insertUserRn(true);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void insertUserRn(boolean hasException){
        userDao.insert();
        System.out.println("...");
        if (hasException) {
            int i = 10 / 0;
        }
    }

}

使用事務(wù)的一些坑


日常開發(fā)過程中税手,遇到聲明式事務(wù)(@Transactional)和其他形式的事務(wù)時一定要親自驗證一波,確保事務(wù)是可以生效的需纳。下面來看看關(guān)于事務(wù)常見的一些坑

private的方法中使用聲明式事務(wù)

首先需要知道的是:在private方法中使用聲明式事務(wù)是不會生效的芦倒。除非特殊配置(比如使用 AspectJ 靜態(tài)織入實現(xiàn) AOP),否則只有定義在 public 方法上的 @Transactional 才能生效不翩。原因是兵扬, Spring 默認(rèn)通過動態(tài)代理的方式實現(xiàn) AOP麻裳,對目標(biāo)方法進(jìn)行增強(qiáng),private方法無法代理到周霉,Spring自然也無法動態(tài)增強(qiáng)事務(wù)處理邏輯掂器。

/**
 * @Transactional 不能放在private方法上,不然事務(wù)不生效
 * @param hasException
 */
@Transactional
private void insertUserPrivate(boolean hasException){
    userDao.insert();
    System.out.println("...");
    if (hasException) {
        int i = 10 / 0;
    }
}

上面的insertUserPrivate方法俱箱,即使拋出異常事務(wù)也不會回滾国瓮,這是因為它是個private方法,而@Transactional注解不能修飾private方法狞谱。

通過this調(diào)用事務(wù)方法

要想@Transactional生效乃摹,必須是通過代理的類調(diào)用目標(biāo)方法,否則事務(wù)將不會生效跟衅。請看如下事務(wù)失效代碼:

/**
 * 自調(diào)用(this調(diào)用)事務(wù)方法孵睬,事務(wù)不會生效
 * @param hasException
 */
public void insetUserWrong2(boolean hasException) {
    insertUserPublic(hasException);
}

@Transactional
public void insertUserPublic(boolean hasException){
    userDao.insert();
    System.out.println("...");
    if (hasException) {
        int i = 10 / 0;
    }
}

事務(wù)在拋出異常時依然不會回滾,這是因為是通過this調(diào)用了事務(wù)方法伶跷,而this指向的是目標(biāo)對象掰读,非代理對象,沒有進(jìn)行事務(wù)增加叭莫,所以事務(wù)不會起作用蹈集。

這種情況下,要想事務(wù)能夠生效的話雇初,可以將代理對象注入其中拢肆,并通過代理對象來調(diào)用事務(wù)方法。

@Service
public class UserService {
    
    @Autowired
    private UserDao userDao;
    /** 要想事務(wù)生效靖诗,必須通過代理類來調(diào)用事務(wù)方法 */
    @Autowired
    private UserService self;

    public void insertUserRight(boolean hasException) {
        self.insertUserPublic(hasException);
    }

    @Transactional
    public void insertUserPublic(boolean hasException){
        userDao.insert();
        System.out.println("...");
        if (hasException) {
            int i = 10 / 0;
        }
    }
}

異常處理不當(dāng)導(dǎo)致事務(wù)失效

要想事務(wù)能夠生效(即在拋出異常時郭怪,事務(wù)能夠回滾),默認(rèn)情況下需要滿足兩個條件:

  1. 事務(wù)方法在執(zhí)行失敗時刊橘,需要將異常拋出去鄙才;
  2. 拋出的異常需要是非受檢異常(RuntimeException)。

當(dāng)然上面兩個條件是默認(rèn)情況下的讓事務(wù)生效的條件促绵,不是說必須得是這樣咒循,是可以被打破的,下面舉例來說明下

@Transactional
public void insertUserWrong3(boolean hasException){
    try {
        userDao.insert();
        System.out.println("...");
        if (hasException) {
            int i = 10 / 0;
        }
    } catch (Exception e) {
        System.out.println("------------------");
        /* 異常被吃掉了绞愚,事務(wù)無法回滾 */
        e.printStackTrace();
    }
}

上面代碼的事務(wù)之所以沒法回滾是因為方法中對異常進(jìn)行了捕獲并沒有繼續(xù)拋出新的異常叙甸,所以效果和正常運行了代碼的效果是一樣的,不會將事務(wù)回滾位衩。針對這種情況裆蒸,如何讓事務(wù)能夠生效呢?除了繼續(xù)拋出新的異常之外糖驴,還可以在catch住異常后僚祷,手動回滾事務(wù)(TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();)佛致,代碼如下:

@Transactional
public void insertUserRight3(boolean hasException){
    try {
        userDao.insert();
        System.out.println("...");
        if (hasException) {
            int i = 10 / 0;
        }
    } catch (Exception e) {
        e.printStackTrace();
        // 手動設(shè)置讓當(dāng)前事務(wù)處于回滾狀態(tài)
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

另外一個例子就是拋出了受檢異常了,例如FileNotFoundException辙谜,默認(rèn)情況下俺榆,事務(wù)也是不生效的,除非設(shè)置rollbackForException装哆。

@Transactional(rollbackFor = Exception.class)
public void insertUserRight4() throws FileNotFoundException {
    userDao.insert();
    System.out.println("...");
    throw new FileNotFoundException();
}

4. 事務(wù)源碼分析


4.1. 說明

本次源碼分析使用Spring的聲明式事務(wù)罐脊,版本基于Spring 5.1.5.RELEASE

4.2. 示例程序


TxConfig.java

@EnableTransactionManagement
@ComponentScan("io.zgc.spring.features.tx")
@Configuration
public class TxConfig {

    @Bean
    public DataSource dataSource() throws PropertyVetoException {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setUser("root");
        dataSource.setPassword("123456");
        dataSource.setDriverClass("com.mysql.jdbc.Driver");
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC");
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate() throws PropertyVetoException {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource());
        return jdbcTemplate;
    }

    @Bean
    public PlatformTransactionManager transactionManager () throws PropertyVetoException {
        return new DataSourceTransactionManager(dataSource());
    }
}

Client.java

public class Client {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(TxConfig.class);
        UserService userService = applicationContext.getBean(UserService.class);
        userService.insertUser();
    }
}

UserService.java

@Service
public class UserService {
    
    @Autowired
    private UserDao userDao;
    
    @Transactional
    public void insertUser(){
        userDao.insert();
        System.out.println("...");
        int i = 10/0;
    }
}

4.3. @EnableTransactionManagement

我們先來看看開啟聲明式事務(wù)的注解@EnableTransactionManagement,它的源碼如下:

@Import({TransactionManagementConfigurationSelector.class})
public @interface EnableTransactionManagement {
    boolean proxyTargetClass() default false;

    AdviceMode mode() default AdviceMode.PROXY;

    int order() default 2147483647;
}

TransactionManagementConfigurationSelector是個ImportSelector,默認(rèn)情況下蜕琴,它會給容器導(dǎo)入兩個組件AutoProxyRegistrarProxyTransactionManagementConfiguration萍桌。

4.4. AutoProxyRegistrar

AutoProxyRegistrar是一個ImportBeanDefinitionRegistrar,它會檢測導(dǎo)入者類上的某個注解是否帶有屬性modeproxyTargetClass凌简,如果檢測到這些屬性上炎,在modePROXY時,它會給容器中注冊一個InfrastructureAdvisorAutoProxyCreator 組件(APC雏搂、auto proxy creator)藕施。

InfrastructureAdvisorAutoProxyCreator是個APC,它的繼承體系如下圖所示:

從圖中我們可以得出的結(jié)論:

  1. InfrastructureAdvisorAutoProxyCreator是個Bean后置處理器凸郑。
  2. 它和AOP源碼中的AnnotationAwareAspectJAutoProxyCreator類類似裳食,都繼承自AbstractAutoProxyCreator,所以原理類似线椰。

InfrastructureAdvisorAutoProxyCreator會利用后置處理器機(jī)制在對象創(chuàng)建以后,包裝對象尘盼,返回一個代理對象(增強(qiáng)器)憨愉,代理對象執(zhí)行方法利用攔截器鏈進(jìn)行調(diào)用;

4.5. ProxyTransactionManagementConfiguration

ProxyTransactionManagementConfiguration是個配置類卿捎,它會給容器中注冊事務(wù)增強(qiáng)器配紫,主要包括三個組件:BeanFactoryTransactionAttributeSourceAdvisorTransactionAttributeSource午阵、TransactionInterceptor躺孝。

@Configuration
public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {

    @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor() {
        BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
        advisor.setTransactionAttributeSource(transactionAttributeSource());
        advisor.setAdvice(transactionInterceptor());
        if (this.enableTx != null) {
            advisor.setOrder(this.enableTx.<Integer>getNumber("order"));
        }
        return advisor;
    }

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public TransactionAttributeSource transactionAttributeSource() {
        return new AnnotationTransactionAttributeSource();
    }

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public TransactionInterceptor transactionInterceptor() {
        TransactionInterceptor interceptor = new TransactionInterceptor();
        interceptor.setTransactionAttributeSource(transactionAttributeSource());
        if (this.txManager != null) {
            interceptor.setTransactionManager(this.txManager);
        }
        return interceptor;
    }

}

1)、BeanFactoryTransactionAttributeSourceAdvisor是事務(wù)的advisor底桂。
2)植袍、事務(wù)advisor要用事務(wù)注解的信息,通過AnnotationTransactionAttributeSource解析事務(wù)注解籽懦。
3)于个、TransactionInterceptor是個advice,它實現(xiàn)了 MethodInterceptor;保存了事務(wù)屬性信息暮顺,事務(wù)管理器厅篓;

4.6. TransactionInterceptor

TransactionInterceptor 繼承了TransactionAspectSupport類秀存,實現(xiàn)MethodInterceptor接口。源碼如下:

public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {

    public TransactionInterceptor(PlatformTransactionManager ptm, TransactionAttributeSource tas) {
        setTransactionManager(ptm);
        setTransactionAttributeSource(tas);
    }


    @Override
    @Nullable
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // Work out the target class: may be {@code null}.
        // The TransactionAttributeSource should be passed the target class
        // as well as the method, which may be from an interface.
        Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

        // Adapt to TransactionAspectSupport's invokeWithinTransaction...
        return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
    }

}

攔截方法invoke最終會請求TransactionAspectSupportinvokeWithinTransaction方法羽氮,這個方法就是處理事務(wù)的邏輯

TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
try {
   // This is an around advice: Invoke the next interceptor in the chain.
   // This will normally result in a target object being invoked.
   retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
   // target invocation exception
   completeTransactionAfterThrowing(txInfo, ex);
   throw ex;
} finally {
   cleanupTransactionInfo(txInfo);
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末或链,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子档押,更是在濱河造成了極大的恐慌澳盐,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汇荐,死亡現(xiàn)場離奇詭異洞就,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)掀淘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門旬蟋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人革娄,你說我怎么就攤上這事倾贰。” “怎么了拦惋?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵匆浙,是天一觀的道長。 經(jīng)常有香客問我厕妖,道長首尼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任言秸,我火速辦了婚禮软能,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘举畸。我一直安慰自己查排,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布抄沮。 她就那樣靜靜地躺著跋核,像睡著了一般。 火紅的嫁衣襯著肌膚如雪叛买。 梳的紋絲不亂的頭發(fā)上砂代,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音率挣,去河邊找鬼泊藕。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的娃圆。 我是一名探鬼主播玫锋,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼讼呢!你這毒婦竟也來了心例?” 一聲冷哼從身側(cè)響起派诬,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后辅搬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體互订,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡吧慢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年马靠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片看蚜。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡叫搁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出供炎,到底是詐尸還是另有隱情渴逻,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布音诫,位于F島的核電站惨奕,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏竭钝。R本人自食惡果不足惜梨撞,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望香罐。 院中可真熱鬧卧波,春花似錦、人聲如沸穴吹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽港令。三九已至,卻和暖如春锈颗,著一層夾襖步出監(jiān)牢的瞬間顷霹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工击吱, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留淋淀,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓覆醇,卻偏偏與公主長得像朵纷,于是被迫代替她去往敵國和親炭臭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容