Java 用注解实现通用功能-csv文件的读取为例

1,837 阅读3分钟

引言

使用java注解可以实现一些共通的功能,假设有几种格式的csv文件,编码,分隔符,头部行数之类的定义各不相同,但我们想统一的处理他们,那就需要一个共通的方法。

也许有人说,不用注解,只用个共通工具类不就行了吗?但是注解让代码更优雅,而且当你增加其他一些需求,比如其他csv格式的时候,只需要加几个注解就能轻松的扩张你的功能。

那么看代码吧。

1. 定义注解

定义一个csv格式的注解,包含文件的分隔符,编码等等信息。如果业务需求增多,可以继续添加功能,比如换行符之类。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface FileFormat {
	// 分隔符
	char delimiter() default ',';
	// 引用符
	char encloseChar() default  Character.MIN_VALUE;
	// 编码
	String fileEncoding() default "UTF-8";
	// 头部行数
	int headerLineCount() default 0;
	// 输出文件是否覆盖
	boolean overWriteFlg() default false;
}

2. 使用注解

FreeTextCSVFormat使用了FileFormat的注解,分隔符,编码等都使用默认值,并没有进行特别的设置。

@Data
@FileFormat()
public class FreeTextCSVFormat  {
	private String nexcoNumber;
	private String msgNumber;
	private String cmdMode;
	private String text;
}

3. 处理注解,读取文件中的一行数据

根据注解的设置,读取一行数据。不管是什么编码,或者换行符,都是用通用的readDataLine()方法。

@Data
public class CSVFileLineIterator<T> implements AutoCloseable {
    private Class<T> format;
    private File file;
    // 分隔符
    private char delimiter;
    // 编码
    private String encoding;
    // 头部行数
    private int headerLineCount;
    private int count;
    private BufferedReader reader;
    
    public CSVFileLineIterator(File file, Class<T> format) {
        FileFormat fileformat = checkParams(file, format);
        this.file = file;
        this.format = format;
        // 从注解中获取分隔符
        this.delimiter = fileformat.delimiter();
        // 从注解中获取编码
        this.encoding = fileformat.fileEncoding();
        // 从注解中获取头部行数
        this.headerLineCount = fileformat.headerLineCount();
    }

    // 检查参数
    private FileFormat checkParams(File file, Class<T> format) {
        Optional.ofNullable(file).orElseThrow(
                () -> new FileException("nullArgument"));
        Optional.ofNullable(format).orElseThrow(
                () -> new FileException("nullArgument"));
        OptionalBoolean.of(!file.exists()).orElseThrow(
                () -> new FileException("fileNoFound"));
        
        
        FileFormat fileformat = format.getAnnotation(FileFormat.class);

        Optional.ofNullable(fileformat).orElseThrow(
                () -> new FileException("noFormatAnnotation"));
        OptionalBoolean.of(fileformat.delimiter() == Character.MIN_VALUE).orElseThrow(
                () -> new FileException("illegalDelimiter"));
        
        return fileformat;
    }

    public void open() {
        try {
            reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), this.encoding));
        } catch (UnsupportedEncodingException | FileNotFoundException e) {
            throw new FileException("csvOpenFailure", e);
        }
    }

    
    public String[] readDataLine() {
        String line = null;

        try {
            // 读取一行
            while ((line = reader.readLine()) != null) {
                count++;
                if (count <= this.headerLineCount) {
                    // 跳过头部行数
                    continue;
                }
                break;
            }
        } catch (IOException e) {
            throw new FileException("csvReadFailure", e);
        }
        // 按照指定的分隔符把行内容分隔成字符串数组
        return line == null ? null : line.split(String.valueOf(this.delimiter));
    }

    public void close() {
        try {
            this.reader.close();
            this.count = 0;
        } catch (IOException e) {
            throw new FileException("csvCloseFailure", e);
        }
    }
    

测试一下

public static void main(String[] args) {
    try(CSVFileLineIterator c = new CSVFileLineIterator(new File("C:\\SiLED\\FRU20191209151700.csv"), FreeTextCSVFormat.class)) {
        c.open();
        String[] s;
        while ((s = c.readDataLine()) != null) {
            System.out.println("直接readline: " + Arrays.asList(s));
        }
    }
}

测试结果

直接readline: [001, 301, 001, 内容1]
直接readline: [001, 302, 002, 内容2, 303, 003, 内容3, 304, 004, 内容4]

CSV文件-FRU20191209151700.csv

001,301,001,内容1
001,302,002,内容2,303,003,内容3,304,004,内容4

4. 功能扩展-读取数据,并封装到类中

刚才只是读取一行,返回字符串数组。但是我们有时候想把数据封装到类里,比如上述的FreeTextCSVFormat类。那么可以再定义一个文件内容的注解。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FileColumn {
	// 列index
	int columnIdex() default 0;
	// 是不是循环列
	boolean isLoop() default false;
}

给FreeTextCSVFormat,添加FileColumn注解。

@Data
@FileFormat()
public class FreeTextCSVFormat {
	@FileColumn(columnIdex = 0)
	private String nexcoNumber;
	
	@FileColumn(columnIdex = 1, isLoop = true)
	private String msgNumber;
	
	@FileColumn(columnIdex = 2, isLoop = true)
	private String cmdMode;
	
	@FileColumn(columnIdex = 3, isLoop = true)
	private String text;
}

最后,可以使用反射获取columnIdex,并把读取的内容封装进去。具体实现就不贴出来了。

结语

使用注解能够提升扩展性,比如添加一种新的csv样式,并不需要修改读取文件的方法,只需要添加使用注解的类就可以了。这样做能够更优雅,还能帮你了解java反射,毕竟平时用框架的注解很多,自己写的机会却很少。