包含“&”的字符串的 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;
}

MarshallerStaxEventItemWriter 中使用,实现如下:

@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()));,但这没有帮助。此外,字符串将从 & 更改为 &amp;,其中还包含一个 &.

我也尝试使用自己的 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 自动将 & 转换为 &amp;,如下所述: 这不会发生。对我来说 & 总是有问题。为什么转义不起作用?同样转义字符串本身并强制将其转换为 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&amp;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 以防发布日期发生变化.