1 项目背景
机器学习平台功能需求之一:对Hadoop文件系统进行操作,实现文件目录的创建、算法组件的删除、修改或上传,算法组件的文件类型暂为jar包,同时一些操作信息记录到MySQL。
2 技术路线
需要做的几个步骤:
- Springboot对HDFS操作的相关配置
- HDFS文件的相关操作业务逻辑 eg: 创建、删除、更新、上传等
- 文件类型检查,对不符规定的文件限制上传
3 代码实现
首先添加依赖,本依赖版本和服务器的3.1.2对应,为解决依赖冲突,排除掉一些module
// 大数据环境相关依赖
implementation ("org.apache.hadoop:hadoop-client:3.1.2"){
exclude(module: 'slf4j-log4j12')
exclude(module: 'servlet-api')
}
implementation 'org.apache.hadoop:hadoop-common:3.1.2'
implementation 'org.apache.hadoop:hadoop-hdfs:3.1.2'
3.1 HDFS配置
3.1.1 application.yaml中的部分设置
spring:
jmx:
default-domain: service-manager
enabled: false
application:
name: srv-dmp-manager-dev
profiles:
active: ${spring.profiles.active}
main:
allow-bean-definition-overriding: true
http:
encoding:
charset: UTF-8
force: true
enabled: true
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/dmp_experiment?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=utf-8
username: root
password: 123456
servlet:
#文件上传大小限制
multipart:
max-request-size: 100MB
max-file-size: 100MB
app:
env: dev
swagger-enable: true
web:
upload-path:
domain: dmp-dev.com.zhg.cn
#添加hdfs路径配置
hdfs:
path: hdfs://nameservice1/
username: zhanghuigen
3.1.2 HDFS文件系统对象的获取
这一步 网上资源 大多把文件系统对象做成单例的工具,亦或设置成类静态变量,然后供外部直接调用。但是实际运行时发现,在执行多个HDFS业务逻辑(删除、读取目录文件、上传文件)时由于前面操作执行完成后对文件系统对象的关闭,造成后续操作执行时没有可用的连接,会报错:java.io.IOException: Filesystem closed。还有一种说法是先设置禁用缓存,将core-site.xml中的fs.hdfs.impl.disable.cache设置为true,然后在创建文件系统实例的时候使用: FileSystem fs=FileSystem.newInstance(conf),而不是用:fs = FileSystem.get(conf);但本项目按照上述配置还是会报错。
所以获取文件系统对象这一步果断不设置静态或单例了,直接封装成类中的一个方法,service中用的时候new一下,用完之后直接close。以下是工具类的封装:
/**
* @author zhanghuigen
* @since 0.1.0
**/
@Component
public class HadoopUtil {
@Value("${app.hdfs.path}")
private String path;
@Value("${app.hdfs.username}")
private String username;
private FileSystem hdfs;
/**
* 获取HDFS文件系统对象
* @return file system
*/
public FileSystem getFileSystem(){
// 返回指定的文件系统,如果在本地测试,需要使用此种方法获取文件系统
if (hdfs==null){
//读取配置文件
Configuration conf = new Configuration();
conf.addResource(new Path("hdfs-site.xml"));
conf.set("fs.defaultFS","hdfs://nameservice1");
conf.setBoolean("fs.hdfs.impl.disable.cache", true);
try {
hdfs = FileSystem.newInstance(conf);
} catch ( Exception e) {
e.printStackTrace();
}
}
return hdfs;
}
}
这里把大数据平台中配置文件中的hdfs-site.xml部分拷贝一份加载进ClassPath中,然后读资源配置,设置名称服务即可。
3.2 业务逻辑
直接贴代码了,具体HDFS文件的操作就是先获取文件系统对象,再创建流,读写完毕后再关闭流和文件系统对象。
/**
* @author zhanghuigen
* @since 0.1.0
**/
@Service
@Transactional(rollbackFor = Exception.class)
public class HdfsFileServiceImpl implements HdfsFileService {
@Resource
private HdfsFileMapper hdfsFileMapper;
@Override
public DataList<HdfsFile> getAllFiles(PageInfo pageInfo) throws Exception {
if(pageInfo.getCurrentPage()==null || pageInfo.getPageSize()==null){
pageInfo.setCurrentPage(PageInfo.DEFAULT_CURRENT_PAGE);
pageInfo.setPageSize(PageInfo.DEFAULT_PAGE_SIZE);
}
Page<HdfsFile> page = PageHelper.startPage(pageInfo.getCurrentPage(),pageInfo.getPageSize(),true);
hdfsFileMapper.getAllFile(pageInfo);
List<HdfsFile> hdfsFileList = page.getResult();
return new DataList<>(page.getTotal(),page.getPageNum(),hdfsFileList);
}
@Override
public boolean mkdir(String path) throws Exception {
if (StringUtils.isEmpty(path)) {
return false;
}
if (existFile(path)) {
return true;
}
HadoopUtil util = new HadoopUtil();
FileSystem fs = util.getFileSystem();
// 目标路径
Path srcPath = new Path(path);
// 创建目录
boolean isOk = fs.mkdirs(srcPath);
fs.close();
return isOk;
}
@Override
public boolean createHdfsFile(HdfsFile hdfsFile, MultipartFile file) throws Exception{
//先上传至HDFS,再在数据库中插入记录
String url = hdfsFile.getUrl();
if (uploadFile(url,file)){
hdfsFile.setUrl(url+"/"+file.getOriginalFilename());
return addFileRecord2Db(hdfsFile);
}
return false;
}
@Override
public boolean editHdfsFile(HdfsFile hdfsFile) throws Exception{
HdfsFile originHdfsFile = hdfsFileMapper.getFileById(hdfsFile.getId());
if (null!=originHdfsFile){
return updateFileRecord2Db(hdfsFile);
}
return false;
}
@Override
public boolean editHdfsFile(HdfsFile hdfsFile, MultipartFile file) throws Exception{
//文件非空,先上传至HDFS,再在数据库中更新记录
String url = hdfsFile.getUrl();
if (uploadFile(url,file)){
hdfsFile.setUrl(url+"/"+file.getOriginalFilename());
return updateFileRecord2Db(hdfsFile);
}
return false;
}
@Override
public boolean uploadFile(String path, MultipartFile file) throws Exception {
if (StringUtils.isEmpty(path) || null == file.getBytes()) {
return false;
}
String fileName = file.getOriginalFilename();
HadoopUtil util = new HadoopUtil();
FileSystem fs = util.getFileSystem();
// 上传时默认当前目录,后面自动拼接文件的目录
Path newPath = new Path(path + "/" + fileName);
// 打开一个输出流
FSDataOutputStream outputStream = fs.create(newPath);
outputStream.write(file.getBytes());
outputStream.close();
fs.close();
return true;
}
@Override
public boolean deleteFileById(Integer id) throws Exception{
HdfsFile fileMeta = hdfsFileMapper.getFileById(id);
if (fileMeta!=null&&existFile(fileMeta.getUrl())){
HadoopUtil util = new HadoopUtil();
FileSystem fs = util.getFileSystem();
Path newPath = new Path(fileMeta.getUrl());
boolean isOk = fs.deleteOnExit(newPath);
fs.close();
if (isOk){
return hdfsFileMapper.deleteFileById(id);
}
}
return false;
}
@Override
public HdfsFile getFileById(Integer id) throws Exception {
return hdfsFileMapper.getFileById(id);
}
@Override
public boolean addFileRecord2Db(HdfsFile meta) throws Exception {
int num = hdfsFileMapper.addFile(meta);
return num > 0;
}
@Override
public boolean updateFileRecord2Db(HdfsFile meta) throws Exception {
return hdfsFileMapper.updateFile(meta);
}
/**
* 判断HDFS文件是否存在
* @param path path
* @return boolean
* @throws Exception ex
*/
private boolean existFile(String path) throws Exception {
if (StringUtils.isEmpty(path)) {
return false;
}
HadoopUtil util = new HadoopUtil();
FileSystem fs = util.getFileSystem();
Path srcPath = new Path(path);
boolean isExisted = fs.exists(srcPath);
fs.close();
return isExisted;
}
}
3.3 文件类型检测
这部分代码网上资源很多,封装一下即可,常见文件头信息放在配置文件里较好,本地测试就直接写在了源码中。
/**
* @author zhaozhengkang
*/
@Slf4j
public class FileTypeChecker {
public final static Map<String, String> FILE_TYPE_MAP = new HashMap<String, String>();
private FileTypeChecker(){}
static{
getAllFileType(); //初始化文件类型信息
}
/**
* Discription:[getAllFileType,常见文件头信息]
*/
private static void getAllFileType() {
FILE_TYPE_MAP.put("ffd8ffe", "jpg");
FILE_TYPE_MAP.put("ffd8ffe1","jpeg");
FILE_TYPE_MAP.put("89504e47", "png");
FILE_TYPE_MAP.put("504b0304", "zip");
FILE_TYPE_MAP.put("52617221", "rar");
FILE_TYPE_MAP.put("d0cf11e0", "doc");
FILE_TYPE_MAP.put("25504446", "pdf");
FILE_TYPE_MAP.put("504b03041", "docx");
FILE_TYPE_MAP.put("6173646b", "txt");
FILE_TYPE_MAP.put("5F27A889", "jar");
FILE_TYPE_MAP.put("47494638", "gif");
FILE_TYPE_MAP.put("49492a00", "tif");
FILE_TYPE_MAP.put("424d", "bmp");
FILE_TYPE_MAP.put("41433130", "dwg");
FILE_TYPE_MAP.put("3c21444f", "html");
FILE_TYPE_MAP.put("3c21646f", "htm");
FILE_TYPE_MAP.put("48544d4c", "css");
FILE_TYPE_MAP.put("696b2e71", "js");
FILE_TYPE_MAP.put("7b5c7274", "rtf");
FILE_TYPE_MAP.put("38425053", "psd");
FILE_TYPE_MAP.put("46726f6d", "eml");
FILE_TYPE_MAP.put("5374616E", "mdb");
FILE_TYPE_MAP.put("25215053", "ps");
FILE_TYPE_MAP.put("2e524d46", "rmvb");
FILE_TYPE_MAP.put("464c5601", "flv");
FILE_TYPE_MAP.put("00000020", "mp4");
FILE_TYPE_MAP.put("49443303", "mp3");
FILE_TYPE_MAP.put("000001ba", "mpg");
FILE_TYPE_MAP.put("3026b2758", "wmv");
FILE_TYPE_MAP.put("52494646e", "wav");
FILE_TYPE_MAP.put("52494646d", "avi");
FILE_TYPE_MAP.put("4d546864", "mid");
FILE_TYPE_MAP.put("23546869", "ini");
FILE_TYPE_MAP.put("4d5a9000", "exe");
FILE_TYPE_MAP.put("3c254020", "jsp");
FILE_TYPE_MAP.put("4d616e69", "mf");
FILE_TYPE_MAP.put("3c3f786d", "xml");
FILE_TYPE_MAP.put("494e5345", "sql");
FILE_TYPE_MAP.put("7061636b", "java");
FILE_TYPE_MAP.put("40656368", "bat");
FILE_TYPE_MAP.put("1f8b0800", "gz");
FILE_TYPE_MAP.put("6c6f6734", "properties");
FILE_TYPE_MAP.put("cafebabe", "class");
FILE_TYPE_MAP.put("49545346", "chm");
FILE_TYPE_MAP.put("04000000", "mxp");
FILE_TYPE_MAP.put("6431303a", "torrent");
FILE_TYPE_MAP.put("6D6F6F76", "mov");
FILE_TYPE_MAP.put("FF575043", "wpd");
FILE_TYPE_MAP.put("CFAD12FE", "dbx");
FILE_TYPE_MAP.put("2142444E", "pst");
FILE_TYPE_MAP.put("AC9EBD8F", "qdf");
FILE_TYPE_MAP.put("E3828596", "pwl");
FILE_TYPE_MAP.put("2E7261FD", "ram");
}
/**
* 得到上传文件的文件头
* @param src
* @return
*/
public static String bytesToHexString(byte[] src) {
StringBuilder stringBuilder = new StringBuilder();
if (src == null || src.length <= 0) {
return null;
}
for (int i = 0; i < src.length; i++) {
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}
/**
* 根据制定文件的文件头判断其文件类型
* @param file
* @return
*/
public static String getFileType(MultipartFile file){
String res = null;
try {
InputStream is = file.getInputStream();
byte[] b = new byte[10];
is.read(b, 0, b.length);
String fileCode = bytesToHexString(b);
System.out.println(fileCode);
//这种方法在字典的头代码不够位数的时候可以用但是速度相对慢一点
Iterator<String> keyIter = FILE_TYPE_MAP.keySet().iterator();
while(keyIter.hasNext()){
String key = keyIter.next();
if(key.toLowerCase().startsWith(fileCode.toLowerCase()) ||
fileCode.toLowerCase().startsWith(key.toLowerCase())){
res = FILE_TYPE_MAP.get(key);
break;
}
}
is.close();
} catch (IOException e) {
log.info(e.getMessage());
}
return res;
}
public static String getSuffix(String fileName){
return fileName.substring(fileName.lastIndexOf(".")+1);
}
}
在controller中定义一个可上传文件类型白名单,当然这最好也写在配置文件里
private static final ArrayList<String> FILE_VALID_TYPES = new ArrayList<>();
static {
FILE_VALID_TYPES.add("jar");
FILE_VALID_TYPES.add("png");
FILE_VALID_TYPES.add("jpg");
FILE_VALID_TYPES.add("jpeg");
FILE_VALID_TYPES.add("zip");
FILE_VALID_TYPES.add("pdf");
FILE_VALID_TYPES.add("rar");
FILE_VALID_TYPES.add("docx");
FILE_VALID_TYPES.add("doc");
}
然后在action处先对文件类型做检查,这里以编辑文件信息为例,前端修改表单数据,点击选择上传的文件,也可以只编辑表单数据而不上传文件(MultipartFile = null),所以方法中用@RequestParam(value=“file”, required=false)修饰MultipartFile参数:
/**
* 编辑HDFS文件信息
* @param hdfsFile hdfsFile
* @param file file
* @return base response
* @throws Exception ex
*/
@ApiOperation(value = "编辑文件信息并上传文件至HDFS")
@ApiImplicitParams({
@ApiImplicitParam(name = "hdfsFile", value = "hdfsFile", dataType = "HdfsFile"),
@ApiImplicitParam(name = "file", value = "file", dataType = "MultipartFile")
})
@PostMapping("/hdfs/edit")
public BaseResponse editFile(HdfsFile hdfsFile, @RequestParam(value="file", required=false) MultipartFile file) throws Exception {
if (null!=file){
String fileType = FileTypeChecker.getFileType(file);
if(!FILE_VALID_TYPES.contains(fileType)){
throw new UploadException(UploadExceptionEnum.INVALID_FILE_TYPE.getCode(),
UploadExceptionEnum.INVALID_FILE_TYPE.getMessage());
}
return BaseResponse.buildSuccessResponse(hdfsService.editHdfsFile(hdfsFile,file));
}else {
return BaseResponse.buildSuccessResponse(hdfsService.editHdfsFile(hdfsFile));
}
}
3.4 测试
安利一个新工具ApiPost,当然Postman也可以用。以上传文件为例,这里body中同时上传文件(jar、pdf、png等白名单指定类型)和对应的文件基本信息(对应类HdfsFile中的字段),上传文件的同时把文件信息存储于MySQL,测试结果如下:
登录HUE查看文件是否已上传至HDFS:
MySQL中的数据记录: