openoffic+java+spring 多線程 轉換doc,ppt,xls -> html/pdf

前提摘要:

你是否想在自己的項目中像百度文庫一樣實現文檔的在線預覽,又不想使用第三方的收費服務,那麼這篇文章你選對了!在搜索大量的資料,我發現網絡上對openoffice的使用時最方便的,但是沒有可以多線程轉換的代碼,試想一下,在一個許多許許多的用戶同時文件轉換的時候,沒有多線程,那不等急死了,一個一個的轉換。。。


科普知識:

1.OpenOffice: OpenOffice.org 是一套跨平臺的辦公室軟件套件,能在Windows、Linux、MacOS X (X11)和 Solaris 等操作系統上執行。它與各個主要的辦公室軟件套件兼容。OpenOffice.org 是自由軟件,任何人都可以免費下載、使用及推廣它。


在Java中引入OpenOffice的使用當然需要藉助Process的使用纔可以,需要調用本地命令的形式使用,Process的使用就不多說了,我就直接放上我所使用的代碼:

private List<Process> process = new ArrayList<Process>();
OpenOffice_HOME = PropertiesUtils.getVal("tools.properties", "OpenOffice_HOME");
String command = OpenOffice_HOME + "/program/soffice.exe -headless -accept=\"socket,host=127.0.0.1,port="+port+";urp;\"";  
process.add(Runtime.getRuntime().exec(command));
這裏的OpenOffice_HOME 就是 openoffice的安裝目錄,到program 文件夾上一層即可;

重點:這裏process 放入集合中,可以在使用完畢,或者服務器關閉時,進行關閉進程;這裏多線程的操作是開多個端口,每個端口同時進行轉換工作,轉換完畢,就是放該端口連接讓給其他的線程使用,這樣就可以多線程操作了;

Java環境下操作openoffice 要使用 JodConverter 來操作,JodCconverter有兩個版本:

1.最新的3.0版本身就加入了線程池的支持,本身就可以開啓多端口進程,看源碼確實使用了線程池,但是實際測試中,並沒有真正的多線程的轉換,並且正統的文檔太少了,源代碼source中都沒有API的解說,而且在3.0版本中可以不借助process,本身的API已經有啓動服務的方法了,這確實很簡單,如果各位有折騰的精神和時間,可以繼續嘗試,maven 引入 3.0 貌似有問題,建議自行下載jar包。


操作:這裏我是用的是JodConverter 2.2.1 的包,直接引入maven依賴就可以用了,前提是maven項目

pom.xml 中配置:

<dependency>
<groupId>com.artofsolving</groupId>
<artifactId>jodconverter</artifactId>
<version>2.2.1</version>
</dependency>

轉換文件使用到的工具類:

package campus_mooc.common_utils.utils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.transaction.Transactional;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import com.artofsolving.jodconverter.DocumentConverter;
import com.artofsolving.jodconverter.openoffice.connection.OpenOfficeConnection;
import com.artofsolving.jodconverter.openoffice.connection.SocketOpenOfficeConnection;
import com.artofsolving.jodconverter.openoffice.converter.OpenOfficeDocumentConverter;

import campus_mooc.core.commons.comstatic.ConfigStatic;

/**
 * 利用jodconverter(基於OpenOffice服務)將文件(*.doc、*.docx、*.xls、*.ppt)轉化爲html格式或者pdf格式,
 * 使用前請檢查OpenOffice服務是否已經開啓, OpenOffice進程名稱:soffice.exe | soffice.bin
 * @author lcx
 */
@Component(value="doc2HtmlUtil")//這裏我直接將工具交由Spring容器管理
@Transactional
public class Doc2HtmlUtil{
    private final Logger logger = Logger.getLogger(Doc2HtmlUtil.class);

    private final String OpenOffice_HOME = PropertiesUtils.getVal("tools.properties", "OpenOffice_HOME");
    
    private List<Process> process = new ArrayList<Process>();//process集合,方便服務器關閉時,關閉openoffice進程
    
    public BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();//這裏用線程安全的queue管理運行的端口號
    
    @Autowired
    JdbcTemplate jdbcTemplate;
    
