当前位置: 首页 > news >正文

设计模式——模板方法

背景说明

在 智能协同云图库项目 中,我们要支持两种上传图片的方式,①本地上传,②通过远程 URL 上传。

本地上传图片

新增接受图片解析信息的包装类

@Data  
public class UploadPictureResult {  /**  * 图片地址  */  private String url;  /**  * 图片名称  */  private String picName;  /**  * 文件体积  */  private Long picSize;  /**  * 图片宽度  */  private int picWidth;  /**  * 图片高度  */  private int picHeight;  /**  * 图片宽高比  */  private Double picScale;  /**  * 图片格式  */  private String picFormat;  }

在 CosManager 中添加上传图片并解析图片的方法

/**  * 上传对象(附带图片信息)  *  * @param key  唯一键  * @param file 文件  */  
public PutObjectResult putPictureObject(String key, File file) {  PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,  file);  // 对图片进行处理(获取基本信息也被视作为一种处理)  PicOperations picOperations = new PicOperations();  // 1 表示返回原图信息  picOperations.setIsPicInfo(1);  // 构造处理参数  putObjectRequest.setPicOperations(picOperations);  return cosClient.putObject(putObjectRequest);  
}

在 fileManager 中编写通用文件上传服务中,编写上传图片的方法

/**  * 上传图片  *  * @param multipartFile    文件  * @param uploadPathPrefix 上传路径前缀  * @return  */  
public UploadPictureResult uploadPicture(MultipartFile multipartFile, String uploadPathPrefix) {  // 校验图片  validPicture(multipartFile);  // 图片上传地址  String uuid = RandomUtil.randomString(16);  String originFilename = multipartFile.getOriginalFilename();  String uploadFilename = String.format("%s_%s.%s", DateUtil.formatDate(new Date()), uuid,  FileUtil.getSuffix(originFilename));  String uploadPath = String.format("/%s/%s", uploadPathPrefix, uploadFilename);  File file = null;  try {  // 创建临时文件  file = File.createTempFile(uploadPath, null);  multipartFile.transferTo(file);  // 上传图片  PutObjectResult putObjectResult = cosManager.putPictureObject(uploadPath, file);  ImageInfo imageInfo = putObjectResult.getCiUploadResult().getOriginalInfo().getImageInfo();  // 封装返回结果  UploadPictureResult uploadPictureResult = new UploadPictureResult();  int picWidth = imageInfo.getWidth();  int picHeight = imageInfo.getHeight();  double picScale = NumberUtil.round(picWidth * 1.0 / picHeight, 2).doubleValue();  uploadPictureResult.setPicName(FileUtil.mainName(originFilename));  uploadPictureResult.setPicWidth(picWidth);  uploadPictureResult.setPicHeight(picHeight);  uploadPictureResult.setPicScale(picScale);  uploadPictureResult.setPicFormat(imageInfo.getFormat());  uploadPictureResult.setPicSize(FileUtil.size(file));  uploadPictureResult.setUrl(cosClientConfig.getHost() + "/" + uploadPath);  return uploadPictureResult;  } catch (Exception e) {  log.error("图片上传到对象存储失败", e);  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");  } finally {  this.deleteTempFile(file);  }  
}  /**  * 校验文件  *  * @param multipartFile multipart 文件  */  
public void validPicture(MultipartFile multipartFile) {  ThrowUtils.throwIf(multipartFile == null, ErrorCode.PARAMS_ERROR, "文件不能为空");  // 1. 校验文件大小  long fileSize = multipartFile.getSize();  final long ONE_M = 1024 * 1024L;  ThrowUtils.throwIf(fileSize > 2 * ONE_M, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2M");  // 2. 校验文件后缀  String fileSuffix = FileUtil.getSuffix(multipartFile.getOriginalFilename());  // 允许上传的文件后缀  final List<String> ALLOW_FORMAT_LIST = Arrays.asList("jpeg", "jpg", "png", "webp");  ThrowUtils.throwIf(!ALLOW_FORMAT_LIST.contains(fileSuffix), ErrorCode.PARAMS_ERROR, "文件类型错误");  
}  /**  * 删除临时文件  */  
public void deleteTempFile(File file) {  if (file == null) {  return;  }  // 删除临时文件  boolean deleteResult = file.delete();  if (!deleteResult) {  log.error("file delete error, filepath = {}", file.getAbsolutePath());  }  
}

注意:在文件上传时,要先在本地创建临时文件,无论是否上传成功,都要删除,不然容易导致资源泄露

Service接口编写

/**  * 上传图片  *  * @param multipartFile  * @param pictureUploadRequest  * @param loginUser  * @return  */  
PictureVO uploadPicture(MultipartFile multipartFile,  PictureUploadRequest pictureUploadRequest,  User loginUser);

