前言
开发是一个不断积累的过程,可以把我们平时重复用得比较多的内容,封装起来。这样是对自己个人的一种沉淀,以至于不会过一段时间,学了新的,却忘了旧的。所以从很早开始我就封装了一套框架来提升自己的开发效率,经过几年不断的优化。
在平时开发过程中,用得最多的还是spring boot + mybatis这一套。但是mybatis用得那是又爱又恨啊,自由度是挺高的,能够完成各种复杂的场景。但是用法上来说,还是比较繁琐的,需要不断的去实现Mapper,去写CURD的sql,甚至是写mybatis的xml配置。有时候想用hibernate,但是效率不够好,而且不够灵活。
我列举了一下,我封装了这些内容:
- mybatis实体操作相关:
- 引入通用mapper,并扩展BaseMapper;
- 审计字段注解、拦截器,创建时间、修改时间、创建人、修改人、版本号。
- Example构造工具,使用Filter和OrFilter去构造简单的查询条件。
- 自动生成po对应的通用AutoMapper
- 数据库表自动生成:自动扫描PO类,根据注解创建数据库表。
- controller相关:
- UserContext抽象类,
@CurrentUser
和@CurrentUserId
注解,绑定到controller方法字段。 - 查询条件构造器注解
@Filter
,自动将查询DTO转换成查询表达式List<Filter>
- vo字段填充,提供
MessageHandler
接口。
- UserContext抽象类,
- service相关:
- 实现
BaseService
和BaseServiceImpl
,封装CURD,分页查询相关方法。 - 参数校验拦截器:对
@NotNull
、@NotEmpty
等注解标注的参数校验,可以自定义扩展。
- 实现
- 任务分片
TaskFragment
,以及默认实现基于redis的分片。 - 点击率记录器
HitsRecorder
- 进度条
Progress
- 工具相关:
- 正则表达式
RegexUtils
- 反射工具
RegexUtils
- RSA加密工具
RSAUtils
- 爬虫工具
Spider
- http请求工具
ServiceClient
- 多线程文件下载器
FastFileDownloader
- 视频处理相关:
FfmpegUtils
、HlsUtils
、VideoCompressor
- 正则表达式
用法
引入
引入依赖包,当前最新版本是1.0.3
<!-- 数据库表自动生成器,不需要可以不引入 -->
<dependency>
<groupId>com.github.668mt.web</groupId>
<artifactId>mt-spring-generator-starter</artifactId>
<version>${mt-spring-web-starter.version}</version>
</dependency>
<!-- 必须引入 -->
<dependency>
<groupId>com.github.668mt.web</groupId>
<artifactId>mt-spring-web-starter</artifactId>
<version>${mt-spring-web-starter.version}</version>
</dependency>
需要配置如下参数:
#通用mapper的配置项
mapper.not-empty=false
mapper.style=camelhump
mapper.enum-as-simple-type=true
#如果需要配置自动创建表
project.generator-enable=true
project.generate-entity-packages=mt.spring.mos.server.entity.po
#需要参数拦截校验的包名,不需要可以不写
project.assert-package-name=mt.spring.mos.server.service
其它的配置和mybatis、jdbc一样。
审计字段
当需要自动注入创建时间、修改时间等审计字段的时候:
/**
* @Author Martin
* @Date 2020/5/16
*/
@Data
public class BaseEntity implements Serializable {
private static final long serialVersionUID = -1294407818709225639L;
@CreatedDate
private Date createdDate;
@CreatedByUserName
private String createdBy;
@UpdatedDate
private Date updatedDate;
@UpdatedByUserName
private String updatedBy;
}
配置用户信息上下文
package mt.spring.mos.server.config;
import lombok.extern.slf4j.Slf4j;
import mt.common.currentUser.UserContext;
import mt.spring.mos.server.entity.po.User;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
/**
* @Author Martin
* @Date 2020/5/23
*/
@Component
@Slf4j
public class MosUserContext implements UserContext<User, Long> {
@Override
public User getCurrentUser() {
try {
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
if (authentication == null) {
return null;
}
Object principal = authentication.getPrincipal();
if (principal instanceof User) {
return (User) principal;
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return null;
}
@Override
public Long getCurrentUserId() {
User currentUser = getCurrentUser();
if (currentUser != null) {
return currentUser.getId();
}
return null;
}
@Override
public String getCurrentUserName() {
User currentUser = getCurrentUser();
if (currentUser != null) {
return currentUser.getUsername();
}
return null;
}
}
单表CURD流程
PO的配置:
/**
* @Author Martin
* @Date 2020/5/20
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Table(name = "mos_dir")
@Index(columns = {"path", "bucket_id"}, type = IndexType.unique)
public class Dir extends BaseEntity {
private static final long serialVersionUID = -5233564826534911410L;
@Id
@KeySql(useGeneratedKeys = true)
private Long id;
@Column(nullable = false)
private String path;
@ForeignKey(tableEntity = Dir.class, casecadeType = ForeignKey.CascadeType.ALL)
private Long parentId;
@ForeignKey(tableEntity = Bucket.class)
@Column(nullable = false)
private Long bucketId;
@Transient
private Dir child;
private Boolean isDelete;
private Date deleteTime;
}
DirService
package mt.spring.mos.server.service;
import lombok.extern.slf4j.Slf4j;
import mt.common.service.BaseServiceImpl;
import mt.common.tkmapper.Filter;
import mt.spring.mos.server.entity.dto.DirUpdateDto;
import mt.spring.mos.server.entity.po.Dir;
import mt.spring.mos.server.entity.po.Resource;
import mt.spring.mos.server.entity.vo.DirDetailInfo;
import mt.utils.common.Assert;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.File;
import java.util.*;
/**
* @Author Martin
* @Date 2020/5/20
*/
@Service
@Slf4j
public class DirService extends BaseServiceImpl<Dir> {
@Autowired
private BucketService bucketService;
@Autowired
private AuditService auditService;
@Autowired
@Lazy
private ResourceService resourceService;
public List<Dir> findAllParentDir(Dir dir) {
List<Dir> dirs = new ArrayList<>();
Long parentId = dir.getParentId();
while (parentId != null) {
Dir parent = findById(parentId);
dirs.add(parent);
parentId = parent.getParentId();
}
return dirs;
}
@Transactional(readOnly = true)
public Dir findOneByPathAndBucketId(String path, Long bucketId, Boolean isDelete) {
if (!path.startsWith("/")) {
path = "/" + path;
}
List<Filter> filters = new ArrayList<>();
filters.add(new Filter("path", Filter.Operator.eq, path));
filters.add(new Filter("bucketId", Filter.Operator.eq, bucketId));
if (isDelete != null) {
filters.add(new Filter("isDelete", Filter.Operator.eq, isDelete));
}
return findOneByFilters(filters);
}
@Transactional(readOnly = true)
public Dir findOneByDirIdAndBucketId(Long dirId, Long bucketId, Boolean isDelete) {
List<Filter> filters = new ArrayList<>();
filters.add(new Filter("id", Filter.Operator.eq, dirId));
filters.add(new Filter("bucketId", Filter.Operator.eq, bucketId));
if (isDelete != null) {
filters.add(new Filter("isDelete", Filter.Operator.eq, isDelete));
}
return findOneByFilters(filters);
}
public String getParentPath(String pathname) {
if (!pathname.startsWith("/")) {
pathname = "/" + pathname;
}
int lastIndexOf = pathname.lastIndexOf("/");
String parentPath = pathname.substring(0, lastIndexOf);
if (StringUtils.isBlank(parentPath)) {
return "/";
}
return parentPath;
}
@Transactional(rollbackFor = Exception.class)
public Dir addDir(String path, Long bucketId) {
bucketService.lockForUpdate(bucketId);
return addDir0(path, bucketId);
}
public Dir addDir0(String path, Long bucketId) {
if (!"/".equals(path) && path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
if (!path.startsWith("/")) {
path = "/" + path;
}
String finalPath = path;
Dir findDir = findOneByPathAndBucketId(finalPath, bucketId, null);
if (findDir != null) {
log.debug("dir[{}]存在", finalPath);
if (findDir.getIsDelete()) {
recover(bucketId, findDir.getId(), false, true);
return findById(findDir.getId());
} else {
return findDir;
}
}
log.debug("dir[{}]不存在,进行创建", finalPath);
Dir parentDir = null;
if (!"/".equalsIgnoreCase(finalPath)) {
String parentPath = getParentPath(finalPath);
parentDir = addDir0(parentPath, bucketId);
}
Dir dir = new Dir();
dir.setPath(finalPath);
dir.setBucketId(bucketId);
if (parentDir != null) {
dir.setParentId(parentDir.getId());
}
log.debug("创建dir:{}", finalPath);
save(dir);
auditService.writeRequestsRecord(bucketId, 1);
return dir;
}
@Override
public Dir findOneByFilters(List<Filter> filters) {
return super.findOneByFilters(filters);
}
@Transactional
public void updatePath(Long bucketId, DirUpdateDto dirUpdateDto) {
String newPath = dirUpdateDto.getPath();
Assert.notBlank(newPath, "路径不能为空");
Assert.state(!newPath.contains(".."), "非法路径:" + newPath);
if (!newPath.startsWith("/")) {
newPath = "/" + newPath;
}
bucketService.lockForUpdate(bucketId);
Dir findDir = findOneByPathAndBucketId(newPath, bucketId, null);
if (findDir != null) {
if (findDir.getIsDelete()) {
deleteById(findDir);
} else {
throw new IllegalStateException("路径" + newPath + "已存在");
}
}
Dir parentDir = addDir(getParentPath(newPath), bucketId);
Dir currentDir = findById(dirUpdateDto.getId());
Assert.state(!"/".equals(currentDir.getPath()), "不能修改根的路径");
auditService.writeRequestsRecord(bucketId, 1);
currentDir.setParentId(parentDir.getId());
currentDir.setPath(newPath);
updateById(currentDir);
updateChildDirPath(currentDir);
}
@Transactional
public void updateChildDirPath(Dir dir) {
List<Dir> children = findList("parentId", dir.getId());
String parentPath = dir.getPath();
if (CollectionUtils.isNotEmpty(children)) {
for (Dir child : children) {
child.setPath(parentPath + child.getName());
updateById(child);
updateChildDirPath(child);
}
}
}
private void updateParentDir(Dir dir, Dir parentDir) {
String path = dir.getPath();
File file = new File(path);
String name = file.getName();
String desPath = parentDir.getPath() + "/" + name;
dir.setParentId(parentDir.getId());
dir.setPath(desPath);
updateById(dir);
List<Dir> children = findList("parentId", dir.getId());
if (CollectionUtils.isNotEmpty(children)) {
for (Dir child : children) {
updateParentDir(child, dir);
}
}
}
/**
* 合并文件夹,同名文件将进行覆盖
*
* @param bucketId 桶
* @param srcId 源路径
* @param desId 目标路径
*/
@Transactional
public void mergeDir(Long bucketId, Long srcId, Long desId) {
Dir srcDir = findOneByDirIdAndBucketId(srcId, bucketId, false);
Dir desDir = findOneByDirIdAndBucketId(desId, bucketId, false);
Assert.notNull(srcDir, "源路径不存在");
Assert.notNull(desDir, "目标路径不存在");
//把srcDir下的子目录移过去
List<Dir> children = findList("parentId", srcDir.getId());
if (CollectionUtils.isNotEmpty(children)) {
for (Dir child : children) {
updateParentDir(child, desDir);
}
}
//把srcDir下的文件移过去
List<Resource> desResources = resourceService.findByFilter(new Filter("dirId", Filter.Operator.eq, desId));
if (CollectionUtils.isNotEmpty(desResources)) {
//判断文件是否重名
for (Resource desResource : desResources) {
List<Filter> filters = new ArrayList<>();
filters.add(new Filter("dirId", Filter.Operator.eq, srcId));
filters.add(new Filter("name", Filter.Operator.eq, desResource.getName()));
Resource findResource = resourceService.findOneByFilters(filters);
if (findResource != null) {
//重名文件存在,删除进行覆盖
resourceService.deleteById(desResource);
}
}
}
resourceService.changeDir(srcId, desId);
//删除原文件夹
deleteById(srcDir);
}
@Transactional
public void deleteDir(Long bucketId, long dirId) {
bucketService.lockForUpdate(bucketId);
Dir dir = findOneByDirIdAndBucketId(dirId, bucketId, false);
if (dir == null) {
return;
}
dir.setIsDelete(true);
dir.setDeleteTime(new Date());
updateById(dir);
List<Resource> resources = resourceService.findResourcesInDir(dirId);
if (CollectionUtils.isNotEmpty(resources)) {
for (Resource resource : resources) {
resourceService.deleteResource(bucketId, resource.getId());
}
}
List<Dir> children = findChildren(dirId);
if (CollectionUtils.isNotEmpty(children)) {
for (Dir child : children) {
deleteDir(bucketId, child.getId());
}
}
}
@Transactional
public void recover(Long bucketId, Long dirId, boolean recoverChildren, boolean recoverParent) {
bucketService.lockForUpdate(bucketId);
Dir dir = findOneByDirIdAndBucketId(dirId, bucketId, true);
if (dir == null) {
return;
}
dir.setIsDelete(false);
dir.setDeleteTime(null);
updateById(dir);
if (recoverChildren) {
List<Resource> resources = resourceService.findResourcesInDir(dirId);
if (CollectionUtils.isNotEmpty(resources)) {
for (Resource resource : resources) {
resourceService.recover(bucketId, resource.getId(), false);
}
}
List<Dir> children = findChildren(dirId);
if (CollectionUtils.isNotEmpty(children)) {
for (Dir child : children) {
recover(bucketId, child.getId(), true, false);
}
}
}
Long parentId = dir.getParentId();
if (recoverParent && parentId != null) {
recover(bucketId, parentId, false, true);
}
}
public List<Dir> findChildren(long dirId) {
return findList("parentId", dirId);
}
@Transactional
public void realDeleteDir(Long bucketId, long dirId) {
bucketService.lockForUpdate(bucketId);
List<Filter> filters = new ArrayList<>();
filters.add(new Filter("id", Filter.Operator.eq, dirId));
filters.add(new Filter("bucketId", Filter.Operator.eq, bucketId));
Dir dir = findOneByFilters(filters);
org.springframework.util.Assert.notNull(dir, "路径不存在");
auditService.writeRequestsRecord(bucketId, 1);
deleteById(dir);
}
@Transactional(rollbackFor = Exception.class)
public boolean realDeleteDir(Long bucketId, String path) {
org.springframework.util.Assert.state(StringUtils.isNotBlank(path), "路径不能为空");
if (!path.startsWith("/")) {
path = "/" + path;
}
List<Filter> filters = new ArrayList<>();
filters.add(new Filter("path", Filter.Operator.eq, path));
filters.add(new Filter("bucketId", Filter.Operator.eq, bucketId));
Dir dir = findOneByFilters(filters);
if (dir != null) {
realDeleteDir(bucketId, dir.getId());
return true;
}
return false;
}
@Transactional
public List<Dir> getRealDeleteDirsBefore(Integer beforeDays) {
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DAY_OF_MONTH, -Math.abs(beforeDays));
List<Filter> filters = new ArrayList<>();
filters.add(new Filter("isDelete", Filter.Operator.eq, true));
filters.add(new Filter("deleteTime", Filter.Operator.le, instance.getTime()));
return findByFilters(filters);
}
public DirDetailInfo findDetailInfo(@NotNull Long id, int thumbCount) {
List<Resource> thumbs = resourceService.findDirThumbs(id, thumbCount);
DirDetailInfo dirDetailInfo = new DirDetailInfo();
dirDetailInfo.setThumbs(thumbs);
dirDetailInfo.setDirCount((long) count(Collections.singletonList(new Filter("parentId", Filter.Operator.eq, id))));
dirDetailInfo.setFileCount((long) resourceService.count(Collections.singletonList(new Filter("dirId", Filter.Operator.eq, id))));
return dirDetailInfo;
}
}
Controller
/**
* @Author Martin
* @Date 2020/12/5
*/
@RestController
@RequestMapping("/member/dir")
public class DirController {
@Autowired
private DirService dirService;
@Autowired
private BucketService bucketService;
@GetMapping("/{bucketName}/detailInfo/{id}")
@ApiOperation("获取文件夹详细信息")
@NeedPerm(perms = BucketPerm.SELECT)
public DirDetailInfo detailInfo(@PathVariable String bucketName, @ApiIgnore Bucket bucket,
@PathVariable Long id,
@RequestParam(defaultValue = "3") Integer thumbCount) {
Assert.state(thumbCount > 0 && thumbCount <= 100, "thumbCount只能是0-10");
return dirService.findDetailInfo(id, thumbCount);
}
@GetMapping("/{bucketName}/select")
@NeedPerm(BucketPerm.SELECT)
@ApiOperation("模糊查找")
public ResResult selectByPath(@RequestParam(required = false, defaultValue = "30") Integer pageSize, @PathVariable String bucketName, @ApiIgnore @CurrentUser User currentUser, String path) {
Bucket bucket = bucketService.findBucketByUserIdAndBucketName(currentUser.getId(), bucketName);
Assert.notNull(bucket, "不存在bucket:" + bucketName);
if (path == null) {
path = "/";
}
if (!path.startsWith("/")) {
path = "/" + path;
}
PageHelper.startPage(1, pageSize, "(length(path) - length(replace(path,'/',''))) asc");
List<Filter> filters = new ArrayList<>();
filters.add(new Filter("bucketId", Filter.Operator.eq, bucket.getId()));
filters.add(new Filter("path", Filter.Operator.like, '%' + path + '%'));
return ResResult.success(dirService.findByFilters(filters));
}
@GetMapping("/{bucketName}/findByPath")
@NeedPerm(BucketPerm.SELECT)
@ApiOperation("精确查找")
public ResResult findByPath(@ApiIgnore @CurrentUser User currentUser, @PathVariable String bucketName, String path) {
Bucket bucket = bucketService.findBucketByUserIdAndBucketName(currentUser.getId(), bucketName);
Assert.notNull(bucket, "不存在bucket:" + bucketName);
Dir dir = dirService.findOneByPathAndBucketId(path, bucket.getId(), false);
return ResResult.success(dir);
}
@PutMapping("/{bucketName}/{id}")
@NeedPerm(BucketPerm.UPDATE)
@ApiOperation("修改")
public ResResult update(@ApiIgnore @CurrentUser User currentUser, @PathVariable String bucketName, @PathVariable Long id, @RequestBody DirUpdateDto dirUpdateDto) {
Bucket bucket = bucketService.findBucketByUserIdAndBucketName(currentUser.getId(), bucketName);
Assert.notNull(bucket, "不存在bucket:" + bucketName);
dirUpdateDto.setBucketName(bucketName);
dirUpdateDto.setId(id);
dirService.updatePath(bucket.getId(), dirUpdateDto);
return ResResult.success();
}
@PutMapping("/{bucketName}/merge/{srcId}/to/{desId}")
@NeedPerm(BucketPerm.UPDATE)
@ApiOperation("合并")
public ResResult merge(@ApiIgnore @CurrentUser User currentUser, @PathVariable String bucketName, @PathVariable Long srcId, @PathVariable Long desId) {
Bucket bucket = bucketService.findBucketByUserIdAndBucketName(currentUser.getId(), bucketName);
Assert.notNull(bucket, "不存在bucket:" + bucketName);
dirService.mergeDir(bucket.getId(), srcId, desId);
return ResResult.success();
}
@PostMapping("/{bucketName}")
@NeedPerm(BucketPerm.INSERT)
@ApiOperation("新增")
public ResResult add(@ApiIgnore @CurrentUser User currentUser, @PathVariable String bucketName, @RequestBody DirAddDto dirAddDto) {
Bucket bucket = bucketService.findBucketByUserIdAndBucketName(currentUser.getId(), bucketName);
Assert.notNull(bucket, "不存在bucket:" + bucketName);
dirService.addDir(dirAddDto.getPath(), bucket.getId());
return ResResult.success();
}
}
查询条件DTO的定义
这个例子跟上面的dir无关。
/**
* @Author Martin
* @Date 2022/10/31
*/
@Data
public class ResourceLogCondition {
@Filter
private Long userId;
@Filter
private ResourceType resourceType;
@Filter(operator = mt.common.tkmapper.Filter.Operator.in)
private List<Long> resourceId;
@Filter
private LogType logType;
}
这个例子跟上面的dir无关。
调用baseService的findPage方法进行分页查询
@GetMapping("/list")
public PageInfo<ResourceLog> list(@ApiIgnore @CurrentUserId Long userId,
Integer pageNum,
Integer pageSize,
ResourceLogCondition condition
) {
condition.setUserId(userId);
return resourceLogService.findPage(pageNum, pageSize, "id desc", condition);
}
Vo字段填充
/**
* @Author Martin
* @Date 2022/10/18
*/
@Data
public class VideoVo implements Serializable {
private static final long serialVersionUID = -61658698405813771L;
private Long id;
private String name;
private String videoLength;
private String pathnamePrefix;
/**
* 缩略图
*/
@Message(params = {"#thumbPicPathname", "#pathnamePrefix"}, handlerClass = StorageUrlMessageHandler.class)
private String thumbPicPathname;
@Message(params = {"#previewVideoPathname", "#pathnamePrefix"}, handlerClass = StorageUrlMessageHandler.class)
private String previewVideoPathname;
/**
* 实际发布日期
*/
private Date publishedDate;
/**
* 上传用户
*/
private Long uploadedUserId;
/**
* 价格
*/
private Long price;
/**
* 分类
*/
private String category;
/**
* 点击量
*/
private Long hits;
/**
* 点赞量
*/
private Long likes;
private Long notLikes;
/**
* 文件大小
*/
private Long fileSize;
/**
* 比特率
*/
private Integer bitRate;
/**
* 是否原创
*/
private Boolean isOriginal;
@Message(params = "#fileSize", handlerClass = ReadableFileSizeMessageHandler.class)
private String readableFileSize;
@Message(params = "#bitRate", handlerClass = IsHighDefinitionMessageHandler.class)
private Boolean isHd;
@BatchMessage(column = "uploadedUserId", handlerClass = UserVoBatchMessageHandler.class)
private UserVo uploadedUser;
@BatchMessage(column = "id", params = "video", handlerClass = IsLikeBatchMessageHandler.class)
private Boolean isLike;
private Long rankScore;
private Set<String> tags;
}
UserVoBatchMessageHandler:
/**
* @Author Martin
* @Date 2022/10/19
*/
@Component
public class UserVoBatchMessageHandler implements BatchMessageHandler<Long, UserVo> {
@Autowired
private UserService userService;
@Override
public Map<Long, UserVo> handle(Collection<?> list, Set<Long> userIds, String[] params) {
Map<Long, UserVo> map = new HashMap<>(16);
if (CollectionUtils.isNotEmpty(userIds)) {
List<User> users = userService.findByFilter(new Filter("id", Filter.Operator.in, userIds));
users.stream().map(user -> BeanUtils.transform(UserVo.class, user)).forEach(userVo -> {
map.put(userVo.getId(), userVo);
});
}
return map;
}
}
ReadableFileSizeMessageHandler:
/**
* @Author Martin
* @Date 2022/10/18
*/
@Component
public class ReadableFileSizeMessageHandler implements MessageHandler<Object, String> {
@Override
public String handle(Object o, Object[] params, String mark) {
Long fileSize = getParam(params, 0, Long.class);
if (fileSize != null) {
return FileSizeUtils.getReadableSize(fileSize, "#");
}
return null;
}
}
总结
还有列举的其它包的用法,就不挨个解释了,详细的用法可以结合实际项目看我的github。
- 框架项目:https://github.com/668mt/mt-spring-web.git
- 对象存储MOS:https://github.com/668mt/mos.git