    @PostConstruct
    public void startAllService() throws IOException, NumberFormatException, InterruptedException{
    	
    	String portsStr = ConfigStatic.OPENOFFICE_PORT_STR;//我將使用的端口號卸載properties文件中,便於寫改
    	
    	String[] ports = portsStr.split(",");
    	
		for (String port : ports) {
			//添加到隊列 用於線程獲取端口 進行連接
			queue.put(Integer.parseInt(port));
			//啓動OpenOffice的服務  
	        String command = OpenOffice_HOME  
	                + "/program/soffice.exe -headless -accept=\"socket,host=127.0.0.1,port="+port+";urp;\"";//這裏根據port進行進程開啓
	        process.add(Runtime.getRuntime().exec(command));
	        logger.debug("[startAllService-port-["+port+"]-success]");
		}
		logger.debug("[startAllService-success]");
    }
    
    @PreDestroy//服務器關閉時執行 循環關閉所有的打開的openoffice進程
    public void stopAllService(){
    	for (Process p : process) {
			p.destroy();
		}
    	logger.debug("[stopAllService-success]");
    }
    
    /**
    * 根據端口獲取連接服務  每個轉換操作時,JodConverter需要用一個連接連接到端口,(這裏類比數據庫的連接)
    * @throws ConnectException 
    */
    public OpenOfficeConnection getConnect(int port) throws ConnectException{
    	logger.debug("[connectPort-port:"+port+"]");
    	return new SocketOpenOfficeConnection(port);
    }
    
	public Doc2HtmlUtil(){
	}
	
	/**
	 * 轉換文件成html
	 * 
	 * @param fromFileInputStream:
	 * @throws IOException
	 * @throws InterruptedException 
	 */
	public String file2Html(InputStream fromFileInputStream, String toFilePath, String type) throws IOException, InterruptedException {
		String methodName = "[file2Html]";
		Date date = new Date();
		SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
		String timesuffix = UUID.randomUUID().toString()+sdf.format(date);
		String docFileName = null;
		String htmFileName = null;
		if ("doc".equals(type) || "docx".equals(type)) {
			docFileName = "doc_" + timesuffix + ".doc";
			htmFileName = "doc_" + timesuffix + ".html";
		} else if ("xls".equals(type) || "xlsx".equals(type)) {
			docFileName = "xls_" + timesuffix + ".xls";
			htmFileName = "xls_" + timesuffix + ".html";
		} else if ("ppt".equals(type) || "pptx".equals(type)) {
			docFileName = "ppt_" + timesuffix + ".ppt";
			htmFileName = "ppt_" + timesuffix + ".html";
		} else {
			return null;
		}

		File htmlOutputFile = new File(toFilePath + File.separatorChar + htmFileName);
		File docInputFile = new File(toFilePath + File.separatorChar + docFileName);
		if (htmlOutputFile.exists())
			htmlOutputFile.delete();
		htmlOutputFile.createNewFile();
		if (docInputFile.exists())
			docInputFile.delete();
		docInputFile.createNewFile();
		
		/**
		 * 由fromFileInputStream構建輸入文件
		 */
		try {
			OutputStream os = new FileOutputStream(docInputFile);
			int bytesRead = 0;
			byte[] buffer = new byte[1024 * 8];
			while ((bytesRead = fromFileInputStream.read(buffer)) != -1) {
				os.write(buffer, 0, bytesRead);
			}

			os.close();
			fromFileInputStream.close();
		} catch (IOException e) {
		}
		
		//這裏是重點,每次轉換從集合讀取一個未使用的端口(直接拿走,這樣其他線程就不會讀取到這個端口號,不會嘗試去使用)
		//計時並讀取一個未使用的端口
		long old = System.currentTimeMillis();
		int port = queue.take();
		//獲取並開啓連接
		OpenOfficeConnection connection = getConnect(port);
		connection.connect();
		DocumentConverter converter = new OpenOfficeDocumentConverter(connection); 
		try {
			converter.convert(docInputFile, htmlOutputFile);
		} catch (Exception e) {
			System.out.println("exception:" + e.getMessage());
		}
		//關閉連接
		connection.disconnect();
		//計算花費時間 將端口放入池中
		System.out.println(Thread.currentThread().getName() + "disConnect-port=" + port + "-time=" + (System.currentTimeMillis() - old));
		queue.put(port);//端口號使用完畢之後 放回隊列中,其他線程有機會使用
		
		// 轉換完之後刪除word文件
		docInputFile.delete();
		logger.debug(methodName + "htmFileName:" + htmFileName);
		return htmFileName;
	}