Service层实现类编写

@Override  
public PictureVO uploadPicture(MultipartFile multipartFile, PictureUploadRequest pictureUploadRequest, User loginUser) {  ThrowUtils.throwIf(loginUser == null, ErrorCode.NO_AUTH_ERROR);  // 用于判断是新增还是更新图片  Long pictureId = null;  if (pictureUploadRequest != null) {  pictureId = pictureUploadRequest.getId();  }  // 如果是更新图片,需要校验图片是否存在  if (pictureId != null) {  boolean exists = this.lambdaQuery()  .eq(Picture::getId, pictureId)  .exists();  ThrowUtils.throwIf(!exists, ErrorCode.NOT_FOUND_ERROR, "图片不存在");  }  // 上传图片,得到信息  // 按照用户 id 划分目录  String uploadPathPrefix = String.format("public/%s", loginUser.getId());  UploadPictureResult uploadPictureResult = fileManager.uploadPicture(multipartFile, uploadPathPrefix);  // 构造要入库的图片信息  Picture picture = new Picture();  picture.setUrl(uploadPictureResult.getUrl());  picture.setName(uploadPictureResult.getPicName());  picture.setPicSize(uploadPictureResult.getPicSize());  picture.setPicWidth(uploadPictureResult.getPicWidth());  picture.setPicHeight(uploadPictureResult.getPicHeight());  picture.setPicScale(uploadPictureResult.getPicScale());  picture.setPicFormat(uploadPictureResult.getPicFormat());  picture.setUserId(loginUser.getId());  // 如果 pictureId 不为空,表示更新,否则是新增  if (pictureId != null) {  // 如果是更新,需要补充 id 和编辑时间  picture.setId(pictureId);  picture.setEditTime(new Date());  }  boolean result = this.saveOrUpdate(picture);  ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "图片上传失败");  return PictureVO.objToVo(picture);  
}

Controller层接口开发

/**  * 上传图片(可重新上传)  */  
@PostMapping("/upload")  
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)  
public BaseResponse<PictureVO> uploadPicture(  @RequestPart("file") MultipartFile multipartFile,  PictureUploadRequest pictureUploadRequest,  HttpServletRequest request) {  User loginUser = userService.getLoginUser(request);  PictureVO pictureVO = pictureService.uploadPicture(multipartFile, pictureUploadRequest, loginUser);  return ResultUtils.success(pictureVO);  
}

通过Url上传图片

和之前 本地上传图片方法 一样,只需要改动 4 处

  1. 方法接受参数:之前是 MultipartFile,现在是 String 字符串类型。
  2. 校验:之前是校验文件,现在是校验 Url。
  3. 获取文件名:之前是根据文件获取,现在是根据 Url 获取。
  4. 保存临时文件:之前是将 MultipartFile 写入临时文件,现在是从 Url 下载文件
public UploadPictureResult uploadPictureByUrl(String fileUrl, String uploadPathPrefix) {  // 校验图片  // validPicture(multipartFile);  validPicture(fileUrl);  // 图片上传地址  String uuid = RandomUtil.randomString(16);  // String originFilename = multipartFile.getOriginalFilename();  String originFilename = FileUtil.mainName(fileUrl);  String uploadFilename = String.format("%s_%s.%s", DateUtil.formatDate(new Date()), uuid,  FileUtil.getSuffix(originFilename));  String uploadPath = String.format("/%s/%s", uploadPathPrefix, uploadFilename);  File file = null;  try {  // 创建临时文件  file = File.createTempFile(uploadPath, null);  // multipartFile.transferTo(file);  HttpUtil.downloadFile(fileUrl, file);  // 上传图片  // ... 其余代码保持不变  } catch (Exception e) {  log.error("图片上传到对象存储失败", e);  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");  } finally {  this.deleteTempFile(file);  }  
}

校验 Url 图片

