摘要: 嘿,各位努力学习的小伙伴们,我是默语!今天我们要攻克一个在实际项目中非常常见的需求:用户上传图片后,系统自动给图片加上预设的水印,比如公司LOGO或者版权文字。这不仅能保护图片版权,还能起到品牌宣传的作用。很多小白同学可能觉得这个功能听起来有点复杂,涉及到文件处理、图像操作等等。别担心!本文将手把手带你使用Spring Boot,结合Java原生的图像处理能力,轻松实现图片上传和动态添加水印的功能。我会把每一步都讲得仔仔(细细)白白(透透),保证你看完就能上手!
引言:
在互联网应用中,图片资源无处不在。无论是社交分享、电商平台商品图,还是企业内部的文档图片,我们经常需要在用户上传图片后进行一些自动化处理。其中,“添加水印”就是一个非常实用的功能。想象一下,你辛辛苦苦拍摄或制作的图片,被别人随意盗用,是不是很郁闷?或者,你想让用户分享出去的图片都带上你的品牌标识,增加曝光度。这时候,自动水印功能就派上大用场了。
Spring Boot以其“约定大于配置”的理念,极大地简化了Java应用的开发。对于文件上传,Spring Boot也提供了非常便捷的支持。而图片水印的添加,我们可以利用Java AWT (Abstract Window Toolkit) 和 Java 2D API 来实现,无需引入过多的第三方库,保持项目的轻量级。
那么,具体怎么做呢?别急,跟着默语的节奏,一步一步来,你会发现原来这么简单!

