JavaWeb 笔记 04 (46 - 49)
SpringBootWeb 案例, 分层解耦相关 (46 - 49)
提前声明:本文章仅作个人学习记录,不涉及商业用途。
内容记录于学习途中, 没有按分类进行总结, 可能存在错误或不完整之处。
第一章 SpringBootWeb 案例
1. 常用依赖
启动依赖
Developer Tools -> Lombok
// 自动生成 Getter / Setter 与构造函数
Developer Tools -> Spring Boot DevTools
// 提供代码热重启与浏览器实时重载
Developer Tools -> Spring Configuration Processor
// 为自定义配置属性提供 IDE 自动补全提示
Web -> Spring Web
// 构建 RESTful 及 MVC 网络服务
SQL -> JDBC API
// 连接数据库并执行 SQL 语句
SQL -> MyBatis Framework
// 支持自定义 SQL、存储过程的持久层框架
常用依赖
<!-- JSON 对象转换 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.40</version>
</dependency>
<!-- 阿里云OSS Java SDK V2 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibabacloud-oss-v2</artifactId>
<version>0.3.2</version>
</dependency>
<!-- knife4j-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
<!-- MyBatis-Plus -->
<!-- SpringBoot4 需删除 mybatis-spring-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
<version>3.5.16</version>
</dependency>
<!-- MyBatis-Plus JSqlParser (分页插件) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>3.5.16</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>4.0.0-M2</version>
<scope>compile</scope>
</dependency>
可选依赖
<!-- MySQL JDBC driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- Junit -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.0.3</version>
<scope>test</scope>
</dependency>
<!-- 命令行调试用 -->
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.11.0</version>
<scope>compile</scope>
</dependency>
<!-- PageHelper -->
<!-- 已被 MyBatis-Plus JSqlParser 代替 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
plugin
<!-- 显式声明 mockito-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>
-javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar
</argLine>
</configuration>
</plugin>
<plugin>
3. 配置文件 application.yaml
spring:
application:
name: tlias-dev
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/tlias
username: fallw
password: real-password
servlet:
multipart:
# 单个文件的最大限制(默认 1MB)
max-file-size: 10MB
# 单个请求的总大小限制(包含表单其他字段,默认 10MB)
max-request-size: 50MB
aliyun:
oss:
endpoint: https://oss-cn-beijing.aliyuncs.com
bucketName: real-bucket-name
region: cn-beijing
jwt:
secretKey: SVRIRUlNQQ==DefaultFallbackKeyForDevOnly
expire: 43200000
mybatis-plus:
mapper-locations: classpath:com/fallw/mapper/*.xml
type-aliases-package: com.fallw.entity
configuration:
auto-mapping-behavior: full
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 输出 SQL 到控制台
map-underscore-to-camel-case: true
global-config:
db-config:
logic-delete-field: isDeleted # 全局逻辑删除字段名
logic-delete-value: 1 # 逻辑已删除值
logic-not-delete-value: 0 # 逻辑未删除值
# 日志配置
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
可选配置
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
mapper-locations: classpath*:mapper/*.xml
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
2. 入门案例
静态资源存放位置:src/main/resources/static
@ResponseBody 注解的作用:
- 将 Controller 方法的返回值直接写入 HTTP 响应体
- 如果返回值是对象或集合,SpringMVC 会自动将数据转换为 JSON 再响应
@RestController=@Controller+@ResponseBody
// RestController 将对象/集合 -> JSON -> 响应体
@RestController
public class UserController {
@RequestMapping("/list")
public List<User> list() throws Exception {
// 1. 数据访问:获取用户数据
InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");
ArrayList<String> lines = IoUtils.readLines(in, StandardCharsets.UTF_8, new ArrayList<>());
// 2. 逻辑处理:解析数据、封装成对象
List<User> userList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String username = parts[1];
String password = parts[2];
String name = parts[3];
Integer age = Integer.parseInt(parts[4]);
LocalDateTime updateTime = LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new User(id, username, password, name, age, updateTime);
}).collect(Collectors.toList());
// 3. 接收请求、响应数据
return userList;
}
}
第二章 分层解耦
一、三层架构
1. 三层架构
- 控制层 (Controller): 接收前端发送的请求,对请求进行处理,并响应数据。
- 业务逻辑层 (Service): 处理具体的业务逻辑。
- 数据访问层 (Data Acess Object): 持久层,负责数据库访问操作。
2. 分层解耦
- 耦合:衡量软件中各个层 / 模块之间的依赖关联程度。
- 内聚:软件中各个模块内部的功能关系。
- 软件设计原则:高内聚低耦合。
3. MyBatis-Plus:
- Service 类继承
IService<T>,ServiceImpl 类继承ServiceImpl<M, T>。 - Service 层同模块下使用
LambdaQueryWrapper查询同模块表。注入 Service 实现跨表调用。 - Mapper 类继承
BaseMapper<T>。 - Mapper 层只写复杂的查询逻辑。增删改永远优先使用 Service。
二、IOC & DI
IOC (Inversion of Control): 控制反转,对象的控制权由程序自身转移到外部(容器)。
DI (Dependency Injection): 依赖注入,容器为应用程序提供运行时,所依赖的资源。
Bean 对象:IOC 容器中创建、管理的对象。
1. Bean 对象定义 Bean Definition (IOC)
| 注解 | 说明 | 位置 |
|---|---|---|
@Component |
声明 bean 的基础注解 | 不属于以下三类时,用此注解 使用 @Component(value = "userMapper") 指定 bean 的名称 |
@Controller |
@Component 的衍生注解 |
标注在控制层类上 声明控制器只能用 @Controlller |
@RestController |
控制层常用注解 | @Controller + @ResponseBody |
@Service |
@Component 的衍生注解 |
标注在业务层类上 |
@Repository |
@Component 的衍生注解 |
标注在数据访问层类上 由于与 Mybatis 整合,用的少 |
@Mapper |
MyBatis 提供的持久层注解 | 标注在 Mapper 接口上 由 MyBatis 框架提供,非 Spring 原生 |
@ComponentScan |
组件扫描注解 | @SpringBootApplication 默认扫描范围是启动类所在包 (com.fallw) 及子包 |
2. 依赖注入 Dependency Injection (DI)
推荐使用 @RequiredArgsConstructor
// @RequiredArgsConstructor: 自动为所有标注为 final 的字段生成构造函数
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
...
}
属性注入 @Autowired
@Service
public class UserServiceImpl implements UserService {
// @Autowied: Spring 自动从容器中查找并注入
@Autowired
private UserMapper userMapper;
...
}
构造器注入
@Service
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
// Spring 4.x 后,如果只有一个构造函数,@Autowired 可省略
@Autowired
public UserServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
...
}
setter 注入
@Service
public class UserServiceImpl implements UserService {
private UserMapper userMapper;
@Autowired
public void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}
...
}
3. 重复注入处理
@Autowired 默认按类型注入,找不到再按名称注入。
@Primary
// @Primary 指定优先注入对象
@Primary
@Service
public class UserServiceImpl implements UserService {
...
}
@Autowird + @Qualifier
@Service
public class UserServiceImpl implements UserService {
// @Qualifier 指定注入对象名称
@Qualifier("userMapper")
@Autowired
private UserMapper userMapper;
...
}
@Resource
@Service
public class UserServiceImpl implements UserService {
// @Resource 指定注入对象名称
@Resource(name = "userMapper")
private UserMapper userMapper;
...
}
@Autowired |
@Resource |
|---|---|
| Spring 框架提供 | Java EE 规范提供 |
| 默认按类型注入 | 默认按名称注入 |
第三章 Tlias 班级管理模块
一、实体类 Entity / VO / DTO
1. 班级类 Course
// com.fallw.entity.Course
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("course")
public class Course {
@TableId(type = IdType.AUTO)
private Integer id; //ID
private String name; //班级名称
private String room; //班级教室
private LocalDate beginDate; //开课时间
private LocalDate endDate; //结课时间
private Integer masterId; //班主任
private Integer subject; //学科
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime; //创建时间
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime; //修改时间
@TableLogic
private Integer isDeleted;
@TableField(exist = false)
private String masterName; //班主任姓名
@TableField(exist = false)
private String status; //班级状态 - 未开班 , 在读 , 已结课
}
2. 前端传递的查询参数 CourseQueryParam
// com.fallw.pojo.dto.CourseQueryParam
@Data
public class CourseQueryParam {
private Integer page = 1;
private Integer pageSize = 10;
private String name;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate begin;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate end;
}
3. 分页查询结果 PageResult
// com.fallw.pojo.vo.PageResult
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResult<T> {
private Long total; //总记录数
private List<T> rows; //当前页数据列表
}
4. 响应类 Result
// com.fallw.pojo.vo.Result
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Integer code;
private String msg;
private Object data;
public static Result success() {
return new Result(1, "success", null);
}
public static Result success(Object object) {
return new Result(1, "success", object);
}
public static Result error(String msg) {
return new Result(0, msg, null);
}
}
二、Controller 层
// com.fallw.controller.CourseController
@Slf4j
@RequestMapping("/clazzs")
@RestController
@RequiredArgsConstructor
@Tag(name = "班级管理")
public class CourseController {
private final CourseService courseService;
@Operation(summary = "班级列表查询")
@GetMapping
public Result pageQuery(CourseQueryParam courseQueryParam) {
log.info("查询请求参数: {}", courseQueryParam);
PageResult<Course> pageResult = courseService.pageQuery(courseQueryParam);
return Result.success(pageResult);
}
@Operation(summary = "删除班级信息")
@DeleteMapping("/{id}")
public Result deleteById(@PathVariable Integer id) {
log.info("删除班级信息:{}", id);
courseService.deleteById(id);
return Result.success();
}
@Operation(summary = "添加班级")
@PostMapping
public Result insertCourse(@RequestBody Course course) {
log.info("添加班级: {}", course);
courseService.insertCourse(course);
return Result.success();
}
@Operation(summary = "根据ID查询")
@GetMapping("/{id}")
public Result getCourseById(@PathVariable Integer id) {
return Result.success(courseService.getCourseById(id));
}
@Operation(summary = "修改班级")
@PutMapping
public Result updateCourse(@RequestBody Course course) {
log.info("修改班级: {}", course);
courseService.updateCourse(course);
return Result.success();
}
@Operation(summary = "查询所有班级")
@GetMapping("/list")
public Result queryList() {
return Result.success(courseService.queryList());
}
}
三、Service 层
1. 接口 CourseService
// com.fallw.service.CourseService
public interface CourseService {
PageResult<Course> pageQuery(CourseQueryParam courseQueryParam);
void deleteById(Integer id);
void insertCourse(Course course);
Course getCourseById(Integer id);
void updateCourse(Course course);
List<Course> queryList();
}
2. 实现类 CourseServiceImpl
// com.fallw.service.impl.CourseServiceImpl
@Slf4j
@Service
@RequiredArgsConstructor
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> implements CourseService {
final private StudentService studentService;
@Transactional(rollbackFor = Exception.class)
@Override
public PageResult<Course> pageQuery(CourseQueryParam courseQueryParam) {
Page<Course> page = new Page<>(courseQueryParam.getPage(), courseQueryParam.getPageSize());
baseMapper.pageQuery(page, courseQueryParam);
return new PageResult<>(page.getTotal(), page.getRecords());
}
@Override
public void deleteById(Integer courseId) {
Long count = studentService.lambdaQuery().eq(Student::getCourseId, courseId).count();
if (count > 0) {
throw new BusinessException("对不起, 该班级下有学生, 不能直接删除");
}
baseMapper.deleteById(courseId);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void insertCourse(Course course) {
baseMapper.insert(course);
}
@Override
public Course getCourseById(Integer id) {
return baseMapper.selectById(id);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void updateCourse(Course course) {
baseMapper.updateById(course);
}
@Override
public List<Course> queryList() {
Page<Course> page = new Page<>();
baseMapper.pageQuery(page, new CourseQueryParam());
return page.getRecords();
}
}
三、Mapper 层
1. 接口 CourseMapper
// com.fallw.mapper.CourseMapper
@Mapper
public interface CourseMapper extends BaseMapper<Course> {
IPage<Course> pageQuery(Page<Course> page, @Param("courseQueryParam") CourseQueryParam courseQueryParam);
}
2. XML 文件 CourseMapper.xml
<!-- resources/com/fallw/mapper/CourseMapper.xml -->
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fallw.mapper.CourseMapper">
<select id="pageQuery" resultType="com.fallw.entity.Course">
select c.*, e.name as masterName,
case
when curdate() < c.begin_date then '未开班'
when curdate() > c.end_date then '已结课'
else '在读'
end as status
from course as c
left join employee as e on c.master_id = e.id
<where>
<if test="courseQueryParam.name != null and courseQueryParam.name != ''">
and c.name like concat('%',#{courseQueryParam.name},'%')
</if>
<if test="courseQueryParam.begin != null and courseQueryParam.end != null">
and c.begin_date between #{courseQueryParam.begin} and #{courseQueryParam.end}
</if>
and c.is_deleted = 0
</where>
order by c.update_time desc
</select>
</mapper>
四、配置类 Config
1. 配置 MP 自动填充
// com.fallw.config.MyMetaObjectHandler
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
2. 配置 MP 分页插件
// com.fallw.config.MybatisPlusConfig
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
五、异常类 Exception
1. 业务异常类 BusinessException
// com.fallw.exception.BusinessException
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
2. 全局异常捕获 GlobalExceptionHandler
// com.fallw.exception.GlobalExceptionHandler
@Slf4j
@RestControllerAdvice
@Hidden
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
log.error("业务校验失败: {}", e.getMessage());
return Result.error(e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
log.error("系统异常: ", e);
return Result.error("对不起,操作失败,请联系管理员");
}
}
第四章 工具类存档
0. 启动类注解
@SpringBootApplication
@MapperScan("com.fallw.mapper")
@ServletComponentScan
public class TliasDevApplication {
public static void main(String[] args) {
SpringApplication.run(TliasDevApplication.class, args);
}
}
一、阿里云 OSS 上传图片
1. pom.xml
<!-- 阿里云OSS Java SDK V2 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibabacloud-oss-v2</artifactId>
<version>0.3.2</version>
</dependency>
2. application.yaml
aliyun:
oss:
endpoint: https://oss-cn-beijing.aliyuncs.com
bucketName: real-bucket-name
region: cn-beijing
3. 控制层 UploadController
// com.fallw.controller.UploadController
@Slf4j
@RestController
@Tag(name = "文件上传")
@RequiredArgsConstructor
public class UploadController {
final private AliyunOSSOperator aliyunOSSOperator;
@Operation(summary = "上传图片接口")
@PostMapping("/upload")
public Result upload(MultipartFile file) throws Exception {
log.info("上传文件:{}", file);
if (!file.isEmpty()) {
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String extName = originalFilename.substring(originalFilename.lastIndexOf("."));
String uniqueFileName = UUID.randomUUID().toString().replace("-", "") + extName;
// 上传文件
String url = aliyunOSSOperator.upload(file.getBytes(), uniqueFileName);
return Result.success(url);
}
return Result.error("上传失败");
}
}
4. 配置类 AliyunOSSProperties
// com.fallw.config.AliyunOSSProperties
@Component
@Data
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOSSProperties {
private String endpoint;
private String bucketName;
private String region;
}
5. 工具类 AliyunOSSOperator
// com.fallw.utils.AliyunOSSOperator
@Slf4j
@Component
@RequiredArgsConstructor
public class AliyunOSSOperator implements InitializingBean, AutoCloseable {
private static final String TEMP_DIR = "temp";
private static final String AVATAR_DIR = "employee/avatar";
private static final String DATE_PATTERN = "yyyy/MM";
private final AliyunOSSProperties properties;
private OSSClient ossClient; // 改为长连接成员变量
@Override
public void afterPropertiesSet() {
// 初始化客户端(避免每次调用都重新创建)
CredentialsProvider provider = new EnvironmentVariableCredentialsProvider();
this.ossClient = OSSClient.newBuilder()
.credentialsProvider(provider)
.region(properties.getRegion())
.endpoint(properties.getEndpoint())
.build();
}
public String upload(byte[] content, String originalFilename) {
String dir = String.format("%s/%s", TEMP_DIR, LocalDate.now().format(DateTimeFormatter.ofPattern(DATE_PATTERN)));
String objectKey = generateObjectKey(dir, originalFilename);
PutObjectRequest request = PutObjectRequest.newBuilder()
.bucket(properties.getBucketName())
.key(objectKey)
.body(BinaryData.fromBytes(content))
.build();
ossClient.putObject(request);
return buildFullUrl(objectKey);
}
public String moveToAvatar(String tempUrl) {
// 复制
String avatarUrl = copy(tempUrl, AVATAR_DIR);
// 尝试删除临时文件(失败仅记录,不阻塞业务逻辑)
try {
delete(tempUrl);
} catch (Exception e) {
log.warn("OSS临时文件清理失败, path: {}, 错误: {}", tempUrl, e.getMessage());
}
return avatarUrl;
}
public String copy(String sourceUrl, String targetDir) {
String bucketName = properties.getBucketName();
String sourceKey = extractObjectKey(sourceUrl);
// 提取原文件名并生成新Key
String fileName = sourceKey.substring(sourceKey.lastIndexOf("/") + 1);
String targetPath = String.format("%s/%s", targetDir, LocalDate.now().format(DateTimeFormatter.ofPattern(DATE_PATTERN)));
String targetKey = generateObjectKey(targetPath, fileName);
CopyObjectRequest copyRequest = CopyObjectRequest.newBuilder()
.bucket(bucketName)
.key(targetKey)
.sourceBucket(bucketName)
.sourceKey(sourceKey)
.build();
ossClient.copyObject(copyRequest);
return buildFullUrl(targetKey);
}
public void delete(String url) {
if (url == null || url.isBlank()) return;
try {
String objectKey = extractObjectKey(url);
DeleteObjectRequest deleteRequest = DeleteObjectRequest.newBuilder()
.bucket(properties.getBucketName())
.key(objectKey)
.build();
ossClient.deleteObject(deleteRequest);
log.info("OSS文件删除成功: {}", objectKey);
} catch (Exception e) {
// 关键:记录警告但不抛出异常,防止干扰主业务事务
log.warn("OSS文件删除失败, URL: {}, 原因: {}", url, e.getMessage());
}
}
public void deleteBatch(List<String> urls) {
if (CollectionUtils.isEmpty(urls)) return;
// 1. 提取所有有效 Key
List<ObjectIdentifier> identifiers = urls.stream()
.filter(url -> url != null && !url.isBlank())
.map(url -> {
try {
return ObjectIdentifier.newBuilder()
.key(extractObjectKey(url))
.build();
} catch (Exception e) {
log.warn("批量删除中跳过无效URL: {}", url);
return null;
}
})
.filter(Objects::nonNull)
.toList();
if (identifiers.isEmpty()) return;
try {
// 2. 构建批量删除请求
Delete deleteBody = Delete.newBuilder()
.objects(identifiers)
.quiet(true)
.build();
DeleteMultipleObjectsRequest batchRequest = DeleteMultipleObjectsRequest.newBuilder()
.bucket(properties.getBucketName())
.delete(deleteBody)
.build();
// 3. 执行删除
ossClient.deleteMultipleObjects(batchRequest);
log.info("成功发起批量删除请求,文件数量: {}", identifiers.size());
} catch (Exception e) {
log.error("OSS批量删除异常", e);
}
}
// --- 辅助私有方法 ---
private String generateObjectKey(String dir, String originalFilename) {
String ext = originalFilename.contains(".") ? originalFilename.substring(originalFilename.lastIndexOf(".")) : "";
return String.format("%s/%s%s", dir, UUID.randomUUID(), ext);
}
private String buildFullUrl(String objectKey) {
String cleanEndpoint = properties.getEndpoint().replace("https://", "").replace("http://", "");
return String.format("https://%s.%s/%s", properties.getBucketName(), cleanEndpoint, objectKey);
}
private String extractObjectKey(String url) {
// 兼容处理:寻找域名后的第一个斜杠
try {
java.net.URL parsedUrl = new java.net.URL(url);
String path = parsedUrl.getPath();
return path.startsWith("/") ? path.substring(1) : path;
} catch (Exception e) {
throw new IllegalArgumentException("无法从URL提取ObjectKey: " + url);
}
}
@Override
public void close() throws Exception {
if (ossClient != null) {
ossClient.close();
}
}
}
6. 生命周期
数据管理 \(\rightarrow\) 生命周期 \(\rightarrow\) 创建规则
| 选项 | 描述 |
|---|---|
| 状态 | 启动 |
| 策略 | 按前缀匹配 |
| 是否允许重叠前缀 | 是 |
| 前缀 | temp/ |
| 文件时间策略 | 指定天数 |
| 生命周期管理规则 | 最后一次 修改时间 7 天后,数据自动转换成 数据删除 |
| 碎片过期策略 | 碎片过期 |
| 碎片过期规则 | 碎片生成时间早于 7 天,碎片数据自动删除 |
二、JWT 登录认证
1. pom.xml
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
2. application.yaml
jwt:
secretKey: SVRIRUlNQQ==DefaultFallbackKeyForDevOnly
expire: 43200000
3. JwtProperties
// com.fallw.config.JwtProperties
@Component
@Data
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
private String secretKey;
private Long expire;
}
4. JwtUtils
// com.fallw.utils.JwtUtils
@Component
@RequiredArgsConstructor
public class JwtUtils {
final private JwtProperties jwtProperties;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8));
}
public String generateJwt(Map<String, Object> claims) {
return Jwts.builder()
.claims(claims)
.expiration(new Date(System.currentTimeMillis() + jwtProperties.getExpire()))
.signWith(getSigningKey())
.compact();
}
public Claims parseJWT(String jwt) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(jwt)
.getPayload();
}
}
5. WebConfig
// com.fallw.config.WebMvcConfigurer
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
//拦截器对象
final private TokenInterceptor tokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/**") // 拦截所有
.excludePathPatterns(
"/login", // 登录
"/register", // 注册
"/doc.html", // Knife4j 主页
"/webjars/**", // Knife4j 静态资源 (JS/CSS)
"/swagger-resources/**", // Swagger 资源配置
"/v3/api-docs/**", // OpenAPI 3.0 接口定义数据
"/favicon.ico" // 网站图标(防止控制台报错)
);
}
}
6. TokenInterceptor
// com.fallw.interceptor.TokenInterceptor
@Slf4j
@Component
@RequiredArgsConstructor
public class TokenInterceptor implements HandlerInterceptor {
private final JwtUtils jwtUtils;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取令牌 (通常建议从 Authorization 头获取,或者保持你现在的 "token")
String jwt = request.getHeader("token");
// 2. 校验令牌是否存在
if (!StringUtils.hasLength(jwt)) {
log.info("令牌为空,拒绝访问: {}", request.getRequestURI());
return handleUnauthorized(response, "未登录,请先登录");
}
// 3. 解析令牌
try {
jwtUtils.parseJWT(jwt);
log.info("令牌合法,放行: {}", request.getRequestURI());
return true;
} catch (Exception e) {
// 这里可以细化捕获 ExpiredJwtException 等
log.error("令牌解析失败: {}", e.getMessage());
return handleUnauthorized(response, "登录已过期或令牌无效");
}
}
/**
* 辅助方法:返回统一的 JSON 错误结果
*/
private boolean handleUnauthorized(HttpServletResponse response, String msg) throws Exception {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=utf-8");
// 这里手动构造 Result.error(...) 的 JSON 字符串返回
response.getWriter().write("{\"code\":0, \"msg\":\"" + msg + "\", \"data\":null}");
return false;
}
}
7. TokenFilter
// com.fallw.filter.TokenFilter
@Slf4j
@WebFilter(urlPatterns = "/*")
@RequiredArgsConstructor
public class TokenFilter implements Filter {
private final JwtUtils jwtUtils;
private static final String[] WHITE_LIST = {
"/doc.html",
"/webjars/",
"/swagger-resources",
"/v3/api-docs",
"/favicon.ico"
};
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 获取请求的url地址
String uri = request.getRequestURI(); // /employee/login
//String url = request.getRequestURL().toString(); // http://localhost:8080/employee/login
// 检查当前路径是否在白名单中
for (String path : WHITE_LIST) {
if (uri.contains(path)) {
filterChain.doFilter(request, response);
return;
}
}
// 判断是否是登录请求, 如果url地址中包含 login, 则说明是登录请求, 放行
if (uri.contains("login")) {
log.info("登录请求, 放行");
filterChain.doFilter(request, response);
return;
}
// 获取请求中的token
String token = request.getHeader("token");
// 判断token是否为空, 如果为空, 响应401状态码
if (token == null || token.isEmpty()) {
log.info("token为空, 响应401状态码");
response.setStatus(401); // 响应401状态码
return;
}
// 如果token不为空, 调用JWtUtils工具类的方法解析token, 如果解析失败, 响应401状态码
try {
Claims claims = jwtUtils.parseJWT(token);
Integer empId = Integer.valueOf(claims.get("id").toString());
CurrentHolder.setCurrentId(empId);
log.info("token解析成功, 放行");
} catch (Exception e) {
log.info("token解析失败, 响应401状态码");
response.setStatus(401);
return;
}
// 放行
filterChain.doFilter(request, response);
// 清空当前线程绑定的id
CurrentHolder.remove();
}
}
三、AOP 操作日志
1. pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>4.0.0-M2</version>
<scope>compile</scope>
</dependency>
2. OperateLog
// com.fallw.entity.OperateLog
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("operate_log")
public class OperateLog {
@TableId(type = IdType.AUTO)
private Integer id; //ID
private Integer operateEmpId; //操作人ID
private LocalDateTime operateTime; //操作时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时
// @TableField(exist = false)
// private String operateEmpName;
}
3. OperateLogMapper
// com.fallw.mapper.OperateLogMapper
@Mapper
public interface OperateLogMapper extends BaseMapper<OperateLog> {
// IPage<OperateLog> pageQuery(Page<OperateLog> page);
}
4. LogOperation
// com.fallw.annotation.LogOperation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation { }
5. OperationLogAspect
// com.fallw.aspect.OperationLogAspect
@Aspect
@Component
@RequiredArgsConstructor
public class OperationLogAspect {
final private OperateLogMapper operateLogMapper;
// 移除参数中的 LogOperation log,让 execution 独立生效
@Around("execution(* com.fallw.controller..*.insert*(..)) || " +
"execution(* com.fallw.controller..*.delete*(..)) || " +
"execution(* com.fallw.controller..*.update*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
// 执行原始方法
Object result = joinPoint.proceed();
long costTime = System.currentTimeMillis() - startTime;
// 构建日志对象
OperateLog operateLog = new OperateLog();
operateLog.setOperateEmpId(CurrentHolder.getCurrentId());
operateLog.setOperateTime(LocalDateTime.now());
operateLog.setClassName(joinPoint.getTarget().getClass().getName());
operateLog.setMethodName(joinPoint.getSignature().getName());
operateLog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
// 安全处理:防止 void 方法导致 result 为空
operateLog.setReturnValue(result != null ? result.toString() : "void");
operateLog.setCostTime(costTime);
operateLogMapper.insert(operateLog);
return result;
}
// 示例方法,获取当前用户ID
private int getCurrentUserId() {
return CurrentHolder.getCurrentId();
}
}
6. CurrentHolder
// com.fallw.utils.CurrentHolder
public class CurrentHolder {
private static final ThreadLocal<Integer> CURRENT_LOCAL = new ThreadLocal<>();
public static void setCurrentId(Integer employeeId) {
CURRENT_LOCAL.set(employeeId);
}
public static Integer getCurrentId() {
return CURRENT_LOCAL.get();
}
public static void remove() {
CURRENT_LOCAL.remove();
}
}
四、日志配置文件
1. logback.xml 存档
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %logger{50}: 最长50个字符(超出.切割) %msg:日志消息,%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件输出的文件名, %i表示序号 -->
<FileNamePattern>D:/Project_log/tlias-%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<!-- 最多保留的历史日志文件数量 -->
<MaxHistory>30</MaxHistory>
<!-- 最大文件大小,超过这个大小会触发滚动到新文件,默认为 10MB -->
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d 表示日期,%thread 表示线程名,%-5level表示级别从左显示5个字符宽度,%msg表示日志消息,%n表示换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n</pattern>
</encoder>
</appender>
<!-- 日志输出级别 -->
<root level="INFO">
<!--输出到控制台-->
<appender-ref ref="STDOUT"/>
<!--输出到文件-->
<appender-ref ref="FILE"/>
</root>
</configuration>
2. Knif4j 无法访问
WebMvcConfig中增加排除路径
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/**") // 拦截所有
.excludePathPatterns(
"/login", // 登录
"/register", // 注册
"/doc.html", // Knife4j 主页
"/webjars/**", // Knife4j 静态资源 (JS/CSS)
"/swagger-resources/**", // Swagger 资源配置
"/v3/api-docs/**", // OpenAPI 3.0 接口定义数据
"/favicon.ico" // 网站图标(防止控制台报错)
);
}
TokenFilter增加白名单
private static final String[] WHITE_LIST = {
"/doc.html",
"/webjars/",
"/swagger-resources",
"/v3/api-docs",
"/favicon.ico"
};
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String uri = request.getRequestURI();
// 检查当前路径是否在白名单中
for (String path : WHITE_LIST) {
if (uri.contains(path)) {
filterChain.doFilter(request, response);
return;
}
}
...
}
- 在 Knif4j 中调用登录接口,复制返回的
token填入 文档管理 \(\rightarrow\) 全局参数设定。
浙公网安备 33010602011771号