private void validPicture(String fileUrl) {  ThrowUtils.throwIf(StrUtil.isBlank(fileUrl), ErrorCode.PARAMS_ERROR, "文件地址不能为空");  try {  // 1. 验证 URL 格式  new URL(fileUrl); // 验证是否是合法的 URL  } catch (MalformedURLException e) {  throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件地址格式不正确");  }  // 2. 校验 URL 协议  ThrowUtils.throwIf(!(fileUrl.startsWith("http://") || fileUrl.startsWith("https://")),  ErrorCode.PARAMS_ERROR, "仅支持 HTTP 或 HTTPS 协议的文件地址");  // 3. 发送 HEAD 请求以验证文件是否存在  HttpResponse response = null;  try {  response = HttpUtil.createRequest(Method.HEAD, fileUrl).execute();  // 未正常返回,无需执行其他判断  if (response.getStatus() != HttpStatus.HTTP_OK) {  return;  }  // 4. 校验文件类型  String contentType = response.header("Content-Type");  if (StrUtil.isNotBlank(contentType)) {  // 允许的图片类型  final List<String> ALLOW_CONTENT_TYPES = Arrays.asList("image/jpeg", "image/jpg", "image/png", "image/webp");  ThrowUtils.throwIf(!ALLOW_CONTENT_TYPES.contains(contentType.toLowerCase()),  ErrorCode.PARAMS_ERROR, "文件类型错误");  }  // 5. 校验文件大小  String contentLengthStr = response.header("Content-Length");  if (StrUtil.isNotBlank(contentLengthStr)) {  try {  long contentLength = Long.parseLong(contentLengthStr);  final long TWO_MB = 2 * 1024 * 1024L; // 限制文件大小为 2MB  ThrowUtils.throwIf(contentLength > TWO_MB, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2M");  } catch (NumberFormatException e) {  throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件大小格式错误");  }  }  } finally {  if (response != null) {  response.close();  }  }  
}

模板方法优化

两种上传方法 流程完全一直,只是具体方法有差别。
两种文件上传上传发放的流程都是:

  1. 校验文件
  2. 获取上传地址
  3. 获取本地临时文件
  4. 上传到对象存储
  5. 封装解析得到的图片信息
  6. 清理临时文件

可以将这些流程抽象为一套模板,将不一样的实现步骤都定义为一个抽象方法

  1. 校验图片
  2. 获取文件名称
  3. 保存临时文件

新建一个图片上传模板抽象类 PictureUploadTemplate

@Slf4j  
public abstract class PictureUploadTemplate {  @Resource  protected CosManager cosManager;  @Resource  protected CosClientConfig cosClientConfig;  /**  * 模板方法,定义上传流程  */  public final UploadPictureResult uploadPicture(Object inputSource, String uploadPathPrefix) {  // 1. 校验图片  validPicture(inputSource);  // 2. 图片上传地址  String uuid = RandomUtil.randomString(16);  String originFilename = getOriginFilename(inputSource);  String uploadFilename = String.format("%s_%s.%s", DateUtil.formatDate(new Date()), uuid,  FileUtil.getSuffix(originFilename));  String uploadPath = String.format("/%s/%s", uploadPathPrefix, uploadFilename);  File file = null;  try {  // 3. 创建临时文件  file = File.createTempFile(uploadPath, null);  // 处理文件来源(本地或 URL)  processFile(inputSource, file);  // 4. 上传图片到对象存储  PutObjectResult putObjectResult = cosManager.putPictureObject(uploadPath, file);  ImageInfo imageInfo = putObjectResult.getCiUploadResult().getOriginalInfo().getImageInfo();  // 5. 封装返回结果  return buildResult(originFilename, file, uploadPath, imageInfo);  } catch (Exception e) {  log.error("图片上传到对象存储失败", e);  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");  } finally {  // 6. 清理临时文件  deleteTempFile(file);  }  }  /**  * 校验输入源(本地文件或 URL)  */  protected abstract void validPicture(Object inputSource);  /**  * 获取输入源的原始文件名  */  protected abstract String getOriginFilename(Object inputSource);  /**  * 处理输入源并生成本地临时文件  */  protected abstract void processFile(Object inputSource, File file) throws Exception;  /**  * 封装返回结果  */  private UploadPictureResult buildResult(String originFilename, File file, String uploadPath, ImageInfo imageInfo) {  UploadPictureResult uploadPictureResult = new UploadPictureResult();  int picWidth = imageInfo.getWidth();  int picHeight = imageInfo.getHeight();  double picScale = NumberUtil.round(picWidth * 1.0 / picHeight, 2).doubleValue();  uploadPictureResult.setPicName(FileUtil.mainName(originFilename));  uploadPictureResult.setPicWidth(picWidth);  uploadPictureResult.setPicHeight(picHeight);  uploadPictureResult.setPicScale(picScale);  uploadPictureResult.setPicFormat(imageInfo.getFormat());  uploadPictureResult.setPicSize(FileUtil.size(file));  uploadPictureResult.setUrl(cosClientConfig.getHost() + "/" + uploadPath);  return uploadPictureResult;  }  /**  * 删除临时文件  */  public void deleteTempFile(File file) {  if (file == null) {  return;  }  boolean deleteResult = file.delete();  if (!deleteResult) {  log.error("file delete error, filepath = {}", file.getAbsolutePath());  }  }  
}

为了让模板同时兼容 MultipartFile 和 String 类型的文件参数,将这两种情况同一定义为 Object 类型的 inputSource 输入源。

新建本地图片上传子类

@Service  
public class FilePictureUpload extends PictureUploadTemplate {  @Override  protected void validPicture(Object inputSource) {  MultipartFile multipartFile = (MultipartFile) inputSource;  ThrowUtils.throwIf(multipartFile == null, ErrorCode.PARAMS_ERROR, "文件不能为空");  // 1. 校验文件大小  long fileSize = multipartFile.getSize();  final long ONE_M = 1024 * 1024L;  ThrowUtils.throwIf(fileSize > 2 * ONE_M, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2M");  // 2. 校验文件后缀  String fileSuffix = FileUtil.getSuffix(multipartFile.getOriginalFilename());  // 允许上传的文件后缀  final List<String> ALLOW_FORMAT_LIST = Arrays.asList("jpeg", "jpg", "png", "webp");  ThrowUtils.throwIf(!ALLOW_FORMAT_LIST.contains(fileSuffix), ErrorCode.PARAMS_ERROR, "文件类型错误");  }  @Override  protected String getOriginFilename(Object inputSource) {  MultipartFile multipartFile = (MultipartFile) inputSource;  return multipartFile.getOriginalFilename();  }  @Override  protected void processFile(Object inputSource, File file) throws Exception {  MultipartFile multipartFile = (MultipartFile) inputSource;  multipartFile.transferTo(file);  }  
}

新建 Url 图片上传子类,继承模板

@Service  
public class UrlPictureUpload extends PictureUploadTemplate {  @Override  protected void validPicture(Object inputSource) {  String fileUrl = (String) inputSource;  ThrowUtils.throwIf(StrUtil.isBlank(fileUrl), ErrorCode.PARAMS_ERROR, "文件地址不能为空");  // ... 跟之前的校验逻辑保持一致  }  @Override  protected String getOriginFilename(Object inputSource) {  String fileUrl = (String) inputSource;  // 从 URL 中提取文件名  return FileUtil.mainName(fileUrl);  }  @Override  protected void processFile(Object inputSource, File file) throws Exception {  String fileUrl = (String) inputSource;  // 下载文件到临时目录  HttpUtil.downloadFile(fileUrl, file);  }  
}

通过自动解析,识别是本地文件上传还是 Url 上传。

@Resource  
private FilePictureUpload filePictureUpload;  @Resource  
private UrlPictureUpload urlPictureUpload;  // 上传图片  
public PictureVO uploadPicture(Object inputSource, PictureUploadRequest pictureUploadRequest, User loginUser) {  if (inputSource == null) {  throw new BusinessException(ErrorCode.PARAMS_ERROR, "图片为空");  }  // ...  // 按照用户 id 划分目录  String uploadPathPrefix = String.format("public/%s", loginUser.getId());  // 根据 inputSource 类型区分上传方式  PictureUploadTemplate pictureUploadTemplate = filePictureUpload;  if (inputSource instanceof String) {  pictureUploadTemplate = urlPictureUpload;  }  UploadPictureResult uploadPictureResult = pictureUploadTemplate.uploadPicture(inputSource, uploadPathPrefix);  // 构造要入库的图片信息  // ...  
}

相关文章:

  • Qt生成日志与以及报错文件(mingw64位,winDbg)————附带详细解说
  • 《深度体验 Egg.js:打造企业级 Node.js 应用的全景指南》
  • AI生成的基于html+marked.js实现的Markdown转html工具,离线使用,可实时预览 [
  • 如何使用Webhook触发器,在 ONLYOFFICE 协作空间构建智能工作流
  • 自建 dnslog 回显平台:渗透测试场景下的隐蔽回显利器
  • stm32_DMA
  • 引领AI安全新时代 Accelerate 2025北亚巡展·北京站成功举办
  • 从失效文档到知识资产:Gitee Wiki 引领研发知识管理变革
  • 模板方法模式:优雅封装不变,灵活扩展可变
  • 电脑定时关机工具推荐
  • Transformer架构解析:Encoder与Decoder核心差异、生成式解码技术详解
  • 浏览器工作原理06 [#]渲染流程(下):HTML、CSS和JavaScript是如何变成页面的
  • MySQL技术内幕1:内容介绍+MySQL编译使用介绍
  • 10个成功案例剖析|融质AI创新实践
  • php中实现邮件发送功能
  • Spring Boot 类加载机制深度解析
  • 浪潮交换机配置track检测实现高速公路收费网络主备切换NQA
  • 1、cpp实现Python的print函数
  • http头部注入攻击
  • 多模态大语言模型arxiv论文略读(110)
  • 有没有做租赁的网站/厦门网络关键词排名
  • 做公司网站注意事项/网页制作培训网站
  • 有口碑的合肥网站建设/崇左seo
  • 做网站卖什么发财/免费留电话号码的广告
  • 连云港建设公司网站/深圳网络推广平台
  • 2015年做那个网站能致富/免费推广网站推荐