在开始编码之前,我们先确保开发环境和项目依赖都已就绪。
我们需要一个基础的Spring Boot Web项目。确保你的pom.xml文件中包含以下核心依赖:
XML
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>*默语解说:*
spring-boot-starter-web:这是我们构建Web应用的基础,处理文件上传请求就靠它了。spring-boot-devtools:开发时的好帮手,修改代码后能自动重启应用,省去手动重启的麻烦。lombok:能帮你少写很多样板代码,比如@Getter, @Setter, @ToString等。spring-boot-starter-thymeleaf:如果你想快速搭建一个前端页面来测试文件上传,Thymeleaf是个不错的选择。当然,你也可以用Postman等工具直接测试API接口。在 src/main/resources/application.properties (或 application.yml) 文件中,我们可以配置一下文件上传的相关限制,比如单个文件大小、总请求大小等。
Properties
# 服务器端口
server.port=8080
# Spring MVC 文件上传配置
spring.servlet.multipart.enabled=true
# 单个文件最大值,默认1MB,这里设置为10MB
spring.servlet.multipart.max-file-size=10MB
# 总请求最大值,默认10MB,这里设置为100MB
spring.servlet.multipart.max-request-size=100MB
# 文件大小阈值,超过后会写入临时文件,默认0,表示所有都写入磁盘
# spring.servlet.multipart.file-size-threshold=2MB
# 自定义配置:文件存储路径(请根据你的实际情况修改)
# 注意:生产环境建议使用更健壮的路径管理方式或对象存储服务
file.upload-dir=D:/myapp_uploads/images
# 水印图片路径 (如果使用图片水印)
watermark.image.path=classpath:watermark/logo.png # 假设水印图片在resources/watermark/logo.png默语解说:
spring.servlet.multipart.enabled=true:确保开启了文件上传支持。max-file-size 和 max-request-size:这两个参数很重要,可以防止用户上传过大的文件导致服务器资源耗尽。根据你的业务需求调整它们的值。file.upload-dir:这是我们自定义的一个属性,用来指定上传的文件保存到服务器的哪个位置。请务必修改为你自己机器上存在的有效路径!
watermark.image.path:如果我们要添加图片水印,这里指定水印图片的路径。classpath:表示从项目的resources目录下查找。
首先,我们需要一个Controller来接收前端上传的文件。
Java
package com.moyu.blog.controller;
import com.moyu.blog.service.FileStorageService;
import com.moyu.blog.service.WatermarkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.nio.file.Path;
import java.nio.file.Paths;
@Controller
@RequestMapping("/files")
public class FileUploadController {
private final FileStorageService fileStorageService;
private final WatermarkService watermarkService;
@Value("${file.upload-dir}")
private String uploadDir;
@Autowired
public FileUploadController(FileStorageService fileStorageService, WatermarkService watermarkService) {
this.fileStorageService = fileStorageService;
this.watermarkService = watermarkService;
}
// 提供一个简单的上传页面
@GetMapping("/upload")
public String showUploadForm(Model model) {
// 可选: 列出已上传文件
// model.addAttribute("files", fileStorageService.loadAll().map(
// path -> MvcUriComponentsBuilder.fromMethodName(FileUploadController.class,
// "serveFile", path.getFileName().toString()).build().toUri().toString())
// .collect(Collectors.toList()));
return "upload-form"; // 返回 src/main/resources/templates/upload-form.html
}
@PostMapping("/upload-with-watermark")
public String handleFileUploadWithWatermark(@RequestParam("file") MultipartFile file,
@RequestParam(value = "watermarkText", required = false, defaultValue = "默语博客") String watermarkText,
@RequestParam(value = "watermarkType", defaultValue = "text") String watermarkType, // "text" or "image"
RedirectAttributes redirectAttributes,
Model model) {
if (file.isEmpty()) {
redirectAttributes.addFlashAttribute("message", "请选择一个文件上传!");
return "redirect:/files/upload";
}
try {
// 1. 保存原始文件 (可选,或者直接处理流)
// String originalFilename = fileStorageService.store(file);
// 2. 为添加水印后的文件生成新文件名
String originalFilename = file.getOriginalFilename();
String fileExtension = "";
if (originalFilename != null && originalFilename.contains(".")) {
fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
}
String watermarkedFilename = "watermarked_" + System.currentTimeMillis() + "_" + originalFilename;
Path watermarkedFilePath = Paths.get(uploadDir).resolve(watermarkedFilename);
// 确保上传目录存在
File directory = new File(uploadDir);
if (!directory.exists()) {
directory.mkdirs();
}
// 3. 添加水印并保存
try (InputStream inputStream = file.getInputStream()) {
if ("text".equalsIgnoreCase(watermarkType)) {
watermarkService.addTextWatermark(inputStream, watermarkText, watermarkedFilePath.toFile());
} else if ("image".equalsIgnoreCase(watermarkType)) {
// 假设水印图片路径在 application.properties 中配置 watermark.image.path
watermarkService.addImageWatermark(inputStream, watermarkedFilePath.toFile());
} else {
redirectAttributes.addFlashAttribute("message", "无效的水印类型!");
return "redirect:/files/upload";
}
}
redirectAttributes.addFlashAttribute("message", "文件 '" + originalFilename + "' 上传并添加水印成功,保存为: '" + watermarkedFilename + "'");
redirectAttributes.addFlashAttribute("watermarkedFileUrl", "/files/download/" + watermarkedFilename);
} catch (IOException e) {
e.printStackTrace();
redirectAttributes.addFlashAttribute("message", "文件上传失败: " + e.getMessage());
} catch (IllegalArgumentException e) {
e.printStackTrace();
redirectAttributes.addFlashAttribute("message", "处理失败: " + e.getMessage());
}
return "redirect:/files/upload";
}
// 文件下载接口,用于查看带水印的图片
@GetMapping("/download/{filename:.+}")
@ResponseBody
public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
try {
Path file = Paths.get(uploadDir).resolve(filename);
Resource resource = new UrlResource(file.toUri());
if (resource.exists() || resource.isReadable()) {
String contentType = null;
try {
// 尝试获取文件类型,对于图片尤其重要
contentType = java.nio.file.Files.probeContentType(file);
} catch (IOException e) {
// log.info("Could not determine file type for: " + filename);
}
// 如果无法确定,则使用通用类型
if (contentType == null) {
contentType = "application/octet-stream";
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resource.getFilename() + "\"")
// inline 会尝试在浏览器中直接显示, attachment 会提示下载
.body(resource);
} else {
// throw new RuntimeException("Could not read the file!");
return ResponseEntity.notFound().build();
}
} catch (MalformedURLException e) {
// throw new RuntimeException("Error: " + e.getMessage());
return ResponseEntity.badRequest().build();
}
}
}默语解说:
@Controller:标记这是一个Spring MVC控制器。@RequestMapping("/files"):此类下的所有请求路径都会以/files开头。@Value("${file.upload-dir}"):从application.properties中注入我们自定义的文件存储路径。showUploadForm():GET请求,返回一个名为upload-form.html的视图,我们稍后会创建它。handleFileUploadWithWatermark():POST请求,处理实际的文件上传。@RequestParam("file") MultipartFile file:Spring MVC会自动将名为file的上传部件绑定到MultipartFile对象。MultipartFile是Spring对上传文件的封装,非常方便使用。@RequestParam("watermarkText") String watermarkText:接收前端传来的水印文字。redirectAttributes.addFlashAttribute():用于在重定向后依然能向页面传递消息(比如成功或失败的提示)。serveFile():文件下载接口,用于在浏览器中查看或下载处理后的文件。为了让Controller更专注于请求处理,我们可以把文件存储的逻辑抽取到一个Service中。
Java
package com.moyu.blog.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.PostConstruct; // 注意: Spring Boot 3.x 使用 jakarta
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
@Service
public class FileStorageService {
private final Path rootLocation;
public FileStorageService(@Value("${file.upload-dir}") String uploadDir) {
this.rootLocation = Paths.get(uploadDir);
}
@PostConstruct
public void init() {
try {
Files.createDirectories(rootLocation);
System.out.println("Upload directory created/initialized: " + rootLocation.toString());
} catch (IOException e) {
throw new RuntimeException("Could not initialize storage location", e);
}
}
public String store(MultipartFile file) {
try {
if (file.isEmpty()) {
throw new IllegalArgumentException("Failed to store empty file.");
}
String filename = System.currentTimeMillis() + "_" + file.getOriginalFilename();
Path destinationFile = this.rootLocation.resolve(Paths.get(filename))
.normalize().toAbsolutePath();
if (!destinationFile.getParent().equals(this.rootLocation.toAbsolutePath())) {
// This is a security check
throw new IllegalArgumentException("Cannot store file outside current directory.");
}
try (InputStream inputStream = file.getInputStream()) {
Files.copy(inputStream, destinationFile, StandardCopyOption.REPLACE_EXISTING);
}
return filename;
} catch (IOException e) {
throw new RuntimeException("Failed to store file.", e);
}
}
}*默语解说:*
@Service:标记这是一个1服务类。@PostConstruct init():这个方法会在服务实例化后自动执行,我们用它来创建上传目录(如果不存在的话)。store():核心的文件保存方法。它会生成一个唯一的文件名(避免重名覆盖),然后将上传文件的内容复制到目标路径。HTML
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>默语博客 - 文件上传与水印</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
.container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h1 { color: #007bff; }
.message { padding: 10px; margin-bottom: 15px; border-radius: 4px; }
.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
label { display: block; margin-bottom: 8px; font-weight: bold; }
input[type="file"], input[type="text"], select {
width: calc(100% - 22px); padding: 10px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px;
}
button {
background-color: #007bff; color: white; padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px;
}
button:hover { background-color: #0056b3; }
.preview img { max-width: 300px; max-height: 300px; margin-top: 15px; border: 1px solid #ddd; }
</style>
</head>
<body>
<div class="container">
<h1>上传图片并添加水印</h1>
<div th:if="${message}" th:text="${message}"
th:class="${(message != null and (message.contains('失败') or message.contains('请选择'))) ? 'message error' : 'message success'}">
</div>
<form method="POST" th:action="@{/files/upload-with-watermark}" enctype="multipart/form-data">
<div>
<label for="file">选择图片文件:</label>
<input type="file" name="file" id="file" accept="image/*" required />
</div>
<div>
<label for="watermarkType">选择水印类型:</label>
<select name="watermarkType" id="watermarkType">
<option value="text" selected>文字水印</option>
<option value="image">图片LOGO水印</option>
</select>
</div>
<div id="textWatermarkInput">
<label for="watermarkText">输入水印文字 (可选):</label>
<input type="text" name="watermarkText" id="watermarkText" placeholder="默语博客" />
</div>
<div>
<button type="submit">上传并添加水印</button>
</div>
</form>
<div th:if="${watermarkedFileUrl}" class="preview">
<h3>带水印图片预览:</h3>
<img th:src="${watermarkedFileUrl}" alt="Watermarked Image" />
<p><a th:href="${watermarkedFileUrl}" target="_blank">查看或下载带水印的图片</a></p>
</div>
</div>
<script>
// 简单JS,根据选择显示/隐藏文字水印输入框
const watermarkTypeSelect = document.getElementById('watermarkType');
const textWatermarkDiv = document.getElementById('textWatermarkInput');
watermarkTypeSelect.addEventListener('change', function() {
if (this.value === 'text') {
textWatermarkDiv.style.display = 'block';
} else {
textWatermarkDiv.style.display = 'none';
}
});
</script>
</body>
</html>默语解说:
-enctype=“multipart/form-data” :文件上传表单必须设置这个属性。
input type="file" name="file":文件选择框,name="file"要和Controller中@RequestParam("file")对应。accept="image/*":限制用户只能选择图片文件。th:if=“${message}”:用于显示Controller通过 RedirectAttributes传递过来的提示信息。
到这里,基本的文件上传框架就搭好了。你可以运行Spring Boot应用,访问 http://localhost:8080/files/upload 看看效果(此时还不能加水印)。
这部分是我们的重头戏!我们将使用Java的java.awt包和java.awt.image包中的类来操作图片。
Java
package com.moyu.blog.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.imageio.ImageIO; // 注意:如果JDK 9+ 模块化,可能需要 --add-modules java.desktop
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
@Service
public class WatermarkService {
@Value("${watermark.image.path:classpath:watermark/default-logo.png}") // Provide a default if not set
private String watermarkImagePath; // 从配置文件读取水印图片路径
private static final int DEFAULT_WATERMARK_WIDTH = 120; // 默认图片水印宽度
private static final float DEFAULT_ALPHA = 0.4f; // 默认透明度
private static final int DEFAULT_TEXT_SIZE = 20; // 默认文字大小
private static final Color DEFAULT_TEXT_COLOR = Color.LIGHT_GRAY; // 默认文字颜色
/**
* 给图片添加文字水印
*
* @param sourceImageStream 源图片输入流
* @param text 水印文字
* @param targetImageFile 添加水印后的图片输出文件
* @throws IOException IO异常
* @throws IllegalArgumentException 参数异常
*/
public void addTextWatermark(InputStream sourceImageStream, String text, File targetImageFile) throws IOException, IllegalArgumentException {
if (sourceImageStream == null) {
throw new IllegalArgumentException("源图片流不能为空");
}
if (!StringUtils.hasText(text)) {
text = "Moyu Blog"; // 默认水印文字
}
if (targetImageFile == null) {
throw new IllegalArgumentException("目标文件不能为空");
}
Image srcImg = ImageIO.read(sourceImageStream);
if (srcImg == null) {
throw new IllegalArgumentException("无法读取源图片,请确保是有效的图片格式!");
}
int srcImgWidth = srcImg.getWidth(null);
int srcImgHeight = srcImg.getHeight(null);
BufferedImage bufferedImage = new BufferedImage(srcImgWidth, srcImgHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = bufferedImage.createGraphics();
// 1. 绘制原始图片
g2d.drawImage(srcImg, 0, 0, srcImgWidth, srcImgHeight, null);
// 2. 设置水印文字的属性
g2d.setColor(DEFAULT_TEXT_COLOR);
g2d.setFont(new Font("Arial", Font.BOLD, DEFAULT_TEXT_SIZE));
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, DEFAULT_ALPHA)); // 设置透明度
// 3. 计算文字位置(右下角)
FontMetrics fm = g2d.getFontMetrics();
int textWidth = fm.stringWidth(text);
int textHeight = fm.getHeight(); // 获取字体高度,实际绘制时 y 是基线
int x = srcImgWidth - textWidth - 10; // 10px padding from right
int y = srcImgHeight - textHeight / 2 + 5; // 调整Y使文字更靠下边缘,10px padding from bottom
// 4. 绘制水印文字 (可以绘制多次,形成平铺效果)
g2d.drawString(text, x, y); // 在右下角绘制一个
// 如果需要平铺效果(可选)
// g2d.rotate(Math.toRadians(-30), bufferedImage.getWidth()/2.0, bufferedImage.getHeight()/2.0); //旋转
// for (int i = -srcImgHeight / 2; i < srcImgHeight * 1.5; i += textHeight * 3) {
// for (int j = -srcImgWidth / 2; j < srcImgWidth * 1.5; j += textWidth * 2) {
// g2d.drawString(text, j, i);
// }
// }
g2d.dispose(); // 释放图形上下文使用的系统资源
// 5. 输出图片
String formatName = getFormatName(targetImageFile.getName());
try (OutputStream out = Files.newOutputStream(targetImageFile.toPath())) {
ImageIO.write(bufferedImage, formatName, out);
}
System.out.println("文字水印添加成功: " + targetImageFile.getAbsolutePath());
}
/**
* 给图片添加图片水印
*
* @param sourceImageStream 源图片输入流
* @param targetImageFile 添加水印后的图片输出文件
* @throws IOException IO异常
* @throws IllegalArgumentException 参数异常
*/
public void addImageWatermark(InputStream sourceImageStream, File targetImageFile) throws IOException, IllegalArgumentException {
if (sourceImageStream == null) {
throw new IllegalArgumentException("源图片流不能为空");
}
if (targetImageFile == null) {
throw new IllegalArgumentException("目标文件不能为空");
}
Image srcImg = ImageIO.read(sourceImageStream);
if (srcImg == null) {
throw new IllegalArgumentException("无法读取源图片,请确保是有效的图片格式!");
}
int srcImgWidth = srcImg.getWidth(null);
int srcImgHeight = srcImg.getHeight(null);
BufferedImage bufferedImage = new BufferedImage(srcImgWidth, srcImgHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = bufferedImage.createGraphics();
g2d.drawImage(srcImg, 0, 0, srcImgWidth, srcImgHeight, null);
// 加载水印图片
Image watermarkImg;
try {
ClassPathResource cpr = new ClassPathResource(watermarkImagePath.replace("classpath:", ""));
if (!cpr.exists()) {
throw new IllegalArgumentException("水印图片未找到: " + watermarkImagePath);
}
watermarkImg = ImageIO.read(cpr.getInputStream());
} catch (IOException e) {
throw new IOException("无法加载水印图片: " + watermarkImagePath, e);
}
if (watermarkImg == null) {
throw new IllegalArgumentException("无法读取水印图片,请确保是有效的图片格式!");
}
int watermarkWidth = watermarkImg.getWidth(null);
int watermarkHeight = watermarkImg.getHeight(null);
// 动态调整水印图片大小,使其宽度为原图的1/5,或不超过预设的最大/最小宽度
// 这里简单处理,使用默认宽度,或者按比例缩放
double scale = (double) DEFAULT_WATERMARK_WIDTH / watermarkWidth;
int newWatermarkWidth = DEFAULT_WATERMARK_WIDTH;
int newWatermarkHeight = (int) (watermarkHeight * scale);
// 如果希望水印大小根据原图动态变化,可以这样:
// int dynamicWatermarkWidth = srcImgWidth / 5; // 水印宽度为原图的1/5
// double scale = (double) dynamicWatermarkWidth / watermarkWidth;
// int newWatermarkWidth = dynamicWatermarkWidth;
// int newWatermarkHeight = (int) (watermarkHeight * scale);
// 设置透明度
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, DEFAULT_ALPHA));
// 计算水印位置(右下角)
int x = srcImgWidth - newWatermarkWidth - 10; // 10px padding
int y = srcImgHeight - newWatermarkHeight - 10; // 10px padding
g2d.drawImage(watermarkImg, x, y, newWatermarkWidth, newWatermarkHeight, null);
g2d.dispose();
String formatName = getFormatName(targetImageFile.getName());
try (OutputStream out = Files.newOutputStream(targetImageFile.toPath())) {
ImageIO.write(bufferedImage, formatName, out);
}
System.out.println("图片水印添加成功: " + targetImageFile.getAbsolutePath());
}
/**
* 根据文件名获取图片格式 (jpg, png等)
*/
private String getFormatName(String fileName) {
String extension = "";
int i = fileName.lastIndexOf('.');
if (i > 0) {
extension = fileName.substring(i + 1);
}
if (StringUtils.hasText(extension) && (extension.equalsIgnoreCase("jpeg") || extension.equalsIgnoreCase("jpg"))) {
return "JPEG"; // ImageIO.write 对于jpeg/jpg需要大写,或者直接用"jpg"
}
if (StringUtils.hasText(extension) && extension.equalsIgnoreCase("png")) {
return "PNG";
}
// 默认或不支持的,尝试jpg
return "JPEG";
}
}默语解说:
核心类:
java.awt.Image: 表示图像的抽象超类。java.awt.image.BufferedImage: Image 的一个实现,它管理内存中的图像数据,我们可以直接操作其像素。java.awt.Graphics2D: 提供了更复杂的图形操作API,如设置字体、颜色、透明度、绘制形状和图像等。它是从BufferedImage对象获取的。javax.imageio.ImageIO: 用于读取和写入图像的实用工具类。java.awt.AlphaComposite: 用于控制绘制操作的透明度。SRC_ATOP模式意味着将源(水印)绘制在目标(原图)之上,并根据源的alpha值混合。
addTextWatermark (添加文字水印) 方法:
读取源图片流到
Image对象。创建一个与源图大小相同、支持ARGB(即带Alpha透明通道)的
BufferedImage。获取
Graphics2D对象。
先绘制原始图片到bufferedImage上。这是基础!
设置水印文字的颜色、字体、透明度 (g2d.setComposite())。
使用FontMetrics计算文字的宽度和高度,以便精确定位(比如放在右下角)。
使用g2d.drawString()在指定位置绘制文字。
释放
Graphics2D资源 (g2d.dispose())。使用
ImageIO.write()将处理后的bufferedImage写入到目标文件。getFormatName是辅助方法,根据文件扩展名判断图片格式。
addImageWatermark (添加图片水印) 方法:
与文字水印类似,先读取源图、创建BufferedImage、获取Graphics2D、绘制原图。
使用
ClassPathResource从classpath加载水印图片 (logo.png)。请确保你在src/main/resources下创建一个watermark文件夹,并放入一个名为logo.png的水印图片文件。
可以根据需要调整水印图片的大小(这里示例是固定宽度缩放)。
设置透明度。
计算图片水印的位置(比如右下角)。
使用g2d.drawImage()
将水印图片绘制到bufferedImage上。
释放资源,写入文件。
JDK 9+ 模块化注意:
如果你的项目使用JDK 9或更高版本,并且遇到了 java.awt 或 javax.imageio 包找不到的问题,这可能是因为Java平台的模块化系统。java.desktop模块(包含了AWT和Swing)默认可能不被所有类型的应用(特别是服务端应用)所包含。你可能需要在编译或运行时添加 --add-modules java.desktop 参数。不过,对于标准的Spring Boot Web应用,通常IDEA或Maven/Gradle配置会自动处理好。如果遇到问题,这是一个排查方向。
现在,我们的FileUploadController和WatermarkService都已经准备好了。Controller会接收文件,然后调用WatermarkService中的方法来添加水印,并将结果保存。
运行和测试:
确保你的
application.properties中的file.upload-dir指向一个你机器上实际存在的、可写的目录。如果你要测试图片水印,请在
src/main/resources/watermark/目录下放置一个名为
logo.png的图片文件(或者修改application.properties中的watermark.image.path为你自己的水印图片路径)。运行你的Spring Boot主应用类 (通常是带有
@SpringBootApplication
注解的类)。
打开浏览器,访问
http://localhost:8080/files/upload
。
选择一个图片文件,选择水印类型(文字或图片),如果选择文字,可以输入自定义文字。
点击“上传并添加水印”。
如果一切顺利,页面会提示成功,并显示带水印图片的预览和下载链接。点击链接可以在浏览器中看到效果。
去你配置的
file.upload-dir目录下查看,应该能找到原始文件(如果你在controller中保存了的话)和带水印的文件(文件名通常以watermarked_开头)。常见问题与调试:
文件路径问题:
确保
file.upload-dir和水印图片路径正确无误,且应用有读写权限。
图片格式:
ImageIO支持常见的图片格式如JPEG, PNG, GIF, BMP。确保你上传的是这些格式。PNG格式更适合做水印,因为它支持透明通道。
依赖问题:
检查
pom.xml中的依赖是否都已正确加载。
JDK版本: 如前所述,高版本JDK的模块化问题。
异常信息: 仔细阅读控制台输出的异常信息,它们通常会给出问题所在。
对于初学者,以上功能已经够用。但如果你想让它更强大,可以考虑:
水印位置可配置: 左上、右上、居中、平铺等。
水印样式更丰富: 旋转角度、字体大小颜色动态调整、图片水印大小动态调整。
异步处理:
对于大图片或大量上传,水印处理可能耗时。可以考虑使用异步任务(
@Async)或消息队列来处理,避免阻塞HTTP请求线程。
使用专业图像处理库:
如 Thumbnailator或 Imgscalr,它们提供了更简洁的API和更优的性能来进行图片缩放、裁剪和水印等操作。
错误处理更完善: 对各种可能的异常(如文件不是图片、水印图片加载失败等)进行更细致的捕获和用户提示。
安全性:
防止路径遍历攻击(FileStorageService中的normalize().toAbsolutePath()和父目录检查有这个作用)。
恭喜你,坚持看到了这里!通过今天的学习,我们一起用Spring Boot和Java AWT实现了图片上传和自动添加文字/图片水印的功能。我们从项目搭建、文件上传接口编写,到核心的水印服务实现,一步步剖析了整个过程。
回顾一下关键点:
希望这篇“默语牌”教程能让你对文件处理和图像操作更有信心。虽然我们只用了Java内置的库,但已经能实现很实用的功能了。记住,技术学习就是这样,从一个个小功能开始,不断积累,最终就能构建出复杂强大的系统。
参考资料:
Spring Boot官方文档 - 文件上传: https://spring.io/guides/gs/uploading-files/
Java™ Platform, Standard Edition 8 A
PI Specification -
java.awt.Graphics2D:
https://docs.oracle.com/javase/8/docs/api/java/awt/Graphics2D.html
Java™ Platform, Standard Edition 8 API Specification -
javax.imageio.ImageIO:
https://docs.oracle.com/javase/8/docs/api/javax/imageio/ImageIO.html
Baeldung - Adding Watermark to an Image in Java: (搜索类似关键词可以找到很多优秀教程)