包含“&”的字符串的 Jaxb2Marshaller 问题
Jaxb2Marshaller problem with String that contains '&'
我在我的 Spring 批处理应用程序中使用 org.springframework.oxm.jaxb.Jaxb2Marshaller
中的 Jaxb2Marshaller
来编组 XML 并注释 类。 Marshaller
的实现是:
@Bean
public Jaxb2Marshaller productMarshaller() {
Map<String, Object> props = new HashMap<String, Object>();
props.put("com.sun.xml.bind.marshaller.CharacterEscapeHandler", new XmlCharacterEscapeHandler());
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(new Class[] {Product.class, TechSpecs.class});
marshaller.setMarshallerProperties(props);
return marshaller;
}
Marshaller
在 StaxEventItemWriter
中使用,实现如下:
@Bean(name = "writer")
@StepScope
public StaxEventItemWriter<Product> writer (
@Value("#{jobParameters['path']}") String path,
@Value("#{stepExecutionContext['currentFile']}") String fileName
) {
Map<String, String> rootElementAttributes = new HashMap<String, String>();
rootElementAttributes.put("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
FileSystemResource file = new FileSystemResource(path + fileName);
return new StaxEventItemWriterBuilder<Product>()
.name("writer")
.version("1.0")
.encoding("UTF-8")
.standalone(false)
.rootTagName("Products")
.rootElementAttributes(rootElementAttributes)
.headerCallback(headerCallback(null, null))
.footerCallback(footerCallback())
.marshaller(productMarshaller())
.resource(file)
.build();
}
现在的问题是,当我 运行 代码时,我得到一个 IndexOutOfBoundsException
。我发现抛出异常是因为我的 Product
对象有一个可能包含 &
的 String 属性。 &
在 XML 中是不允许的,必须转义。
为什么 Jaxb2Marshaller
不自动转义 &
字符?据我了解 Marshaller
应该处理转义字符。
我试图在项目处理器中使用 StringEscapeUtils
转义角色 my self,例如product.setFullName(StringEscapeUtils.escapeXml10(dbExport.getFullName()));
,但这没有帮助。此外,字符串将从 &
更改为 &
,其中还包含一个 &
.
我也尝试使用自己的 CharacterEscapeHandler
实现,但是 marshaller.setMarshallerProperties()
对 Marshaller
没有任何可见的影响。我是否必须以不同方式设置 Marshaller 的属性?
public class XmlCharacterEscapeHandler implements CharacterEscapeHandler {
@Override
public void escape(char[] ch, int start, int length, boolean isAttVal, Writer out) throws IOException {
StringWriter buffer = new StringWriter();
for(int i = start; i < start + length; i++) {
buffer.write(ch[i]);
}
String escapedString = StringEscapeUtils.escapeXml10(buffer.toString());
out.write(escapedString);
}
}
编辑
不幸的是,直到现在我都无法解决我的问题。因此,我从 Jaxb2Marshaller
切换到 XStreamMarshaller
。在这里我遇到了类似的问题。据我所知,底层 XStream
应该使用 PrettyPrintWriter
自动将 &
转换为 &
,如下所述: 这不会发生。对我来说 &
总是有问题。为什么转义不起作用?同样转义字符串本身并强制将其转换为 UTF-8 也无济于事。
最小完整示例
主要:
package com.mwe;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan("com.mwe")
public class Main {
public static void main(String [] args) {
System.exit(SpringApplication.exit(SpringApplication.run(Main.class, args)));
}
}
批处理配置:
package com.mwe;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.batch.item.xml.StaxEventItemWriter;
import org.springframework.batch.item.xml.builder.StaxEventItemWriterBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.oxm.xstream.XStreamMarshaller;
@Configuration
@EnableBatchProcessing
public class BatchConfig {
@Autowired
public JobBuilderFactory jobBuilderFactory;
@Autowired
public StepBuilderFactory stepBuilderFactory;
@Bean
@StepScope
public FlatFileItemReader<Product> reader() {
FlatFileItemReader<Product> reader = new FlatFileItemReader<Product>();
reader.setResource(new FileSystemResource("test.csv"));
DefaultLineMapper<Product> lineMapper = new DefaultLineMapper<>();
lineMapper.setFieldSetMapper(new CustomFieldMapper());
DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
tokenizer.setDelimiter("|");
tokenizer.setNames(new String[] {"ID", "NAME"});
lineMapper.setLineTokenizer(tokenizer);
reader.setLineMapper(lineMapper);
reader.setLinesToSkip(1);
return reader;
}
@Bean
public ItemProcessor<Product, Xml> processor() {
return new Processor();
}
@Bean
@StepScope
public StaxEventItemWriter<Xml> writer () {
return new StaxEventItemWriterBuilder<Xml>()
.name("writer")
.version("1.0")
.encoding("UTF-8")
.standalone(false)
.rootTagName("products")
.marshaller(getMarshaller())
.resource(new FileSystemResource("test.xml"))
.build();
}
@Bean
public Job job() {
return this.jobBuilderFactory.get("job")
.start(step1())
.build();
}
@Bean
public Step step1() {
return (stepBuilderFactory.get("step1")
.<Product, Xml>chunk(2)
.reader(reader())
.processor(processor())
.writer(writer())
.build());
}
@Bean
public XStreamMarshaller getMarshaller() {
XStreamMarshaller marshaller = new XStreamMarshaller();
marshaller.setEncoding("UTF-8");
return marshaller;
}
}
自定义字段映射器
package com.mwe;
import org.springframework.batch.item.file.mapping.FieldSetMapper;
import org.springframework.batch.item.file.transform.FieldSet;
public class CustomFieldMapper implements FieldSetMapper<Product> {
public Product mapFieldSet(FieldSet fs) {
Product product = new Product();
product.setId(fs.readString("ID"));
product.setName(fs.readString("NAME"));
return product;
}
}
物品处理器:
package com.mwe;
import org.springframework.batch.item.ItemProcessor;
public class Processor implements ItemProcessor<Product, Xml> {
@Override
public Xml process(final Product product) {
Xml xml = new Xml();
xml.setId(Integer.parseInt(product.getId()));
xml.setName(product.getName());
return xml;
}
}
产品:
package com.mwe;
public class Product {
private String id;
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Xml:
package com.mwe;
public class Xml {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
应用程序属性:
# Spring config
spring.main.allow-bean-definition-overriding=true
spring.main.banner-mode=off
spring.batch.initialize-schema=never
# Logging data source
spring.datasource.logging.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.logging.maximum-pool-size=10
spring.datasource.logging.hikar.minimum-idle=1
spring.datasource.logging.hikari.data-source-properties.useUnicode=true
spring.datasource.logging.hikari.data-source-properties.characterEncoding=UTF-8
spring.datasource.logging.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
spring.datasource.logging.hibernate.ddl-auto=none
spring.datasource.url=jdbc:mariadb://localhost:3306/logging?UseUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root
Pom:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.mwe</groupId>
<artifactId>mwe</artifactId>
<version>1</version>
<name>mwe</name>
<description>Minimal working example</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.15</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
test.csv:
"ID"|"NAME"
1|"Product 1"
2|"Product 1 & Addition"
您遇到了这个问题:https://github.com/spring-projects/spring-batch/issues/3745。
此问题将在计划于 2021 年 3 月 18 日发布的 v4.3.2/v4.2.6 中修复。请查看 GitHub 上的 milestone page 以防发布日期发生变化.
我在我的 Spring 批处理应用程序中使用 org.springframework.oxm.jaxb.Jaxb2Marshaller
中的 Jaxb2Marshaller
来编组 XML 并注释 类。 Marshaller
的实现是:
@Bean
public Jaxb2Marshaller productMarshaller() {
Map<String, Object> props = new HashMap<String, Object>();
props.put("com.sun.xml.bind.marshaller.CharacterEscapeHandler", new XmlCharacterEscapeHandler());
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(new Class[] {Product.class, TechSpecs.class});
marshaller.setMarshallerProperties(props);
return marshaller;
}
Marshaller
在 StaxEventItemWriter
中使用,实现如下:
@Bean(name = "writer")
@StepScope
public StaxEventItemWriter<Product> writer (
@Value("#{jobParameters['path']}") String path,
@Value("#{stepExecutionContext['currentFile']}") String fileName
) {
Map<String, String> rootElementAttributes = new HashMap<String, String>();
rootElementAttributes.put("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
FileSystemResource file = new FileSystemResource(path + fileName);
return new StaxEventItemWriterBuilder<Product>()
.name("writer")
.version("1.0")
.encoding("UTF-8")
.standalone(false)
.rootTagName("Products")
.rootElementAttributes(rootElementAttributes)
.headerCallback(headerCallback(null, null))
.footerCallback(footerCallback())
.marshaller(productMarshaller())
.resource(file)
.build();
}
现在的问题是,当我 运行 代码时,我得到一个 IndexOutOfBoundsException
。我发现抛出异常是因为我的 Product
对象有一个可能包含 &
的 String 属性。 &
在 XML 中是不允许的,必须转义。
为什么 Jaxb2Marshaller
不自动转义 &
字符?据我了解 Marshaller
应该处理转义字符。
我试图在项目处理器中使用 StringEscapeUtils
转义角色 my self,例如product.setFullName(StringEscapeUtils.escapeXml10(dbExport.getFullName()));
,但这没有帮助。此外,字符串将从 &
更改为 &
,其中还包含一个 &
.
我也尝试使用自己的 CharacterEscapeHandler
实现,但是 marshaller.setMarshallerProperties()
对 Marshaller
没有任何可见的影响。我是否必须以不同方式设置 Marshaller 的属性?
public class XmlCharacterEscapeHandler implements CharacterEscapeHandler {
@Override
public void escape(char[] ch, int start, int length, boolean isAttVal, Writer out) throws IOException {
StringWriter buffer = new StringWriter();
for(int i = start; i < start + length; i++) {
buffer.write(ch[i]);
}
String escapedString = StringEscapeUtils.escapeXml10(buffer.toString());
out.write(escapedString);
}
}
编辑
不幸的是,直到现在我都无法解决我的问题。因此,我从 Jaxb2Marshaller
切换到 XStreamMarshaller
。在这里我遇到了类似的问题。据我所知,底层 XStream
应该使用 PrettyPrintWriter
自动将 &
转换为 &
,如下所述:&
总是有问题。为什么转义不起作用?同样转义字符串本身并强制将其转换为 UTF-8 也无济于事。
最小完整示例 主要:
package com.mwe;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan("com.mwe")
public class Main {
public static void main(String [] args) {
System.exit(SpringApplication.exit(SpringApplication.run(Main.class, args)));
}
}
批处理配置:
package com.mwe;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.batch.item.xml.StaxEventItemWriter;
import org.springframework.batch.item.xml.builder.StaxEventItemWriterBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.oxm.xstream.XStreamMarshaller;
@Configuration
@EnableBatchProcessing
public class BatchConfig {
@Autowired
public JobBuilderFactory jobBuilderFactory;
@Autowired
public StepBuilderFactory stepBuilderFactory;
@Bean
@StepScope
public FlatFileItemReader<Product> reader() {
FlatFileItemReader<Product> reader = new FlatFileItemReader<Product>();
reader.setResource(new FileSystemResource("test.csv"));
DefaultLineMapper<Product> lineMapper = new DefaultLineMapper<>();
lineMapper.setFieldSetMapper(new CustomFieldMapper());
DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
tokenizer.setDelimiter("|");
tokenizer.setNames(new String[] {"ID", "NAME"});
lineMapper.setLineTokenizer(tokenizer);
reader.setLineMapper(lineMapper);
reader.setLinesToSkip(1);
return reader;
}
@Bean
public ItemProcessor<Product, Xml> processor() {
return new Processor();
}
@Bean
@StepScope
public StaxEventItemWriter<Xml> writer () {
return new StaxEventItemWriterBuilder<Xml>()
.name("writer")
.version("1.0")
.encoding("UTF-8")
.standalone(false)
.rootTagName("products")
.marshaller(getMarshaller())
.resource(new FileSystemResource("test.xml"))
.build();
}
@Bean
public Job job() {
return this.jobBuilderFactory.get("job")
.start(step1())
.build();
}
@Bean
public Step step1() {
return (stepBuilderFactory.get("step1")
.<Product, Xml>chunk(2)
.reader(reader())
.processor(processor())
.writer(writer())
.build());
}
@Bean
public XStreamMarshaller getMarshaller() {
XStreamMarshaller marshaller = new XStreamMarshaller();
marshaller.setEncoding("UTF-8");
return marshaller;
}
}
自定义字段映射器
package com.mwe;
import org.springframework.batch.item.file.mapping.FieldSetMapper;
import org.springframework.batch.item.file.transform.FieldSet;
public class CustomFieldMapper implements FieldSetMapper<Product> {
public Product mapFieldSet(FieldSet fs) {
Product product = new Product();
product.setId(fs.readString("ID"));
product.setName(fs.readString("NAME"));
return product;
}
}
物品处理器:
package com.mwe;
import org.springframework.batch.item.ItemProcessor;
public class Processor implements ItemProcessor<Product, Xml> {
@Override
public Xml process(final Product product) {
Xml xml = new Xml();
xml.setId(Integer.parseInt(product.getId()));
xml.setName(product.getName());
return xml;
}
}
产品:
package com.mwe;
public class Product {
private String id;
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Xml:
package com.mwe;
public class Xml {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
应用程序属性:
# Spring config
spring.main.allow-bean-definition-overriding=true
spring.main.banner-mode=off
spring.batch.initialize-schema=never
# Logging data source
spring.datasource.logging.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.logging.maximum-pool-size=10
spring.datasource.logging.hikar.minimum-idle=1
spring.datasource.logging.hikari.data-source-properties.useUnicode=true
spring.datasource.logging.hikari.data-source-properties.characterEncoding=UTF-8
spring.datasource.logging.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
spring.datasource.logging.hibernate.ddl-auto=none
spring.datasource.url=jdbc:mariadb://localhost:3306/logging?UseUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root
Pom:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.mwe</groupId>
<artifactId>mwe</artifactId>
<version>1</version>
<name>mwe</name>
<description>Minimal working example</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.15</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
test.csv:
"ID"|"NAME"
1|"Product 1"
2|"Product 1 & Addition"
您遇到了这个问题:https://github.com/spring-projects/spring-batch/issues/3745。
此问题将在计划于 2021 年 3 月 18 日发布的 v4.3.2/v4.2.6 中修复。请查看 GitHub 上的 milestone page 以防发布日期发生变化.