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: ipbank-dev.cetiti.liuzhaoqi.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 [email protected]
* @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 [email protected]
* @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 [email protected]
*/
@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中的數據記錄: