最近一直在鼓捣图片相关的代码,今天抽时间写篇总结。此文没有什么高深的知识点,不汲及第三方的OSS相关点,更不汲及分布式文件存储框架,算是一篇关于WEB项目中图片相关功能的扫盲文; 同时与大家分享码字时的心得。文章中的服务器开发语言使用的是java。文中代码汲及到一个工具子模块(util)在文章最后提供下载连接,放心不需要您有下载积分,防止资源若审核过不去同时提供百度网盘地址。

A. 客户端:

A1. html表单,客户端预览

有同事认为这像输入数字时调出数字键盘一样是高级功能。其实不然,现代浏览器提供多种方案可以实现,代码也不复杂。先看图:


以下代码使用FileReader生成图片的BASE64值:

<div class="form-group row"><label for="imageAddr" class="col-12 col-sm-3 col-form-label text-sm-right">图标</label><div class="col-12 col-sm-8 col-lg-6" id="upload-section"><label class="custom-control custom-checkbox"><input type="text" class="form-control" value="" tabindex="2" readonly="readonly" id="inputfile-names"/><span class="form-text text-muted">可选项,不填写将使用默认值</span></label><label class="custom-control custom-checkbox"><input type="file" name="file" id="inputfile" accept="image/*" multiple="multiple" data-multiple-caption="{count} 文件被选中"/><div class="images"><div class="pic">选择图片</div></div></label></div>
</div>

201491026增补

input type=file的accept属性
可以粗略的划分:accept=“image/*”, 也可用Content-type来细分: accept=“image/gif, image/jpeg”,还可以用扩展名过滤:accept=".gif,.jpeg,.png,.jpg"
需要注意不同浏览器对上面的三种情况的待遇也不同,但无一例外都有选择所有文件的选项,问题来了:怎么删除它或让不可用呢?

function uploadImage() {var uploader = $('#inputfile');var images = $('.images');uploader.on('change', function () {var reader = new FileReader();reader.onload = function(event) {images.prepend('<div class="img" style="background-image: url(\'' + event.target.result + '\');" rel="'+ event.target.result  +'"><span>删除</span></div>');images.find('.pic').hide();};reader.readAsDataURL(uploader[0].files[0]);});images.on('click', '.img', function () {$(this).remove();images.find('.pic').show();});
}

图片BASE64更多讨论:how-to-convert-image-into-base64-string-using-javascript

A2. 富文本编辑,CKEditor中的图片上传

CKEditor的版本号:4.11.4; 若使用其它富文本编辑器请自行参阅官方文档。以下代码用一个函数来封装了CKEditor的实例

//v:4.11.4
function initCkEditor(config){var options=$.extend({textarea: 'content', plugs: 'uploadimage', width: 600, height: 150, UploadUrl: '/upload/ckeditor'}, config);CKEDITOR.config.pasteFilter = null;CKEDITOR.config.height = options.height;CKEDITOR.config.width = options.width;CKEDITOR.replace( options.textarea ,{extraPlugins: options.plugs});if(options.plugs.indexOf('uploadimage') != -1){CKEDITOR.config.filebrowserUploadUrl = options.UploadUrl+'?type=Files';CKEDITOR.config.filebrowserImageUploadUrl = options.UploadUrl+'?type=Images';}
};

这里提到CKEditor是因为后面的服务器端上传时需要。因为不同版本的CKEditor的图片上传响应格式不同。此版本的响应格式为:

{"uploaded": 1,"fileName": "foo(2).jpg","url": "/files/foo(2).jpg","error": {"message": "A file with the same name already exists. The uploaded file was renamed to \"foo(2).jpg\"."}
}

CKEditor的参考文档: Uploading Dropped or Pasted Files

B. 本地存储(服务器端)

如果项目中的图片不是很多。像一般企业CMS,本地存储也是可以的,具体情况具体对待。为了在需求变动时不改动代码,服务器端对图片的地址进行编码和解码,在数据库中存储的图片地址都是编码后的. 例 :<img src="data:image://xxx/yy.z"/>。这种格式没有域名部分,没有具体的目录信息,只有图片的文件名(yy.z)及上一级目录的名称(xxx)。根据具体的配置信息浏览器中html的图片src解码为具体的可访问地址。

B1. 外部化上传需要的参数信息, 以下是资源文件中代码:

img.bucket.domain=http://www.test.com
img.bucket.upload.direct=imagestore

为了在代码中使用方便,而不是到处使用@Value.需要在spring bean工厂中实例一个单例的值对象,例:

 <!-- 站内本地存储 --><bean id="imageStorageExecutor" class="com.apobates.forum.trident.fileupload.LocalImageStorage"><constructor-arg name="imageBucketDomain" value="${img.bucket.domain}"/><constructor-arg name="uploadImageDirectName" value="${img.bucket.upload.direct}"/><constructor-arg name="localRootPath" value="C:\apache-tomcat-8.5.37\webapps\ROOT\"/> </bean>

localRootPath值等于:servletContext.getRealPath("/");。为了以后站外存储或第三方存储图片,ImageStorageExecutor设计成一个接口。相关代码如下:

public interface ImageStorageExecutor extends ImageStorage{/*** 存储图片* * @param file 图片文件* @return 成功返回图片的访问连接* @throws IOException*/Result<String> store(MultipartFile file) throws IOException;
}

Result是一个类似Optional的一个工具类,同时提供Empty,Success,Fail三种情况; 代码在工具子模块(util)中。 ImageStorage为一获取配置信息的接口:

public interface ImageStorage {/*** 图片存储的域名,例:http://x.com* * @return*/String imageBucketDomain();/*** 图片存储的目录名* * @return*/String uploadImageDirectName();
}

B2. spring mvc获取表单中的图片。并上传图片

formbean中的代码大致如下:

public class BoardForm extends ActionForm{private String title;private String imageAddr;private MultipartFile file;//ETCpublic MultipartFile getFile() {return file;}public void setFile(MultipartFile file) {this.file = file;}public String getEncodeIcoAddr(ImageStorageExecutor executor){return super.uploadAndEncodeFile(getFile(), executor);}
}

方法getEncodeIcoAddr使用imageStorageExecutor(注入到控制器类中的)来获得上传后的图片访问地址并对其进行编码。具体上传工作ActionForm.uploadAndEncodeFile方法来作。代码如下:

public abstract class ActionForm implements Serializable{private String record = "0";private String token; // (message="错误代码:1051;抽像的操作标识丢失")private String status = "0";// 来源地址// 只允许是本站地址,例:/xprivate String refer;private final static Logger logger = LoggerFactory.getLogger(ActionForm.class);//ETC/*** * @param file     上传的文件* @param executor 上传的执行器* @return*/protected String uploadAndEncodeFile(MultipartFile file, ImageStorageExecutor executor)throws FileUploadFailException{Result<String> data = Result.empty();try{data = executor.store(file);}catch(IOException e){//只关心上传产生的错误throw new FileUploadFailException(e.getMessage()); //只关心上传产生的错误}if(data.isFailure()){ //上传过程中的错误throw new FileUploadFailException(data.failureValue().getMessage());}//String defaultValue = "image://defat/ico/default_icon.png";if(data.isEmpty()){ //新增返回默认值//编辑返回nullreturn (isUpdate())?null:defaultValue;}//编码ImagePathCoverter ipc = new ImagePathCoverter(data.successValue());return ipc.encodeUploadImageFilePath(executor.imageBucketDomain(), executor.uploadImageDirectName()).getOrElse(defaultValue);}
}

ImagePathCoverter类的构造参数即为图片的可访问地址,接下来上传工作交给ImageStorageExecutor接口的实现类:LocalImageStorage,代码如下:

public class LocalImageStorage implements ImageStorageExecutor{private final String imageBucketDomain;private final String uploadImageDirectName;//servletContext.getRealPath("/")private final String localRootPath;/*** * @param imageBucketDomain     本站的域名,例:http://x.com* @param uploadImageDirectName 存储图片的目录名称* @param localRootPath          servletContext.getRealPath("/")的结果*/public LocalImageStorage(String imageBucketDomain, String uploadImageDirectName, String localRootPath) {super();this.imageBucketDomain = imageBucketDomain;this.uploadImageDirectName = uploadImageDirectName;this.localRootPath = localRootPath;}@Overridepublic Result<String> store(MultipartFile file) throws IOException {//空白if(file==null || file.isEmpty()){return Result.failure("arg file is null or empty");}//子目录String childDirect = DateTimeUtils.getYMD();// 项目路径final String realPath = localRootPath + File.separator + uploadImageDirectName + File.separator +childDirect;// 前台访问路径final String frontVisitPath = imageBucketDomain + "/" + uploadImageDirectName + "/" + childDirect + "/";CommonInitParamers cip = new CommonInitParamers() {@Overridepublic String getFileSaveDir() {return realPath;}@Overridepublic String getCallbackFunctionName() {// 没有回调函数return null;}};// 使用fileuploadSpringFileUploadConnector connector = new SpringFileUploadConnector(new CKEditorHightHandler(frontVisitPath));try{return Result.ofNullable(connector.upload(cip, file)); //返回文件的访问地址}catch(ServletException e){return Result.failure("upload has exception: "+ e.getMessage());}}@Overridepublic String imageBucketDomain() {return imageBucketDomain;}@Overridepublic String uploadImageDirectName() {return uploadImageDirectName;}
}

CKEditorHightHandler负责处理CKEditor需要的响应,上面提到过因为版本不同CKEditor需要不同的响应。SpringFileUploadConnector类负责具体的上传工作,代码如下:

public class SpringFileUploadConnector extends ApacheFileUploadConnector{public SpringFileUploadConnector(UploadHandler handler) {super(handler);}public String upload(CommonInitParamers params, MultipartFile file) throws IOException, ServletException{if(!(file instanceof CommonsMultipartFile)){return "500";}CommonsMultipartFile cmf=(CommonsMultipartFile)file;//保存的路径File uploadDir=new File(params.getFileSaveDir());if (!uploadDir.exists()) {uploadDir.mkdirs();}String callbackFun=params.getCallbackFunctionName();return execute(cmf.getFileItem(), uploadDir, callbackFun);}
}

ApacheFileUploadConnector类的代码在工具子模块(util)中。这里都不继续贴了。

B3. CKEditor的图片上传

上面的代码中有写过CKEditor图片上传的连接:/upload/ckeditor; 此连接是一个单独的控制器,代码如下:

@Controller
@RequestMapping("/upload")
public class UploadController {@Autowiredprivate ServletContext servletContext;@Autowiredprivate ImageIOInfo imageIOInfo;private final static Logger logger = LoggerFactory.getLogger(UploadController.class);// CK4:V4.11.4[LocalStorage|本地存储]@RequestMapping(value = "/ckeditor", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")@ResponseBodypublic String ckeditorUploadAction(@RequestParam("upload") MultipartFile file, @RequestParam("type") final String fileType) {String localReal = servletContext.getRealPath("/");ImageStorageExecutor ise = new LocalImageStorage(imageIOInfo.getImageBucketDomain(), imageIOInfo.getUploadImageDirectName(), localReal);Result<String> data = Result.empty();try{data = ise.store(file);}catch(Exception e){data = Result.failure(e.getMessage(), e);}if(!data.isSuccess()){return buildCkeditorResponse(false, "上传失败", null, null);}String responsebody = data.successValue();return buildCkeditorResponse(true, "上传成功", getFileName(responsebody), responsebody);}// CK4:V4.11.4的响应格式private String buildCkeditorResponse(boolean isCompleted, String message, String fileName, String imageUrl) {Map<String, Object> data = new HashMap<>();data.put("uploaded", isCompleted ? "1" : "0");if (fileName != null) {data.put("fileName", fileName);}if (imageUrl != null) {data.put("url", imageUrl);}if (!isCompleted) {Map<String,String> tmp = new HashMap<>();tmp.put("message", Commons.optional(message, "未知的网络错误"));data.put("error", tmp);}return new Gson().toJson(data);}//获得图片地址的文件名private String getFileName(String imageURL){return imageURL.substring(imageURL.lastIndexOf("/") + 1);}
}

C. 站外存储

本地开发的时候会频繁的改动代码,若图片随项目走,真是太烦人的。
同时也是检验图片灵活存储的一个机会,开始新拉一个小项目:bucket,这个小项目目前只有一项工作都是保存图片。

C1. 接受上传工作, UploadEditorFileServlet

public class UploadEditorFileServlet extends HttpServlet {private static final long serialVersionUID = 1L;private String uploadImageDirectName; private String siteDomain; private final static Logger logger = LoggerFactory.getLogger(UploadEditorFileServlet.class);/*** @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse*      response)*/protected void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {response.setContentType("application/json");response.setCharacterEncoding("UTF-8");//子目录String childDirect = DateTimeUtils.getYMD();// 项目路径final String realPath = request.getServletContext().getRealPath("/") + File.separator + uploadImageDirectName + File.separator + childDirect;// 前台访问路径final String frontVisitPath = siteDomain + uploadImageDirectName + "/"+childDirect+"/";UploadInitParamers cip = new UploadInitParamers() {@Overridepublic String getFileSaveDir() {return realPath;}@Overridepublic String getCallbackFunctionName() {// 没有回调函数return null;}@Overridepublic String getUploadFileInputName() {return "upload";}@Overridepublic HttpServletRequest getRequest() {return request;}};try (PrintWriter out = response.getWriter()) {try {ServletPartUploadConnector connector = new ServletPartUploadConnector(new CKEditorHightHandler(frontVisitPath));String responsebody = connector.upload(cip);out.println(buildCkeditorResponse(true, "上传成功", responsebody.replace(frontVisitPath, ""), responsebody));} catch (Exception e) {if (logger.isDebugEnabled()) {logger.debug(e.getMessage(), e);}out.println(buildCkeditorResponse(false, e.getMessage(), null, null));}out.flush();}}// CK4:V4.11.4的响应格式// https://ckeditor.com/docs/ckeditor4/latest/guide/dev_file_upload.htmlprivate String buildCkeditorResponse(boolean isCompleted, String message, String fileName, String imageUrl) {Map<String, Object> data = new HashMap<>();data.put("uploaded", isCompleted ? "1" : "0");if (fileName != null) {data.put("fileName", fileName);}if (imageUrl != null) {data.put("url", imageUrl);}if (!isCompleted) {Map<String,String> tmp = new HashMap<>();tmp.put("message", Commons.optional(message, "未知的网络错误"));data.put("error", tmp);}return new Gson().toJson(data);}@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("text/html");resp.setCharacterEncoding("UTF-8");try (PrintWriter out = resp.getWriter()) {out.println("<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title>Access Denied</title></head><body><p>暂不支持目录的功能</p></body></html>");out.flush();}}/*** 获得初始化值*/public void init(ServletConfig config) throws ServletException {super.init(config);this.siteDomain = config.getInitParameter("siteDomain"); //站点URL, http://xx.comthis.uploadImageDirectName = config.getInitParameter("uploadImageDirectName"); //图片保存的目录}
}

为了减小依赖这里使用ServletPartUploadConnector来保存图片,代码在工具子模块(util)中。同时也将配置外部化方便更改。web.xml如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns="http://xmlns.jcp.org/xml/ns/javaee"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"version="3.1"><display-name>bucket</display-name><welcome-file-list><welcome-file>index.html</welcome-file></welcome-file-list><servlet><servlet-name>UploadEditorFileServlet</servlet-name><servlet-class>com.apobates.forum.bucket.servlet.UploadEditorFileServlet</servlet-class><init-param><param-name>siteDomain</param-name><param-value>http://pic.test.com/</param-value></init-param><init-param><param-name>uploadImageDirectName</param-name><param-value>imagestore</param-value></init-param><multipart-config><max-file-size>10485760</max-file-size><max-request-size>20971520</max-request-size><file-size-threshold>5242880</file-size-threshold></multipart-config></servlet><servlet-mapping><servlet-name>UploadEditorFileServlet</servlet-name><url-pattern>/upload/ckeditor</url-pattern></servlet-mapping>
</web-app>

这里可以看到访问图片的域名为:pic.test.com, 图片保存在imagestore目录中,上传的地址为: http://pic.test.com/upload/ckeditor。

下面来在tomcat中配置这个域名, <tomcat安装目录>/conf/server.xml

      <Host name="www.test.com"  appBase="webapps"unpackWARs="true" autoDeploy="true"><!-- Access log processes all example.Documentation at: /docs/config/valve.htmlNote: The pattern used is equivalent to using pattern="common" --><Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"prefix="localhost_access_log" suffix=".txt"pattern="%h %l %u %t &quot;%r&quot; %s %b" /></Host><Host name="pic.test.com" appBase="bucket" unpackWARs="true" autoDeploy="true"><Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"prefix="bucket_access_log" suffix=".txt"pattern="%h %l %u %t &quot;%r&quot; %s %b" /><Context path="" docBase="ROOT" reloadable="true" useHttpOnly="true"/></Host>

因为跨域了所以还需要在web.xml配置一个CORS 过滤器。这里都不贴了,请参考: http://tomcat.apache.org/tomcat-8.5-doc/config/filter.html#CORS_Filter

C2. 更改CKEditor编辑图片上传的地址

这个比较容易,找到项目的js文件中的initCkEditor函数,将UploadUrl: '/upload/ckeditor' 变成 : UploadUrl: 'http://pic.test.com/upload/ckeditor'

C3. formbean中的图片上传

C3.1先将资源文件中的配置修改如下:

img.bucket.domain=http://pic.test.com
img.bucket.upload.direct=imagestore
img.bucket.upload.uri=/upload/ckeditor
img.bucket.upload.input=upload

C3.2 将spring配置文件中的 imageStorageExecutor Bean换一个新实现:

 <!-- 站外存储图片 --><bean id="imageStorageExecutor" class="com.apobates.forum.trident.fileupload.OutsideImageStorage"><constructor-arg name="imageBucketDomain" value="${img.bucket.domain}"/><constructor-arg name="uploadImageDirectName" value="${img.bucket.upload.direct}"/><constructor-arg name="imageBucketUploadURL" value="${img.bucket.domain}${img.bucket.upload.uri}"/><constructor-arg name="imageBucketUploadInputFileName" value="${img.bucket.upload.input}"/></bean>

OutsideImageStorage代码如下:

public class OutsideImageStorage implements ImageStorageExecutor{private final String imageBucketDomain;private final String uploadImageDirectName;private final String imageBucketUploadURL;private final String imageBucketUploadInputFileName;private final static Logger logger = LoggerFactory.getLogger(OutsideImageStorage.class);/*** * @param imageBucketDomain              站外的访问域名,例:http://x.com* @param uploadImageDirectName          站外保存图片的目录* @param imageBucketUploadURL           站外上传程序的访问地址,例:http://x.com/y* @param imageBucketUploadInputFileName 站外上传程序接受的type=file输入项的名称*/public OutsideImageStorage(String imageBucketDomain, String uploadImageDirectName, String imageBucketUploadURL, String imageBucketUploadInputFileName) {super();this.imageBucketDomain = imageBucketDomain;this.uploadImageDirectName = uploadImageDirectName;this.imageBucketUploadURL = imageBucketUploadURL;this.imageBucketUploadInputFileName = imageBucketUploadInputFileName;}@Overridepublic String imageBucketDomain() {return imageBucketDomain;}@Overridepublic String uploadImageDirectName() {return uploadImageDirectName;}public String getImageBucketUploadURL() {return imageBucketUploadURL;}public String getImageBucketUploadInputFileName() {return imageBucketUploadInputFileName;}@Overridepublic Result<String> store(MultipartFile file) throws IOException {if(file==null || file.isEmpty()){logger.info("[AFU][OU] file is null or empty");return Result.failure("arg file is null or empty");}HttpHeaders parts = new HttpHeaders();  parts.setContentType(MediaType.TEXT_PLAIN);final ByteArrayResource byteArrayResource = new ByteArrayResource(file.getBytes()) {@Overridepublic String getFilename() {return file.getOriginalFilename();}};final HttpEntity<ByteArrayResource> partsEntity = new HttpEntity<>(byteArrayResource, parts);//HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.MULTIPART_FORM_DATA);MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();body.add(imageBucketUploadInputFileName, partsEntity);HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);RestTemplate restTemplate = new RestTemplate();String responseBody = restTemplate.postForEntity(imageBucketUploadURL, requestEntity, String.class).getBody();return parseFileURL(responseBody);}private Result<String> parseFileURL(String responseBody){if(responseBody == null){logger.info("[AFU][AF]3> upload fail used default value");return Result.failure("upload fail used default value");}Map<String, Object> rbMap = new Gson().fromJson(responseBody, new TypeToken<Map<String, Object>>() {}.getType());if(rbMap==null || rbMap.isEmpty()){logger.info("[AFU][AF]4> from json map is not used");return Result.failure("from json map is not used");}if(rbMap.containsKey("error")){@SuppressWarnings("unchecked")Map<String,String> tmp = (Map<String,String>)rbMap.get("error");logger.info("[AFU][AF]5> uploaded response error: "+ tmp.get("message"));return Result.failure("uploaded response error: "+ tmp.get("message"));}//String fileVisitPath=null;try{String fileVisitPath = rbMap.get("url").toString();return Result.success(fileVisitPath);}catch(NullPointerException | ClassCastException e){return Result.failure("parse image url for response has exception: "+ e.getMessage());}}
}

这里使用Spring的RestTemplate将MultipartFile交给bucket项目的UploadEditorFileServlet,工作结束。

D. 生成缩略图

小可也曾用过img标签上加width和height属性来控制图片,不让它破坏CSS布局; 也有各种hack让图片自适应父div。但这都不治本,因为服务器还是要输出哪么大的图片,试想如果用oss或第三方的解决方案都是按流量收费的。现代app的图片大多采用webp, 因为它有高压缩比,经过优化,可以在网络上实现更快,更小的图像。再者看看你日常去的哪些大网站,哪个对图片原样输出了。

D1 新模块:thumbnail

对外服务的 ThumbBuilderServlet,它接受这几个参数:dir目录名,file文件名,scale支持:auto|widthheight.代码如下:

public class ThumbBuilderServlet extends HttpServlet {private static final long serialVersionUID = 1L;private static final Logger logger = LoggerFactory.getLogger(ThumbBuilderServlet.class);private String originalDir;private String thumbDir;private int maxWidth;protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String scaleParameter = req.getParameter("scale"); //接受:auto|width<x>heightString imageFileName = req.getParameter("file");String directory = req.getParameter("dir");//if (scaleParameter==null || scaleParameter.isEmpty()) {resp.sendError(400, "scale parameter lost");return;}if (imageFileName.isEmpty() || imageFileName.indexOf(".") == -1) {resp.sendError(400, "image file is not exist");return;}try {String originalImageDirect = getServletContext().getRealPath("/") + originalDir + ThumbConstant.FS;String originalImagePath;if(!"/".equals(directory)){originalImagePath = directory + ThumbConstant.FS + imageFileName;}else{originalImagePath = imageFileName;}//是否裁剪int imageWidth = getOriginalImageWidth(originalImageDirect+originalImagePath);if(imageWidth <= maxWidth && "auto".equals(scaleParameter)){ //RequestDispatcher rd = getServletContext().getRequestDispatcher("/"+ originalDir + ThumbConstant.WEB_FS + originalImagePath);rd.forward(req, resp);return;}//裁剪开始ImagePath ip = new ImagePath(scaleParameter, (originalImageDirect+originalImagePath), imageWidth, new File(getServletContext().getRealPath("/") + thumbDir + ThumbConstant.FS));if (!ip.getImagePhysicalPath().exists()) {//ip.makeThumb();}//redirectThumbImage(req, resp, ip);} catch (IOException e) {if(logger.isDebugEnabled()){logger.debug("[Thumb][TBS]image thumb create fail");}}}/*** 获得初始化值*/public void init(ServletConfig config) throws ServletException {super.init(config);this.originalDir = config.getInitParameter("original"); //原目录this.thumbDir = config.getInitParameter("thumb"); //封面目录this.maxWidth = 900;try{maxWidth = Integer.valueOf(config.getInitParameter("maxWidth"));}catch(NullPointerException | NumberFormatException e){}}/*** 跳转到缩略图地址* * @param req* @param resp* @param ip* @throws ServletException* @throws IOException*/protected void redirectThumbImage(HttpServletRequest req, HttpServletResponse resp, ImagePath ip)throws ServletException, IOException {RequestDispatcher rd = getServletContext().getRequestDispatcher(ip.getThumbWebHref(thumbDir));rd.forward(req, resp);}/*** 计算原始图片的宽度* * @param originalImagePath 原始图片的物理地址* @return*/private int getOriginalImageWidth(String originalImagePath){int imageWidth = 0;try{BufferedImage bimg = ImageIO.read(new File(originalImagePath));imageWidth = bimg.getWidth();}catch(IOException e){}return imageWidth;}
}

对于图片宽度小于maxWidth设置的不允以裁剪,原样输出。thumbDir值为裁剪后的目录名。originalDir值为原始图片的存放目录,因此只要将thumbnail模加加入到图片存储项目的依赖中并为servlet配置一个地址即可。ImagePath为裁剪的入口类,代码如下:

public class ImagePath {//接受:auto|width<x>heightprivate final String scaleParameter;//图片的缩放比例(1-100)private final int imageScale;//裁剪保存的目录private final File thumbDirectory;//被裁剪的图片文件private final File sourceImageFile;public ImagePath(String scale, String sourceFilePath, int imageWidthSize, File thumbDir) {this.scaleParameter = scale;this.imageScale = getScaleNumber(imageWidthSize);this.thumbDirectory = thumbDir;this.sourceImageFile = new File(sourceFilePath);}/*** 裁剪后的WEB访问地址* @param thumbDir* @return*/public String getThumbWebHref(String thumbDir) {return ThumbConstant.WEB_FS + thumbDir + ThumbConstant.WEB_FS + getCropDirect() + ThumbConstant.WEB_FS + sourceImageFile.getName();}/*** 裁剪后的物理地址* @return*/public File getImagePhysicalPath() {File ipp = new File(thumbDirectory, ThumbConstant.FS + getCropDirect() + ThumbConstant.FS);if (!ipp.exists()) {//ipp.mkdirs();}return new File(ipp, ThumbConstant.FS + sourceImageFile.getName());}/*** 参数中的宽度和高度信息* @return*/private Map<String, Integer> getImageWidthAndHeight() {Map<String, Integer> data = new HashMap<String, Integer>();String[] sas = scaleParameter.split(ThumbConstant.WIDTH_HEIGHT_SPLITER);if(sas.length != 2){return Collections.EMPTY_MAP;}data.put(ThumbConstant.IMAGE_WIDTH, Integer.valueOf(sas[0]));data.put(ThumbConstant.IMAGE_HEIGHT, Integer.valueOf(sas[1]));return data;}public void makeThumb() throws IOException {Map<String, Integer> d = getImageWidthAndHeight();int cropWidth=0, cropHeight=0;if(!d.isEmpty()){cropWidth = d.get(ThumbConstant.IMAGE_WIDTH);cropHeight = d.get(ThumbConstant.IMAGE_HEIGHT);}//new ThumbnailsScaleHandler(sourceImageFile, imageScale).cropImage(getImagePhysicalPath(), cropWidth, cropHeight);}/*** 根据图片的宽度返回缩放的值* * @param originalImageWidth 原始图片的宽度* @return*/private int getScaleNumber(int originalImageWidth){if(originalImageWidth > 1440){return 25;}if(originalImageWidth > 992){return 50;}return 75;}/*** 返回图片裁剪后的存储目录名* @return*/private String getCropDirect(){if(scaleParameter.indexOf(ThumbConstant.WIDTH_HEIGHT_SPLITER) == -1){return imageScale+"";}return scaleParameter; //width<x>height}
}

ThumbnailsScaleHandler类使用了Thumbnails框架来缩放图片。更多关于Thumbnails访问: https://github.com/coobird/thumbnailator.
如果scale=auto,根据图片的宽度来适当的裁剪,getScaleNumber方法来计算得出。ThumbnailsScaleHandler类的代码如下:

public class ThumbnailsScaleHandler extends AbstractCropImageHandler{//图片的缩放比例(1-100)private final int scale;private final static Logger logger = LoggerFactory.getLogger(ThumbnailsScaleHandler.class);public ThumbnailsScaleHandler(File sourceImagePath, int scale) {super(sourceImagePath);this.scale = scale;}@Overridepublic void cropImage(File cropSaveFile, int width, int height) throws IOException{String fileName = getFileName(cropSaveFile); String fileExt = getFileExtension(fileName);File sourceImageFile = getOrginalImageFile();//("[Thumb][TSH]Thumbnails.cropImage 参数: {w:"+width+", h:"+height+", scale:"+scale+", ext:"+fileExt+", name:"+fileName+"}");if(width > 0 && height > 0){ //固定大小if(logger.isDebugEnabled()){String descrip = "[Thumbnails]" + ThumbConstant.NEWLINE;descrip += "/*----------------------------------------------------------------------*/" +ThumbConstant.NEWLINE;descrip += "width: " + width + ", height: " + height + ThumbConstant.NEWLINE;descrip += "ext: " + fileExt + ThumbConstant.NEWLINE;descrip += "source: " + sourceImageFile.getAbsolutePath() + ThumbConstant.NEWLINE;descrip += "thumb: " + cropSaveFile.getAbsolutePath() + ThumbConstant.NEWLINE;descrip += "/*----------------------------------------------------------------------*/" + ThumbConstant.NEWLINE;logger.debug(descrip);}//Thumbnails.of(sourceImageFile).size(width, height).outputFormat(fileExt).toFile(cropSaveFile);}else{if(logger.isDebugEnabled()){String descrip = "[Thumbnails]" + ThumbConstant.NEWLINE;descrip += "/*----------------------------------------------------------------------*/" + ThumbConstant.NEWLINE;descrip += "scale: " +  scale + ThumbConstant.NEWLINE;descrip += "ext: " + fileExt + ThumbConstant.NEWLINE;descrip += "source: " + sourceImageFile.getAbsolutePath() + ThumbConstant.NEWLINE;descrip += "thumb: " + cropSaveFile.getAbsolutePath() + ThumbConstant.NEWLINE;descrip += "/*----------------------------------------------------------------------*/" + ThumbConstant.NEWLINE;logger.debug(descrip);}Thumbnails.of(sourceImageFile).scale(Math.abs(scale) / 100.00D).outputFormat(fileExt).toFile(cropSaveFile);}}
}

这里需要注意一下即使Thumbnails有调用size也不会裁剪成固定的宽和高,它还是缩放到一个宽和高。裁剪出固定的宽和高的图片其实并不难,这里给出一个从中心点开始裁剪的思路


实现类代码如下:

public class FixedCenterCropHandler extends AbstractCropImageHandler{public FixedCenterCropHandler(File orginalImageFile) {super(orginalImageFile);}@Overridepublic void cropImage(File cropSaveFile, int width, int height) throws IOException{BufferedImage originalImage = ImageIO.read(getOrginalImageFile());int[] xyPointers = getCropXYPointer(originalImage.getWidth(), originalImage.getHeight(), width, height);if(xyPointers.length == 0){return;}BufferedImage croppedImage = originalImage.getSubimage(xyPointers[0], xyPointers[0], width, height);String fileExt = getFileExtension(getFileName(cropSaveFile));ImageIO.write(croppedImage, fileExt, cropSaveFile);}/*** 获取裁剪的起始点坐标* * @param originalImageWidth  原始图片的宽度* @param originalImageHeight 原始图片的高度* @param width               裁剪的宽度* @param height              裁剪的高度* @return*/private int[] getCropXYPointer(int originalImageWidth, int originalImageHeight, int width, int height){if(originalImageWidth <= width && originalImageHeight <= height){return new int[]{};}int startX=0;if(originalImageWidth > width){int centerX = originalImageWidth / 2; //宽度中心点XstartX = centerX - width / 2;}int startY=0;if(originalImageHeight > height){int centerY = originalImageHeight / 2; //高度中心点YstartY = centerY - height / 2;}return new int[]{startX, startY};}
}

这并不是一个很美的思路,因为不是所有图片中心物都出现在中心点左右,例如:一些风景画往往在右下或右上有动物。

E. 图片URL地址重写

这里介绍使用: urlrewritefilter 框架,官方网站: http://www.tuckey.org/urlrewrite/。
若裁剪的ThumbBuilderServlet配置的地址为: /thumb, 加上参数后可能是这样的: /thumb?dir=20191025&scale=auto&file=xxxx.png,我希望地址重写后地址更自然,例:http://pic.test.com/thumbs/20191025/auto/xxxx.png,这样更直观一些。

        <dependency><groupId>org.tuckey</groupId><artifactId>urlrewritefilter</artifactId><version>4.0.3</version></dependency>

像thumbnail模块的使用一样,它也随是图片存储走,加载入图片绑定的项目中。在绑定的项目的web.xml中加入以下:

    <filter><filter-name>UrlRewriteFilter</filter-name><filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class></filter><filter-mapping><filter-name>UrlRewriteFilter</filter-name><url-pattern>/thumbs/*</url-pattern><dispatcher>REQUEST</dispatcher><dispatcher>FORWARD</dispatcher></filter-mapping>

框架还需要在WEB-INF下有一个配置文件:urlrewrite.xml,示例代码如下:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 4.0//EN""http://www.tuckey.org/res/dtds/urlrewrite4.0.dtd"><urlrewrite><rule match-type="regex"><name>imageServlet</name><from>/thumbs/(.*)/(.*)/(.*)\.(png|jpg|jpeg|gif)$</from><to type="forward">/thumb?dir=$1&amp;file=$3.$4&amp;scale=$2</to></rule></urlrewrite>

F. 浏览器中图片的懒加载

这里介绍使用: Lazy Load Js框架,官方网站:https://github.com/tuupola/lazyload. 先看滚动加载效果图:


注意滚动条的位置,及网络加载的图片。使用方法也及其简单,这里都不贴了。

G 总结

G1. 文章中提到的util模块下载地址

白渡网盘
CSDN资源

G2. 项目的依赖及版本

 <properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target><project.version>0.0.1-SNAPSHOT</project.version><spring-framework.version>5.0.7.RELEASE</spring-framework.version><jackson.version>2.9.8</jackson.version><junit.version>4.12</junit.version><log4j2.version>2.11.2</log4j2.version><servlet.version>3.1.0</servlet.version><fileupload.version>1.4</fileupload.version><slf4j.version>1.7.25</slf4j.version></properties>

G3. 20191026补充

修正:

R1: 上传时的文件扩展名检查.

ApacheFileUploadConnector|ServletPartUploadConnector
AbstractHandler(allowFileExtension方法默认实现只允许图片文件)|UploadHandler(接口新增方法:String[] allowFileExtension())

R2: 上抛上传时产生的错误信息

HandleOnUploadException(接口方法增加IO异常声明)|AbstractHandler.forceException

R3: ActionForm在上传失败时上抛错误信息. 不上传时使用默认文件名

com.apobates.forum.trident.controller.form.ActionForm.uploadAndEncodeFile
新增FileUploadFailException,类继承了IllegalStateException
控制器方法若需要知道上传错误可以捕获FileUploadFailException异常

R4: CKEditor的图片类型设置

研究了一天文档没找到配置参数。只能手写代码:

//v:4.x.x
function initCkEditor(config){//ETCif(options.plugs.indexOf('uploadimage') != -1){CKEDITOR.config.filebrowserUploadUrl = BASE+options.UploadUrl+'?type=Files';CKEDITOR.config.filebrowserImageUploadUrl = BASE+options.UploadUrl+'?type=Images';//ADD 20191026CKEDITOR.on('dialogDefinition', function( evt ) {var dialogName = evt.data.name;if (dialogName == 'image') {var uploadTab =  evt.data.definition.getContents('Upload'); // get tab of the dialogvar browse = uploadTab.get('upload'); //get browse server buttonbrowse.onClick = function() {var input = this.getInputElement();input.$.accept = 'image/*'};browse.onChange = function(){var input = this.getInputElement();var fn = input.$.value; // 文件路径var imageReg = /\.(gif|jpg|jpeg|png)$/i;if(!imageReg.test(fn)){alert('非法的文件类型');input.$.value='';}};}});}}

R5: 修复CKEditor的错误响应格式

com.apobates.forum.trident.controller.UploadController.buildCkeditorResponse
com.apobates.forum.bucket.servlet.UploadEditorFileServlet.buildCkeditorResponse
com.apobates.forum.trident.fileupload.OutsideImageStorage.parseFileURL

R6: 上传保存的文件名更改,原来是写死的

CKEditorHandler|CKEditorHightHandler,两者的父类:AbstractHandler默认方法实现输出null
ApacheFileUploadConnector|ServletPartUploadConnector
未写包名的即为util模块中的修改,反之即为本文提及的代码。 修正后的util模块在白渡网盘中更新

H. 有关MaxUploadSizeExceededException

不论是使用Servlet 3 Part还是ASF commons fileupload框架都有可能在设置文件大小(除了不设置,默认是-1不限制)时出来这个异常,这时连接会被重置。

Spring MultipartFile时出现的异常:

nested exception is org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size of 5242880 bytes exceeded;
nested exception is org.apache.commons.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (8718571) exceeds the configured maximum (5242880)] with root causeorg.apache.commons.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (8718571) exceeds the configured maximum (5242880)at org.apache.commons.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:977)at org.apache.commons.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:309)at org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:333)at org.apache.commons.fileupload.servlet.ServletFileUpload.parseRequest(ServletFileUpload.java:113)at org.springframework.web.multipart.commons.CommonsMultipartResolver.parseRequest(CommonsMultipartResolver.java:158)at org.springframework.web.multipart.commons.CommonsMultipartResolver.resolveMultipart(CommonsMultipartResolver.java:142)at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1128)at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:960)at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:877)at javax.servlet.http.HttpServlet.service(HttpServlet.java:661)at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)

只使用fileupload时出现的异常:

java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (12119773) exceeds the configured maximum (1048576)at org.apache.catalina.connector.Request.parseParts(Request.java:2947)at org.apache.catalina.connector.Request.parseParameters(Request.java:3242)at org.apache.catalina.connector.Request.getParameter(Request.java:1136)at org.apache.catalina.connector.RequestFacade.getParameter(RequestFacade.java:381)at com.apobates.forum.utils.fileupload.ServletPartUploadConnector.upload(ServletPartUploadConnector.java:81)at com.apobates.forum.bucket.servlet.UploadEditorFileServlet.doPost(UploadEditorFileServlet.java:71)at javax.servlet.http.HttpServlet.service(HttpServlet.java:661)at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)

这时正常的错误提示不会由response输出,网络有一堆如下的解答:

H1. 1兆不行10兆,通过提升允许的上限

个人不建议,这是不是有点妥协的意味呢?

H2. 使用ExceptionHandler

    @ExceptionHandler(MultipartException.class)public String handleError1(MultipartException e, RedirectAttributes redirectAttributes) {redirectAttributes.addFlashAttribute("message", e.getCause().getMessage());return "redirect:/uploadStatus";}

源文地址: How to handle max upload size exceeded exception, 我试了一下电脑里的浏览器只有win10 自带Edge会正常工作, Firefox, Chrome都没效果。

H3. 继承CommonsMultipartResolver 法

public class DropOversizeFilesMultipartResolver extends CommonsMultipartResolver {/*** Parse the given servlet request, resolving its multipart elements.* * Thanks Alexander Semenov @ http://forum.springsource.org/showthread.php?62586* * @param request*            the request to parse* @return the parsing result*/@Overrideprotected MultipartParsingResult parseRequest(final HttpServletRequest request) {String encoding = determineEncoding(request);FileUpload fileUpload = prepareFileUpload(encoding);List fileItems;try {fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);} catch (FileUploadBase.SizeLimitExceededException ex) {request.setAttribute(EXCEPTION_KEY, ex);fileItems = Collections.EMPTY_LIST;} catch (FileUploadException ex) {throw new MultipartException("Could not parse multipart servlet request", ex);}return parseFileItems(fileItems, encoding);}
}

源文地址: Using Spring 3 @ExceptionHandler with commons FileUpload and SizeLimitExceededException/MaxUploadSizeExceededException, 我用的Spring5上不好使,所有浏览器都会提示连接重置,我试着在调用parseRequest之前判断上传文件的大小,若超出设置的上限上抛MultipartException,还是提示连接被重置。

H4. 继承OncePerRequestFilter 法

public class MultipartExceptionHandler extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {try {filterChain.doFilter(request, response);} catch (MaxUploadSizeExceededException e) {handle(request, response, e);} catch (ServletException e) {if(e.getRootCause() instanceof MaxUploadSizeExceededException) {handle(request, response, (MaxUploadSizeExceededException) e.getRootCause());} else {throw e;}}}private void handle(HttpServletRequest request,HttpServletResponse response, MaxUploadSizeExceededException e) throws ServletException, IOException {String redirect = UrlUtils.buildFullRequestUrl(request) + "?error";response.sendRedirect(redirect);}
}

源文地址: How to nicely handle file upload MaxUploadSizeExceededException with Spring Security

H5. 个人方案

即然不论Part,ASF common fileupload或Spring MultipartResolver在超出允许的上限时都会重置连接,说明这可能是上佳的可行方案,出于什么原因这样作不是我辈菜鸟可想到的。但又需要在超出上限时给用户一个提示,操作得以继续进行而不是让用户看浏览器的错误页:


== 这里说明一下:我试着用http watch之类的工具看一看网络情况。发现只要开了代理都可以一遍跑过。错误提示原本不会响应的也出来了。==

即然服务器上无法作文章,又需要给用户一个提示,又不要影响操作继续只能选用客户端异步上传。即使上传的连接被重置,用户当前的操作也不会因此打断,像CKEditor哪样:

这里推荐一下: bootstrap-fileinput, 它可以在客户端对文件大小检测,只不过单位是字节,示例如下


JS代码示例:

