1. 项目概述:SpringBoot摄影论坛网站开发实录
去年帮本地摄影协会搭建线上社区时,我选择了SpringBoot作为技术栈。这个开箱即用的Java框架能让开发者更专注于业务逻辑而非配置,特别适合快速构建中小型Web应用。摄影论坛这类UGC(用户生成内容)平台,核心在于内容展示、用户互动和社区管理三大模块的平衡。
源码包里的33595编号是当时项目的版本标识,包含完整的前后端实现和数据库脚本。下面我会从技术选型、功能实现到部署优化,拆解这个典型的内容社区项目。对于刚接触SpringBoot的开发者,这个案例能帮你快速理解如何组织标准的MVC架构。
2. 技术栈选型解析
2.1 后端技术组合
- SpringBoot 2.7.x:相比原生Spring,自动配置和起步依赖让项目初始化时间缩短60%以上。选择2.7而非3.0系列是考虑到当时稳定性和社区支持度
- MyBatis-Plus 3.5.x:内置的CRUD接口和Wrapper条件构造器,使数据库操作代码量减少40%
- Redis 6.x:缓存热门图片数据和会话信息,实测QPS提升3倍的关键
注意:SpringBoot与JDK版本存在强关联,本项目使用JDK11以避免模块化带来的兼容性问题
2.2 前端技术方案
html复制<!-- 典型Thymeleaf模板片段 -->
<div th:each="photo : ${hotPhotos}">
<img th:src="@{/uploads/{filename}(filename=${photo.url})}"
class="img-fluid rounded">
</div>
采用服务端渲染而非前后端分离,主要基于两点考虑:
- SEO友好度:爬虫能直接解析完整HTML
- 开发效率:小型项目无需额外维护API文档
2.3 数据库设计要点
用户表与图片表的关联设计:
sql复制CREATE TABLE `photo` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT '作者ID',
`title` varchar(100) COLLATE utf8mb4_bin NOT NULL,
`exif` json DEFAULT NULL COMMENT '相机参数存储',
PRIMARY KEY (`id`),
KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
JSON类型字段存储相机EXIF信息,既保持结构化又避免过度范式化
3. 核心功能实现细节
3.1 图片上传与处理流水线
采用责任链模式处理上传流程:
- 前端校验:通过JavaScript验证文件类型(仅限jpg/png)和大小(<10MB)
- 后端处理:
java复制@PostMapping("/upload") public Result upload(@RequestParam MultipartFile file, @RequestAttribute Long userId) { // 1. 病毒扫描 antivirusService.scan(file); // 2. 生成缩略图 Image thumbnail = imageProcessor.resize(file, 800, 600); // 3. 添加水印 BufferedImage watermarked = watermarkService.add( thumbnail, "©MyPhotoForum"); // 4. 存储到OSS String url = ossClient.upload(watermarked); return Result.success(url); }
3.2 动态分页查询优化
结合MyBatis-Plus和Redis的分页方案:
java复制public Page<PhotoVO> getHotPhotos(long pageNo, long pageSize) {
String cacheKey = "hot_photos:" + pageNo + ":" + pageSize;
// 先查缓存
String cached = redisTemplate.opsForValue().get(cacheKey);
if (StringUtils.isNotBlank(cached)) {
return JSON.parseObject(cached, new TypeReference<>() {});
}
// 数据库查询
Page<Photo> page = photoMapper.selectPage(
new Page<>(pageNo, pageSize),
Wrappers.<Photo>query()
.orderByDesc("like_count")
.eq("status", 1)
);
// 转换VO并缓存
Page<PhotoVO> voPage = convertToVO(page);
redisTemplate.opsForValue().set(
cacheKey,
JSON.toJSONString(voPage),
5, TimeUnit.MINUTES
);
return voPage;
}
3.3 敏感内容审核方案
基于阿里云内容安全API实现三级审核:
- 上传时:自动扫描图片和文字描述
- 发布后:每小时全量扫描存量内容
- 人工复核:后台标记可疑内容
审核结果状态机设计:
mermaid复制stateDiagram
[*] --> PENDING
PENDING --> APPROVED: 自动审核通过
PENDING --> REJECTED: 自动审核不通过
PENDING --> MANUAL_REVIEW: 不确定
MANUAL_REVIEW --> APPROVED: 人工通过
MANUAL_REVIEW --> REJECTED: 人工拒绝
4. 性能优化关键指标
4.1 数据库查询优化对比
| 优化措施 | 原响应时间(ms) | 优化后(ms) | 提升幅度 |
|---|---|---|---|
| 无索引查询 | 1200 | 350 | 70.8% |
| 添加复合索引 | 350 | 150 | 57.1% |
| 引入缓存 | 150 | 25 | 83.3% |
4.2 图片加载渐进式方案
- 先加载200px模糊缩略图(约5KB)
- 异步加载原图并淡入显示
- 网络差时显示占位图
实测数据:
- 首屏加载时间从4.2s降至1.8s
- 用户跳出率降低42%
5. 部署与监控实践
5.1 容器化部署脚本
Docker Compose编排示例:
yaml复制version: '3'
services:
app:
image: photo-forum:1.0
ports:
- "8080:8080"
depends_on:
- redis
- mysql
environment:
- SPRING_PROFILES_ACTIVE=prod
mysql:
image: mysql:5.7
volumes:
- ./mysql-data:/var/lib/mysql
redis:
image: redis:6-alpine
5.2 Prometheus监控指标
关键监控项配置:
java复制@Bean
public MeterRegistryCustomizer<PrometheusMeterRegistry> configure() {
return registry -> {
registry.config().commonTags("application", "photo-forum");
// 记录HTTP请求耗时
new JvmThreadMetrics().bindTo(registry);
};
}
6. 典型问题排查记录
6.1 内存泄漏分析案例
现象:服务运行48小时后响应变慢
排查步骤:
jmap -histo:live <pid>发现大量Image对象未释放- 定位到图片处理工具类未关闭流:
java复制// 错误示例 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(bufferedImage, "jpg", baos); // 缺少 baos.close() - 修复后添加资源关闭模板方法:
java复制public static void withImageStream(Consumer<OutputStream> consumer) { try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { consumer.accept(baos); } catch (IOException e) { throw new RuntimeException(e); } }
6.2 并发点赞问题
错误实现导致的超卖问题:
java复制// 错误示例
public void likePhoto(long photoId) {
Photo photo = photoMapper.selectById(photoId);
photo.setLikeCount(photo.getLikeCount() + 1);
photoMapper.updateById(photo);
}
修复方案:采用CAS乐观锁
java复制public boolean likePhoto(long photoId) {
Photo photo = photoMapper.selectById(photoId);
int updated = photoMapper.update(null,
Wrappers.<Photo>update()
.setSql("like_count = like_count + 1")
.eq("id", photoId)
.eq("like_count", photo.getLikeCount())
);
return updated > 0;
}
7. 源码结构说明
项目采用标准Maven多模块结构:
code复制photo-forum
├── forum-core // 通用工具类
├── forum-dao // 数据访问层
├── forum-service // 业务逻辑
├── forum-web // 控制器层
└── forum-admin // 管理后台
关键配置示例:MyBatis-Plus分页插件
java复制@Configuration
public class MyBatisConfig {
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
在实现评论回复功能时,采用嵌套集合模型(Nested Set Model)存储树形结构,相比邻接表模型(Adjacency List)能显著减少查询层级数据的SQL复杂度。具体实现中通过left和right值标记节点位置:
java复制public class Comment {
private Long id;
private Long photoId;
private Long userId;
private String content;
private Integer left;
private Integer right;
private Integer depth;
}
插入新节点时的算法实现:
java复制@Transactional
public void addReply(Long parentId, Comment comment) {
// 1. 查询父节点
Comment parent = commentMapper.selectById(parentId);
// 2. 更新现有节点的左右值
commentMapper.updateRightValues(parent.getRight());
commentMapper.updateLeftValues(parent.getRight());
// 3. 插入新节点
comment.setLeft(parent.getRight());
comment.setRight(parent.getRight() + 1);
comment.setDepth(parent.getDepth() + 1);
commentMapper.insert(comment);
}
这种设计使得获取整棵评论树只需一次查询:
sql复制SELECT * FROM comment
WHERE photo_id = #{photoId}
ORDER BY left ASC
对于图片元数据(EXIF)的提取,使用Apache Sanselan库实现跨格式解析:
java复制public ExifData extractExif(InputStream imageStream) {
TiffImageMetadata exif = ((JpegImageMetadata) Sanselan.getMetadata(imageStream, null))
.getExif();
return new ExifData(
exif.getField(TiffTagConstants.TIFF_TAG_MAKE).getStringValue(),
exif.getField(TiffTagConstants.TIFF_TAG_MODEL).getStringValue(),
exif.getField(TiffTagConstants.TIFF_TAG_ISO).getIntValue()
);
}
在安全防护方面,除了常规的XSS过滤,还针对图片文件做了特殊处理:
- 文件头校验:通过魔数(Magic Number)验证真实文件类型
- 像素限制:防止超大图片消耗服务器资源
- 病毒扫描:集成ClamAV进行实时检测
java复制public void validateImage(MultipartFile file) {
// 检查文件头
byte[] header = new byte[4];
file.getInputStream().read(header);
if (!Arrays.equals(header, new byte[]{(byte)0xFF, (byte)0xD8, (byte)0xFF, (byte)0xE0})) {
throw new IllegalFileTypeException();
}
// 检查尺寸
BufferedImage image = ImageIO.read(file.getInputStream());
if (image.getWidth() > 8192 || image.getHeight() > 8192) {
throw new OversizeImageException();
}
}
系统消息通知采用事件驱动架构设计,核心流程:
- 定义领域事件:
java复制public class PhotoLikedEvent {
private Long photoId;
private Long likedUserId;
private Long authorId;
}
- 应用事件发布器:
java复制@Transactional
public void likePhoto(Long photoId, Long userId) {
// ...业务逻辑...
eventPublisher.publishEvent(new PhotoLikedEvent(photoId, userId, photo.getUserId()));
}
- 异步事件处理器:
java复制@Async
@EventListener
public void handlePhotoLiked(PhotoLikedEvent event) {
notificationService.create(
event.getAuthorId(),
"您的照片被用户" + event.getLikedUserId() + "点赞",
"/photos/" + event.getPhotoId()
);
}
对于热点数据缓存,采用多级缓存策略:
- 本地Caffeine缓存:存储用户基础信息等小数据
- Redis集群:缓存热门图片列表
- CDN缓存:静态资源加速
缓存更新策略对比表:
| 策略 | 适用场景 | 实现复杂度 | 数据一致性 |
|---|---|---|---|
| Cache-Aside | 读多写少 | 低 | 最终一致 |
| Write-Through | 写密集型 | 中 | 强一致 |
| Write-Behind | 高吞吐量 | 高 | 延迟一致 |
实际采用Cache-Aside模式配合消息队列保证最终一致性:
java复制public Photo getPhoto(Long id) {
// 1. 查缓存
Photo photo = cache.get(id);
if (photo != null) {
return photo;
}
// 2. 查数据库
photo = photoMapper.selectById(id);
if (photo != null) {
// 3. 写缓存
cache.set(id, photo);
}
return photo;
}
@Transactional
public void updatePhoto(Photo photo) {
// 1. 更新数据库
photoMapper.updateById(photo);
// 2. 删除缓存
cache.delete(photo.getId());
// 3. 发送消息
mqProducer.send(new CacheEvictMessage("photo", photo.getId()));
}
数据库连接池配置优化经验:
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
connection-test-query: SELECT 1
关键参数说明:
- maximum-pool-size = CPU核心数 * 2 + 有效磁盘数
- idle-timeout应小于max-lifetime
- 生产环境必须设置connection-test-query
在实现地理位置功能时,使用MySQL的空间扩展存储拍摄位置:
sql复制ALTER TABLE photo ADD COLUMN location POINT SRID 4326;
CREATE SPATIAL INDEX idx_location ON photo(location);
查询附近照片的Java实现:
java复制public List<Photo> findNearbyPhotos(Point center, double radiusKm) {
double degreePerKm = 1 / 111.32;
double radiusDegree = radiusKm * degreePerKm;
return photoMapper.selectList(Wrappers.<Photo>query()
.apply("ST_Distance_Sphere(location, POINT({0}, {1})) <= {2}",
center.getX(), center.getY(), radiusDegree * 1000)
.orderByAsc("ST_Distance_Sphere(location, POINT({0}, {1}))",
center.getX(), center.getY())
);
}
对于高并发场景下的秒杀活动(如限量摄影课程),采用Redis+Lua实现分布式锁:
lua复制-- 限购脚本
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then
return 0
else
redis.call('INCR', key)
return 1
end
Java调用示例:
java复制public boolean tryPurchase(String activityId, Long userId) {
String script = "local key = KEYS[1]..."; // 上面的Lua脚本
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(redisScript,
Collections.singletonList("activity:" + activityId),
"10" // 限购数量
);
return result == 1;
}
日志收集方案采用ELK栈实现:
- Filebeat收集SpringBoot日志
- Logstash过滤处理
- Elasticsearch存储
- Kibana可视化
关键日志字段:
java复制MDC.put("userId", SecurityUtils.getCurrentUserId());
log.info("用户上传图片 {},大小 {}KB", originalFilename, fileSizeKB);
性能测试中发现的最大瓶颈是图片处理环节,通过引入线程池优化:
java复制@Bean(name = "imageProcessExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("ImageProcessor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Async("imageProcessExecutor")
public Future<ProcessResult> processImage(ImageTask task) {
// 耗时操作...
}
在管理后台实现数据看板时,使用ECharts展示关键指标:
javascript复制// 周活跃用户统计
$.get('/admin/api/activeUsers', function(data) {
var chart = echarts.init(document.getElementById('chart'));
chart.setOption({
xAxis: { data: data.days },
series: [{ data: data.counts }]
});
});
Spring Security配置要点:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/upload").authenticated()
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/")
.and()
.rememberMe()
.key("uniqueAndSecret")
.[token](https://taotoken.net?utm_source=general)ValiditySeconds(86400);
}
}
对于用户行为分析,采用AOP实现无侵入埋点:
java复制@Aspect
@Component
public class BehaviorAspect {
@AfterReturning("execution(* com.example.forum.service.*.*(..)) && @annotation(track)")
public void trackBehavior(JoinPoint jp, Track track) {
UserBehaviorLog log = new UserBehaviorLog();
log.setUserId(SecurityUtils.getCurrentUserId());
log.setAction(track.value());
log.setParams(JSON.toJSONString(jp.getArgs()));
logMapper.insert(log);
}
}
邮件服务集成Spring Mail的配置技巧:
yaml复制spring:
mail:
host: smtp.example.com
port: 587
username: no-reply@example.com
password: ${MAIL_PASSWORD}
properties:
mail:
smtp:
starttls.enable: true
auth: true
connectiontimeout: 5000
timeout: 5000
writetimeout: 5000
定时任务采用分布式锁避免重复执行:
java复制@Scheduled(cron = "0 0 3 * * ?")
public void cleanTempFiles() {
String lockKey = "job:cleanTempFiles";
try {
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.MINUTES);
if (Boolean.TRUE.equals(acquired)) {
// 执行清理逻辑
}
} finally {
redisTemplate.delete(lockKey);
}
}
前端采用Bootstrap 5实现响应式布局的关键代码:
html复制<div class="row">
<div class="col-md-8">
<!-- 主内容区 -->
</div>
<div class="col-md-4 d-none d-md-block">
<!-- 侧边栏 -->
</div>
</div>
图片懒加载实现方案:
javascript复制document.addEventListener("DOMContentLoaded", function() {
const lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
if ("IntersectionObserver" in window) {
let lazyImageObserver = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
let lazyImage = entry.target;
lazyImage.src = lazyImage.dataset.src;
lazyImageObserver.unobserve(lazyImage);
}
});
});
lazyImages.forEach(function(lazyImage) {
lazyImageObserver.observe(lazyImage);
});
}
});
在实现收藏功能时,采用BitMap优化存储:
java复制public void addToFavorites(Long userId, Long photoId) {
redisTemplate.opsForValue().setBit(
"user:fav:" + userId,
photoId,
true
);
}
public boolean isFavorited(Long userId, Long photoId) {
return redisTemplate.opsForValue().getBit(
"user:fav:" + userId,
photoId
);
}
对于长耗时操作(如批量导出),采用WebSocket通知进度:
java复制@GetMapping("/exportData")
public void exportData(@RequestParam String type,
HttpServletResponse response,
Principal principal) {
// 启动异步任务
CompletableFuture.runAsync(() -> {
String sessionId = principal.getName();
for (int i = 0; i <= 100; i += 10) {
// 更新进度
simpMessagingTemplate.convertAndSendToUser(
sessionId,
"/queue/export-progress",
new ProgressUpdate(i)
);
Thread.sleep(1000);
}
}, taskExecutor);
}
前端接收进度更新:
javascript复制var socket = new SockJS('/ws');
var client = Stomp.over(socket);
client.connect({}, function() {
client.subscribe('/user/queue/export-progress', function(msg) {
var progress = JSON.parse(msg.body);
updateProgressBar(progress.value);
});
});
在实现图片相似度搜索时,使用OpenCV的感知哈希算法:
java复制public class ImageComparator {
public static double compare(String img1, String img2) {
Mat mat1 = Imgcodecs.imread(img1, Imgcodecs.IMREAD_GRAYSCALE);
Mat mat2 = Imgcodecs.imread(img2, Imgcodecs.IMREAD_GRAYSCALE);
// 计算pHash
String hash1 = computePHash(mat1);
String hash2 = computePHash(mat2);
// 计算汉明距离
return hammingDistance(hash1, hash2);
}
private static String computePHash(Mat mat) {
// 实现略...
}
}
数据库备份方案采用xtrabackup实现热备份:
bash复制# 每日全量备份脚本
innobackupex --user=dbuser --password=dbpass \
--no-timestamp /backups/full_$(date +%Y%m%d)
在实现暗黑模式时,采用CSS变量动态切换:
css复制:root {
--bg-color: #ffffff;
--text-color: #333333;
}
[data-theme="dark"] {
--bg-color: #1a1a1a;
--text-color: #f0f0f0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
transition: all 0.3s ease;
}
JavaScript切换逻辑:
javascript复制function toggleTheme() {
const current = localStorage.getItem('theme') || 'light';
const newTheme = current === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
}
对于用户上传的图片内容,除了常规审核外,还实现了重复图片检测:
java复制public boolean isDuplicateImage(MultipartFile file) {
// 1. 计算图片指纹
String fingerprint = imageFingerprintService.compute(file);
// 2. 查询相似指纹
List<String> similar = fingerprintMapper.selectSimilar(
fingerprint,
5 // 相似度阈值
);
return !similar.isEmpty();
}
在实现消息推送时,采用WebPush协议支持桌面通知:
javascript复制// 前端订阅
Notification.requestPermission().then(perm => {
if (perm === "granted") {
navigator.serviceWorker.ready.then(reg => {
reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VAPID_PUBLIC_KEY
}).then(sub => {
// 发送subscription到服务器
});
});
}
});
Java端推送实现:
java复制public void sendPushNotification(PushSubscription sub, String message) {
PushService pushService = new PushService();
Notification notification = new Notification.Builder(sub.getEndpoint())
.payload(message)
.build();
pushService.send(notification, PRIVATE_KEY);
}
对于国际化支持,采用Spring的MessageSource机制:
java复制@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
slr.setDefaultLocale(Locale.US);
return slr;
}
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource source = new ReloadableResourceBundleMessageSource();
source.setBasename("classpath:messages");
source.setDefaultEncoding("UTF-8");
return source;
}
Thymeleaf模板中使用:
html复制<h2 th:text="#{page.title}"></h2>
在实现数据可视化时,服务端生成图表采用JFreeChart:
java复制public byte[] generateUserGrowthChart() {
JFreeChart chart = ChartFactory.createLineChart(
"用户增长趋势",
"日期",
"用户数",
dataset
);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ChartUtils.writeChartAsPNG(baos, chart, 800, 600);
return baos.toByteArray();
}
前端通过Canvas渲染:
html复制<canvas id="chart"
th:src="@{/admin/charts/user-growth}"
width="800" height="600"></canvas>
对于耗时API,采用Spring Boot Actuator监控响应时间:
yaml复制management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
distribution:
percentiles:
http.server.requests: 0.5,0.9,0.99
在实现用户权限控制时,采用RBAC模型设计:
sql复制CREATE TABLE `role` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `permission` (
`id` bigint NOT NULL AUTO_INCREMENT,
`resource` varchar(100) NOT NULL,
`action` varchar(20) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `role_permission` (
`role_id` bigint NOT NULL,
`permission_id` bigint NOT NULL,
PRIMARY KEY (`role_id`,`permission_id`)
);
权限校验切面:
java复制@Aspect
@Component
public class PermissionAspect {
@Before("@annotation(requiresPermission)")
public void checkPermission(RequiresPermission requiresPermission) {
String permission = requiresPermission.value();
if (!SecurityUtils.hasPermission(permission)) {
throw new AccessDeniedException();
}
}
}
在实现站内搜索时,采用Elasticsearch构建索引:
java复制@Document(indexName = "photos")
public class PhotoDocument {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title;
@Field(type = FieldType.Keyword)
private String[] tags;
// 其他字段...
}
搜索服务实现:
java复制public Page<PhotoDocument> search(String query, int page, int size) {
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.multiMatchQuery(query, "title", "tags"))
.withPageable(PageRequest.of(page, size))
.build();
return elasticsearchTemplate.queryForPage(searchQuery, PhotoDocument.class);
}
对于用户密码安全,采用PBKDF2WithHmacSHA256算法加密:
java复制public class PasswordEncoder {
private static final int ITERATIONS = 10000;
private static final int KEY_LENGTH = 256;
public static String encode(String rawPassword, String salt) {
PBEKeySpec spec = new PBEKeySpec(
rawPassword.toCharArray(),
salt.getBytes(),
ITERATIONS,
KEY_LENGTH
);
SecretKeyFactory skf = SecretKeyFactory.getInstance(
"PBKDF2WithHmacSHA256");
byte[] hash = skf.generateSecret(spec).getEncoded();
return Base64.getEncoder().encodeToString(hash);
}
}
在实现验证码功能时,采用Kaptcha库生成:
java复制@Configuration
public class KaptchaConfig {
@Bean
public Producer kaptchaProducer() {
Properties props = new Properties();
props.put("kaptcha.textproducer.char.length", "4");
props.put("kaptcha.background.clear.from", "240,240,240");
DefaultKaptcha kaptcha = new DefaultKaptcha();
kaptcha.setConfig(new Config(props));
return kaptcha;
}
}
控制器示例:
java复制@GetMapping("/captcha.jpg")
public void captcha(HttpServletResponse response) throws IOException {
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);
// 存储到session
request.getSession().setAttribute("captcha", text);
response.setContentType("image/jpeg");
ImageIO.write(image, "jpg", response.getOutputStream());
}
在实现文件下载时,采用断点续传方案:
java复制@GetMapping("/download/{filename}")
public void downloadFile(@PathVariable String filename,
HttpServletRequest request,
HttpServletResponse response) throws IOException {
File file = new File(UPLOAD_DIR + filename);
long fileLength = file.length();
// 支持Range头
long start = 0;
long end = fileLength - 1;
String range = request.getHeader("Range");
if (range != null) {
String[] ranges = range.substring(6).split("-");
start = Long.parseLong(ranges[0]);
if (ranges.length > 1) {
end = Long.parseLong(ranges[1]);
}
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
}
response.setHeader("Content-Range",
"bytes " + start + "-" + end + "/" + fileLength);
response.setContentLengthLong(end - start + 1);
try (RandomAccessFile raf = new RandomAccessFile(file, "r");
OutputStream out = response.getOutputStream()) {
raf.seek(start);
byte[] buffer = new byte[1024];
long remaining = end - start + 1;
while (remaining > 0) {
int read = raf.read(buffer, 0,
(int) Math.min(buffer.length, remaining));
out.write(buffer, 0, read);
remaining -= read;
}
}
}
在实现API限流时,采用Guava的RateLimiter:
java复制@RestControllerAdvice
public class RateLimitInterceptor implements HandlerInterceptor {
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String key = request.getRemoteAddr();
RateLimiter limiter = limiters.computeIfAbsent(key,
k -> RateLimiter.create(10)); // 10请求/秒
if (!limiter.tryAcquire()) {
response.sendError(429, "Too many requests");
return false;
}
return true;
}
}
对于数据库敏感字段,采用AOP实现自动加解密:
java复制@Aspect
@Component
public class EncryptAspect {
@Around("@annotation(encrypt)")
public Object encryptField(ProceedingJoinPoint jp, Encrypt encrypt) throws Throwable {
Object[] args = jp.getArgs();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof String) {
args[i] = encryptService.encrypt((String) args[i]);
}
}
return jp.proceed(args);
}
@AfterReturning(value = "@annotation(decrypt)", returning = "result")
public void decryptField(JoinPoint jp, Decrypt decrypt, Object result) {
if (result instanceof String) {
((String) result).setValue(
encryptService.decrypt((String) result.getValue())
);
}
}
}
在实现WebSocket消息广播时,采用STOMP协议:
java复制@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins("*")
.withSockJS();
}
}
消息控制器示例:
java复制@Controller
public class NotificationController {
@MessageMapping("/notify")
@SendTo("/topic/notifications")
public Notification sendNotification(NotificationMessage message) {
return new Notification(
message.getContent(),
LocalDateTime.now()
);
}
}
在实现分布式锁时,采用Redisson客户端:
java复制public boolean tryLock(String lockKey, long waitTime, long leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
try {
return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
public void unlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
在实现数据导出为Excel时,采用Apache POI:
java复制public void exportUsersToExcel(HttpServletResponse response) throws IOException {
List<User> users = userService.listAll();
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("Users");
// 创建表头
Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("ID");
headerRow.createCell(1).setCellValue("Username");
// 填充数据
for (int i = 0; i < users.size(); i++) {
Row row = sheet.createRow(i + 1);
row.createCell(0).setCellValue(users.get(i).getId());
row.createCell(1).setCellValue(users.get(i).getUsername());
}
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content