	/**
	 * 轉換文件成pdf
	 * 
	 * @param fromFileInputStream:
	 * @throws IOException
	 * @throws InterruptedException 
	 */
	public String file2pdf(InputStream fromFileInputStream, String toFilePath, String type) throws IOException, InterruptedException {
		String methodName = "[file2pdf]";
		Date date = new Date();
		SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
		String timesuffix = UUID.randomUUID().toString()+sdf.format(date);
		String docFileName = null;
		String htmFileName = null;
		if ("doc".equals(type) || "docx".equals(type)) {
			docFileName = "doc_" + timesuffix + ".doc";
			htmFileName = "doc_" + timesuffix + ".pdf";
		} else if ("xls".equals(type) || "xlsx".equals(type)) {
			docFileName = "xls_" + timesuffix + ".xls";
			htmFileName = "xls_" + timesuffix + ".pdf";
		} else if ("ppt".equals(type) || "pptx".equals(type)) {
			docFileName = "ppt_" + timesuffix + ".ppt";
			htmFileName = "ppt_" + timesuffix + ".pdf";
		} else {
			return null;
		}

		File htmlOutputFile = new File(toFilePath + File.separatorChar + htmFileName);
		File docInputFile = new File(toFilePath + File.separatorChar + docFileName);
		if (htmlOutputFile.exists())
			htmlOutputFile.delete();
		htmlOutputFile.createNewFile();
		if (docInputFile.exists())
			docInputFile.delete();
		docInputFile.createNewFile();
		/**
		 * 由fromFileInputStream構建輸入文件
		 */
		try {
			OutputStream os = new FileOutputStream(docInputFile);
			int bytesRead = 0;
			byte[] buffer = new byte[1024 * 8];
			while ((bytesRead = fromFileInputStream.read(buffer)) != -1) {
				os.write(buffer, 0, bytesRead);
			}

			os.close();
			fromFileInputStream.close();
		} catch (IOException e) {
		}

		//計時並讀取一個未使用的端口
		long old = System.currentTimeMillis();
		int port = queue.take();
		//獲取並開啓連接
		OpenOfficeConnection connection = getConnect(port);
		connection.connect();
		//OfficeDocumentConverter converter = new OfficeDocumentConverter(officeManager);
		DocumentConverter converter = new OpenOfficeDocumentConverter(connection); 
		try {
			converter.convert(docInputFile, htmlOutputFile);
		} catch (Exception e) {
			System.out.println("exception:" + e.getMessage());
		}
		//關閉連接
		connection.disconnect();
		//計算花費時間 將端口放入池中
		System.out.println(Thread.currentThread().getName() + "disConnect-port=" + port + "-time=" + (System.currentTimeMillis() - old));
		queue.put(port);
		// 轉換完之後刪除word文件
		docInputFile.delete();
		logger.debug(methodName + "htmFileName:" + htmFileName);
		return htmFileName;
	}
}


然後我們寫一個單元測試去測試Spring環境下的工具類使用情況:

package jod;

import java.io.IOException;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import campus_mooc.common_utils.utils.FileInfoHelps;
import campus_mooc.common_utils.utils.ThreadManager;