    $('.bootCoverImage').bind('initEvent', function(){var self=$(this); var token=new Date().getTime();var option={uploadUrl: BASE+"upload/fileinput",uploadExtraData: {'uploadToken':token, 'id':token},maxFileCount: 5,maxFileSize: 1024,maxFilesNum: 1,allowedFileTypes: ['image'],allowedFileExtensions: ['jpg', 'png', 'gif', 'jpeg'],overwriteInitial: true,theme: 'fa',uploadAsync: true,showPreview: true};if(self.attr('data-bind')){option.initialPreviewAsData=true;option.initialPreview=[self.attr('data-bind')];}if(self.attr('data-element')){hiddenElement=self.attr('data-element');}var coverCmp=self.fileinput(option);coverCmp.on('fileuploaded', function(event, data, id, index) {var response=data.response,currentImage=response.data[0].location;//currentImage即为上传成功后图片的访问地址;}).on('fileuploaderror', function(event, data, msg){console.log('File Upload Error', 'ID: ' + data.fileId + ', Thumb ID: ' + data.previewId);}).on('filebatchuploadcomplete', function(event, preview, config, tags, extraData) {console.log('File Batch Uploaded', preview, config, tags, extraData);});}).trigger('initEvent');

上传成功后服务器站的响应格式如下:

{"data":[{"id":"101","name":"snapshot-20191101000913773.png","location":"http://www.test.com/imagestore/20191101/snapshot-20191101000913773.png"
}],
"success":true
}

聊聊WEB项目中的图片相关推荐

  1. java web添加背景图片_java web项目中如何插入背景图片

    对于java可视化界面插入背景图片这个倒是轻而易举,只需要background-inage:url(图片路径就行),而对于与web项目中,我开始时也是采用这种方法,但是不尽然,代码如下: 效果如下: ...

  2. 在Eclipse中 Web项目 插入背景图片

    标题在Eclipse中 Web项目 插入背景图片 将存放图片的文件夹放在WebContend文件夹内: 在jsp中用CSS时:

  3. JAVA Web项目中所出现错误及解决方式合集(不断更新中)

    JAVA Web项目中所出现错误及解决方式合集 前言 一.几个或许会用到的软件下载官网 二.Eclipse的[preferences]下没有[sever]选项 三.Tomcat的安装路径找不到 四.T ...

  4. ssm把图片保存到项目中_项目中的图片跨域问题解决方式

    现象 首先,在生产环境中,由于进行编辑图片时,将图片回显到ReactCrop组件中进行可裁剪编辑,然而回显时,需要将图片转化为base64的格式或者blob对象, 此时需要将图片次绘制成canvas进 ...

  5. 由web项目中上传图片所引出的路径问题

    我在做javaweb项目的时候,有个项目中需要进行图片的上传,有次我重新部署项目后,发现之前上传的图片不见了,最后找出原因:图片上传在服务器目录上,而不是绝对路径,所以特别想弄清楚javaweb项目中 ...

  6. react前端显示图片_如何在react项目中引用图片?

    如何在react项目中引用图片?本文码云笔记将为大家整理在react项目中插入图片以及背景图片的方法,希望对需要的小伙伴提供一些参考. 在react项目中插入图片以及背景图片的方法共有2种: 1.im ...

  7. Java Web项目中使用Freemarker生成Word文档

    Web项目中生成Word文档的操作屡见不鲜,基于Java的解决方案也是很多的,包括使用Jacob.Apache POI.Java2Word.iText等各种方式,其实在从Office 2003开始,就 ...

  8. html调用腾讯地图定位当前位置,vue web项目中调用腾讯地图API获取当前位置的经纬度...

    vue web项目中调用腾讯地图API获取当前位置的经纬度 vue web项目中调用腾讯地图API获取当前位置的经纬度 在main.js 中添加一下代码 import axios from 'axio ...

  9. 详细阐述Web开发中的图片上传问题

    Web开发中,图片上传是一种极其常见的功能.但是呢,每次做上传,都花费了不少时间. 一个"小功能"花费我这么多时间,真心不愉快. So,要得认真分析下原因. 1.在最初学习Java ...

最新文章

  1. 让语音助手听懂方言,这个数据集能搞定
  2. 函数语法:Js之on和addEventListener的使用与不同
  3. php用不了for循环吗,php中的这两个for循环有什么区别吗?
  4. java 一对一的关系_与休眠一对一关系 - java
  5. 万字长文:解读区块链7类共识算法
  6. linux下查询日志sed与或非,Linux命令之sed命令使用介绍
  7. LeetCode(404)——左叶子之和(JavaScript)
  8. 问题1、图像分割预测时原始图片大小与预测图片大小不一致
  9. ryzen linux 搭配显卡,R5 1500X配什么显卡好 适合AMD锐龙5 1500X搭配的显卡推荐
  10. 亚马逊黑五哑火,中国跨境电商高歌猛进!
  11. java处理器,JAVA注解处理器
  12. 移动魔百盒CM311-1sa_ZG代工_S905L3A 安卓9.0 鸿蒙动画_线刷固件包
  13. Yolo-v1~v3学习关键点整理
  14. Scala 将时间字符串转为时间戳
  15. TCP/IP协议中的端口
  16. 入门:因果推断 简介
  17. 软构习题课一内容总结
  18. 如何在linux中关闭一个进程
  19. 黑龙江认识电子计算机ppt,[IT认证]认识计算机配件.ppt
  20. ryu---北向接口(利用socket对外通信)

热门文章

  1. Android Studio App开发之网络通信中使用POST方式调用HTTP接口实现应用更新功能(附源码 超详细必看)
  2. 为什么硬盘插在计算机上不显示,硬盘插在电脑上不显示怎么办
  3. trac linux,Ubuntu搭建trac平台步骤
  4. linux安装trac+svn+apache+wike,搭建apache+svn+trac平台
  5. 无线衰落信道、多径与OFDM、均衡技…
  6. ADAMS三维路面重构
  7. Java日期格式2019-11-05T00:00:00转换标准日期
  8. WebDAV之葫芦儿·派盘+书藏家
  9. GO 语言常用工具类-通用方法集合
  10. C语言:零幺串(N0为最大连续零串的个数,N1为最大一串的个数)