项目介绍
最近在开发工程中,需要根据excel中的数据生成标签类,手写太费劲啦,还得一边写一边对,受不了啦 决定写一个解析工具,自动生成代码
项目架构
项目采用springboot +vue的开发方式,但vue并不是分离项目,而是集成在项目内部,加入定时任务每天零点删除指定文件夹的生成文件,避免产生垃圾文件,提供工具生成和下载功能
项目开发
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>com.squareup</groupId>
<artifactId>javapoet</artifactId>
<version>1.11.1</version>
</dependency>
</dependencies>
配置文件
server.port=10086
spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=25MB
日志配置
<?xml version="1.0" encoding="utf-8" ?>
<!-- 从高到地低 OFF 、 FATAL 、 ERROR 、 WARN 、 INFO 、 DEBUG 、 TRACE 、 ALL -->
<!-- 日志输出规则 根据当前ROOT 级别,日志输出时,级别高于root默认的级别时 会输出 -->
<!-- 以下 每个配置的 filter 是过滤掉输出文件里面,会出现高级别文件,依然出现低级别的日志信息,通过filter 过滤只记录本级别的日志-->
<!-- 属性描述 scan:性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,
默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 定义日志文件 输入位置 -->
<property name="logPath" value="../excel-service-log" />
<!-- 日志最大的历史 30天 -->
<property name="maxHistory" value="30"/>
<appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
<!-- layout代表输出格式 -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger -%msg%n</pattern>
</layout>
</appender>
<!-- 日志输出文件 -->
<appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger -%msg%n</pattern>
</encoder>
<!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 RollingFileAppender-->
<!-- 滚动策略,它根据时间来制定滚动策略.既负责滚动也负责触发滚动 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 输出路径 -->
<fileNamePattern>${logPath}/info/%d.log</fileNamePattern>
<!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件假设设置每个月滚动,且<maxHistory>是6,
则只保存最近6个月的文件,删除之前的旧文件。注意,删除旧文件是,那些为了归档而创建的目录也会被删除-->
<maxHistory>${maxHistory}</maxHistory>
</rollingPolicy>
</appender>
<!-- 特殊记录Error日志 -->
<appender name="fileErrorLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 只记录ERROR级别日志,添加范围过滤,可以将该类型的日志特殊记录到某个位置 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger -%msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${logPath}/error/%d.log</fileNamePattern>
<maxHistory>60</maxHistory>
</rollingPolicy>
</appender>
<!--定义日志输出级别-->
<logger name="com.test.excelservice.controller" level="INFO"/>
<logger name="com.test.excelservice.scheduled" level="INFO"/>
<root level="info">
<!-- 引入控制台输出规则 -->
<appender-ref ref="consoleLog" />
<appender-ref ref="fileInfoLog" />
<appender-ref ref="fileErrorLog" />
</root>
</configuration>
引入vue、axios、element-ui以及字体文件
具体项目会放在资源中,想要的私聊或者下载
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>微服务生成Tag工具</title>
<!--引入网站图标-->
<link rel="shortcut icon" href="/image/favicon.ico">
<!-- 引入Vue -->
<script type="text/javascript" th:src="@{/scripts/vue.min.js}"></script>
<!-- 引入axios -->
<script type="text/javascript" th:src="@{/scripts/axios-0.18.0.js}"></script>
<!-- 引入组件库 -->
<script type="text/javascript" th:src="@{/scripts/index.js}"></script>
<!-- 引入样式 -->
<link type="text/css" href="/css/index.css" rel="stylesheet">
</head>
<body>
<div id="app">
<el-container>
<el-header id="header">
<div id="title">微服务生成Tag工具</div>
</el-header>
<el-main>
<el-tabs v-model="activeName">
<el-tab-pane label="Tag生成" name="first">
<el-card class="box-card" shadow="always">
<el-row>
<el-col :span="12">
<div><h2>自动生成表单信息</h2></div>
<el-form :model="formSelectData" :rules="rules" ref="ruleForm" class="demo-ruleForm">
<el-form-item label="请输入生成文件文件夹" prop="dirPath">
<el-input v-model="formSelectData.dirPath"
disabled="true"></el-input>
</el-form-item>
<el-form-item label="请输入要生成的sheet页名" prop="sheetName">
<el-input v-model="formSelectData.sheetName"
placeholder="对应excel中sheet页名"></el-input>
</el-form-item>
<el-form-item label="生成的包名" prop="packageName">
<el-input v-model="formSelectData.packageName"
placeholder="对应java代码中的包名"></el-input>
</el-form-item>
<el-form-item label="生成的客户端报文接口名" prop="clientInterFaceName">
<el-input v-model="formSelectData.clientInterFaceName"
placeholder="对应接口名称"></el-input>
</el-form-item>
<el-form-item label="生成的服务端报文接口名" prop="ServerInterFaceName">
<el-input v-model="formSelectData.ServerInterFaceName"
placeholder="对应接口名称"></el-input>
</el-form-item>
</el-form>
</el-col>
<el-col :span="10" :offset="2">
<div id="empty"></div>
<el-upload
class="upload-demo"
drag
action="String"
:before-upload="beforeUploadHandle"
:http-request="handleUploadForm"
auto-upload="false"
:show-file-list="false"
multiple="false"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
</el-upload>
</el-col>
</el-row>
</el-card>
</el-tab-pane>
<el-tab-pane label="Tag下载" name="second">
<el-card class="box-card" shadow="always">
<el-table :data="tableData" border style="width: 100%">
<el-table-column
label="文件夹路径"
prop="dirPath"
width="180">
</el-table-column>
<el-table-column
label="文件名"
width="180"
prop="fileName"
>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" @click="downLoadFile(scope.row)">
下载
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-tab-pane>
</el-tabs>
</el-main>
</el-container>
</div>
<script>
let app = new Vue(
{
el: "#app", // 挂载根节点
mounted() {
},
data: {
activeName: 'first',
formSelectData: {
dirPath: "/opt/exceldata/data",
sheetName: "",
packageName: '',
clientInterFaceName: '',
ServerInterFaceName: '',
},
rules: {
dirPath: [
{required: true, message: '请输入生成文件夹', trigger: 'change'}
],
sheetName: [
{required: true, message: '请输入excelSheet页名称', trigger: 'change'}
],
packageName: [
{required: true, message: '请输入接口对应包名', trigger: 'change'}
],
clientInterFaceName: [
{required: true, message: '请输入要生成的客户端接口名称', trigger: 'change'}
],
ServerInterFaceName: [
{required: true, message: '请输入要生成的服务端接口名称', trigger: 'change'}
],
},
tableData:[]
},
methods: {
// 开启全局loading
beginGlobalLoading() {
this.globalLoad = this.$loading({ //开启loading动画
lock: true,
text: '正在玩命加载,请稍等···',
spinner: 'el-icon-loading',// 设置图标
background: 'rgba(0,0,0,.7)'
})
},
// 关闭全局loading
endGlobalLoading() {
if (this.globalLoad) {
this.globalLoad.close()
}
},
// 上传文件之前校验
async beforeUploadHandle(file) {
try {
await this.$refs['ruleForm'].validate()
const extensionList = ["xlsx", "xls", "XLSX", "XLS"]
const fileExtension = file.name.split(".")[1]
if (file.size / 1024 / 1024 > 20) {
this.$message.warning('上传文件大小不能超过20MB!');
return Promise.reject(false);
}
if (!extensionList.includes(fileExtension)) {
this.$message.warning("文件类型必须是excel,请重新上传!!!");
return Promise.reject(false);
}
} catch (e) {
this.$message.warning("请填写左侧表单项之后在次上传文件!!!");
return Promise.reject(false);
}
},
// 覆盖默认上传行为
handleUploadForm(param) {
let formData = new FormData();
formData.append('file', param.file);
formData.append('sheetName', this.formSelectData.sheetName);
formData.append('dirPath', this.formSelectData.dirPath);
formData.append('packageName', this.formSelectData.packageName);
formData.append('clientInterFaceName', this.formSelectData.clientInterFaceName);
formData.append('ServerInterFaceName', this.formSelectData.ServerInterFaceName);
axios.post("/parse/file", formData, {'Content-type': 'multipart/form-data'}).then((res) => {
if (res.data.code == 200) {
const data = res.data.data;
this.tableData = data;
this.endGlobalLoading();
this.$message.success('自动生成成功');
} else {
this.endGlobalLoading();
this.$message.error('自动生成失败');
}
})
},
// 下载方法
downLoadFile(param){
console.log(param,"param");
let strings = param.packageName.split('.');
let packagePath = strings.join("\\");
const filePath = `${param.dirPath}\\${packagePath}\\${param.fileName}.java`
axios.get("/downLoadFile",{
params: {
path: filePath,
name: `${param.fileName}.java`
},
responseType: 'blob'
}).then(res => {
const blob = new Blob([res.data]);
const fileName = res.headers["content-disposition"].split(";")[1].split("filename=")[1];
let downloadElement = document.createElement("a");
let href = window.URL.createObjectURL(blob);
downloadElement.href = href;
downloadElement.download = decodeURIComponent(fileName);
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
window.URL.revokeObjectURL(href);
})
}
}
}
);
</script>
<style>
html,
body {
width: 100%;
height: 100%;
}
#app {
width: 100%;
height: 100%;
}
#header {
background-color: #409EFF;
}
#title {
margin: 0 auto;
font-family: ui-sans-serif;
font-size: larger;
position: relative;
top: 50%;
/*transform: translateX(-50%);*/
transform: translateY(-50%);
text-align: center;
color: white;
font-weight: bold;
}
.el-upload-dragger {
width: 600px;
height: 460px;
}
#empty {
height: 110px;
}
.el-upload-dragger .el-icon-upload {
margin-top: 200px;
}
</style>
</body>
</html>
路由页面配置
package com.test.excelservice.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 跳转页面配置
* @author shilei
*/
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
}
}
解析逻辑
package com.test.excelservice.controller;
import com.alibaba.excel.EasyExcel;
import com.test.excelservice.generateor.GeneratorUtils;
import com.test.excelservice.model.GeneInfo;
import com.test.excelservice.model.ReqParam;
import com.test.excelservice.model.ResParam;
import com.test.excelservice.result.Result;
import com.test.excelservice.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 解析excel信息生成tag
* @author shilei
*/
@RestController
public class ExcelController {
private Logger logger = LoggerFactory.getLogger(ExcelController.class);
private Pattern pattern = Pattern.compile("[\u4e00-\u9fa5]");
@RequestMapping("/parse/file")
public Result<List<GeneInfo>> pareExcel(MultipartFile file, GeneInfo geneInfo) {
try{
logger.info("读取"+file.getOriginalFilename()+"文件");
logger.info("读取"+geneInfo.getSheetName()+"sheet页的excel数据");
// 读取excel数据
List<HashMap<Integer ,String>> data = EasyExcel
.read(file.getInputStream())
.sheet(geneInfo.getSheetName())
.headRowNumber(0)
.doReadSync();
// 请求参数list
List<ReqParam> reqParams = new ArrayList<>();
// 响应参数list
List<ResParam> resParams = new ArrayList<>();
// 读取请求参数标识
boolean readerReqFlag =false;
// 读取响应参数标识
boolean readerResFlag =false;
for (int i = 0; i < data.size(); i++) {
HashMap<Integer, String> stringStringHashMap = data.get(i);
if(stringStringHashMap.containsValue("请求报文")){
// 跳过一条
i+=1;
readerReqFlag = true;
continue;
}
if(stringStringHashMap.containsValue("响应报文")){
// 跳过一条
i+=1;
readerReqFlag = false;
readerResFlag = true;
continue;
}
// 读取请求报文
if(readerReqFlag){
ReqParam reqParam = new ReqParam();
reqParam.setFiledName(stringStringHashMap.get(0));
reqParam.setFiledDesc(stringStringHashMap.get(1));
reqParams.add(reqParam);
}
// 读取响应报文
if(readerResFlag){
String field = stringStringHashMap.get(0).toLowerCase();
Matcher matcher = pattern.matcher(field);
// 跳过List Row和中文描述的响应报文行
if(field.contains("list")||field.contains("row")||matcher.find()){
// 跳过一条
i+=1;
continue;
}
ResParam resParam = new ResParam();
resParam.setFiledName(stringStringHashMap.get(0));
resParam.setFiledDesc(stringStringHashMap.get(1));
resParams.add(resParam);
}
}
logger.info("读取"+geneInfo.getSheetName()+"成功");
if(StringUtils.isNotNull(reqParams) && StringUtils.isNotNull(resParams)){
logger.info("正在生成接口中....");
GeneratorUtils.generatorInterFace(geneInfo,reqParams,resParams);
logger.info("生成接口成功!!!");
}
ArrayList<GeneInfo> geneInfos = new ArrayList<>();
geneInfo.setFileName(geneInfo.getClientInterFaceName());
// 添加客户端下载对象
geneInfos.add(geneInfo);
// 创建服务端下载对象
GeneInfo serverGeneInfo = new GeneInfo();
BeanUtils.copyProperties(geneInfo,serverGeneInfo);
serverGeneInfo.setFileName(serverGeneInfo.getServerInterFaceName());
geneInfos.add(serverGeneInfo);
return new Result<List<GeneInfo>>(200,geneInfos);
}catch (Exception e){
logger.error("自动生成接口失败",e);
return new Result<List<GeneInfo>>(500, Collections.emptyList());
}
}
@GetMapping("/downLoadFile")
public void downLoadFile(@RequestParam("path") String path, @RequestParam("name") String name, HttpServletResponse response) throws Exception {
response.setContentType("text/html;charset=UTF-8");
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
long fileLength = (new File(path)).length();
response.setContentType("application/octet-stream;charset=GBK");
response.setHeader("Content-disposition", "attachment; filename=" + new String(name.getBytes("GB2312"), "ISO-8859-1"));
response.setHeader("Content-Length", String.valueOf(fileLength));
bis = new BufferedInputStream(new FileInputStream(path));
bos = new BufferedOutputStream(response.getOutputStream());
byte[] buff = new byte[2048];
int bytesRead;
while(-1 != (bytesRead = bis.read(buff, 0, buff.length))) {
bos.write(buff, 0, bytesRead);
}
bis.close();
bos.close();
}
}
解析工具类
package com.test.excelservice.generateor;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.TypeSpec;
import com.test.excelservice.model.GeneInfo;
import com.test.excelservice.model.ReqParam;
import com.test.excelservice.model.ResParam;
import com.test.excelservice.util.StringUtils;
import javax.lang.model.element.Modifier;
import java.io.File;
import java.io.IOException;
import java.util.*;
/**
* 生成接口工具类
* @Author shilei
*/
public class GeneratorUtils {
// 请求前缀
public static final String RQ_PREFIX = "RQ_";
// 响应前缀
public static final String RS_PREFIX = "RS_";
// 下划线
public static final String UNDERLINE = "_";
/**
* 生成接口工具类
*/
public static void generatorInterFace(GeneInfo geneInfo, List<ReqParam> reqParams, List<ResParam> resParams) throws IOException {
HashMap<String, List<FieldSpec>> filedHashMap = generatorFiled(reqParams, resParams,geneInfo);
Iterator<Map.Entry<String, List<FieldSpec>>> iterator = filedHashMap.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String, List<FieldSpec>> next = iterator.next();
TypeSpec builder = TypeSpec.interfaceBuilder(next.getKey()).addFields(next.getValue()).build();
JavaFile file = JavaFile.builder(geneInfo.getPackageName(), builder)
.indent(" ")
.build();
File dirFile = new File(geneInfo.getDirPath());
if (!dirFile.getParentFile().exists()) {
dirFile.getParentFile().mkdirs();
}
file.writeTo(dirFile);
}
}
/**
* 生成属性列表
*/
public static HashMap<String,List<FieldSpec>> generatorFiled(List<ReqParam> reqParams, List<ResParam> resParams,GeneInfo geneInfo){
// 最终结果
HashMap<String,List<FieldSpec>> result = new HashMap<>();
// 请求字段列表
List<FieldSpec> reqFiledList = new ArrayList<>();
// 响应字段列表
List<FieldSpec> resFiledList = new ArrayList<>();
// 处理之后的请求参数
List<ReqParam> reqParamResult =new ArrayList<>();
// 处理之后的响应参数
List<ResParam> resParamResult =new ArrayList<>();
for (ReqParam reqParam : reqParams) {
// 响应报文加请求
ResParam resParam = new ResParam();
resParam.setFiledName(RQ_PREFIX+reqParam.getFiledName());
resParam.setFiledDesc(reqParam.getFiledDesc());
resParamResult.add(resParam);
// 请求报文加前缀
reqParam.setFiledName(RQ_PREFIX+reqParam.getFiledName());
reqParamResult.add(reqParam);
}
for (ResParam resParam : resParams) {
// 请求报文加响应
ReqParam reqParam = new ReqParam();
reqParam.setFiledName(RS_PREFIX+resParam.getFiledName());
reqParam.setFiledDesc(resParam.getFiledDesc());
reqParamResult.add(reqParam);
// 响应报文加前缀
resParam.setFiledName(RS_PREFIX+resParam.getFiledName());
resParamResult.add(resParam);
}
if(StringUtils.isNotNull(reqParamResult)){
for (ReqParam reqItem : reqParamResult) {
FieldSpec filed = FieldSpec.builder(String.class, reqItem.getFiledName().toUpperCase())
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.initializer("$S",lowerFirst(removePrefix(reqItem.getFiledName())))
.addJavadoc(reqItem.getFiledDesc())
.build();
reqFiledList.add(filed);
}
}
if(StringUtils.isNotNull(resParamResult)){
for (ResParam resItem : resParamResult) {
FieldSpec filed = FieldSpec.builder(String.class, resItem.getFiledName().toUpperCase())
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.initializer("$S",upperFirst(removePrefix(resItem.getFiledName())))
.addJavadoc(resItem.getFiledDesc())
.build();
resFiledList.add(filed);
}
}
result.put(geneInfo.getClientInterFaceName(),reqFiledList);
result.put(geneInfo.getServerInterFaceName(),resFiledList);
return result;
}
/**
* 去掉字符串前缀
* @return String
*/
public static String removePrefix(String str) {
if (str == null || "".equals(str)) {
return "";
} else {
if (str.contains(UNDERLINE)) {
String[] split = str.split(UNDERLINE);
return split[1];
}
return str;
}
}
/**
* 首字母转小写
* @param str
* @return
*/
public static String lowerFirst(String str) {
str = Character.toLowerCase(str.charAt(0)) + str.substring(1);
return str;
}
/**
* 首字母转大写
* @param str
* @return
*/
public static String upperFirst(String str) {
str = Character.toUpperCase(str.charAt(0)) + str.substring(1);
return str;
}
}
定时任务删除指定文件
package com.test.excelservice.scheduled;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import java.io.File;
/**
* 定时任务删除指定文件夹下的所有文件
* @author shilei
*/
public class RemoveFileScheduled {
/**
* PATH 服务器下指定文件目录
*/
public static final String PATH = "/opt/exceldata/data";
private final Logger logger = LoggerFactory.getLogger(RemoveFileScheduled.class);
@Scheduled(cron = "0 0 00 * * ?")
public void executeRemove() {
removeFile(new File(PATH));
}
public void removeFile(File file){
if (file == null || !file.exists()){
logger.error("文件删除失败,请检查文件路径是否正确");
return;
}
//取得这个目录下的所有子文件对象
File[] files = file.listFiles();
//遍历该目录下的文件对象
for (File f: files){
String name = file.getName();
logger.info("文件名为"+ name);
//判断子目录是否存在子目录,如果是文件则删除
if (f.isDirectory()){
removeFile(f);
}else {
f.delete();
}
}
//删除空文件夹 for循环已经把上一层节点的目录清空。
file.delete();
}
}
运行效果
由于没有持久化到数据库,所以,下载不能够刷新页面,想加可以加一下