@RunWith(value = SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:applicationContext.xml" })
public class Doc2HtmlUtilTest {
	@Autowired
	ThreadManager threadManager;
	@Autowired
	FileInfoHelps fileInfoHelps;

	@Test
	public void generatePreviewFileTest() throws IOException {
		for (int i = 0; i < 15; i++) {
			fileInfoHelps.generatePreviewFile(15, "openoffice\\test.doc");//因爲是爲了項目所使用的,下面會附上fileInfoHel的代碼
		}
	}

}

package campus_mooc.common_utils.utils;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

import javax.transaction.Transactional;

import org.apache.commons.io.FilenameUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import campus_mooc.common_utils.exception.SystemException;
import campus_mooc.core.module.domain.FileResource;
import campus_mooc.core.module.service.IResourceService;

/**
 * 文件幫助類
 * 
 * @author lcx
 */
@Component(value="fileInfoHelps")
@Transactional
public class FileInfoHelps {
	
	Logger logger = Logger.getLogger(FileInfoHelps.class);
	
	@Value("${rootPath}")
	String rootPath;// 保存視頻文件根目錄

	@Autowired
	IResourceService resourceService;
	
	@Autowired
	ThreadManager threadManager;
	
	@Autowired
	Doc2HtmlUtil doc2HtmlUtil;
	
	public FileInfoHelps() {
		//使用spring@value注入屬性文件值
		//rootPath = PropertiesUtils.getVal("ueditor.properties", "attachmentLocation");
	}

	public FileInfo saveFile(String relativePath, MultipartFile mpf) throws SystemException, IOException {
		String methodName = "[FileInfoHelps->saveFile]";
		// 真實文件保存絕對路徑
		String absPath = rootPath + relativePath;
		// 文件擴展名
		String ext = FilenameUtils.getExtension(absPath);
		// 保存封面文件
		try {
			// 創建文件上一層文件夾
			BaseCommonUtil.mkdir(absPath);
			mpf.transferTo(new File(absPath));
			logger.debug(methodName + "【" + mpf.getOriginalFilename() + "】文件保存成功!");
		} catch (Exception e) {
			// 文件保存出錯處理
			e.printStackTrace();
			logger.debug(methodName + "【" + mpf.getOriginalFilename() + "】文件保存失敗!");
			return null;
		}
		return new FileInfo(ext, absPath,relativePath);
	}
	
	public boolean isPreviewFile(String ext) {
		if (ext.toUpperCase().equals("DOC")) {
			return true;
		}
		if (ext.toUpperCase().equals("PPT")) {
			return true;
		}
		if (ext.toUpperCase().equals("XLS")) {
			return true;
		}
		return false;
	}
	
	/**
	*預覽文件生成代碼
	*/
	public void generatePreviewFile(int res_id,String relativePath) throws IOException{
		String ext = FilenameUtils.getExtension(relativePath);
		String absPath = rootPath+relativePath;
		//預覽文件保存在文件的當前目錄下的preview的文件下
		String savePath = absPath.substring(0, BaseCommonUtil.replaceFliePathStr(absPath).lastIndexOf("/"))+File.separatorChar+"preview";
		//創建保存路徑
		if(!new File(savePath).exists()){
			new File(savePath).mkdirs();
		}
		//預覽文件線程操作
		threadManager.execute(new Runnable() {
			@Override
			public void run() {
				try {
					//生成html預覽文件
					FileInputStream fis = new FileInputStream(new File(rootPath + relativePath));
					String htmlName = doc2HtmlUtil.file2Html(fis, savePath, ext.toLowerCase());
					String htmlPreview = relativePath.substring(0, BaseCommonUtil.replaceFliePathStr(relativePath).lastIndexOf("/"))+File.separatorChar+"preview"+File.separatorChar+htmlName;
				
					//生成pdf預覽文件
					FileInputStream fis_ = new FileInputStream(new File(rootPath + relativePath));
					String pdfName = doc2HtmlUtil.file2pdf(fis_,savePath, ext.toLowerCase());
					String pdfPreview = relativePath.substring(0, BaseCommonUtil.replaceFliePathStr(relativePath).lastIndexOf("/"))+File.separatorChar+"preview"+File.separatorChar+pdfName;
					
					FileResource fr = new FileResource();
					fr.setId(res_id);
					fr.setHtmlPreview(htmlPreview);
					fr.setPdfPreview(pdfPreview);
					resourceService.jdbcUpdateFilePreviewPath(fr);//在這裏可以在預覽文件生成後,寫入數據庫,前臺只需要c標籤判斷非空顯示預覽按鈕就可以啦
				} catch (Exception ex) {
					ex.printStackTrace();
				}
			}
		});
	}
	
	public String getRootPath() {
		return rootPath;
	}

	public void setRootPath(String rootPath) {
		this.rootPath = rootPath;
	}

}


PS:說再多的文字不如讀一讀代碼,在這裏再強調一下這裏多線程的思路,實例化多個不同端口的openoffice進程,都保持運行狀態,把端口放在線程安全的集合中,隊列最好,保證每個端口都可以被使用,每個線程獲取到一個端口之後,別的線程不能獲得相同端口,知道該線程操作完,將端口放回集合,其他線程繼續有機會讀取到該端口,

線程的管理使用JDK5 加入的ExecutorService 線程池管理,如果在單元測試單程序測試的話,一定要讓主線程等待子線程結束,否則還沒結果,測試就會結束,可以使用JD5的

CountDownLatch來實現主線程等待,同樣使用thread.join方法也行,建議用CountDownLatch,便捷使用,有問題歡迎聯繫我QQ346640094

代碼:

package campus_mooc.common_utils.utils;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.springframework.stereotype.Component;

/**
 * 線程池
 * @author Administrator
 *
 */
@Component(value="threadManager")//交給Spring管理
public class ThreadManager {
	private ExecutorService executorService;

	/**
	 * 
	 */
	public ThreadManager() {
		executorService = Executors.newFixedThreadPool(10);//初始化10的線程池,當執行的線程超過10,會等待線程池有空位
	}
	
	public void execute(Runnable runnable){
		executorService.execute(runnable);
	}
}








發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章