How do I clean up LOB values in Hibernate UserType

2019-07-19 06:06发布

问题:

I am running Oracle 11.2.0.3 and am attempting to create a workable UserType that maps XMLType or SQLXML columns.

Existing solutions found online all have two problems:

  1. XMLType values are LOB values, so they must be free() prior to Connection.close() or they will leak both database resources and heap memory in Java.

  2. XML values fetched from these columns are connected objects; unless they are copied via a deep copy, they are gone after the connection closes.

So, I wrote this classes at the bottom to store XMLType objects.
My question is this - since these are LOB values, they must be freed after the transaction commits, but before the connection closes. Is there a way to get a Hibernate UserType to do this? Ignore the fact that this is an SQLXML object for a moment - if it were a BLOB or a CLOB, and I had the same requirement (that someone has to call free() after commit but before close()), how would I do it?

Thank you for reading through all this...

package com.mycomp.types;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;

import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;

import oracle.xdb.XMLType;
import oracle.xml.binxml.BinXMLDecoder;
import oracle.xml.binxml.BinXMLException;
import oracle.xml.binxml.BinXMLStream;
import oracle.xml.jaxp.JXSAXTransformerFactory;
import oracle.xml.jaxp.JXTransformer;
import oracle.xml.parser.v2.XMLDOMImplementation;
import oracle.xml.parser.v2.XMLDocument;
import oracle.xml.scalable.InfosetReader;

import org.apache.commons.lang.ObjectUtils;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.usertype.UserType;
import org.w3c.dom.DOMException;

/**
 * This class encapsulates the XMLDocument class into a database XMLType.
 * It is used to allow Hibernate entities to use XMLDocument transparently 
 * for persistence as XMLTypes in an Oracle database.
 * 
 * @author bmarke
 *
 */
public class HibernateXMLType implements UserType
{
    private static final String CAST_EXCEPTION_TEXT = " cannot be cast to a oracle.xml.parser.v2.XMLDocument.";

    @Override
    public int[] sqlTypes()
    {
        return new int[] { Types.SQLXML };
    }

    @Override
    public Class<?> returnedClass()
    {
        return XMLDocument.class;
    }

    @Override
    public boolean equals(Object x, Object y) throws HibernateException
    {
        if (x == y)
        {
            return true;
        }

        if (!(x instanceof XMLDocument && y instanceof XMLDocument))
        {
            throw new HibernateException(x.getClass().toString()
                    + CAST_EXCEPTION_TEXT);
        }

        return ObjectUtils.equals(x, y);
    }

    @Override
    public int hashCode(Object x) throws HibernateException
    {
        if (!(x instanceof XMLDocument))
        {
            throw new HibernateException(x.getClass().toString()
                    + CAST_EXCEPTION_TEXT);
        }

        return x.hashCode();
    }

    @Override
    public Object nullSafeGet(ResultSet rs, String[] names,
            SessionImplementor session, Object owner)
            throws HibernateException, SQLException
    {
        XMLType xmlData = (XMLType) rs.getSQLXML(names[0]);
        XMLDocument doc = null;
        XMLDocument toReturn = null;
        BinXMLStream stream = null;
        InfosetReader reader = null;

        if (xmlData == null)
        {
            doc = null;
            toReturn = null;
        }
        else
        {
            try
            {
                stream = xmlData.getBinXMLStream();
                BinXMLDecoder decoder = stream.getDecoder();
                reader = decoder.getReader();

                XMLDOMImplementation domImpl = new XMLDOMImplementation();

                domImpl.setAttribute(XMLDocument.SCALABLE_DOM, true);
                domImpl.setAttribute(XMLDocument.ACCESS_MODE,
                        XMLDocument.UPDATEABLE);

                doc = (XMLDocument) domImpl.createDocument(reader);

                toReturn = (XMLDocument)deepCopy(doc);
            }
            catch (IllegalArgumentException e)
            {
                throw new HibernateException(e);
            }
            catch (DOMException e)
            {
                throw new HibernateException(e);
            }
            catch (BinXMLException e)
            {
                throw new HibernateException(e);
            }
            finally
            {
                if(doc != null)
                {
                    doc.freeNode();
                }

                if(reader != null)
                {
                    reader.close();
                }

                if(stream != null)
                {
                    stream.close();
                }

                if(xmlData != null)
                {
                    xmlData.close();
                }
            }
        }

        return toReturn;
    }

    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index,
            SessionImplementor session) throws HibernateException, SQLException
    {
        if( value == null )
        {
            st.setNull(index, Types.SQLXML);
        }
        else if( !(value instanceof XMLDocument) )
        {
            throw new HibernateException(value.getClass().toString()
                    + CAST_EXCEPTION_TEXT);
        }
        else
        {
            XMLDocument xml = (XMLDocument) value;
            XMLType xmlData = null;

            try
            {
                xmlData = new XMLType(st.getConnection().getMetaData().getConnection(), xml);

                st.setSQLXML(index, xmlData);
            }
            finally
            {
                if(xmlData != null)
                {
                    xmlData.close();
                }
            }
        }
    }

    @Override
    public Object deepCopy(Object value) throws HibernateException
    {
        XMLDocument orig = (XMLDocument)value;

        DOMResult result;

        try
        {
            JXSAXTransformerFactory tfactory = new oracle.xml.jaxp.JXSAXTransformerFactory();
            JXTransformer tx   = (JXTransformer)tfactory.newTransformer();

            DOMSource source = new DOMSource(orig);
            result = new DOMResult();
            tx.transform(source,result);

            return (XMLDocument)result.getNode();
        }
        catch (Exception e)
        {   
            throw new HibernateException(e);
        }
    }

    @Override
    public boolean isMutable()
    {
        return true;
    }

    @Override
    public Serializable disassemble(Object value) throws HibernateException
    {
        XMLDocument doc = (XMLDocument) deepCopy(value);

        return doc;
    }

    @Override
    public Object assemble(Serializable cached, Object owner)
            throws HibernateException
    {
        XMLDocument doc = (XMLDocument) deepCopy(cached);

        return doc;
    }

    @Override
    public Object replace(Object original, Object target, Object owner)
            throws HibernateException
    {
        return deepCopy(original);
    }
}

