前言
在开发完前台页面之后,一个简单的博客系统就基本开发完成了。尚未完成的部分主要还有三个:富文本编辑器的图片处理、登录、全文检索。还有阅读量的显示等细节部分,比较容易实现。本节完成富文本编辑器的图片处理。
Java操作阿里云oss
富文本编辑器的图片处理包括两个部分:上传和修改。先来完成图片上传。我没有直接把图片保存在服务器里面,而是选择了阿里云的oss存储服务:对象存储 OSS,价格还是比较低的。首先在maven需要引入oss相关的jar包:
com.aliyun.oss
aliyun-sdk-oss
2.7.0
参考他人代码和api的oss工具类:
package com.vansl.utils;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.model.*;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import java.io.*;
import java.util.Calendar;
import java.util.List;
import java.util.Properties;
import java.util.Random;
/**
* @author: vansl
* @create: 18-5-10 下午4:09
*/
public class AliyunOSSUtil {
static Logger logger = LogManager.getLogger(AliyunOSSUtil.class);
private static String END_POINT; //访问域名
private static String ACCESS_KEY_ID; //秘钥id
private static String ACCESS_KEY_SECRET; //秘钥key
static{
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Properties properties = new Properties();
try(InputStream in =loader.getResourceAsStream("config/aliyunOSS.properties");){
properties.load(in);
END_POINT= properties.getProperty("END_POINT"); //访问域名
ACCESS_KEY_ID=properties.getProperty("ACCESS_KEY_ID"); //秘钥id
ACCESS_KEY_SECRET=properties.getProperty("ACCESS_KEY_SECRET"); //秘钥key
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 获得oss连接对象
* @return oss连接对象
*/
private static OSSClient initOssClient(){
return new OSSClient(END_POINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
}
/**
* 创建bucket并赋予权限
*
* @param bucketName buket名
* @param authc 访问权限(1,2,3)(私有,公共读,公共读写)
*/
public static void createBucket(String bucketName, int authc) {
//创建OSS客户端
OSSClient ossClient = initOssClient();
CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
switch (authc) {
case 1:
createBucketRequest.setCannedACL(CannedAccessControlList.Private);
break;
case 2:
createBucketRequest.setCannedACL(CannedAccessControlList.PublicRead);
break;
case 3:
createBucketRequest.setCannedACL(CannedAccessControlList.PublicReadWrite);
break;
default:
createBucketRequest.setCannedACL(CannedAccessControlList.Private);
break;
}
ossClient.createBucket(createBucketRequest);
// 关闭OSS连接
ossClient.shutdown();
}
/**
* 删除bucket
*
* @param bucketName bucket名
*/
public void deleteBucket( String bucketName) {
//创建OSS客户端
OSSClient ossClient = initOssClient();
ossClient.deleteBucket(bucketName);
// 关闭OSS连接
ossClient.shutdown();
}
/**
* 创建文件夹
*
* @param bucketName bucket名
* @param folder 模拟文件夹名
*/
public static void createFolder(String bucketName, String folder) {
//创建OSS客户端
OSSClient ossClient = initOssClient();
//上传空文件以创建目录
ossClient.putObject(bucketName, folder+"/", new ByteArrayInputStream(new byte[0]));
OSSObject object = ossClient.getObject(bucketName, folder);
String fileDir = object.getKey();
logger.info("文件夹创建成功:"+fileDir);
// 关闭OSS连接
ossClient.shutdown();
}
/**
* 删除文件夹
*
* @param bucketName bucket名
* @param folder 模拟文件夹名
*/
public static void deleteFolder(String bucketName, String folder) {
//创建OSS客户端
OSSClient ossClient = initOssClient();
//需要递归删除子文件夹
ObjectListing objectListing = ossClient.listObjects(bucketName, folder);
List sums = objectListing.getObjectSummaries();
for (OSSObjectSummary s : sums) {
if(!s.getKey().equals(folder)){
deleteFolder(bucketName,s.getKey());
}
}
ossClient.deleteObject(bucketName, folder);
logger.info("删除文件夹成功:"+ folder);
// 关闭OSS连接
ossClient.shutdown();
}
/**
* 上传文件
*
* @param file 文件对象
* @param bucketName bucket名
* @param folder 模拟文件夹名
* @return 文件访问url
*/
public static String upload(File file, String bucketName, String folder) {
if (file == null) {
return null;
}
//创建OSS客户端
OSSClient ossClient = initOssClient();
try(
//文件流
InputStream inputStream = new FileInputStream(file);
){
//文件名
String fileName = file.getName();
//文件的后缀名
String fileExtension = fileName.substring(fileName.lastIndexOf("."));
//文件大小
Long fileSize = file.length();
//创建上传文件的Metadata
ObjectMetadata metadata = new ObjectMetadata();
//上传文件的长度
metadata.setContentLength(inputStream.available());
//指定该object被下载时的网页的缓存行为
metadata.setCacheControl("no-cache");
//指定该object下设置Header
metadata.setHeader("Pragma","no-cache");
//指定该object被下载时的内容编码方式
metadata.setContentEncoding("utf-8");
//文件的MIME,定义文件的类型及网页编码,默认值application/octet-stream
metadata.setContentType(getContentType(fileExtension));
//指定该Object被下载时的名称
metadata.setContentDisposition("filename/filesize=" + fileName + "/" + fileSize + "Byte.");
// 生成新的文件名
String newFilename=generateFilename(fileExtension);
//上传文件 (上传文件流的形式)
ossClient.putObject(bucketName, folder+"/"+newFilename, inputStream, metadata);
String fileUrl = "https://"+bucketName+"."+END_POINT+"/"+folder+"/"+newFilename;
return fileUrl;
}catch (Exception e){
e.printStackTrace();
}finally {
// 关闭OSS连接
ossClient.shutdown();
}
return null;
}
/**
* 生成随机文件名
*
* @param fileExtension 文件后缀
* @return 生成的文件名
*/
public static String generateFilename(String fileExtension){
String randomFilename = "";
//生成五位随机数
Random random = new Random();
int randomNum = (int)(random.nextDouble()*90000)+10000;
//返回时间戳+随机数作为文件名
randomFilename = String.valueOf(System.currentTimeMillis())+randomNum+fileExtension;
return randomFilename;
}
/**
* 通过文件名判断并获取文件的contentType
*
* @param fileExtension 文件后缀
* @return 文件contentType
*/
public static String getContentType(String fileExtension){
if(".bmp".equalsIgnoreCase(fileExtension)) {
return "image/bmp";
}
if(".gif".equalsIgnoreCase(fileExtension)) {
return "image/gif";
}
if(".jpeg".equalsIgnoreCase(fileExtension) || ".jpg".equalsIgnoreCase(fileExtension) || ".png".equalsIgnoreCase(fileExtension) ) {
return "image/jpeg";
}
if(".html".equalsIgnoreCase(fileExtension)) {
return "text/html";
}
if(".txt".equalsIgnoreCase(fileExtension)) {
return "text/plain";
}
if(".vsd".equalsIgnoreCase(fileExtension)) {
return "application/vnd.visio";
}
if(".ppt".equalsIgnoreCase(fileExtension) || "pptx".equalsIgnoreCase(fileExtension)) {
return "application/vnd.ms-powerpoint";
}
if(".doc".equalsIgnoreCase(fileExtension) || "docx".equalsIgnoreCase(fileExtension)) {
return "application/msword";
}
if(".xml".equalsIgnoreCase(fileExtension)) {
return "text/xml";
}
//默认返回类型
return "image/jpeg";
}
/**
* 删除OSS文件
*
* @param bucketName bucket名
* @param folder 模拟文件夹名
* @param fileName 文件名(可以包含路径)
*/
public static void deleteFile(String bucketName,String folder,String fileName){
//创建OSS客户端
OSSClient ossClient = initOssClient();
ossClient.deleteObject(bucketName,folder+"/"+fileName);
logger.debug("删除"+bucketName+"下的文件:"+folder+"/"+fileName+"成功!");
// 关闭OSS连接
ossClient.shutdown();
}
}
阿里云域名和秘钥配置文件格式如下,放在config文件夹下:
#阿里云OSS配置
#访问域名
END_POINT=
#秘钥id
ACCESS_KEY_ID=
#秘钥key
ACCESS_KEY_SECRET=
代码测试成功,上传、访问、删除都没有问题。
图片上传
然后是负责图片上传处理的Controller。在这之前我修改了图片上传的请求路径,在富文本编辑器页面插入如下代码:
//修改请求路径
UE.Editor.prototype._bkGetActionUrl = UE.Editor.prototype.getActionUrl;
UE.Editor.prototype.getActionUrl = function(action) {
if (action == 'uploadimage' ){
return '/ued/image';
} else if(action == 'uploadscrawl') {
return '/ued/scrawl';
} else if(action == 'uploadvideo'){
return '/ued/video';
}else {
return this._bkGetActionUrl.call(this, action);
}
}
同时修改上传时的请求参数以判断用户、文章等信息以确定放在哪个文件夹之下:
//自定义上传请求参数
ue.execCommand('serverparam', {
'userId':1,
//'blogId': window.editBlogInfo.blogId
});
但在新建博客时由于没有blogId信息,所以无法确定放在哪个文件夹里。暂时没有想到比较好的解决办法,所以我干脆把同一用户的所有图片都放在一个文件夹下面了。Controller类代码:
package com.vansl.controller;
import com.baidu.ueditor.ActionEnter;
import com.vansl.utils.AliyunOSSUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
/**
* @author: vansl
* @create: 18-4-10 下午10:23
*/
@Controller
@RequestMapping("/ued")
public class UeditorController {
@GetMapping(value="")
public void config(HttpServletRequest request, HttpServletResponse response) {
response.setContentType("application/json");
String rootPath = request.getSession().getServletContext().getRealPath("/");
try {
String exec = new ActionEnter(request, rootPath).exec();
PrintWriter writer = response.getWriter();
writer.write(exec);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@PostMapping("/image")
@ResponseBody
public Map upload(String userId,/*String blogId,*/MultipartFile upfile) throws IOException {
//返回上传结果(url等)
Map result = new HashMap<>();
try {
if(upfile!=null&&upfile.getOriginalFilename()!=""){
//获取文件名
String fileName=upfile.getOriginalFilename();
//上传到阿里云oss
String url=AliyunOSSUtil.upload(upfile.getInputStream(),fileName,"vanslblog",userId);
//返回上传结果
result.put("state","SUCCESS");
result.put("url",url);
}
}catch (Exception e){
result.put("state","FAILED");
e.printStackTrace();
}finally {
return result;
}
}
}
测试成功,图片上传和回显都没有问题。
图片修改
然后是图片变化的监测,包括删除图片和直接删除文章。思路是:
需要说明的是,当同一文章下的图片保存为一个文件夹时,删除文章的操作原本是不需要对比直接删除整个文件夹就可以了,但如前面所说,考虑到当新建文章时上传图片无法确定上传到哪个文件夹下,所以干脆就把所有图片都放到一个文件夹下了。另外,当用户新建文章时上传了图片却没有上传文章时,上传的图片就会浪费存储空间,但由于难以判断用户未上传文章,所以在这种情况下没有去删除这些图片。这些实现应该都不是最优解,但暂时没有想到其他较好的办法。
我使用了一个Spring拦截器来进行处理图片变化,需要拦截updateBlog、deleteBlog两个方法。
由于ServletRequest的请求数据流被设置成只能被读取一次,而Spring拦截器做不到传递对象,所以还需要加一个filter把request对象用HttpServletRequestWrapper包装起来,使请求数据能被重复读取。这个filter的实现参考这篇文章即可:ervletRequest中getReader()和getInputStream()只能调用一次的解决办法。
拦截器HandlerInterceptor的代码如下:
package com.vansl.interceptor;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.vansl.dao.BlogDao;
import com.vansl.utils.AliyunOSSUtil;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author: vansl
* @create: 18-5-15 下午5:54
*
* 处理博客更新时的图片变化
*/
public class BlogImgInterceptor implements HandlerInterceptor {
@Autowired
BlogDao blogDao;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
//拦截updateBlog、deleteBlog两个请求
if(request.getMethod().equals("PUT")||request.getMethod().equals("DELETE")){
//从请求url中提取博客id
String uri=request.getRequestURI();
Integer blogId=Integer.valueOf(uri.substring(uri.lastIndexOf("/")+1));
//将请求体的json字符串转换为json对象
JSONObject requestData=JSON.parseObject(IOUtils.toString(request.getReader()));
//从请求体中提取userId
String userId=String.valueOf(requestData.get("userId"));
//原博客内容
String origin=blogDao.selectContentByBlogId(blogId);
//提取原博客中的图片url
HashSet originUrl=getImageUrl(origin,"");
//如果是删除操作则直接删除所有图片
if(request.getMethod().equals("DELETE")){
//遍历原博客的所有图片url
for (String imgUrl:originUrl) {
String fileName=imgUrl.substring(imgUrl.lastIndexOf("/")+1);
AliyunOSSUtil.deleteFile("vanslblog",userId,fileName);
}
//否则如果是更新操作,对比之后删除
}else{
//提取现博客中的图片url
HashSet currentUrl=getImageUrl((String)requestData.get("content"),"");
//遍历原博客的所有图片url
for (String imgUrl:originUrl) {
//如果现博客不含该url则调用删除
if (!currentUrl.contains(imgUrl)){
System.out.println(imgUrl.substring(imgUrl.lastIndexOf("/")+1) );
String fileName=imgUrl.substring(imgUrl.lastIndexOf("/")+1);
AliyunOSSUtil.deleteFile("vanslblog",userId,fileName);
}
}
}
}
return true;
}
public HashSet getImageUrl(String content,String regex){
HashSet result=new HashSet<>();
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
result.add(matcher.group(1));
}
return result;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) throws Exception {
}
}