在做前前端项目的时候,由于业务图片是通过ftp资源进行管理,所以前端刚开始获取图片,都是在controller层调用service进行ftp图片资源访问,然后进行格式处理后,通过response返回图片资源。后来发现,这样太消耗系统的性能了,最好的方式就是传输的是网站图片的静态资源。所以,开始自己尝试着开发图片服务器的分离系统。
先说明一下我自己对于图片处理的一些业务需求想法:项目的网站是做一个拍卖交易平台,目前主要是书画方面。在后台进行管理,前端进行显示,前后段分离的。在前台,对于同一个书画,会有多个页面显示,每个页面是显示的图片大小,前台的设计人员设计的也都不一样,包括书画本身,高款也都是千奇百怪的。而我,有不想对于每一个页面,都在后台进行上传与它想匹配的图片,那样一来,不仅工作量庞大,而且开发起来的效率也低。于是我想到的是,在后台只上传一次图片,然后所有的网站(包括后台)都是在图片服务器进行图片的获取(也就是图片的网络地址为单独的域名与单独的服务器),然后传输不同的参数,服务器就对原图计算生成对应的缓存图片,并给定一个唯一的图片Url Hash文件名。然后返回生成的缓存文件,当有相同的规格的图片请求时,就可以直接返回已经缓存的静态图片资源(如image.xxx.com/images/xxx.png)。于是,便设计出如下的图片服务器架构:
为方便开发人员传输获取图片的格式条件,我提供一下的vo类与factory类ImageCondition
代码1:ImageCondition.java
import java.io.Serializable; /** * 获取图片的条件 * * path :String 待获取的图像文件在FTP服务器上的全路径 * alpha : float 图片的透明度(0.0到1.0之间的float值,默认1.0) * scale : int 对图像进行缩放处理,小于100为缩小,大于100为放大 * width : int 指定图像的输出宽度 * height : int 指定图像的输出高度 * sign : false:不添加版权水印 默认true (小于100 * 50 的不加水印) * signAlpha : float 水印的透明度(0.0到1.0之间的float值,默认1.0) * * 注意: * 1、width与height成对出现才会生效 * 2、如果指定了width和height,那么scale参数将会失效 * * Copyright 2014-2015 the original BZTWT * Created by QianLong on 2014/7/9 0009. */ public class ImageCondition implements Serializable { private String path; private float alpha; private int scale; private int width; private int height; private boolean sign; private float signAlpha; private boolean recommentSign; public boolean isRecommentSign() { return recommentSign; } public void setRecommentSign(boolean recommentSign) { this.recommentSign = recommentSign; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public float getAlpha() { return alpha; } public void setAlpha(float alpha) { this.alpha = alpha; } public int getScale() { return scale; } public void setScale(int scale) { this.scale = scale; } public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public boolean isSign() { return sign; } public void setSign(boolean sign) { this.sign = sign; } public float getSignAlpha() { return signAlpha; } public void setSignAlpha(float signAlpha) { this.signAlpha = signAlpha; } }
代码2:ImageConditionFactory.java :
import com.cqcae.vo.ImageCondition; /** * 图片条件值类工厂 * 本工厂类返回常用的一些图片条件值类 * 若需要创建拥有透明度(图片透明度或版权水印透明度)的图片条件, * 请手动新建ImageCondition实例 * Copyright 2014-2015 the original BZTWT * Created by QianLong on 2014/7/9 0009. */ public class ImageConditionFactory { /** * 返回原图(默认带有版权水印) * @param path * @return */ public static ImageCondition getNativeImage(String path){ ImageCondition imageCondition = new ImageCondition(); imageCondition.setPath(path); imageCondition.setSign(true); return imageCondition; } /** * 返回不带水印的原图 * @param path * @return */ public static ImageCondition getNativeImageNoSign(String path){ ImageCondition imageCondition = new ImageCondition(); imageCondition.setPath(path); imageCondition.setSign(false); return imageCondition; } /** * 返回指定高宽的图片条件(默认带有版权水印) * @param path * @param width * @param height * @return */ public static ImageCondition getImageCondition(String path,int width,int height){ ImageCondition imageCondition = new ImageCondition(); imageCondition.setPath(path); imageCondition.setWidth(width); imageCondition.setHeight(height); imageCondition.setSign(true); return imageCondition; } /** * 返回没有版权水印的指定高宽的图片条件 * @param path * @param width * @param height * @return */ public static ImageCondition getImageConditionNoSign(String path,int width,int height){ ImageCondition imageCondition = new ImageCondition(); imageCondition.setPath(path); imageCondition.setWidth(width); imageCondition.setHeight(height); imageCondition.setSign(false); return imageCondition; } /** * 返回指定缩放比例的图片条件(默认带有版权水印) * @param path * @param scale * @return */ public static ImageCondition getImageCondition(String path,int scale){ ImageCondition imageCondition = new ImageCondition(); imageCondition.setPath(path); imageCondition.setScale(scale); imageCondition.setSign(true); return imageCondition; } /** * 返回没有版权水印的指定缩放比例的图片条件 * @param path * @param scale * @return */ public static ImageCondition getImageConditionNoSign(String path,int scale){ ImageCondition imageCondition = new ImageCondition(); imageCondition.setPath(path); imageCondition.setScale(scale); imageCondition.setSign(false); return imageCondition; } }
/** * 获取图片的静态资源地址 * @param imageCondition * @return * null : 返回失败 * String :返回的图片路径地址 */ public String getImagePath(ImageCondition imageCondition) { String url = baseUrl + "getImagePath?t=0"; if(imageCondition.getWidth() != 0 && imageCondition.getHeight() != 0){ url += "&width=" + imageCondition.getWidth() + "&height=" + imageCondition.getHeight(); }else if(imageCondition.getScale() != 0){ url += "&scale=" + imageCondition.getScale(); } if(imageCondition.getPath() != null && !imageCondition.getPath().equals("")){ url += "&path=" + imageCondition.getPath(); } if(imageCondition.getAlpha() != 0){ url += "&alpha=" + imageCondition.getAlpha(); } if(imageCondition.getSignAlpha() != 0){ url += "&signAlpha=" + imageCondition.getSignAlpha(); } url += "&recommentSign=" + imageCondition.isRecommentSign(); url += "&sign=" + imageCondition.isSign(); try { return SendRequestUtil.request(url, RequestMethod.GET).replace("\n","").replace("\r",""); } catch (Exception e) { RuntimeExceptionUtil.msgToFile(e); return null; } }
首先是图片服务器中,接受请求并返回图片缓存文件的静态路径方法:
代码4:
/** * 获取图片静态资源的hashUrl网址 * @param request * @param response */ @RequestMapping(value = "getImagePath",method = RequestMethod.GET) @ResponseBody public void getImagepath(HttpServletRequest request,HttpServletResponse response){ log.debug(request.getRemoteAddr() + "获取图片的静态资源路径"); response.setCharacterEncoding("UTF-8"); PrintWriter out; try { out = response.getWriter(); } catch (IOException e) { return; } String path = request.getParameter("path"), scale_str = request.getParameter("scale"), width_str = request.getParameter("width"), height_str = request.getParameter("height"), sign_str = request.getParameter("sign"), signAlpha_str = request.getParameter("signAlpha"), alpha_str = request.getParameter("alpha"), recommentSign_str = request.getParameter("recommentSign"); float signAlpha = (float) 1; float alpha = (float) 1; boolean sign = true; boolean recomment = false; try { sign = !sign_str.equals("false"); } catch (Exception ignored) { } try { recomment = recommentSign_str.equals("true"); } catch (Exception ignored) { } try { if(signAlpha_str != null && !signAlpha_str.equals("")){ signAlpha = Float.parseFloat(signAlpha_str); }else { signAlpha = 1; } } catch (NumberFormatException ignored) { } try { if(alpha_str != null && !alpha_str.equals("")){ alpha = Float.parseFloat(alpha_str); }else { alpha = 1; } } catch (NumberFormatException ignored) {} //获取本图片系统对外的网站地址 URL url = ConfigSign.class.getResource(""); String p = url.getPath(); String pu = FileUtil.getTextFileContent(p + "locationWebURL.txt"); log.debug("获取图片系统对外的网站地址:" + pu); String cacheFileName = ""; if(path != null && !path.trim().equals("") && path.lastIndexOf(".") != -1){ cacheFileName = ImageCatchUtil.getImageCachePathByFtpPath(path, sign,recomment, height_str, width_str, scale_str, signAlpha, alpha); log.debug("获取图片静态资源的hash相对路径:" + cacheFileName); String cacheFileName1 = cacheFileName; //对平台的路径分隔符进行判断 if(File.separator.equals("\\")){ cacheFileName = cacheFileName.replace("/", "\\"); } String webPath = ImageUtil.getWebPath(request); log.debug("根据hash路径生成缓存文件绝对路径:" + webPath + cacheFileName); File cacheFile = new File(webPath + cacheFileName); if(cacheFile.exists()){ log.debug("缓存文件已生成,返回已存在的静态资源网址:" + pu + cacheFileName1); out.write(pu + cacheFileName1); }else { log.debug("缓存文件不存在,正在生成缓存文件"); if(ftpImageToCacheService.imageToCache(request)){ log.debug("缓存文件生成成功,返回静态资源网址:" + pu + cacheFileName1); out.write(pu + cacheFileName1); }else { log.warn("缓存文件生成失败,返回动态图片请求链接:" + pu + "image?path="); out.write(pu + "image?path="); } } }else { out.write(pu + "image?path="); log.warn("传入的path无效,返回动态图片请求链接:" + pu + "image?path="); } }代码4中的ImageCatchUtil.getImageCachePathByFtpPath方法就是用与组建文件hash路径名。
秉着分享原则,下面给出代码4中,生成缓存文件的代码:
代码5:
/** * 根据请求条件,将原图进行转换,并生成缓存文件 * @param request * @return */ @Override public boolean imageToCache(HttpServletRequest request) { String path = request.getParameter("path"), scale_str = request.getParameter("scale"), width_str = request.getParameter("width"), height_str = request.getParameter("height"), sign_str = request.getParameter("sign"), signAlpha_str = request.getParameter("signAlpha"), alpha_str = request.getParameter("alpha"), recommentSign_str = request.getParameter("recommentSign"); boolean sign = true; boolean recomment = false; try { sign = !sign_str.equals("false"); } catch (Exception ignored) { } try { recomment = recommentSign_str.equals("true"); } catch (Exception ignored) { } try { if(signAlpha_str != null && !signAlpha_str.equals("")){ signAlpha = Float.parseFloat(signAlpha_str); }else { signAlpha = 1; } } catch (NumberFormatException ignored) { } try { if(alpha_str != null && !alpha_str.equals("")){ alpha = Float.parseFloat(alpha_str); }else { alpha = 1; } } catch (NumberFormatException ignored) {} String cacheFileName = ""; if(path != null && !path.trim().equals("") && path.lastIndexOf(".") != -1){ cacheFileName = ImageCatchUtil.getImageCachePathByFtpPath(path, sign,recomment, height_str, width_str, scale_str, signAlpha, alpha); log.debug("获取图片静态资源的hash相对路径:" + cacheFileName); if(File.separator.equals("\\")){ cacheFileName = cacheFileName.replace("/",File.separator); } }else { log.warn("ftp文件资源路径错误,返回false"); return false; } File cacheFile; try { if(path == null || path.equals("")){ return false; } String webPath = ImageUtil.getWebPath(request); String cacheDir = (webPath + path.substring(1,path.lastIndexOf("/")+1)) .replace("/",File.separator); log.debug("计算待生成的缓存图片的绝对路径目录:" + cacheDir); String fileType; try { fileType = path.substring(path.lastIndexOf(".")); } catch (Exception e) { return false; } log.debug("原图片的文件类型:" + fileType); cacheFile = new File(webPath + cacheFileName); log.debug("计算待生成图片的绝对路径:" + webPath + cacheFileName); if(!cacheFile.exists()){ log.debug("待生成缓存文件的目录不存在,正在进行自动创建目录:" + cacheDir); File cacheFileDir = new File(cacheDir); cacheFileDir.mkdirs(); } log.debug("从ftp服务器下载path对应的图片资源:" + path); log.debug("保存到文件:" + cacheFile.getAbsolutePath()); imageFtpService.download(cacheFile, path); BufferedImage image = ImageIO.read(cacheFile); if (image == null) {//输出提示错误图像 log.debug("图像下载错误,返回false"); return false; } else { log.debug("图像下载成功,进行图像转换操作"); image = ImageUtil.imageConvert(image,height_str,width_str,sign,recomment,signAlpha,scale_str,alpha); log.debug("保存转换后的图片为缓存文件写入磁盘"); ImageUtil.getInstance().writeImage(fileType,image,cacheFile); if(cacheFile.exists()){ log.debug("缓存文件生成成功"); return true; }else { log.warn("缓存文件生成失败"); return false; } } } catch (IOException e) { RuntimeExceptionUtil.msgToFile(e); log.error("保存request中的图片信息到缓存文件中发生异常",e); return false; } }
代码6:ImageUtil.java :
import com.cqcae.util.CustomerMath; import com.cqcae.util.RuntimeExceptionUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.imageio.ImageIO; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import java.awt.*; import java.awt.image.BufferedImage; import java.awt.image.CropImageFilter; import java.awt.image.FilteredImageSource; import java.awt.image.ImageFilter; import java.io.File; import java.io.IOException; /** * 图片处理工具类 * 单例模式 * Copyright 2014-2015 the original BZTWT * Created by QianLong on 2014/7/9 0009. */ public class ImageUtil { private Logger log = LoggerFactory.getLogger(ImageUtil.class); private static ImageUtil imageUtil = null; private ImageUtil() { } public static String getWebPath(HttpServletRequest request){ String webPath = request.getSession().getServletContext().getRealPath("/"); if(webPath.lastIndexOf(File.separator) != webPath.length()-1){ webPath = webPath + File.separator; } return webPath; } public static ImageUtil getInstance() { if (imageUtil == null) { imageUtil = new ImageUtil(); } return imageUtil; } /** * 图片转换操作 * 注意: * 1、width与height成对出现才会生效 * 2、如果指定了width和height,那么scale参数将会失效 * @param image * 待转换的图像 * @param height_str * 指定图像的输出高度 * @param width_str * 指定图像的输出宽度 * @param sign * false:不添加版权水印 默认true (小于100 * 50 的不加水印) * @param recomment * true:添加重点推荐水印,默认false * @param signAlpha * 水印的透明度(0.0到1.0之间的float值,默认1.0) * @param scale_str * 对图像进行缩放处理,小于100为缩小,大于100为放大 * @param alpha * 图片的透明度(0.0到1.0之间的float值,默认1.0) * @return */ public static BufferedImage imageConvert(BufferedImage image,String height_str,String width_str ,boolean sign,boolean recomment,float signAlpha,String scale_str,float alpha){ if(height_str != null && width_str != null){//转换图像到指定高宽 int width = 0,height = 0; try { width = Integer.parseInt(width_str); height = Integer.parseInt(height_str); image = ImageUtil.getInstance().turnImageToWH(width,height,image,sign,recomment,signAlpha); } catch (NumberFormatException e) {//输出提示错误图像 RuntimeExceptionUtil.msgToFile(e); image = ImageUtil.getInstance().getFontImage("图像参数错误"); } }else if (scale_str != null) {//转换图像的等比缩放 int scale = 100; try { scale = Integer.parseInt(scale_str); image = ImageUtil.getInstance().scale(scale,image,sign,recomment,signAlpha); } catch (NumberFormatException e) {//输出提示错误图像 RuntimeExceptionUtil.msgToFile(e); image = ImageUtil.getInstance().getFontImage("图像参数错误"); } }else{ if(sign) image = ImageUtil.getInstance().addSign(image,signAlpha); } //调整图像透明度 ImageUtil.getInstance().getImageAlpha(image,alpha); return image; } /** * 调整图像透明度 * * @param image * @param alpha */ public void getImageAlpha(BufferedImage image, float alpha) { BufferedImage bufferedImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB); Graphics2D g2 = bufferedImage.createGraphics(); bufferedImage = g2.getDeviceConfiguration().createCompatibleImage(image.getWidth(), image.getHeight(), Transparency.TRANSLUCENT); g2.dispose(); g2 = bufferedImage.createGraphics(); g2.setComposite(AlphaComposite.SrcOver.derive(alpha)); g2.drawImage( image.getScaledInstance(image.getWidth(), image.getHeight(), Image.SCALE_SMOOTH), 0, 0, null); g2.dispose(); } /** * response写出图片 * * @param fileType * @param image * @param servletOutputStream * @throws java.io.IOException */ public void writeImage(String fileType, BufferedImage image, ServletOutputStream servletOutputStream) throws IOException { try { switch (fileType) { case ".jpg": ImageIO.write(image, "PNG", servletOutputStream); break; case ".gif": ImageIO.write(image, "GIF", servletOutputStream); break; case ".png": ImageIO.write(image, "PNG", servletOutputStream); break; } } catch (IndexOutOfBoundsException e) { ImageIO.write(getFontImage("图像输出错误"), "PNG", servletOutputStream); } finally { servletOutputStream.close(); } } /** * response写出图片,同时保存请求图片的缓存文件 * * @param fileType * @param image * @param servletOutputStream * @param cacheFile * @throws IOException */ public void writeImage(String fileType, BufferedImage image, ServletOutputStream servletOutputStream, File cacheFile) throws IOException { try { switch (fileType) { case ".jpg": ImageIO.write(image, "PNG", servletOutputStream); ImageIO.write(image, "PNG", cacheFile); break; case ".gif": ImageIO.write(image, "GIF", servletOutputStream); ImageIO.write(image, "GIF", cacheFile); break; case ".png": ImageIO.write(image, "PNG", servletOutputStream); ImageIO.write(image, "PNG", cacheFile); break; } } catch (IndexOutOfBoundsException e) { ImageIO.write(getFontImage("图像输出错误"), "PNG", servletOutputStream); //若缓存文件保存失败,则删除缓存文件 if (cacheFile.exists()) { cacheFile.delete(); } } finally { servletOutputStream.close(); } } /** * 保存请求图片的缓存文件 * * @param fileType * @param image * @param cacheFile * @throws IOException */ public void writeImage(String fileType, BufferedImage image, File cacheFile) throws IOException { try { switch (fileType) { case ".jpg": ImageIO.write(image, "PNG", cacheFile); break; case ".gif": ImageIO.write(image, "GIF", cacheFile); break; case ".png": ImageIO.write(image, "PNG", cacheFile); break; } } catch (IndexOutOfBoundsException e) { //若缓存文件保存失败,则删除缓存文件 if (cacheFile.exists()) { cacheFile.delete(); } } } /** * 返回指定高宽的图片 * 按照中间的位置自动裁剪图片 * @param width * @param height * @param image * @param sign * @return */ public BufferedImage turnImageToWH(int width, int height, BufferedImage image, boolean sign, boolean recomment, float signAlpha) { /** * 转换图像(高宽至少有目标高宽的最大值) */ double z; if (image.getWidth() == image.getHeight()) { int s = width > height ? width : height; z = CustomerMath.div(s, image.getHeight(), 2); } else if (width > height) { if (image.getHeight() > image.getWidth()) { z = CustomerMath.div(width, image.getWidth(), 2); } else { z = CustomerMath.div(height, image.getHeight(), 2); } } else if (width < height) { if (image.getHeight() > image.getWidth()) { z = CustomerMath.div(width, image.getWidth(), 2); } else { z = CustomerMath.div(height, image.getHeight(), 2); } } else { int cut = image.getWidth() <= image.getHeight() ? image.getWidth() : image.getHeight(); z = CustomerMath.div(width, cut, 2); } double tw = CustomerMath.mul(z,image.getWidth()); double th = CustomerMath.mul(z,image.getHeight()); while (tw < width || th < height){ z = z + 0.01; tw = CustomerMath.mul(z,image.getWidth()); th = CustomerMath.mul(z,image.getHeight()); } int z1 = (int) CustomerMath.mul(z, 100); BufferedImage image1 = scale(z1, image, false, false, signAlpha); int x = 0,y = 0; if (image1.getWidth() > width) { x = (int) (CustomerMath.div(image1.getWidth(), 2) - CustomerMath.div(width, 2)); } if (image1.getHeight() > height) { y = (int) (CustomerMath.div(image1.getHeight(), 2) - CustomerMath.div(height, 2)); } BufferedImage result = cut(image1,x,y,width,height); if(sign) result = addSign(result,signAlpha); if(recomment) result = addRecommentSign(result); return result; } /** * 图像切割(指定切片的宽度和高度) * @param bi 原图像 * @param x 裁剪原图像起点坐标X * @param y 裁剪原图像起点坐标Y * @param width 目标切片宽度 * @param height 目标切片高度 * @return */ public static BufferedImage cut(BufferedImage bi,int x, int y, int width, int height) { BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); int srcWidth = bi.getHeight(); // 源图宽度 int srcHeight = bi.getWidth(); // 源图高度 if (srcWidth > 0 && srcHeight > 0) { ImageFilter cropFilter = new CropImageFilter(x, y, width, height); Image img = Toolkit.getDefaultToolkit().createImage( new FilteredImageSource(bi.getSource(),cropFilter)); Graphics g = tag.getGraphics(); g.drawImage(img, 0, 0, width, height, null); // 绘制切割后的图 g.dispose(); } return tag; } /** * 等比缩放 * * @param scale * @param scaleImage * @param sign * @return */ public BufferedImage scale(int scale, BufferedImage scaleImage, boolean sign, boolean recomment, float signAlpha) { int width = (int) (scaleImage.getWidth(null) * scale / 100.0); int height = (int) (scaleImage.getHeight(null) * scale / 100.0); BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); bufferedImage.getGraphics().drawImage( scaleImage.getScaledInstance(width, height, Image.SCALE_SMOOTH), 0, 0, null); if (sign) { bufferedImage = addSign(bufferedImage, signAlpha); } if (recomment) { bufferedImage = addRecommentSign(bufferedImage); } return bufferedImage; } /** * 获取指定文字的图片 * * @param font * @return */ public BufferedImage getFontImage(String font) { BufferedImage image = new BufferedImage(100, 35, BufferedImage.TYPE_INT_RGB); Graphics g = image.getGraphics(); g.setFont(new Font("宋体", Font.BOLD, 15)); g.setColor(Color.CYAN); g.drawString(font, 0, 25); g.dispose(); return image; } /** * 添加版权水印 * * @param image * @return */ public BufferedImage addSign(BufferedImage image, float signAlpha) { Graphics g = image.getGraphics(); g.setFont(new Font("宋体", Font.BOLD, 15)); g.setColor(Color.GRAY); //小于120 * 50 的不加水印 if (image.getWidth() < 150 || image.getHeight() < 50) return image; Graphics2D g2 = image.createGraphics(); g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, signAlpha)); try { String markImgPath = this.getClass().getClassLoader().getResource("watermark.png").getPath(); File file = new File(markImgPath); BufferedImage markImg = ImageIO.read(file); g2.drawImage(markImg, image.getWidth() - 110, image.getHeight() - 35, null); } catch (IOException e) { g2.drawString("盛世文化", image.getWidth() - 80, image.getHeight() - 25); g2.drawString("版权所有", image.getWidth() - 80, image.getHeight() - 10); } g.dispose(); return image; } /** * 添加推荐水印 * * @param image * @return */ public BufferedImage addRecommentSign(BufferedImage image) { Graphics g = image.getGraphics(); g.setFont(new Font("宋体", Font.BOLD, 15)); g.setColor(Color.GRAY); //小于120 * 50 的不加水印 Graphics2D g2 = image.createGraphics(); try { String markImgPath = this.getClass().getClassLoader().getResource("recomment_ico.png").getPath(); File file = new File(markImgPath); BufferedImage markImg = ImageIO.read(file); g2.drawImage(markImg, 0, 0, null); } catch (IOException ignored) { } g.dispose(); return image; } }
后续再分享我在项目中的其他经验。呵呵。