一 思路
1. 最近公司需要开发许多文件数据同步的程序, 将数据写入文件, 通过SFTP协议同步给其他平台, 本人太懒, 就想弄个一劳永逸的工具.
2. 系统启动时, 创建一个Map结构的容器, 容器中存储文件生成规则与数据Entity的映射属性配置.
3. 文件生成时, 根据根据配置Key查询配置的值(配置值包括文件编码, 文件是否存在文件头, 文件每一列的分隔符), 使用反射机制反射Entity拿到值, 根据配置的规则写入文件.
4. 读取文件时, 根据根据配置Key查询配置的值, 创建List集合, 读取到文件, 根据配置中的Entity属性与文件属性映射关系将每一行数据都转换为Entity对象, 存入List集合.
二 开撸
1. 创建一个XML配置文件 fileMapper.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE context [
<!ELEMENT context (fileMapper | fileTemplate)*>
<!ELEMENT fileMapper (mapper+)>
<!ELEMENT mapper EMPTY>
<!ATTLIST fileMapper id ID #REQUIRED>
<!ATTLIST fileMapper isFileHead (true|false) "true">
<!ATTLIST fileMapper separator CDATA #REQUIRED>
<!ATTLIST fileMapper charEncod CDATA "UTF-8">
<!ATTLIST fileMapper fileTemplate CDATA #IMPLIED >
<!ATTLIST mapper entityAttribute CDATA #REQUIRED>
<!ATTLIST mapper fileAttribute CDATA #REQUIRED>
]>
<context>
<!--
一, fileMapper属性注释:
id = "配置的唯一值, 根据ID可从容器中获取该条配置"
isFileHead = "是否存在文件头 true存在, false不存在"
separator = "每一列数据的分隔符"
charEncod = "文件编码"
二, mapper属性注释
entityAttribute = "Java类的属性名"
fileAttribute = "文件的列名"
-->
<fileMapper id="userInfo_MIIT" isFileHead="false" separator="," charEncod="UTF-8" >
<mapper entityAttribute="name" fileAttribute="fullName" />
<mapper entityAttribute="age" fileAttribute="age" />
<mapper entityAttribute="sex" fileAttribute="gender" />
<mapper entityAttribute="phone" fileAttribute="phoneNumber" />
<mapper entityAttribute="company" fileAttribute="organize" />
<mapper entityAttribute="department" fileAttribute="department" />
<mapper entityAttribute="location" fileAttribute="province" />
</fileMapper>
</context>
2. 创建配置信息模型对象 Context.Java , 用于在系统启动时, 将 fileMapper.xml 配置文件中的数据写入内存的载体
/**
* @功能描述 文件映射模型
* @author 索睿君
* @date 2019年7月5日10:01:21
*/
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "context")
public static class Context {
@XmlElement(name = "fileMapper")
public List<FileMapper> fileMapperList;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "fileMapper")
public static class FileMapper {
/** 唯一标识 */
@XmlAttribute
private String id;
/** 是否拥有文件头 (true是false否) */
@XmlAttribute
private boolean isFileHead;
/** 每列的分隔符 */
@XmlAttribute
private String separator;
/** 字符编码 */
@XmlAttribute
private String charEncod = "UTF-8";/** Java属性名与文件头名称映射 */
@XmlElement(name = "mapper")
private List<Mapper> mapperList;
private Map<String, String> mapper = new LinkedHashMap<>();
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "mapper")
public static class Mapper {
@XmlAttribute
private String entityAttribute;
@XmlAttribute
private String fileAttribute;
}
@Override
public String toString() {
return "FileMapper [id=" + id + ", isFileHead=" + isFileHead + ", separator=" + separator
+ ", charEncod=" + charEncod + ", fileTemplate=" + fileTemplate + ", filePath=" + filePath
+ ", mapperList=" + mapperList + ", mapper=" + mapper + "]";
}
}
}
3. 创建 FileMapperContext.Java 类, 在系统启动时读取 fileMapper.xml 配置信息, 转化为 Context.Java 对象, 存入Map集合
/**
* @功能描述 文件映射容器
* @author 索睿君
* @date 2019年7月5日14:55:35
*/
@Component public static class FileMapperContext{
private final static String CLASSPATH = "com/config/fileMapper/fileMapper.xml";
private final static Log Log = LogFactory.getLog(FileMapperContext.class);
public final static Map<String, Context.FileMapper> MAPPER_CONTEXT = new HashMap<>();static{
try {
File file = new File(Context.class.getClassLoader().getResource(CLASSPATH).getFile());
JAXBContext jaxbContext = JAXBContext.newInstance(Context.class);
Context context = (Context) jaxbContext.createUnmarshaller().unmarshal(file);
context.fileMapperList.forEach(fileMapper -> {
fileMapper.mapperList.forEach(mapper -> {
fileMapper.mapper.put(mapper.entityAttribute, mapper.fileAttribute);
});
MAPPER_CONTEXT.put(fileMapper.id, fileMapper);
Log.info("=============文件映射配置[" + fileMapper.id + "]加载完毕!~");
});
} catch (Exception e) {
Log.error("文件映射配置加载异常, 嵌套异常: \n" + e.toString());
}
}
}
4. 生成文件的函数, 使用时传参数配置的Key(也就是 fileMapper.xml 配置文件中的ID属性值), 文件路径, 数据集合
/**
* @功能描述 数据写入文件
* @param mapperKey
* @param filePath
* @param dataList
* @throws ServiceException
*/
public static <T> void writeFile(String mapperKey, String filePath, List<T> dataList) throws ServiceException {
// 参数验证
if (mapperKey == null || mapperKey.trim().length() < 1 || filePath == null || !PathHandler.initPath(filePath).isFile()) {
throw new ServiceException("数据写入文件异常, 异常信息: 非法参数!~");
}
long startTime = System.currentTimeMillis();
Log.info("开始写入文件["+PathHandler.getFileName(filePath)+"]");
// 获取映射
Context.FileMapper fileMapperObject = FileMapperContext.MAPPER_CONTEXT.get(mapperKey);
if (fileMapperObject == null) {
throw new ServiceException("数据写入文件异常, 异常信息: 无效的文件映射键!~");
}
Charset charEncod = Charset.forName(fileMapperObject.charEncod); //字符编码
boolean isFileHead = fileMapperObject.isFileHead; //是否拥有文件头
String separator = fileMapperObject.separator; //列分隔符
Map<String, String> mapper = fileMapperObject.mapper; //属性文件映射
String fileHeads = mapper.values().stream().collect(Collectors.joining(separator)); //文件头
List<String> variableName = mapper.keySet().stream().collect(Collectors.toList()); //属性名
try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(filePath),charEncod)) {
if(isFileHead) writer.write(fileHeads + System.getProperty("line.separator"));
if(dataList == null || dataList.size() < 1) return;
for (T data : dataList) {
ArrayList<String> variableValueList = new ArrayList<>();
Class<? extends Object> clazz = data.getClass();
for (String variable : variableName) {
Field declaredField = clazz.getDeclaredField(variable);
declaredField.setAccessible(true);
Object object = declaredField.get(data);
if(object == null){
variableValueList.add("");
}else if(declaredField.getType().getName().equals(Date.class.getName())){
String format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(object);
variableValueList.add(format);
}else{
variableValueList.add(String.valueOf(object));
}
}
String fileLine = variableValueList.stream().collect(Collectors.joining(separator));
writer.write(fileLine + System.getProperty("line.separator"));
}
Long endTime = System.currentTimeMillis();
Log.info("文件["+PathHandler.getFileName(filePath)+"]写入结束, 耗时: " + (endTime - startTime) + "ms");
} catch (IOException e) {
throw new ServiceException("数据写入文件IO异常, 嵌套异常: " + e.toString());
} catch (NoSuchFieldException | SecurityException e) {
throw new ServiceException("数据写入文件反射属性异常, 嵌套异常: " + e.toString());
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new ServiceException("数据写入文件读取属性异常, 嵌套异常: " + e.toString());
}
}
5. 读取文件的函数
/**
* @param <T>
* @功能描述 文件读取数据
* @param mapperKey
* @param filePath
* @return
* @throws ServiceException
*/
public static < T> List<T> readFile(String mapperKey,String filePath,Class<T> javaClass) throws ServiceException{
//参数验证
if(mapperKey == null || mapperKey.trim().length() < 1 || !new File(filePath).isFile()){
throw new ServiceException("文件读取数据异常, 异常信息: 非法参数!~");
}
long startTime = System.currentTimeMillis();
Log.info("开始读取文件["+PathHandler.getFileName(filePath)+"]");
try {
//获取映射
Context.FileMapper fileMapperObject = FileMapperContext.MAPPER_CONTEXT.get(mapperKey);
if(fileMapperObject == null){
throw new ServiceException("文件读取数据异常, 异常信息: 无效的文件映射键!~");
}
Charset charEncod = Charset.forName(fileMapperObject.charEncod); //获取字符编码
Map<String, String> fileMapperMap = fileMapperObject.mapper; //获取文件对象映射
String[] variableArray = fileMapperMap.keySet().toArray(new String[]{}); //获取属性数组
String separator = fileMapperObject.separator; //获取文件列分隔符
boolean isFileHead = fileMapperObject.isFileHead; //文件是否存文件头
//读取文件
List<String> fileContext = Files.readAllLines(Paths.get(filePath),charEncod);
if(fileContext == null || fileContext.size() < 1) return null;
List<T> dataList = new LinkedList<>();
//开始解析
for (int lineIndex = isFileHead ? 1 : 0; lineIndex < fileContext.size(); lineIndex++) {
//获取行内容
String fileLine = fileContext.get(lineIndex);
System.out.println("正在读取第["+lineIndex+"]行数据:" + fileLine);
T newInstance = javaClass.newInstance();
if(fileLine == null || fileLine.trim().length() < 1) continue;
String[] fileColumn = fileLine.split("["+separator+"]");
for (int columnIndex = 0; columnIndex < fileColumn.length; columnIndex++) {
Field declaredField = javaClass.getDeclaredField(variableArray[columnIndex]);
declaredField.setAccessible(true);
declaredField.set(newInstance, baseClass(fileColumn[columnIndex], declaredField.getType()) );
declaredField.setAccessible(false);
}
dataList.add(newInstance);
}
fileContext.clear();
long endTime = System.currentTimeMillis();
Log.info("文件["+PathHandler.getFileName(filePath)+"]读取完毕, 耗时: " + (endTime - startTime) + "ms");
return dataList;
} catch (IOException e) {
throw new ServiceException("文件读取数据IO读取异常, 异常信息: " + e.toString());
} catch (InstantiationException | IllegalAccessException e) {
throw new ServiceException("文件读取数据反射对象异常, 异常信息: " + e.toString());
} catch (NoSuchFieldException | SecurityException e) {
throw new ServiceException("文件读取数据反射属性异常, 异常信息: " + e.toString());
}
}
6. 文件生成测试
@Test
public void 生成文件(){
try {
// 指定文件路径
String filePath = "E:/开发需求/25-谢绝来电/04 需求测试/WISH_DATA_A_20190716.txt.删除";
// 创建List, 存放用户意愿数据
List<UserWish> dataList = new ArrayList<>();
//生成100个手机号
List<String> createPhones = TestUtil.createPhones(100);
for (String phone : createPhones) {
UserWish userWish = new UserWish();
userWish.setOperateType(3);
userWish.setPhone(Des3Util.encryptBase64(phone, ybef_key, yber_keyIpv)); //手机号加密
userWish.setWish("1");
userWish.setRegistTime("2019-06-27 04:54:28");
userWish.setUpdateTime(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
userWish.setLocation("BJ");
dataList.add(userWish);
}
//调用工具生成文件
FileMapping.writeFile(FileMapper.KEY_UserWish_BJDX, filePath, dataList);
System.out.println("用户意愿数据生成完毕!~");
} catch (Exception e) {
System.out.println("异常: "+e);
}
}
7. 文件读取测试
@Test
public void 读取数据(){
String 数据文件 = "E:/开发需求/25-谢绝来电/04 需求测试/WISH_DATA_A_20190716.txt.删除";
try {
List<UserWish> readFile = FileMapping.readFile(FileMapper.KEY_UserWish_BJDX, 数据文件, UserWish.class);
readFile.forEach(System.out::println);
System.out.println("共 [" + readFile.size() + "] 条记录!~");
} catch (ServiceException e) {
System.out.println("读取异常:" + e.toString());
}
}
三 总结
生成500万数据大概需要消耗2分钟时间
读取500万数据大概在4到5分钟之间
欢迎各位大神指正以上程序的不足之处和优化点