在沒有使用Spring提供的Open Session In View情況下,因需要在service(or Dao)層里把session關閉,所以lazy loading 為true的話,要在應用層內把關系集合都初始化,如 company.getEmployees(),否則Hibernate拋session already closed Exception; Open Session In View提供了一種簡便的方法,較好地解決了lazy loading問題.
它有兩種配置方式OpenSessionInViewInterceptor和OpenSessionInViewFilter(具體參看SpringSide),功能相同,只是一個在web.xml配置,另一個在application.xml配置而已。
Open Session In View在request把session綁定到當前thread期間一直保持hibernate session在open狀態,使session在request的整個期間都可以使用,如在View層里PO也可以lazy loading數據,如 ${ company.employees }。當View 層邏輯完成后,才會通過Filter的doFilter方法或Interceptor的postHandle方法自動關閉session。
OpenSessionInViewInterceptor配置
- /* 在www.bt285.cn ,www.5a520.cn,www.bjxlz.cn 使用
- <beans>
- <bean name="openSessionInViewInterceptor" class="org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor">
- <property name="sessionFactory">
- <ref bean="sessionFactory"/>
- </property>
- </bean>
- <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
- <property name="interceptors">
- <list>
- <ref bean="openSessionInViewInterceptor"/>
- </list>
- </property>
- <property name="mappings">
- ...
- </property>
- </bean> ... </beans>
<beans>
<bean name="openSessionInViewInterceptor" class="org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor">
<property name="sessionFactory">
<ref bean="sessionFactory"/>
</property>
</bean>
<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="openSessionInViewInterceptor"/>
</list>
</property>
<property name="mappings">
...
</property>
</bean> ... </beans>
OpenSessionInViewFilter配置
- <web-app>
- ...
- <filter>
- <filter-name>hibernateFilter</filter-name>
- <filter-class> org.springframework.orm.hibernate3.support.OpenSessionInViewFilter </filter-class>
-
- <init-param>
- <param-name>singleSession</param-name>
- <param-value>true</param-value>
- </init-param>
- </filter> ... <filter-mapping>
- <filter-name>hibernateFilter</filter-name>
- <url-pattern>*.do</url-pattern>
- </filter-mapping> ... </web-app>
<web-app>
...
<filter>
<filter-name>hibernateFilter</filter-name>
<filter-class> org.springframework.orm.hibernate3.support.OpenSessionInViewFilter </filter-class>
<!-- singleSession默認為true,若設為false則等于沒用OpenSessionInView -->
<init-param>
<param-name>singleSession</param-name>
<param-value>true</param-value>
</init-param>
</filter> ... <filter-mapping>
<filter-name>hibernateFilter</filter-name>
<url-pattern>*.do</url-pattern>
</filter-mapping> ... </web-app>
很多人在使用OpenSessionInView過程中提及一個錯誤:
org.springframework.dao.InvalidDataAccessApiUsageException: Write operations are not allowed in read-only mode (FlushMode.NEVER) - turn your Session into FlushMode.AUTO or remove 'readOnly' marker from transaction definition
看看OpenSessionInViewFilter里的幾個方法
-
-
- protected void doFilterInternal(HttpServletRequest request,
- HttpServletResponse response,
- FilterChain filterChain) throws ServletException, IOException {
- SessionFactory sessionFactory = lookupSessionFactory();
- logger.debug("Opening Hibernate Session in OpenSessionInViewFilter");
- Session session = getSession(sessionFactory);
- TransactionSynchronizationManager.bindResource(
- sessionFactory, new SessionHolder(session));
- try {
- filterChain.doFilter(request, response);
- }
- finally {
- TransactionSynchronizationManager.unbindResource(sessionFactory);
- logger.debug("Closing Hibernate Session in OpenSessionInViewFilter");
- closeSession(session, sessionFactory);
- }
- }
- protected Session getSession(SessionFactory sessionFactory)
- throws DataAccessResourceFailureException {
- Session session = SessionFactoryUtils.getSession(sessionFactory, true);
- session.setFlushMode(FlushMode.NEVER);
- return session;
- }
- protected void closeSession(Session session,
- SessionFactory sessionFactory)throws CleanupFailureDataAccessException {
- SessionFactoryUtils.closeSessionIfNecessary(session, sessionFactory);
- }
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
SessionFactory sessionFactory = lookupSessionFactory();
logger.debug("Opening Hibernate Session in OpenSessionInViewFilter");
Session session = getSession(sessionFactory);
TransactionSynchronizationManager.bindResource(
sessionFactory, new SessionHolder(session));
try {
filterChain.doFilter(request, response);
}
finally {
TransactionSynchronizationManager.unbindResource(sessionFactory);
logger.debug("Closing Hibernate Session in OpenSessionInViewFilter");
closeSession(session, sessionFactory);
}
}
protected Session getSession(SessionFactory sessionFactory)
throws DataAccessResourceFailureException {
Session session = SessionFactoryUtils.getSession(sessionFactory, true);
session.setFlushMode(FlushMode.NEVER);
return session;
}
protected void closeSession(Session session,
SessionFactory sessionFactory)throws CleanupFailureDataAccessException {
SessionFactoryUtils.closeSessionIfNecessary(session, sessionFactory);
}
關于綁定session的方式,通過看spring里TransactionSynchronizationManager的實現,發現:它維護一個
java.lang.ThreadLocal類型的resources,resources負責持有線程局部變量,這里resources持有的是一個 HashMap,通過TransactionSynchronizationManager.bindResource()方法在map里綁定和線程相關的所有變量到他們的標識上,包括如上所述的綁定在sessionFactory上的線程局部session。sessionHolder只不過是存放可以 hold一個session并可以和transtaction同步的容器。可以看到 OpenSessionInViewFilter在getSession的時候,會把獲取回來的session的flush mode 設為FlushMode.NEVER。然后把該sessionFactory綁定到 TransactionSynchronizationManager,使request的整個過程都使用同一個session,在請求過后再接除該 sessionFactory的綁定,最后closeSessionIfNecessary根據該session是否已和transaction綁定來決定是否關閉session。綁定以后,就可以防止每次不會新開一個Session呢?看看HibernateDaoSupport的情況:
-
- public final void setSessionFactory(SessionFactory sessionFactory) {
- this.hibernateTemplate = new HibernateTemplate(sessionFactory);
- }
- rotected final HibernateTemplate getHibernateTemplate() {
- return hibernateTemplate;
-
public final void setSessionFactory(SessionFactory sessionFactory) {
this.hibernateTemplate = new HibernateTemplate(sessionFactory);
}
protected final HibernateTemplate getHibernateTemplate() {
return hibernateTemplate;
}
我們的DAO將使用這個template進行操作.
-
- public abstract class BaseHibernateObjectDao
- extends HibernateDaoSupportimplements BaseObjectDao {
- protected BaseEntityObject getByClassId(final long id) {
- BaseEntityObject obj =(BaseEntityObject)getHibernateTemplate().execute(new HibernateCallback() {
- public Object doInHibernate(Session session)
- throws HibernateException{
- return session.get(getPersistentClass(),new Long(id));
- }
- }
- );
- return obj;
- }
- public void save(BaseEntityObject entity) {
- getHibernateTemplate().saveOrUpdate(entity);
- }
- public void remove(BaseEntityObject entity) {
- try {
- getHibernateTemplate().delete(entity);
- } catch (Exception e) {
- throw new FlexEnterpriseDataAccessException(e);
- }
- }
- public void refresh(final BaseEntityObject entity) {
- getHibernateTemplate().execute(new HibernateCallback(){
- public Object doInHibernate(Session session)
- throws HibernateException {
- session.refresh(entity);
- return null;
- }
- }
- );
- }
- public void replicate(final Object entity) {
- getHibernateTemplate().execute(new HibernateCallback(){
- public Object doInHibernate(Session session)
- throws HibernateException{
- session.replicate(entity,ReplicationMode.OVERWRITE);
- eturn null;
- }
- });
- }
- }
public abstract class BaseHibernateObjectDao
extends HibernateDaoSupportimplements BaseObjectDao {
protected BaseEntityObject getByClassId(final long id) {
BaseEntityObject obj =(BaseEntityObject)getHibernateTemplate().execute(new HibernateCallback() {
public Object doInHibernate(Session session)
throws HibernateException{
return session.get(getPersistentClass(),new Long(id));
}
}
);
return obj;
}
public void save(BaseEntityObject entity) {
getHibernateTemplate().saveOrUpdate(entity);
}
public void remove(BaseEntityObject entity) {
try {
getHibernateTemplate().delete(entity);
} catch (Exception e) {
throw new FlexEnterpriseDataAccessException(e);
}
}
public void refresh(final BaseEntityObject entity) {
getHibernateTemplate().execute(new HibernateCallback(){
public Object doInHibernate(Session session)
throws HibernateException {
session.refresh(entity);
return null;
}
}
);
}
public void replicate(final Object entity) {
getHibernateTemplate().execute(new HibernateCallback(){
public Object doInHibernate(Session session)
throws HibernateException{
session.replicate(entity,ReplicationMode.OVERWRITE);
eturn null;
}
});
}
}
而HibernateTemplate試圖每次在execute之前去獲得Session,執行完就力爭關閉Session
-
- public Object execute(HibernateCallback action) throws DataAccessException {
- Session session = (!this.allowCreate)SessionFactoryUtils.getSession(getSessionFactory(),
- false);
- SessionFactoryUtils.getSession(getSessionFactory(),
- getEntityInterceptor(),
- getJdbcExceptionTranslator()));
- boolean existingTransaction = TransactionSynchronizationManager.hasResource(
- getSessionFactory());
- if (!existingTransaction && getFlushMode() == FLUSH_NEVER) {
- session.setFlushMode(FlushMode.NEVER);
- }
- try {
- Object result = action.doInHibernate(session);
- flushIfNecessary(session, existingTransaction);
- return result;
- }
- catch (HibernateException ex) {
- throw convertHibernateAccessException(ex);
- }
- finally {
- SessionFactoryUtils.closeSessionIfNecessary(
- session, getSessionFactory());
- }
- }
public Object execute(HibernateCallback action) throws DataAccessException {
Session session = (!this.allowCreate)SessionFactoryUtils.getSession(getSessionFactory(),
false);
SessionFactoryUtils.getSession(getSessionFactory(),
getEntityInterceptor(),
getJdbcExceptionTranslator()));
boolean existingTransaction = TransactionSynchronizationManager.hasResource(
getSessionFactory());
if (!existingTransaction && getFlushMode() == FLUSH_NEVER) {
session.setFlushMode(FlushMode.NEVER);
}
try {
Object result = action.doInHibernate(session);
flushIfNecessary(session, existingTransaction);
return result;
}
catch (HibernateException ex) {
throw convertHibernateAccessException(ex);
}
finally {
SessionFactoryUtils.closeSessionIfNecessary(
session, getSessionFactory());
}
}
而這個SessionFactoryUtils能否得到當前的session以及closeSessionIfNecessary是否真正關閉 session,取決于這個session是否用sessionHolder和這個sessionFactory在我們最開始提到的 TransactionSynchronizationManager綁定。
-
- public static void closeSessionIfNecessary(Session session,
- SessionFactory sessionFactory)
- throws CleanupFailureDataAccessException {
- if (session == null || TransactionSynchronizationManager.hasResource(sessionFactory)) {
- return;
- }
- logger.debug("Closing Hibernate session");
- try {
- session.close();
- } catch (JDBCException ex) {
- throw new CleanupFailureDataAccessException("Could not close Hibernate session",
- ex.getSQLException());
- } catch (HibernateException ex) {
- throw new CleanupFailureDataAccessException("Could not close Hibernate session",
- ex);
- }
- }
public static void closeSessionIfNecessary(Session session,
SessionFactory sessionFactory)
throws CleanupFailureDataAccessException {
if (session == null || TransactionSynchronizationManager.hasResource(sessionFactory)) {
return;
}
logger.debug("Closing Hibernate session");
try {
session.close();
} catch (JDBCException ex) { // SQLException underneath
throw new CleanupFailureDataAccessException("Could not close Hibernate session",
ex.getSQLException());
} catch (HibernateException ex) {
throw new CleanupFailureDataAccessException("Could not close Hibernate session",
ex);
}
}
在這個過程中,若HibernateTemplate 發現自當前session有不是readOnly的transaction,就會獲取到FlushMode.AUTO Session,使方法擁有
寫權限。也即是,如果有不是readOnly的transaction就可以由Flush.NEVER轉為 Flush.AUTO,擁有insert,update,delete操作權限,如果沒有transaction,并且沒有另外人為地設flush model的話,則doFilter的整個過程都是Flush.NEVER。所以受transaction保護的方法有寫權限,沒受保護的則沒有。
可能的解決方式有:
1、將singleSession設為false,這樣只要改web.xml,缺點是Hibernate Session的Instance可能會大增,使用的JDBC Connection量也會大增,如果Connection Pool的maxPoolSize設得太小,很容易就出問題。
2、在控制器中自行管理Session的FlushMode,麻煩的是每個有Modify的Method都要多幾行程式。
session.setFlushMode(FlushMode.AUTO);
session.update(user);
session.flush();
3、Extend OpenSessionInViewFilter,Override protected Session getSession(SessionFactory sessionFactory),將FlushMode直接改為Auto。
4、讓方法受Spring的事務控制。這就是常使用的方法: 采用spring的事務聲明,使方法受transaction控制
-
- <bean id="baseTransaction" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
- abstract="true">
- <property name="transactionManager" ref="transactionManager"/>
- <property name="proxyTargetClass" value="true"/>
- <property name="transactionAttributes">
- <props>
- <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
- <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
- <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
- <prop key="save*">PROPAGATION_REQUIRED</prop>
- <prop key="add*">PROPAGATION_REQUIRED</prop>
- <prop key="update*">PROPAGATION_REQUIRED</prop>
- <prop key="remove*">PROPAGATION_REQUIRED</prop>
- </props>
- </property>
- </bean>
- <bean id="userService" parent="baseTransaction">
- <property name="target">
- <bean class="com.phopesoft.security.service.impl.UserServiceImpl"/>
- </property>
- </bean>
<bean id="baseTransaction" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
abstract="true">
<property name="transactionManager" ref="transactionManager"/>
<property name="proxyTargetClass" value="true"/>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="save*">PROPAGATION_REQUIRED</prop>
<prop key="add*">PROPAGATION_REQUIRED</prop>
<prop key="update*">PROPAGATION_REQUIRED</prop>
<prop key="remove*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
<bean id="userService" parent="baseTransaction">
<property name="target">
<bean class="com.phopesoft.security.service.impl.UserServiceImpl"/>
</property>
</bean>
對于上例,則以save,add,update,remove開頭的方法擁有可寫的事務,如果當前有某個方法,如命名為importExcel(),則因沒有transaction而沒有寫權限,這時若方法內有insert,update,delete操作的話,則需要手動設置flush model為Flush.AUTO,如 session.setFlushMode(FlushMode.AUTO); session.save(user); session.flush();
盡管Open Session In View看起來還不錯,其實副作用不少。看回上面OpenSessionInViewFilter的doFilterInternal方法代碼,這個方法實際上是被父類的doFilter調用的,因此,我們可以大約了解的OpenSessionInViewFilter調用流程: request(請求)->open session并開始transaction->controller->View(Jsp)->結束transaction并 close session.
一切看起來很正確,尤其是在本地開發測試的時候沒出現問題,但試想下如果流程中的某一步被阻塞的話,那在這期間connection就一直被占用而不釋放。最有可能被阻塞的就是在寫Jsp這步,一方面可能是頁面內容大,response.write的時間長,另一方面可能是網速慢,服務器與用戶間傳輸時間久。當大量這樣的情況出現時,就有連接池連接不足,造成頁面假死現象。 Open Session In View是個雙刃劍,放在公網上內容多流量大的網站請慎用。
另外:這樣會產生一點危險性,畢竟把數據庫訪問的環境放到了表現層。(:用VO)
Hibernate是對JDBC的輕量級對象封裝,Hibernate本身是不具備Transaction處理功能的,Hibernate的 Transaction實際上是底層的JDBC Transaction的封裝,或者是JTA Transaction的封裝,下面我們詳細的分析:
Hibernate可以配置為JDBCTransaction或者是JTATransaction,這取決于你在hibernate.properties中的配置:
引用
#hibernate.transaction.factory_classnet.sf.hibernate.transaction.JTATransactionFactory #hibernate.transaction.factory_classnet.sf.hibernate.transaction.JDBCTransactionFactory
如果你什么都不配置,默認情況下使用JDBCTransaction,如果你配置為:
引用
hibernate.transaction.factory_classnet.sf.hibernate.transaction.JTATransactionFactory
將使用JTATransaction,不管你準備讓Hibernate使用JDBCTransaction,還是JTATransaction,我的忠告就是
什么都不配,將讓它保持默認狀態,如下:
引用
#hibernate.transaction.factory_classnet.sf.hibernate.transaction.JTATransactionFactory #hibernate.transaction.factory_classnet.sf.hibernate.transaction.JDBCTransactionFactory
在下面的分析中我會給出原因。
一、JDBC Transaction
看看使用JDBC Transaction的時候我們的代碼例子:
-
- Session session = sf.openSession();
- Transaction tx = session.beginTransactioin();
- ... session.flush();
- tx.commit();
- session.close();
Session session = sf.openSession();
Transaction tx = session.beginTransactioin();
... session.flush();
tx.commit();
session.close();
這是默認的情況,當你在代碼中使用Hibernate的Transaction的時候實際上就是JDBCTransaction。那么JDBCTransaction究竟是什么東西呢?來看看源代碼就清楚了: Hibernate2.0.3源代碼中的類net.sf.hibernate.transaction.JDBCTransaction:
-
- public void begin() throws HibernateException {
- ...
- if (toggleAutoCommit) session.connection().setAutoCommit(false);
- ...
- }
public void begin() throws HibernateException {
...
if (toggleAutoCommit) session.connection().setAutoCommit(false);
...
}
這是啟動Transaction的方法,看到 connection().setAutoCommit(false) 了嗎?是不是很熟悉?
再來看
-
- public void commit() throws HibernateException {
- ...
- try {
- if (
- session.getFlushMode()!=FlushMode.NEVER )
- session.flush();
- try {
- session.connection().commit();
- committed = true;
- }
- ...
- toggleAutoCommit();
- }
public void commit() throws HibernateException {
...
try {
if (
session.getFlushMode()!=FlushMode.NEVER )
session.flush();
try {
session.connection().commit();
committed = true;
}
...
toggleAutoCommit();
}
這是提交方法,看到connection().commit() 了嗎?下面就不用我多說了,這個類代碼非常簡單易懂,通過閱讀使我們明白Hibernate的Transaction都在干了些什么?我現在把用 Hibernate寫的例子翻譯成JDBC,大家就一目了然了:
-
- Connection conn = ...;
- <--- session = sf.openSession();
- conn.setAutoCommit(false);
- <--- tx = session.beginTransactioin();
- ...
- <--- ... conn.commit();
- <--- tx.commit();
- (對應左邊的兩句) conn.setAutoCommit(true);
- conn.close();
- <--- session.close();
Connection conn = ...;
<--- session = sf.openSession();
conn.setAutoCommit(false);
<--- tx = session.beginTransactioin();
...
<--- ... conn.commit();
<--- tx.commit();
(對應左邊的兩句) conn.setAutoCommit(true);
conn.close();
<--- session.close();
看明白了吧,Hibernate的JDBCTransaction根本就是conn.commit而已,根本毫無神秘可言,只不過在Hibernate 中,Session打開的時候,就會自動conn.setAutoCommit(false),不像一般的JDBC,默認都是true,所以你最后不寫 commit也沒有關系,由于Hibernate已經把AutoCommit給關掉了,所以用Hibernate的時候,你在程序中不寫 Transaction的話,數據庫根本就沒有反應。
二、JTATransaction
如果你在EJB中使用Hibernate,或者準備用JTA來管理跨Session的長事務,那么就需要使用JTATransaction,先看一個例子:
-
- javax.transaction.UserTransaction tx = new InitialContext().lookup("javax.transaction.UserTransaction");
- Session s1 = sf.openSession();
- ...
- s1.flush();
- s1.close();
- ...
- Session s2 = sf.openSession();
- ...
- s2.flush();
- s2.close();
- tx.commit();
javax.transaction.UserTransaction tx = new InitialContext().lookup("javax.transaction.UserTransaction");
Session s1 = sf.openSession();
...
s1.flush();
s1.close();
...
Session s2 = sf.openSession();
...
s2.flush();
s2.close();
tx.commit();
這是標準的使用JTA的代碼片斷,Transaction是跨Session的,它的生命周期比Session要長。如果你在EJB中使用 Hibernate,那么是最簡單不過的了,你什么Transaction代碼統統都不要寫了,直接在EJB的部署描述符上配置某某方法是否使用事務就可以了。 現在我們來分析一下JTATransaction的源代碼,
net.sf.hibernate.transaction.JTATransaction:
-
- public void begin(InitialContext context,
- ...
- ...
- ut = (UserTransaction) context.lookup(utName);
- ...
public void begin(InitialContext context,
...
...
ut = (UserTransaction) context.lookup(utName);
...
看清楚了嗎? 和我上面寫的代碼 “tx = new Initial Context?().lookup("javax.transaction.UserTransaction"); ”是不是完全一樣?
-
- public void commit()
- ...
- ...
- if (newTransaction)
- ut.commit();
- ...
public void commit()
...
...
if (newTransaction)
ut.commit();
...
JTATransaction的控制稍微復雜,不過仍然可以很清楚的看出來Hibernate是如何封裝JTA的Transaction代碼的。 但是你現在是否看到了什么問題? 仔細想一下,Hibernate Transaction是從Session中獲得的,tx = session.beginTransaction(),最后要先提交tx,然后再session.close,這完全符合JDBC的 Transaction的操作順序,但是這個順序是和JTA的Transactioin操作順序徹底矛盾的!!! JTA是先啟動Transaction,然后啟動Session,關閉Session,最后提交Transaction,因此當你使用JTA的 Transaction的時候,那么就千萬不要使用Hibernate的Transaction,而是應該像我上面的JTA的代碼片斷那樣使用才行。
總結:
1、在JDBC上使用Hibernate 必須寫上Hibernate Transaction代碼,否則數據庫沒有反應。此時Hibernate的Transaction就是Connection.commit而已;
2、在JTA上使用Hibernate 寫JTA的Transaction代碼,不要寫Hibernate的Transaction代碼,否則程序會報錯;
3、在EJB上使用Hibernate 什么Transactioin代碼都不要寫,在EJB的部署描述符里面配置
|---CMT(Container Managed Transaction) |
|---BMT(Bean Managed Transaction) |
|----JDBC Transaction |
|----JTA Transaction
關于session:
1. servlet的session機制基于cookies,關閉瀏覽器的cookies則session失效即不能用網站的登錄功能。
2. Hibernate Session.
1>. session 清理緩存時,按照以下順序執行SQL語句:
session.save()的實體insert
實體的update
對集合的delete
集合元素的delete,update,insert
集合的insert
session.delete()的先后,執行實體的delete
2>. 默認時,session在以下時間點清理緩存: net.sf.hibernate.Transaction.commit():先清理緩存,再向數據庫提交事務Session.find()或iterate()時,若緩存中持久化對象的屬性發生了變化,就會先清緩存,以保證查詢結果正確
3>. Session的commit()和flush()的區別:
flush()只執行SQL語句,不提交事務;commit()先調用flush(),再提交事務
4>. Session.setFlushMode()用于設定清理緩存的時間點:
清理緩存的模式 Session的查詢方法 Session.commit() Session.flush() FlushMode.AUTO 清理清理清理 FlushMode.COMMIT 不清理清理清理 FlushMode.NEVER 不清理不清理清