(Yes, the above is Oracle-specific... for those of you looking for a DBMS-agnostic class, it looks like this, but note the warning, and I haven't tested it):

package com.mycomp.types;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLXML;
import java.sql.Types;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;

import org.apache.commons.lang.ObjectUtils;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.usertype.UserType;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;

/**
 * This class encapsulates the XMLDocument class into a database XMLType.
 * It is used to allow Hibernate entities to use XMLDocument transparently 
 * for persistence as XMLTypes in an Oracle database.
 * 
 * @author bmarke
 *
 */
public class HibernateSQLXML implements UserType
{
    private static final String CAST_EXCEPTION_TEXT = " cannot be cast to a oracle.xml.parser.v2.XMLDocument.";

    @Override
    public int[] sqlTypes()
    {
        return new int[] { Types.SQLXML };
    }

    @Override
    public Class<?> returnedClass()
    {
        return SQLXML.class;
    }

    @Override
    public boolean equals(Object x, Object y) throws HibernateException
    {
        if (x == y)
        {
            return true;
        }

        if (!(x instanceof SQLXML && y instanceof SQLXML))
        {
            throw new HibernateException(x.getClass().toString()
                    + CAST_EXCEPTION_TEXT);
        }

        return ObjectUtils.equals(x, y);
    }

    @Override
    public int hashCode(Object x) throws HibernateException
    {
        if (!(x instanceof SQLXML))
        {
            throw new HibernateException(x.getClass().toString()
                    + CAST_EXCEPTION_TEXT);
        }

        return x.hashCode();
    }

    @Override
    public Object nullSafeGet(ResultSet rs, String[] names,
            SessionImplementor session, Object owner)
            throws HibernateException, SQLException
    {
        SQLXML xmlData = rs.getSQLXML(names[0]);
        Document toReturn = null;

        if (xmlData == null)
        {
            toReturn = null;
        }
        else
        {
            try
            {
                DOMSource source = xmlData.getSource(DOMSource.class);

                toReturn = (Document)deepCopy(source);
            }
            catch (IllegalArgumentException e)
            {
                throw new HibernateException(e);
            }
            catch (DOMException e)
            {
                throw new HibernateException(e);
            }
            finally
            {   
                if(xmlData != null)
                {
                    xmlData.free();
                }
            }
        }

        return toReturn;
    }

    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index,
            SessionImplementor session) throws HibernateException, SQLException
    {
        if( value == null )
        {
            st.setNull(index, Types.SQLXML);
        }
        else if( !(value instanceof Document) )
        {
            throw new HibernateException(value.getClass().toString()
                    + CAST_EXCEPTION_TEXT);
        }
        else
        {
            Document xml = (Document) value;
            SQLXML xmlData = null;

            try
            {
                xmlData = st.getConnection().createSQLXML();

                DOMResult res = xmlData.setResult(DOMResult.class);

                res.setNode(xml);

                st.setSQLXML(index, xmlData);
            }
            finally
            {
                if(xmlData != null)
                {
                    xmlData.free();
                }
            }
        }
    }

    public Object deepCopy(DOMSource orig) throws HibernateException
    {   
        DOMResult result;

        try
        {
            TransformerFactory tfactory = TransformerFactory.newInstance();
            Transformer tx   = tfactory.newTransformer();

            result = new DOMResult();
            tx.transform(orig,result);

            return (Document)result.getNode();
        }
        catch (Exception e)
        {   
            throw new HibernateException(e);
        }
    }

    @Override
    public Object deepCopy(Object value) throws HibernateException
    {
        Document orig = (Document)value;

        DOMResult result;

        try
        {
            TransformerFactory tfactory = TransformerFactory.newInstance();
            Transformer tx   = tfactory.newTransformer();

            DOMSource source = new DOMSource(orig);

            result = new DOMResult();
            tx.transform(source,result);

            return (Document)result.getNode();
        }
        catch (Exception e)
        {   
            throw new HibernateException(e);
        }
    }

    @Override
    public boolean isMutable()
    {
        return true;
    }

    @Override
    public Serializable disassemble(Object value) throws HibernateException
    {
        //NOTE: We're making a really ugly assumption here, that the particular parser 
        //impelementation creates a Document object that is Serializable.  In the case
        //of the Oracle XDK parser, it is, but it may not be for the default Xerces 
        //implementation - you have been warned.
        Serializable doc = (Serializable) deepCopy(value);

        return doc;
    }

    @Override
    public Object assemble(Serializable cached, Object owner)
            throws HibernateException
    {
        Document doc = (Document) deepCopy(cached);

        return doc;
    }

    @Override
    public Object replace(Object original, Object target, Object owner)
            throws HibernateException
    {
        return deepCopy(original);
    }
}

