我的方向和要求
- 实体应将 XML 存储为字符串 (java.lang.String)
- 数据库应在 XDB.XMLType 列中保留 XML
- 允许索引和更高效的 xpath/ExtractValue/xquery 类型查询
- 整合我上周找到的十几个部分解决方案
- 工作环境
- 甲骨文 11g r2 x64
- 休眠 4.1.x
- Java 1.7.x x64
- 视窗 7 专业版 x64
分步解决方案
第 1 步:找到 xmlparserv2.jar (~1350kb)
此 jar 是编译步骤 2 所必需的,并且包含在此处的 oracle 安装中:%ORACLE_11G_HOME%/LIB/xmlparserv2.jar
步骤 1.5:找到 xdb6.jar (~257kb)
如果您使用 Oracle 11gR2 11.2.0.2 或更高版本,或者存储为 BINARY XML,这一点至关重要。
为什么?
- 在 11.2.0.2+ 中,XMLType 列默认使用SECUREFILE BINARY XML存储,而早期版本将存储为BASICFILE CLOB
- 旧版本的 xdb*.jar 无法正确解码二进制 xml 并静默失败
- Google Oracle Database 11g 第 2 版 JDBC 驱动程序并下载 xdb6.jar
- 此处概述的二进制 XML 解码问题的诊断和解决方案
第 2 步:为 XMLType 列创建一个休眠用户类型
使用 Oracle 11g 和 Hibernate 4.x,这比听起来容易。
public class HibernateXMLType implements UserType, Serializable {
static Logger logger = Logger.getLogger(HibernateXMLType.class);
private static final long serialVersionUID = 2308230823023l;
private static final Class returnedClass = String.class;
private static final int[] SQL_TYPES = new int[] { oracle.xdb.XMLType._SQL_TYPECODE };
@Override
public int[] sqlTypes() {
return SQL_TYPES;
}
@Override
public Class returnedClass() {
return returnedClass;
}
@Override
public boolean equals(Object x, Object y) throws HibernateException {
if (x == null && y == null) return true;
else if (x == null && y != null ) return false;
else return x.equals(y);
}
@Override
public int hashCode(Object x) throws HibernateException {
return x.hashCode();
}
@Override
public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
XMLType xmlType = null;
Document doc = null;
String returnValue = null;
try {
//logger.debug("rs type: " + rs.getClass().getName() + ", value: " + rs.getObject(names[0]));
xmlType = (XMLType) rs.getObject(names[0]);
if (xmlType != null) {
returnValue = xmlType.getStringVal();
}
} finally {
if (null != xmlType) {
xmlType.close();
}
}
return returnValue;
}
@Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
if (logger.isTraceEnabled()) {
logger.trace(" nullSafeSet: " + value + ", ps: " + st + ", index: " + index);
}
try {
XMLType xmlType = null;
if (value != null) {
xmlType = XMLType.createXML(getOracleConnection(st.getConnection()), (String)value);
}
st.setObject(index, xmlType);
} catch (Exception e) {
throw new SQLException("Could not convert String to XML for storage: " + (String)value);
}
}
@Override
public Object deepCopy(Object value) throws HibernateException {
if (value == null) {
return null;
} else {
return value;
}
}
@Override
public boolean isMutable() {
return false;
}
@Override
public Serializable disassemble(Object value) throws HibernateException {
try {
return (Serializable)value;
} catch (Exception e) {
throw new HibernateException("Could not disassemble Document to Serializable", e);
}
}
@Override
public Object assemble(Serializable cached, Object owner) throws HibernateException {
try {
return (String)cached;
} catch (Exception e) {
throw new HibernateException("Could not assemble String to Document", e);
}
}
@Override
public Object replace(Object original, Object target, Object owner) throws HibernateException {
return original;
}
private OracleConnection getOracleConnection(Connection conn) throws SQLException {
CLOB tempClob = null;
CallableStatement stmt = null;
try {
stmt = conn.prepareCall("{ call DBMS_LOB.CREATETEMPORARY(?, TRUE)}");
stmt.registerOutParameter(1, java.sql.Types.CLOB);
stmt.execute();
tempClob = (CLOB)stmt.getObject(1);
return tempClob.getConnection();
} finally {
if ( stmt != null ) {
try {
stmt.close();
} catch (Throwable e) {}
}
}
}
第 3 步:注释实体中的字段。
我正在使用带有 spring/hibernate 的注释,而不是映射文件,但我想语法会相似。
@Type(type="your.custom.usertype.HibernateXMLType")
@Column(name="attribute_xml", columnDefinition="XDB.XMLTYPE")
private String attributeXml;
第 4 步:处理 Oracle JAR 导致的 appserver/junit 错误
在您的类路径中包含 %ORACLE_11G_HOME%/LIB/xmlparserv2.jar (1350kb) 以解决编译错误后,您现在会从应用程序服务器收到运行时错误...
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd<Line 43, Column 57>: XML-24509: (Error) Duplicated definition for: 'identifiedType'
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd<Line 61, Column 28>: XML-24509: (Error) Duplicated definition for: 'beans'
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd<Line 168, Column 34>: XML-24509: (Error) Duplicated definition for: 'description'
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd<Line 180, Column 29>: XML-24509: (Error) Duplicated definition for: 'import'
... more ...
为什么会出现错误?
xmlparserv2.jar 使用 JAR 服务 API(服务提供者机制)来更改用于 SAXParserFactory、DocumentBuilderFactory 和 TransformerFactory 的默认 javax.xml 类。
它是怎么发生的?
javax.xml.parsers.FactoryFinder 在使用JDK (com.sun.org.*) 中包含的默认实现。
在 xmlparserv2.jar 中存在一个 META-INF/services 目录,javax.xml.parsers.FactoryFinder 类会获取该目录。文件如下:
META-INF/services/javax.xml.parsers.DocumentBuilderFactory (which defines oracle.xml.jaxp.JXDocumentBuilderFactory as the default)
META-INF/services/javax.xml.parsers.SAXParserFactory (which defines oracle.xml.jaxp.JXSAXParserFactory as the default)
META-INF/services/javax.xml.transform.TransformerFactory (which defines oracle.xml.jaxp.JXSAXTransformerFactory as the default)
解决方案?
将所有 3 个切换回来,否则您会看到奇怪的错误。
- javax.xml.parsers.* 修复可见错误
- javax.xml.transform.* 修复了更细微的 XML 解析错误
- 就我而言,使用apache commons 配置读取/写入
解决应用程序服务器启动错误的快速解决方案:JVM Arguments
要覆盖 xmlparserv2.jar 所做的更改,请将以下 JVM 属性添加到您的应用程序服务器启动参数中。java.xml.parsers.FactoryFinder 逻辑将首先检查环境变量。
-Djavax.xml.parsers.SAXParserFactory=com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl -Djavax.xml.parsers.DocumentBuilderFactory=com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl -Djavax.xml.transform.TransformerFactory=com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl
但是,如果您使用 @RunWith(SpringJUnit4ClassRunner.class) 或类似方法运行测试用例,您仍然会遇到错误。
应用程序服务器启动错误和测试用例错误的更好解决方案?2 个选项
选项 1:对应用服务器使用 JVM 参数,对测试用例使用 @BeforeClass 语句
System.setProperty("javax.xml.parsers.DocumentBuilderFactory","com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl");
System.setProperty("javax.xml.parsers.SAXParserFactory","com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl");
System.setProperty("javax.xml.transform.TransformerFactory","com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl");
如果你有很多测试用例,这会变得很痛苦。即使你把它放在一个超级。
选项 2:在项目的编译/运行时类路径中创建自己的服务提供者定义文件,这将覆盖 xmlparserv2.jar 中包含的文件
在 maven spring 项目中,通过在 %PROJECT_HOME%/src/main/resources 目录中创建以下文件来覆盖 xmlparserv2.jar 设置:
%PROJECT_HOME%/src/main/resources/META-INF/services/javax.xml.parsers.DocumentBuilderFactory (which defines com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl as the default)
%PROJECT_HOME%/src/main/resources/META-INF/services/javax.xml.parsers.SAXParserFactory (which defines com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl as the default)
%PROJECT_HOME%/src/main/resources/META-INF/services/javax.xml.transform.TransformerFactory (which defines com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl as the default)
这些文件被应用程序服务器引用(不需要 JVM 参数),并且无需任何代码更改即可解决任何单元测试问题。
完毕。