回答1:

I figured Hibernate would have some way to add an ActionListener sort of thing that can do some work after a commit was completed. Someone from the #hibernate room on freenode suggested we try to use a AfterTransactionCompletionProcess to do what we need.

So the next obvious question is... where is an example I can use? I opened a SOF question and answered it myself: How to use org.hibernate.action.spi.AfterTransactionCompletionProcess?

So using this example plus the HibernateXMLType class you presented, we can now register an AfterTransactionCompletionProcess process so that it gets called to hopefully meet your requirement: "Must get called after the transaction commits, but before the connection closes."

Below is the source code.

Please see the comment where I got kind of stuck. I don't know exactly what to call from the entity to clear the memory manually. I'm wondering how I can call the free() method on the java.sql.SQLXML object in the entity from the doAfterTransactionCompletion method... thus eliminating the memory leak.

I'll take this back up in the morning and see if I can figure that out. Maybe this is all you need to get the solution? If so, great!

HibernateTest.java

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.action.spi.AfterTransactionCompletionProcess;
import org.hibernate.cfg.Configuration;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.event.spi.PostInsertEvent;
import org.hibernate.event.spi.PostInsertEventListener;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;

public class HibernateTest {
    public static void main(String [] args) {
        PostInsertTransactionBoundaryListener listener = new PostInsertTransactionBoundaryListener();
        Configuration configuration = new Configuration();
        configuration.configure();
        ServiceRegistry serviceRegistry = new ServiceRegistryBuilder().applySettings(configuration.getProperties()).buildServiceRegistry();
        EventListenerRegistry registry = serviceRegistry.getService(EventListenerRegistry.class);
        registry.getEventListenerGroup(EventType.POST_COMMIT_INSERT).appendListener(listener);
        SessionFactory sessionFactory = configuration.buildSessionFactory(serviceRegistry);        
        Session session = sessionFactory.openSession();
        session.getTransaction().begin();

        TestEntity entity = new TestEntity();
        session.save(entity);
        session.getTransaction().commit();
        session.close();

    }
    private static class PostInsertTransactionBoundaryListener implements PostInsertEventListener {
        private static final long serialVersionUID = 1L;
        public void onPostInsert(final PostInsertEvent event) {
            event.getSession().getActionQueue().registerProcess(new AfterTransactionCompletionProcess() {
                public void doAfterTransactionCompletion(boolean success, SessionImplementor session) {
                    TestEntity testEntity = (TestEntity)event.getEntity();
                    if (testEntity != null) {
                        // How can I free the memory here to avoid the memory leak???
                    }
                }
            });
        }

    }
}

TestEntity.java

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "TEST")
public class TestEntity {
    @Id
    @GeneratedValue
    private Integer id;

    private HibernateXMLType xml;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public HibernateXMLType getXml() {
        return xml;
    }

    public void setXml(HibernateXMLType xml) {
        this.xml = xml;
    }

}