刮开看看:此篇文章旨总结在各种项目中学习到的点,无论是在工作中还是项目开发学习中,暂时不细分,等内容多了再拆分
Git常用命令
这个就应该被置顶,在你看到这个的时候你可能会想,Git不是直接使用IDEA提供图形化界面就可以了吗?我想说是的,但是在我提交修复前端Bug的时候,VsCode我就没习惯他的插件提交:
故 笔者列出来了常用的命令创建新分支
更新本地远程分支情况
更新本地远程分支情况
查看目前的所有分支
切换分支
拉取远程代码更新
如果要使其推送变得干净(减少merge信息,让其变成一条线)
1
| git pull origin 分支名 rebase
|
设置git pull默认使用rebase
1
| git config pull.rebase true
|
合并本地分支到当前本地分支(master指从master合并到当前分支)
1
| git merge master // rebase可切换merge
|
或者直接指定两个分支(不常用)
1
| git merge master feature
|
下面两个指令配套使用(删除远程文件夹)
# -n:加上这个参数,执行命令时不会从git跟踪里删除任何文件,而是展示此命令要删除的文件列表预览。(只用于展示,给你看的)
1
| git rm -r -n --cached your_dir
|
# 最终执行命令,取消跟踪,但不会真的删除本地文件
1
| git rm -r --cached your_dir
|
# 上面的命令不支持删除.idea,建议本地忽略文件先去掉然后本地删除,然后推上去再本地加上忽略文件推送
git暂缓操作
當你在開發某一功能時,突然被PM打斷,要求你現在要先去修正一個急單,這時候你可以有兩個選擇:
直接下commit,然後開始改急單
使用 git stash,一樣開始改急單
不管 commit 跟 git stash 都是不錯的選擇,不過還是有一些不同: 使用 commit 的話,會有紀錄,git stash 不會,如果今天你剛好開發到一段落,那下個 commit 可能無傷大雅,不過如果開發到一半要下 commit ,說實話筆者個人是會覺得不恰當。
1 2
| git stash git stash save "保存名标签 # 操作2
|
两个效果是一模一样的
1 2 3 4 5 6
| git stash list git stash show git stash apply git stash pop git stash clear git stash drop
|
git stash apply vs git stash pop
以上這兩個語法,都可以讓你從 stash list 裡面取出最新一次 stash
的資料,不過兩者有一個很大的不同,就是資料拿出來以後,是否會刪除儲存記錄
結論: git stash apply 不會刪除紀錄,git stash pop 會刪除紀錄
可以配合 IDEA 使用。
如果是在 idea 中,我们可以勾选
- Rebase the current branch, on top of incoming changes
然后选择不在提示
Git 冲突(t->master)
我们和 master 是没有权限的,此时合并主分支发生冲突
正确做法:从主分支拉一个新的分支,然后把我们冲突的 t 分支代码合并到新开的分支,然后再从此分支发起合并至主分支。
常用操作
如果你需要开发新需求,此时应该是从 master 分支拉取正式的代码(可能 test 分支代码有还未测试完的代码),然后拉出来之后,你需要在你刚刚从主分支拉去出来的分支上进行操作。
- 如何合并代码上去?以及提测?
开发完之后,你从你的分支推送到远程,然后切换到 test 分支,合并本地你的分支,然后推上去即可(test 分支先 pull 再 push),然后再让测试在 test 分支测试你的代码即可。
- 测试完如何从你的分支合并到主分支?
这个直接通过可视化界面(比如 gitlab 的 web 端或者其他 git 托管 web),我们在自己的分支,选择合并请求,source 分支为我们自己的,target 为目标分支即我们需要合并的分支,然后先代码 review,再发起合并即可。Arthas
查看当前用户的 Git 提交字符
1
| find . -name "*.jsonSchema" -or -name "*.java" | xargs grep -v "^$" | wc -l
|
目前在公司 git 仓库(截止到 20240808):71381
查看操作行数(username 修改为自己的)
1
| git log --author="username" --pretty=tformat: --numstat | awk '{ add += $1; subs += $2; loc += $1 - $2 } END { printf "added lines: %s, removed lines: %s, total lines: %s\n", add, subs, loc }'
|
Arthas
Arthas 官网
神级调试工具,笔者目前还在了解中,笔记待更新
简介
Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。
背景
通常,本地开发环境无法访问生产环境。如果在生产环境中遇到问题,则无法使用 IDE 远程调试。更糟糕的是,在生产环境中调试是不可接受的,因为它会暂停所有线程,导致服务暂停。
开发人员可以尝试在测试环境或者预发环境中复现生产环境中的问题。但是,某些问题无法在不同的环境中轻松复现,甚至在重新启动后就消失了。
如果您正在考虑在代码中添加一些日志以帮助解决问题,您将必须经历以下阶段:测试、预发,然后生产。这种方法效率低下,更糟糕的是,该问题可能无法解决,因为一旦 JVM 重新启动,它可能无法复现,如上文所述。
Arthas 旨在解决这些问题。开发人员可以在线解决生产问题。无需 JVM 重启,无需代码更改。 Arthas 作为观察者永远不会暂停正在运行的线程。
Arthas(阿尔萨斯)能为你做什么?
Arthas 是 Alibaba 开源的 Java 诊断工具,深受开发者喜爱。
当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:
这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
是否有一个全局视角来查看系统的运行状况?
有什么办法可以监控到 JVM 的实时运行状态?
怎么快速定位应用的热点,生成火焰图?
怎样直接从 JVM 内查找某个类的实例?
IDEA 插件辅助
IDEA 下载插件先 Arthas Idea
比如我们现在有一个代码为
1 2 3 4
| @Override public boolean addOrUpdate(TagModel tagModel) { }
|
我需要对其进行监控调用
我们可以右键有Arthas Command,然后可以看到有 watch、trace 等命令
点击之后就会在剪切板上有一个快捷命令
然后在运行的 Arthas 输入即可
使用
把 arthas 拉取下来,然后启动
1
| java -jar arthas-boot.jar
|
然后就会让我们选择诊断的 Java 进程,选择对应进程,例如 [1] my.jar,我们输入 1 就可以了。
使用命令
- dashboard 总体jvm面板 线程 jvm内存 环境变量
- thead -n 高cpu线程堆栈 thead -b死锁
- jad 反编译类 用于看看自己的代码提交没
- getstatic 用于得到静态属性的值
- ognl 直接修改线上属性的值 救急
以下配合 idea 插件
- watch 查看一个方法的入参出参
- trance 查看一个方法的栈调用耗时
- stack 查看一个方法的栈调用 主要用于查看方法哪里调用
springboot项目
- tt 监控接口入参出参耗时 并且回放接口调用
游标翻页
游标翻页应对复杂变换的列表
深翻页问题
我们在一般的后端开发场景中,比如管理系统,常常都会有分页条。她可以指定一页的条数以及快捷的调整页码。
现在我们假如前端想查看第11页的内容,传的值为 pageNo=11,pageSize=10
那么对于数据库的查询语句就是:
1
| select * from table limit 100,10
|
其中100
代表需要跳过的条数,10
代表跳过指定条数后,往后需要再取的条数。
对应就是这样的一个效果,需要在数据库的位置先读出100条,然后丢弃。丢弃完100条后,再继续取10条选用。
那么假如我们需要查询到100000条后的10条数据, 那么前面的是不是都被没用的丢弃了?
我们经常需要定时任务全量去跑一张表的数据,普通翻页去跑的话,到后面数据量大的时候,就会越跑越慢,这就是深翻页带来的问题。
游标翻页解决深翻页问题
游标翻页可以完美的解决深翻页问题,依赖的就是我们的游标,即cursor
。针对mysql的游标翻页,我们需要通过cursor
快速定位到指定记录,意味着游标必须添加索引。
下面这个示例就是游标翻页的例子: 我们需要查询101-110的数据, 我们通过索引直接定位到100条的位置, 然后再取十条则是我们想要的
1
| select * from table where id > 100 order by id limit 0,10
|
只要id这个字段有索引,就能直接定位到101这个字段,然后去10条记录。以后无论翻页到多大,通过索引直接定位到读取的位置,效率基本是一样的。这个id>100
就是我们的游标,这就是游标翻页。
前端之前传的pageNo
字段改成了cursor
字段。cursor
是上一次查询结果的位置,作为下一次查询的游标,由后端返回:
那么我们则需要定义示例下面的游标类
1 2 3 4 5 6
| @ApiModelProperty("页大小") @Max(50) private Integer pageSize = 10;
@ApiModelProperty("游标(初始为null,后续请求附带上一次翻页的游标)") private String cursor;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @Data @ApiModel("游标翻页返回") @AllArgsConstructor @NoArgsConstructor public class CursorPageBaseResp<T> {
@ApiModelProperty("游标(下次翻页带上这参数)") private String cursor;
@ApiModelProperty("是否最后一页") private Boolean isLast = Boolean.FALSE;
@ApiModelProperty("数据列表") private List<T> list;
public static <T> CursorPageBaseResp<T> init(CursorPageBaseResp cursorPage, List<T> list) { CursorPageBaseResp<T> cursorPageBaseResp = new CursorPageBaseResp<T>(); cursorPageBaseResp.setIsLast(cursorPage.getIsLast()); cursorPageBaseResp.setList(list); cursorPageBaseResp.setCursor(cursorPage.getCursor()); return cursorPageBaseResp; }
@JsonIgnore public Boolean isEmpty() { return CollectionUtil.isEmpty(list); }
public static <T> CursorPageBaseResp<T> empty() { CursorPageBaseResp<T> cursorPageBaseResp = new CursorPageBaseResp<T>(); cursorPageBaseResp.setIsLast(true); cursorPageBaseResp.setList(new ArrayList<T>()); return cursorPageBaseResp; }
}
|
工具类封装
CursorUtils
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| public class CursorUtils {
public static <T> CursorPageBaseResp<Pair<T, Double>> getCursorPageByRedis(CursorPageBaseReq cursorPageBaseReq, String redisKey, Function<String, T> typeConvert) { Set<ZSetOperations.TypedTuple<String>> typedTuples; if (StrUtil.isBlank(cursorPageBaseReq.getCursor())) { typedTuples = RedisUtils.zReverseRangeWithScores(redisKey, cursorPageBaseReq.getPageSize()); } else { typedTuples = RedisUtils.zReverseRangeByScoreWithScores(redisKey, Double.parseDouble(cursorPageBaseReq.getCursor()), cursorPageBaseReq.getPageSize()); } List<Pair<T, Double>> result = typedTuples .stream() .map(t -> Pair.of(typeConvert.apply(t.getValue()), t.getScore())) .sorted((o1, o2) -> o2.getValue().compareTo(o1.getValue())) .collect(Collectors.toList()); String cursor = Optional.ofNullable(CollectionUtil.getLast(result)) .map(Pair::getValue) .map(String::valueOf) .orElse(null); Boolean isLast = result.size() != cursorPageBaseReq.getPageSize(); return new CursorPageBaseResp<>(cursor, isLast, result); }
public static <T> CursorPageBaseResp<T> getCursorPageByMysql(IService<T> mapper, CursorPageBaseReq request, Consumer<LambdaQueryWrapper<T>> initWrapper, SFunction<T, ?> cursorColumn) { Class<?> cursorType = LambdaUtils.getReturnType(cursorColumn); LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>(); initWrapper.accept(wrapper); if (StrUtil.isNotBlank(request.getCursor())) { wrapper.lt(cursorColumn, parseCursor(request.getCursor(), cursorType)); } wrapper.orderByDesc(cursorColumn);
Page<T> page = mapper.page(request.plusPage(), wrapper); String cursor = Optional.ofNullable(CollectionUtil.getLast(page.getRecords())) .map(cursorColumn) .map(CursorUtils::toCursor) .orElse(null); Boolean isLast = page.getRecords().size() != request.getPageSize(); return new CursorPageBaseResp<>(cursor, isLast, page.getRecords()); }
private static String toCursor(Object o) { if (o instanceof Date) { return String.valueOf(((Date) o).getTime()); } else { return o.toString(); } }
private static Object parseCursor(String cursor, Class<?> cursorClass) { if (Date.class.isAssignableFrom(cursorClass)) { return new Date(Long.parseLong(cursor)); } else { return cursor; } } }
|
LambdaUtils
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| public class LambdaUtils {
private static final Map<String, Map<String, ColumnCache>> COLUMN_CACHE_MAP = new ConcurrentHashMap<>();
private static final Map<String, WeakReference<com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda>> FUNC_CACHE = new ConcurrentHashMap<>();
private static Pattern RETURN_TYPE_PATTERN = Pattern.compile("\\(.*\\)L(.*);"); private static Pattern PARAMETER_TYPE_PATTERN = Pattern.compile("\\((.*)\\).*"); private static final WeakConcurrentMap<String, SerializedLambda> cache = new WeakConcurrentMap<>();
public static Class<?> getReturnType(Serializable serializable) { String expr = _resolve(serializable).getInstantiatedMethodType(); Matcher matcher = RETURN_TYPE_PATTERN.matcher(expr); if (!matcher.find() || matcher.groupCount() != 1) { throw new RuntimeException("获取Lambda信息失败"); } String className = matcher.group(1).replace("/", "."); try { return Class.forName(className); } catch (ClassNotFoundException e) { throw new RuntimeException("无法加载类", e); } }
@SneakyThrows public static <T> Class<?> getReturnType(SFunction<T, ?> func) { com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda lambda = com.baomidou.mybatisplus.core.toolkit.LambdaUtils.resolve(func); Class<?> aClass = lambda.getInstantiatedType(); String fieldName = PropertyNamer.methodToProperty(lambda.getImplMethodName()); Field field = aClass.getDeclaredField(fieldName); field.setAccessible(true); return field.getType(); }
public static List<Class<?>> getParameterTypes(Serializable serializable) { String expr = _resolve(serializable).getInstantiatedMethodType(); Matcher matcher = PARAMETER_TYPE_PATTERN.matcher(expr); if (!matcher.find() || matcher.groupCount() != 1) { throw new RuntimeException("获取Lambda信息失败"); } expr = matcher.group(1);
return Arrays.stream(expr.split(";")) .filter(StrUtil::isNotBlank) .map(s -> s.replace("L", "").replace("/", ".")) .map(s -> { try { return Class.forName(s); } catch (ClassNotFoundException e) { throw new RuntimeException("无法加载类", e); } }) .collect(Collectors.toList()); }
private static SerializedLambda _resolve(Serializable func) { return cache.computeIfAbsent(func.getClass().getName(), (key) -> ReflectUtil.invoke(func, "writeReplace")); }
}
|
封装Starter
虽然在前面的SpringBoot篇章我们也封装了一次,但是现在是实战
此时我们需要封装一个OSS的starter,支持自定义切换(简略,仅提供思路)
众所周知,封装一个Starter我们需要提供一个AutoConfiguration给SpringBoot
那么肯定需要创建resource/META-INF/spirng.factories
1 2 3
| org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.calyee.chat.oss.MinIOConfiguration
|
这样boot就会扫描到这个自动配置类,然后加载配置
MinIOConfiguration
自动配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(OssProperties.class) @ConditionalOnExpression("${oss.enabled}") @ConditionalOnProperty(value = "oss.type", havingValue = "minio") public class MinIOConfiguration { @Bean @SneakyThrows @ConditionalOnMissingBean(MinioClient.class) public MinioClient minioClient(OssProperties ossProperties) { return MinioClient.builder() .endpoint(ossProperties.getEndpoint()) .credentials(ossProperties.getAccessKey(), ossProperties.getSecretKey()) .build(); } @Bean @ConditionalOnBean({MinioClient.class}) @ConditionalOnMissingBean(MinIOTemplate.class) public MinIOTemplate minioTemplate(MinioClient minioClient, OssProperties ossProperties) { return new MinIOTemplate(minioClient, ossProperties); } }
|
OssProperties
读取yml配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @Data @ConfigurationProperties(prefix = "oss") public class OssProperties {
Boolean enabled;
OssType type;
String endpoint;
String accessKey;
String secretKey;
String bucketName; }
|
仅仅列出上述两点,对于其他的文件大同小异,仅有当前两项是重点,一个是自动配置类,一个是读取配置文件,其中还有一些注解需要理解(一些可以参见SpringBoot章节)
抽象类的使用(最佳实践)
当前抽象类为:Redis批量缓存
先抽象接口
BatchCache interface
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public interface BatchCache<IN, OUT> {
OUT get(IN req);
Map<IN, OUT> getBatch(List<IN> req);
void delete(IN req);
void deleteBatch(List<IN> req); }
|
在抽象类,抽象的原方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
public Map<Long, User> getUserInfoBatch(Set<Long> uids) { List<String> keys = uids.stream().map(a -> RedisKey.getKey(RedisKey.USER_INFO_STRING, a)).collect(Collectors.toList()); List<User> mget = RedisUtils.mget(keys, User.class); Map<Long, User> map = mget.stream().filter(Objects::nonNull).collect(Collectors.toMap(User::getId, Function.identity())); List<Long> needLoadUidList = uids.stream().filter(a -> !map.containsKey(a)).collect(Collectors.toList()); if (CollUtil.isNotEmpty(needLoadUidList)) { List<User> needLoadUserList = userDao.listByIds(needLoadUidList); Map<String, User> redisMap = needLoadUserList.stream().collect(Collectors.toMap(a -> RedisKey.getKey(RedisKey.USER_INFO_STRING, a.getId()), Function.identity())); RedisUtils.mset(redisMap, 5 * 60); map.putAll(needLoadUserList.stream().collect(Collectors.toMap(User::getId, Function.identity()))); } return map; }
|
抽象后的方法,因为后面都需要复用这样的方法,所以抽取公共的
AbstractRedisStringCache
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| import cn.hutool.core.collection.CollectionUtil; import com.calyee.chat.common.common.utils.RedisUtils; import org.springframework.data.util.Pair;
import java.lang.reflect.ParameterizedType; import java.util.*; import java.util.stream.Collectors;
public abstract class AbstractRedisStringCache<IN, OUT> implements BatchCache<IN, OUT> {
private Class<OUT> outClass;
protected AbstractRedisStringCache() { ParameterizedType genericSuperclass = (ParameterizedType) this.getClass().getGenericSuperclass(); this.outClass = (Class<OUT>) genericSuperclass.getActualTypeArguments()[1]; }
protected abstract String getKey(IN req);
protected abstract Long getExpireSeconds();
protected abstract Map<IN, OUT> load(List<IN> req);
@Override public OUT get(IN req) { return getBatch(Collections.singletonList(req)).get(req); }
@Override public Map<IN, OUT> getBatch(List<IN> req) { if (CollectionUtil.isEmpty(req)) { return new HashMap<>(); } req = req.stream().distinct().collect(Collectors.toList()); List<String> keys = req.stream().map(this::getKey).collect(Collectors.toList()); List<OUT> valueList = RedisUtils.mget(keys, outClass); List<IN> loadReqs = new ArrayList<>(); for (int i = 0; i < valueList.size(); i++) { if (Objects.isNull(valueList.get(i))) { loadReqs.add(req.get(i)); } } Map<IN, OUT> load = new HashMap<>(); if (CollectionUtil.isNotEmpty(loadReqs)) { load = load(loadReqs); Map<String, OUT> loadMap = load.entrySet().stream() .map(a -> Pair.of(getKey(a.getKey()), a.getValue())) .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond)); RedisUtils.mset(loadMap, getExpireSeconds()); }
Map<IN, OUT> resultMap = new HashMap<>(); for (int i = 0; i < req.size(); i++) { IN in = req.get(i); OUT out = Optional.ofNullable(valueList.get(i)) .orElse(load.get(in)); resultMap.put(in, out); } return resultMap; }
@Override public void delete(IN req) { deleteBatch(Collections.singletonList(req)); }
@Override public void deleteBatch(List<IN> req) { List<String> keys = req.stream().map(this::getKey).collect(Collectors.toList()); RedisUtils.del(keys); } }
|
其中 RedisUtils见[开发小手册 | Calyee`Blog](https://blog.calyee.top/2023/11/04/开发小手册/)
样例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Component public class UserInfoCache extends AbstractRedisStringCache<Long, User> { @Autowired private UserDao userDao;
@Override protected String getKey(Long uid) { return RedisKey.getKey(RedisKey.USER_INFO_STRING, uid); }
@Override protected Long getExpireSeconds() { return 5 * 60L; }
@Override protected Map<Long, User> load(List<Long> uidList) { List<User> needLoadUserList = userDao.listByIds(uidList); return needLoadUserList.stream().collect(Collectors.toMap(User::getId, Function.identity())); } }
|
请求上下文RequestHolder
对于登录的用户,我们会将uid设置为请求属性,在CollectorInterceptor中统一收集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @Order @Slf4j @Component public class CollectorInterceptor implements HandlerInterceptor, WebMvcConfigurer {
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(this) .addPathPatterns("/**"); }
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { RequestInfo info = new RequestInfo(); info.setUid(Optional.ofNullable(request.getAttribute(TokenInterceptor.ATTRIBUTE_UID)).map(Object::toString).map(Long::parseLong).orElse(null)); info.setIp(ServletUtil.getClientIP(request)); RequestHolder.set(info); return true; }
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { RequestHolder.remove(); } }
|
聊天项目中的工厂模式和策略模式组合
例如我们现在有一个需求,对于传入的不同数据进行不同的处理(比如QQ发消息,他聊天框可以发送不同类型的消息,这个是不是就可以用策略模式+工厂模式进行推送处理,即我需要对不同的数据进行不同的处理)
那么就可以使用这个组合:
我们先定义一个抽象类模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
|
public abstract class AbstractMsgHandler<Req> { @Autowired private MessageDao messageDao; private Class<Req> bodyClass;
@PostConstruct private void init() { ParameterizedType genericSuperclass = (ParameterizedType) this.getClass().getGenericSuperclass(); this.bodyClass = (Class<Req>) genericSuperclass.getActualTypeArguments()[0]; MsgHandlerFactory.register(getMsgTypeEnum().getType(), this); }
abstract MessageTypeEnum getMsgTypeEnum();
protected void checkMsg(Req body, Long roomId, Long uid) {
}
@Transactional public Long checkAndSaveMsg(ChatMessageReq request, Long uid) { Req body = this.toBean(request.getBody()); AssertUtil.allCheckValidateThrow(body); checkMsg(body, request.getRoomId(), uid); Message insert = MessageAdapter.buildMsgSave(request, uid); messageDao.save(insert); saveMsg(insert, body); return insert.getId(); } private Req toBean(Object body) { if (bodyClass.isAssignableFrom(body.getClass())) { return (Req) body; } return BeanUtil.toBean(body, bodyClass); } protected abstract void saveMsg(Message message, Req body);
public abstract Object showMsg(Message msg);
public abstract Object showReplyMsg(Message msg);
public abstract String showContactMsg(Message msg);
}
|
提供一个实现类样例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| @Component public class EmojisMsgHandler extends AbstractMsgHandler<EmojisMsgDTO> { @Autowired private MessageDao messageDao;
@Override MessageTypeEnum getMsgTypeEnum() { return MessageTypeEnum.EMOJI; }
@Override public void saveMsg(Message msg, EmojisMsgDTO body) { MessageExtra extra = Optional.ofNullable(msg.getExtra()).orElse(new MessageExtra()); Message update = new Message(); update.setId(msg.getId()); update.setExtra(extra); extra.setEmojisMsgDTO(body); messageDao.updateById(update); }
@Override public Object showMsg(Message msg) { return msg.getExtra().getEmojisMsgDTO(); }
@Override public Object showReplyMsg(Message msg) { return "表情"; }
@Override public String showContactMsg(Message msg) { return "[表情包]"; } }
|
然后我们需要提供一个工厂创建场景对象
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class MsgHandlerFactory { private static final Map<Integer, AbstractMsgHandler> STRATEGY_MAP = new HashMap<>();
public static void register(Integer code, AbstractMsgHandler strategy) { STRATEGY_MAP.put(code, strategy); }
public static AbstractMsgHandler getStrategyNoNull(Integer code) { AbstractMsgHandler strategy = STRATEGY_MAP.get(code); AssertUtil.isNotEmpty(strategy, CommonErrorEnum.PARAM_INVALID); return strategy; } }
|
使用
1 2 3 4 5 6 7 8 9 10 11 12 13
|
@Override @Transactional public Long sendMsg(ChatMessageReq request, Long uid) { check(request, uid); AbstractMsgHandler<?> msgHandler = MsgHandlerFactory.getStrategyNoNull(request.getMsgType()); Long msgId = msgHandler.checkAndSaveMsg(request, uid); applicationEventPublisher.publishEvent(new MessageSendEvent(this, msgId)); return msgId; }
|
实际开发也能用到(笔者在实习的时候就用上了!)
一步一步改造
场景:我 需要对不同数据进行不同的处理,什么个不同法,比如这个数据需要求和 另外一个需要求平均 还有需要求时间最早的等等等场景
1 2
| CountModeType: 求和/平均 ... Data: List<?>/String[] ...
|
实际开发中的工厂加策略模式
笔者扩展的
工厂模式 1(策略抽象一层)
对于固定的模版(比如处理 excel 的数据,他是固定的)
(下面图例为工厂 1 的模式)
那么我们可以定义一个模版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import javax.annotation.PostConstruct; import java.util.ArrayList; import java.util.List;
public abstract class AbstractDataHandler {
@PostConstruct private void init() { DataHandlerFactory.register(getCountModeEnums(), this); }
public abstract CountModeEnums getCountModeEnums(); public abstract Object processData(List<?> o); }
|
策略(例如我们在此是求和策略,当然也可以求平均)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| import java.util.List;
@Component public class OperationSumBaseDataHandler extends AbstractDataHandler {
@Override public CountModeEnums getCountModeEnums() { return CountModeEnums.sum; }
@Override public Object processData(List<?> o) { Assert.notNull(o, "当前处理的数据不能为空"); Object type = o.get(0); switch (type.getClass().getSimpleName()) { case "Integer": return o.stream() .mapToInt(Integer.class::cast) .sum(); case "Long": return o.stream() .mapToLong(Long.class::cast) .sum(); case "Double": return o.stream() .mapToDouble(Double.class::cast) .sum(); case "Float": return o.stream() .mapToDouble(Float.class::cast) .sum(); default: throw new RuntimeException("该类型暂不支持"); } } }
|
工厂类型 1:我们需要提供方法给他进行调用计算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import org.springframework.util.Assert;
import java.util.HashMap; import java.util.Map;
public class DataHandlerFactory { private static final Map<CountModeEnums, AbstractDataHandler> STRATEGY_MAP = new HashMap<>();
public static void register(CountModeEnums enums, AbstractDataHandler strategy) { STRATEGY_MAP.put(enums, strategy); }
public static AbstractDataHandler getStrategyNoNullByObj(Object o) { CountModeEnums countModeEnums = null; if (o instanceof Example) { Example p = (Example) o; countModeEnums = p.getCountModeEnums(); }else if(){ }else{ } return haveSuitableStrategy(countModeEnums); } private static AbstractDataHandler haveSuitableStrategy(CountModeEnums enums) { AbstractDataHandler handler = STRATEGY_MAP.get(enums); Assert.notNull(handler, "没有对应的处理策略"); return handler; } }
|
使用的表现形式体现为:我们需要手动调用,这种方式常常用于策略模式很多的情况
1 2 3 4 5 6 7 8
| Example avg = new Example(); avg.setCountModeEnums(CountModeEnums.avg); avg.setData(Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0)); AbstractHandler avgHandler = DataHandlerFactory.getStrategyNoNullByObj(avg);
Object oavg = avgHandler.processData(sum.getData());
log.info("avg: {}, exp: {}, equals: {}", oavg, 3.0, oavg.equals(3.0));
|
工厂模式一总结:对于策略固定且策略较多的情况 ,我们可以使用此类型,即我们模版可以使用同一个,然后具体实现具体分析,通过给对象然后从工厂生产出具体的策略,然后自己调用
工厂模式 2(策略抽象两层 可拓展性更强)
当前模式适用于更复杂的场景,例如:
(工厂 2 模式 出场)
对于当前的策略:
定义基础抽象父类模版
1 2 3 4 5 6 7
|
public abstract class AbstractHandler { }
|
分区抽象模版
基础数据处理抽象类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
public abstract class AbstractDataBaseOperationHandler extends AbstractHandler { @PostConstruct private void init() { DataHandlerFactory.register(getCountModeEnums(), this); }
public abstract CountModeEnums getCountModeEnums();
public abstract Object processData(List<?> o); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
public abstract class AbstractDataTimeOperationHandler extends AbstractHandler { @PostConstruct private void init() { DataHandlerFactory.register(getCountModeEnums(), this); }
public abstract CountModeEnums getCountModeEnums();
public abstract Object processData(List<Date> data1, List<?> data2); }
|
策略具体实现(仅给出一个案例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
@Component public class OperationSumBaseDataHandler extends AbstractDataBaseOperationHandler {
@Override public CountModeEnums getCountModeEnums() { return CountModeEnums.sum; }
@Override public Object processData(List<?> o) { Assert.notNull(o, "当前处理的数据不能为空"); Object type = o.get(0); switch (type.getClass().getSimpleName()) { case "Integer": return o.stream() .mapToInt(Integer.class::cast) .sum(); case "Long": return o.stream() .mapToLong(Long.class::cast) .sum(); case "Double": return o.stream() .mapToDouble(Double.class::cast) .sum(); case "Float": return o.stream() .mapToDouble(Float.class::cast) .sum(); default: throw new RuntimeException("该类型暂不支持"); } } }
|
工厂方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public class DataHandlerFactory { private static final Map<CountModeEnums, AbstractHandler> STRATEGY_MAP = new HashMap<>();
public static void register(CountModeEnums enums, AbstractHandler strategy) { STRATEGY_MAP.put(enums, strategy); }
public static Object getStrategyNoNullByObj(Object o) { if (o instanceof ExampleBaseDataCalculate) { ExampleBaseDataCalculate p = (ExampleBaseDataCalculate) o; AbstractDataBaseOperationHandler handler = (AbstractDataBaseOperationHandler) haveSuitableStrategy(p.getCountModeEnums()); return handler.processData(p.getData()); } else if (o instanceof ExampleYearCalculate) { ExampleYearCalculate p = (ExampleYearCalculate) o; AbstractDataTimeOperationHandler handler = (AbstractDataTimeOperationHandler) haveSuitableStrategy(p.getEnums()); return handler.processData(p.getData1(), p.getData2()); } else { return "暂时没有该处理"; } }
private static AbstractHandler haveSuitableStrategy(CountModeEnums enums) { AbstractHandler handler = STRATEGY_MAP.get(enums); Assert.notNull(handler, "没有对应的处理策略"); return handler; } }
|
测试用例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ExampleBaseDataCalculate sum = new ExampleBaseDataCalculate();
sum.setCountModeEnums(CountModeEnums.sum); sum.setData(Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0));
Object ans = DataHandlerFactory.getStrategyNoNullByObj(sum);
log.info("sum: {}, exp:{}, equals: {}", ans, 15.0, ans.equals(15.0));
ExampleBaseDataCalculate avg = new ExampleBaseDataCalculate();
avg.setCountModeEnums(CountModeEnums.avg); avg.setData(Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0));
Object avgAns = DataHandlerFactory.getStrategyNoNullByObj(avg); log.info("avg: {}, exp:{}, equals: {}", avgAns, 3.0, avgAns.equals(3.0));
|
通过测试案例 可以知道,我们现在的话 不是通过自己去调用方法去处理了,而是直接传入对象及其行为,直接返回数据处理结果即可。
小总结
对于 1,我们是采用的 直接回送策略,然后自己进行处理,然后模版是一致的
对于 2,我们采用的是 传入对象,我们直接内部处理 让调用者不需要关心里面具体的逻辑,传入对象即可,然后抽象是多层的,我们可以通过这个定义某一系列的行为,然后去实现
MyBatis
MyBatisPlus saveBatch 优化
设置rewriteBatchedStatements=true批量插入
下面我们为数据库的连接加上rewriteBatchedStatements=true的属性,再测试批量加入的耗时。
1
| rewriteBatchedStatements=true
|
rewriteBatchedStatements
Mybatis 插件机制Interceptor与InnerInterceptor
在 Mybatis 中,插件机制提供了强大的扩展能力,在 sql 最终执行之前,提供了四个拦截点,支持不同场景的功能扩展:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
例如我们需要对查询出来的数据进行解密(数据库保存的是加密信息,然后我们可以定义一个注解取修饰字段,假如字段被这个注解修饰的话,那么后续的操作我们则需要进行加密解密)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
|
@Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) }) @Component @Slf4j public class DecryptInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object resultObject = invocation.proceed(); if (Objects.isNull(resultObject)) return null; if (resultObject instanceof ArrayList) { ArrayList resultList = (ArrayList) resultObject; if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) { for (Object result : resultList) { decrypt(result); } } } else if (needToDecrypt(resultObject)) decrypt(resultObject); return resultObject; } @Override public Object plugin(Object o) { return Plugin.wrap(o, this); } }
@Slf4j public class EncryptAndVerfyWholenessInterceptor extends JsqlParserSupport implements InnerInterceptor {
private static final Pattern PARAM_PAIRS_RE = Pattern.compile("#\\{ew\\.paramNameValuePairs\\.(" + Constants.WRAPPER_PARAM + "\\d+)\\}");
@Override public void beforeQuery(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { }
@Override public void beforeUpdate(Executor executor, MappedStatement mappedStatement, Object parameterObject) throws SQLException { } }
|
关系型数据库实现类动态字段数据库
为什么需要用到动态数据库呢?
场景:我们有一个需求就是(以前端的视角来描述),前端的需求需要动态渲染指标列,假如后端多了一个指标,那么前端也需要对应上去也多一个列去动态渲染。
这个时候对于普通的关系型数据库是做不到的,因为普通的我们指标字段是定下来的,此时的这个老传统数据结构是解决不了这个需求的,那么我和同事想到了可以使用动态模版数据库。(什么是动态模版数据库,其实就是通过定义模版表单,然后具体的表单对应具体的字段指标,然后数据绑定表单与具体的指标列)
那么 — 因为动态数据库对应的数据,一次性填的是一个指标的项,然后到最后查询查询出来的却是列级别数据, 这个时候与前端渲染模式有点出入,那么这个时候我们需要使用到数据翻转。
那么表结构可以参考如下:
指标表
column name |
column type |
键 |
desc |
id |
varchar |
PK |
|
indicator_name |
varchar |
|
指标名称 |
parent_id |
varchar |
|
上级指标 |
usable |
varchar |
|
是否可用 |
sort_num |
int |
|
排序字段 |
填报表单
column name |
column type |
键 |
desc |
id |
varchar |
PK |
|
form_id |
varchar |
|
表单id |
form_desc |
varchar |
|
表单描述 |
sec_indicator_id |
varchar |
FK |
表单二级指标id |
sec_indicator_name |
varchar |
|
表单二级指标名字 |
year |
int |
|
填报年份(用于统计) |
post_status |
varchar |
|
表单发表状态(发布则不可再选,具体业务具体分析) |
usable |
varchar |
|
表单是否可用 |
editable |
varchar |
|
表单是否可编辑 |
post_org_id |
varchar |
FK |
表单发布单位id |
post_org_name |
varchar |
|
表单发布单位名 |
当前的二级指标指的是:可以理解为一共就只有三个指标,一级指标就是一级路由,然后展开就是二级指标(路由),二级路由对应绑定了页面,页面渲染的是三级指标
填报数据
column name |
column type |
键 |
desc |
id |
varchar |
PK |
|
form_id |
varchar |
FK |
填报表单id |
form_name |
varchar |
|
填报表单名称 |
org_id |
varchar |
FK |
填报单位id |
person_id |
varchar |
FK |
填报人id |
indicator_id |
varchar |
FK |
指标id(三级指标) |
data |
varchar |
|
指标值(例如: 100) |
status |
varchar |
|
数据状态 |
data:为填报的数据
可能这个表部分地方设计不合理,但是大概步骤是这样的,如果有人看到或者发现有啥建议,可以在下面评论,笔者会第一时间回复(虽然应该没人看😆)
数据翻转与 Stream 流操作
渲染分析与翻转
对于我们现在有一个这样的要求就是,我们的数据在第一次处理完成之后是这样的
Year |
Data [ 对应的指标列,即模版数据库的一行数据 ] |
2023 |
{ groupName: “指标 1”,value: 100 , other: “其他描述” } |
2024 |
{ groupName: “指标 2”,value: 100 , other: “其他描述” } |
这个一行是指:例如我们在 excel 中,一次填报一行的数据,其中一行就对应一个指标值。此时可以填报多列(多个指标),只不过在数据库中不在同一行存储
可能这个多个指标有点抽象,那么我们在此场景仅理解为一个列即可
上面那个表格也许在数据库中,我们通过联表查询后是这样:
year |
groupName |
value |
other |
2023 |
指标 1 |
100 |
其他描述 |
2024 |
指标 1 |
100 |
其他描述 |
那么对应 Java 中,我们在通过 ORM 框架查询后,转换为 Bean 后,他就是对应一个 List list。其中 bean 很容易知道属性有 year、groupName …
此时我们可以通过 stream 流处理数据,把他变成一个 map
1 2
| Map<Integer, List<OurBean>> groupByYear = list.stream().collect(Collectors.groupingBy(OurBean::getYear));
|
然后就变成一个 Map 了,Key 是我们根据年份分组的,value 就是剩下的字段
此时就是笔者第一次列出来的表格。
如果是这样的结构,返回给前端是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| { code: 200, data:{ [ { year: 2023, otherData:{ groupName: "指标 1", value: 100, other: "其他描述" } }, { year: 2024, otherData:{ groupName: "指标 1", value: 100, other: "其他描述" } } ] } msg: "查询成功" }
|
因为前端是渲染表格,我们这样的话,返回的是一个列的数据 [ 就是他们本来就是属于一个指标下的 ],按照逻辑来说,一个指标下的数据我们应该这样显示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| { code: 200, data:{ "2023": [ { groupName: "指标 1", value: 100, other: "其他描述" }, { groupName: "指标 2", value: 300, other: "这是本年度指标二数据下的值(前面没有列出)" } ], "2024": [ { groupName: "指标 1", value: 100, other: "其他描述" }, { groupName: "指标 2", value: 200, other: "描述" } ] }, msg: "查询成功" }
|
就是行级数据,这样才是正常的渲染逻辑,一次性渲染一行,第一次渲染 2023 年度的一指标与二指标的数据,第二次渲染第二行 2024 年度的数据。
对于这种列数据转换行数据,我们同样是使用 map 操作,前面那个渲染结果结构,显然就是 key-value 的结构,只不过 value 是对应多个的情况,那么即可定义
ByYearInnerDataVO:为我们需要的渲染内部结构
1
| Map<Integer, List<ByYearInnerDataVO>> result = new HashMap<>();
|
按照年度分组
1 2
| Map<String, List<PreviCalculateVo>> processMap = resultList.stream().collect(Collectors.groupingBy(PreviCalculateVo::getYear));
|
第一层遍历每一个年份组数据
1 2 3 4
| for (Map.Entry<String, List<PreviCalculateVo>> entry : processMap.entrySet()) { }
|
第二层处理 value 的 list 结构
1 2 3
| entry.getValue().forEach((k) -> { }
|
最后的总体结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| Map<String, List<PreviCalculateVo>> processMap = resultList.stream().collect(Collectors.groupingBy(PreviCalculateVo::getYear));
for (Map.Entry<String, List<PreviCalculateVo>> entry : processMap.entrySet()) { int year = Integer.parseInt(entry.getKey()); entry.getValue().forEach((k) -> { List<ByYearInnerDataVO> list = yearIndicatorData.get(year); if (CollectionUtil.isEmpty(list)) { list = new ArrayList<>(); } list.add(new ByYearInnerDataVO() .setValue(k.getValue()) .setData(k.getData())); result.put(Integer.parseInt(entry.getKey()), list); }); }
|
似山代码
开始写似山了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
Map<Integer, List<ByYearInnerDataVO>> optimizedData = yearIndicatorData.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, entry -> { Set<String> indicator = yearAndIndicator.keySet(); return indicator.stream() .map(indicatorName -> { return entry.getValue().stream() .filter(i -> indicatorName.equals(i.getIndicatorName())) .findFirst() .orElseGet(() -> { return new ByYearInnerDataVO() .setIndicatorName(indicatorName) .setCount("0") .setRate("/") .setIncr("/") .setValue(0); }); }).collect(Collectors.toList()); } ));
|
替换方案
改用传统 for 循环
替换代码如下(减少了流的使用、它需要频繁创建流对象,采用迭代器,,对于业务无关紧要的代码提取)
TIPS: 使用for循环的版本避免了内部流操作的多次迭代,减少了函数调用,并且直接操作了数据结构,这通常会带来更好的性能。在某些情况下,特别是在处理复杂的数据转换和操作时,流可以提供更简洁、更易读的代码。但是,对于性能敏感的操作,特别是在数据量很大时,传统的for循环可能是一个更好的选择。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| Map<Integer, List<ByYearInnerDataVO>> result = new HashMap<>(); for (Map.Entry<Integer, List<ByYearInnerDataVO>> entry : yearIndicatorData.entrySet()) { List<ByYearInnerDataVO> dataList = new ArrayList<>(); for (String indicatorName : indicatorData) { Optional<ByYearInnerDataVO> found = entry.getValue().stream() .filter(i -> indicatorName.equals(i.getIndicatorName())) .findFirst(); dataList.add(found.orElseGet( () -> createEmptyData(indicatorName))); } result.put(entry.getKey(), dataList); }
private static ByYearInnerDataVO createEmptyData(String indicatorName) { ByYearInnerDataVO innerDataVO = new ByYearInnerDataVO(); innerDataVO.setIndicatorName(indicatorName); innerDataVO.setCount("0"); innerDataVO.setRate("/"); innerDataVO.setIncr("/"); innerDataVO.setValue(0); return innerDataVO; }
private ByYearInnerDataVO createEmptyData(String indicatorName) { return new ByYearInnerDataVO() .setIndicatorName(indicatorName) .setCount("0") .setRate("/") .setIncr("/") .setValue(0); }
|
再优化(不使用链式调用创建对象,采用手动 填充数据,并且声明为静态类):
1 2 3 4 5 6 7 8 9
| private static ByYearInnerDataVO createEmptyData(String indicatorName) { ByYearInnerDataVO innerDataVO = new ByYearInnerDataVO(); innerDataVO.setIndicatorName(indicatorName); innerDataVO.setCount("0"); innerDataVO.setRate("/"); innerDataVO.setIncr("/"); innerDataVO.setValue(0); return innerDataVO; }
|
Excel/Word 数据相关
EasyExcel
Excel 可以使用阿里的 easyexcel
这里什么都没有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
| import com.alibaba.excel.EasyExcel; import com.alibaba.excel.ExcelWriter; import com.alibaba.excel.converters.longconverter.LongStringConverter; import com.alibaba.excel.util.DateUtils; import com.alibaba.excel.write.metadata.WriteSheet; import com.alibaba.excel.write.metadata.fill.FillConfig; import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.List; import java.util.Map;
public class ExcelUtils {
public static <T> void templateFillWrite(HttpServletResponse response, List<T> dataList, Map<String, Object> dataMap, String filename, String templatePath) throws IOException { response.setCharacterEncoding("utf-8"); String preFileName = filename + DateUtils.format(new Date(),DateUtils.DATE_FORMAT_14); response.setHeader("Content-disposition", "attachment; filename=" + URLEncoder.encode(preFileName, "UTF-8") + ".xlsx"); response.setContentType("application/vnd.ms-excel;charset=UTF-8");
ExcelWriter excelWriter = EasyExcel .write(response.getOutputStream()) .withTemplate(templatePath) .autoCloseStream(Boolean.FALSE) .build(); WriteSheet writeSheet = EasyExcel.writerSheet() .build(); FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build(); excelWriter.fill(dataList,fillConfig,writeSheet); excelWriter.fill(dataMap, writeSheet); excelWriter.finish(); }
public static <T> void writeWithCusTomHeader(HttpServletResponse response, String filename, String sheetName, List<List<String>> head, List<T> data) throws IOException { EasyExcel.write(response.getOutputStream()) .head(head) .autoCloseStream(false) .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .registerConverter(new LongStringConverter()) .sheet(sheetName).doWrite(data); response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name())); response.setContentType("application/vnd.ms-excel;charset=UTF-8"); }
public static <T> void write(HttpServletResponse response, String filename, String sheetName, Class<T> head, List<T> data) throws IOException { EasyExcel.write(response.getOutputStream(), head) .autoCloseStream(false) .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .registerConverter(new LongStringConverter()) .sheet(sheetName).doWrite(data); response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name())); response.setContentType("application/vnd.ms-excel;charset=UTF-8"); }
public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException { return EasyExcel.read(file.getInputStream(), head, null) .autoCloseStream(false) .autoTrim(true) .sheet() .doReadSync(); }
}
|
Apache poi
Apache poi
如果是需要创建 Word 直接使用下面这个(他是创建很棒的Word文档)
https://deepoove.com/poi-tl
类加载读取resource
有用!(可以与上面的导出生成Excel/Word相结合)
如果是使用模板填充的方式,那么需要保存模板,我们则需要读取模板文件,如果传统的放入代码文件夹,我们会发现是打包不了的(笔者血的教训),bane五年则需要采用下面的方式
类加载读取 resource 文件
[springboot项目读取 resources 目录下的文件的9种方式(总结)-阿里云开发者社区 (aliyun.com)](https://developer.aliyun.com/article/1462486#:~:text=10:总结 1 使用 ClassLoader.getResourceAsStream () 方法 这是一种通用的方式,可以适用于大多数情况。 2,目录下的文件,可以使用 Spring 提供的 ClassPathResource 类来加载文件。 这种方式比较简单,不需要提供完整的文件路径。 需要注意的是%3A使用不同的方式需要了解其适用的场景和使用方法。 对于不同的项目和需求,可能需要选择不同的方式。)
XxlJob定时任务
数据同步方案
2024.8.12今天mentor给我布置了一个数据同步的任务,其实要做到数据同步,方案还是挺多的,我们可以使用
– 需要内嵌代码
- Spring Listener发布监听事件
- Spring Schedule自带的定时任务
– 不需要内嵌代码
- alibaba canel:监听MySQL的Binlog日志,需要开启MySQL的日志,需要读取二进制文件,可以用 但是不是很友好对于笔者当前的场景
- XxlJog:在笔者当场景适合,不在原有的业务逻辑上新增代码,我们只需要定时去轮询数据库的数据(对于仅仅需要操作少量数据),如果你是ToC可能这个轮询数据库的操作不是很适合,对于笔者ToG的项目当然合适,反正用户量也不多,定时同步完全够了
我们只需要做到增量同步/全量同步即可
动态数据源
具体如何切换,参考(我们一般在 Service 业务层上控制切换的切面)
1 2 3 4 5
| @Service @DS("fzzyk") public class FzzykSAuRoleServiceImpl extends ServiceImpl<FzzykSAuRoleMapper, FzzykSAuRole> implements FzzykSAuRoleService {
}
|
如果是这样的设置,我们在注入该服务的时候就是用的从数据源,当然还可以在方法上面去使用该注解
例如 MyBatisPlus 提供的样例
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Service @DS("slave") public class UserServiceImpl implements UserService {
@Autowired private JdbcTemplate jdbcTemplate;
@Override @DS("slave_1") public List selectByCondition() { return jdbcTemplate.queryForList("select * from user where age >10"); } }
|
当前执行顺序以局部为主,类上面决定里面默认数据源,方法上决定该局部的数据源
XxlJob 正轨
具体按照参考[分布式任务调度平台XXL-JOB (xuxueli.com)](https://www.xuxueli.com/xxl-job/#1.5 下载)
我们直接把源码拉取过来即可。
在前面的所有步骤都完成之后(即到官网的 HelloWord 步骤时),我们可以开始创建自己的 Job 模块
在把代码拉取下来之后
我们的项目结构是现在这个样子的
xxl-job
- xxl-job-admin:网页调度中心
- xxl-job-core:公共依赖
- xxl-job-executor:我们的 Job 任务模块
- xxl-job-executor-my-job:我们自己创建的业务
怎么创建 Job:[分布式任务调度平台XXL-JOB (xuxueli.com)](https://www.xuxueli.com/xxl-job/#3.2 BEAN模式(方法形式))
1 2 3 4 5
| @XxlJob("demoJobHandler") public void demoJobHandler() throws Exception { XxlJobHelper.log("XXL-JOB, Hello World."); }
|
到这里了,那么剩下的其实就和普通的 SpringBoot 项目开发业务逻辑一模一样的,我们要操作数据库,那么先定义实体,然后 Service+Mapper(dao)去数据访问以及业务处理。
但是这里我们可以用类似于工厂模式的方法去实现动态的对象处理
eg: 假如我们的 Job 是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Component @Slf4j public class SAuUserJob {
@Autowired private BasePushExecutor executor; @Autowired private ISAuUserService sAuUserService;
@XxlJob("SAuUserJob") public void doJobHandler() { log.info("[系统用户数据同步开始]"); String params = XxlJobHelper.getJobParam(); if (StrUtil.isNotEmpty(params)) XxlJobHelper.log("接收到参数:" + params);
executor.baseHandler(sAuUserService, "USER_PUSH"); log.info("[系统用户数据同步结束]"); } }
|
BasePushExecutor 其实就是定义的一个抽象接口
1 2 3
| public interface BasePushExecutor { void baseHandler(BaseService service, String type); }
|
具体实现
1 2 3 4 5 6 7
| @Component public class BasePushExecutorImpl implements BasePushExecutor { @Override public void baseHandler(BaseService baseService, String type) { baseService.pushData(); } }
|
其中这个 baseService 就是具体业务实现类的抽象接口父类
可以理解成为这个结构
BaseService
- AService:具体业务 A
- BService:具体业务 B
我们观察到这个 BaseService 他有一个方法规范为 void pushData();
那么我们在使用 MyBatisPlus 的时候,在实现 完 IService 的时候,也得实现 BaseService
1
| public interface ISAuUserService extends IService<SAuUser>, BaseService {}
|
这样的话,在后续继承实现该类的时候,我们只需要实现父类处理业务的接口即可
1 2 3 4 5 6 7 8 9 10 11 12
| @Service public class SAuUserServiceImpl extends ServiceImpl<SAuUserMapper, SAuUser> implements ISAuUserService {
@Autowired @Qualifier("sAuUserServiceTargetlfgj") private com.xxl.job.executor.dataTarget.service.SAuUserServiceLfgj sAuUserServiceTargetlfgj; @Override public void pushData() { } }
|
项目结构如下:
笔者的场景:数据同步。
那么对于源数据其实就是原始数据,主数据。
对于目标数据处理,那么不就是把主数据同步到目标数据吗,只不过拉取数据的规范在源数据里面实现了。
对于 XxlJob 业务,其实就是定义控制,类似于 Controller,我们可以在 Web 端控制任务的执行以及参数的传入,根据不同的需求执行不同的业务定义。
异步任务
CompletableFuture
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| if (firstPush) { long startT = System.currentTimeMillis(); batchData = this.lambdaQuery().eq(STreeNode::getDeleteFlag, 0).list(); List<FzzykSTreeNode> fzzykSTreeNodes = BeanUtil.copyToList(batchData, FzzykSTreeNode.class); List<LfgjSTreeNode> lfgjSTreeNodes = BeanUtil.copyToList(batchData, LfgjSTreeNode.class); CompletableFuture<Void> fzzykFuture = CompletableFuture.runAsync( () -> fzzykISTreeNodeService.saveBatch(fzzykSTreeNodes)); CompletableFuture<Void> lfgjFuture = CompletableFuture.runAsync( () -> lfgjistreeNodeService.saveBatch(lfgjSTreeNodes)); CompletableFuture.allOf(fzzykFuture, lfgjFuture).join(); long endT = System.currentTimeMillis(); log.info("[STreeNode Copy Success!,{}, 执行时间:{} 毫秒", new Date(), (endT - startT)); }
|
在上述案例中,我们在批量更新/插入的时候,可能不是对于一个数据源,当前部分应该与上一个部分以及上上个部分相结合(#MyBatisPlus saveBatch 优化 && #XxlJob -> 多数据源),实现异步多批量插入,一个任务的执行不会阻塞另外一个
当前方案笔者测试未通过
ThreadPoolTaskExecutor 线程池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| if (firstPush) { long startT = System.currentTimeMillis(); batchData = this.lambdaQuery().eq(STreeNode::getDeleteFlag, 0).list(); if (CollectionUtil.isEmpty(batchData)) { return; } List<FzzykSTreeNode> fzzykSTreeNodes = BeanUtil.copyToList(batchData, FzzykSTreeNode.class); List<LfgjSTreeNode> lfgjSTreeNodes = BeanUtil.copyToList(batchData, LfgjSTreeNode.class); int split = 3, size = batchData.size(); for (int i = 0; i < split; i++) { int start = i * size / split; int end = (i + 1) * size / split; executor.execute(() -> { fzzykISTreeNodeService.saveBatch(fzzykSTreeNodes.subList(start, end)); lfgjistreeNodeService.saveBatch(lfgjSTreeNodes.subList(start, end)); }); } long endT = System.currentTimeMillis(); log.info("[STreeNode Copy Success!,{}, 执行时间:{} 毫秒", new Date(), (endT - startT)); }
|
还是这个熟悉
新 项目架构
这个好理解,其实就是controller
调用链路:
1 2
| @RestController @Autowired @Autowired @Autowired UserController -> UserFacade -> UserService -> UserMapper
|
ElasticSearch
依赖
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency>
|
EsUtil
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.toolkit.LambdaUtils; import com.baomidou.mybatisplus.core.toolkit.support.SFunction; import com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda; import lombok.SneakyThrows; import org.springframework.data.elasticsearch.annotations.Field;
public class EsUtil {
@SneakyThrows public static <T> String getColumn(SFunction<T, ?> func) { SerializedLambda resolve = LambdaUtils.resolve(func); Class<?> implClass = resolve.getImplClass(); String implMethodName = resolve.getImplMethodName(); String fieldName = getFieldName(implMethodName); java.lang.reflect.Field field = implClass.getDeclaredField(fieldName); Field fieldAnno = field.getAnnotation(Field.class); if (null != fieldAnno && ObjectUtil.isNotEmpty(fieldAnno.name())) { return fieldAnno.name(); } else { return getFieldName(implMethodName); } }
public static String getFieldName(String getterOrSetterName) { if (getterOrSetterName.startsWith("get") || getterOrSetterName.startsWith("set")) { return StrUtil.removePreAndLowerFirst(getterOrSetterName, 3); } else if (getterOrSetterName.startsWith("is")) { return StrUtil.removePreAndLowerFirst(getterOrSetterName, 2); } else { throw new IllegalArgumentException("Invalid Getter or Setter name: " + getterOrSetterName); } } }
|
EsInit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| public class ElasticSearchInit {
public static void createIndex(ElasticsearchRestTemplate restTemplate,String indexName, Class T){ if (!restTemplate.indexOps(IndexCoordinates.of(indexName)).exists()){ restTemplate.indexOps(IndexCoordinates.of(indexName)).create(); Document mapping = restTemplate.indexOps(IndexCoordinates.of(indexName)).createMapping(T); restTemplate.indexOps(IndexCoordinates.of(indexName)).putMapping(mapping); } }
public static void createIndex(ElasticsearchRestTemplate restTemplate,String indexName){ if (!restTemplate.indexOps(IndexCoordinates.of(indexName)).exists()){ restTemplate.indexOps(IndexCoordinates.of(indexName)).create(); } }
public static void createIndexMappings(ElasticsearchRestTemplate restTemplate,String indexName, Class T){ if (!restTemplate.indexOps(IndexCoordinates.of(indexName)).exists()){ Document mapping = restTemplate.indexOps(IndexCoordinates.of(indexName)).createMapping(T); restTemplate.indexOps(IndexCoordinates.of(indexName)).putMapping(mapping); } }
public static void deleteIndex(ElasticsearchRestTemplate restTemplate,String indexName){ if (restTemplate.indexOps(IndexCoordinates.of(indexName)).exists()){ restTemplate.indexOps(IndexCoordinates.of(indexName)).delete(); } } }
|
1 2 3 4
| @Component public interface XxxxSearchRepository extends ElasticsearchRepository<XxxxSearch, String> { }
|
那么这个其实就是有点类似于 MyBatisPlus 那一套
其中Repository 就是实现的他原封装的
操作:Controller -> Service -> Repository
到这里 基本结构已经没什么问题了
分页查询 + 高亮 + 内联查询
例如我现在有一个需求,我需要查询 一页二十条数据 并且 我需要查询的字段为 Name 其中查询出来的数据得高亮
那么对于构建复杂的查询,笔者是推荐使用NativeSearchQueryBuilder 结合 BoolQueryBuilder。
那么大体结构我们可以这样:
1 2 3 4
| NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withPageable(PageRequest.of((page - 1), size));
|
收集内联条件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); BoolQueryBuilder innerQuery = QueryBuilders.boolQuery(); if (!StrUtil.isEmptyOrUndefined(name)) { innerQuery.must(QueryBuilders.matchQuery( EsUtil.getColumn(XxxxSearch::getName), name )); highlightBuilder.field(EsUtil.getColumn(XxxxSearch::getName)) .preTags("<span style='color:red'>") .postTags("</span>") .numOfFragments(0) .fragmentSize(250); }
boolQueryBuilder.must(innerQuery());
|
然后条件补充完之后,再吧当前内条件(innerQuery)给封装到外条件(boolQueryBuilder)中,最后在封装到原生查询条件(NativeSearchQueryBuilder)中
1 2 3 4 5 6
| queryBuilder.withQuery(boolQueryBuilder);
queryBuilder.withHighlightBuilder(highlightBuilder);
queryBuilder.withSort(SortBuilders.fieldSort("_score").order(SortOrder.DESC));
|
结果集查询
此时则不能使用旧的那个 search(即实现了ElasticsearchRepository 的那个类,因为它在高版本已经被废弃了),我们可以采用注入
1 2
| @Autowired private ElasticsearchRestTemplate elasticsearchTemplate;
|
然后所以这个模版方法进行查询
1 2
| SearchHits<XxxxSearch> searchHits = elasticsearchTemplate.search(queryBuilder.build(), XxxxSearch.class);
|
处理高亮
高亮其实就行遍历我们刚刚拿到的SearchHits,然后里面会有一个getHighlightFields( )
1 2 3 4 5 6 7 8 9 10 11
| for (SearchHit<XxxxSearch> searchHit : searchHits) { Map<String, Object> map = new HashMap<>(); Map<String, List<String>> highlightFields = searchHit.getHighlightFields(); XxxxSearch obj = searchHit.getContent(); map.put("id", obj.getId()); map.put("name", flag1 ? highlightFields.get("name").get(0) : obj.getName()); titleMaps.add(map); }
|
分页对象封装
1 2 3 4 5 6 7
| PageInfo<Map<String, Object>> info = new PageInfo<>(); info.setSize((int) searchHits.getTotalHits()); info.setList(titleMaps); info.setTotal(searchHits.getTotalHits()); info.setPageSize(size); info.setPageNum(page); return info;
|
可视化 Edge 小插件
去拓展商店搜索 ‘es-client’即可下载,连接即可使用
问题项
- 如果你的实体字段日期属性使用的是 Date,那么在格式化的时候会出现异常。
解决方案:改为LocalDate
- 对于 ES 的数据同步问题:我们有两个方案 ① 一个是前端对于一个数据库操作发送两个请求(一个是操作数据库,一个是对于刚刚的数据进行 es 同步)② 后端操作:我们可以在需要数据同步的方法里同步进行 es 和 sql 操作。
很显然:② 比较合适,因为前端可能会因为网络波动而导致第二个请求失效。
做聊天项目遇到一些题
ThreadLocal
RequestHolder的内部类为 ThreadLocal, ThreadLocal的key为弱引用, Value为强引用, 面试题: ThreadLocal为什么会内存泄漏?
Redis与MySQL的数据一致性
进程 线程 多线程
进程:是系统运行的基本单位,例如 qq 等,他运行其实就可以看作成进程,从 qq 的运行到结束的过程可以看作成进程的创建到消亡的过程,其中进程可以包含很多个线程组成
线程:进程和线程很像,但是线程是一个比进程更小的执行单位。
什么时候使用多线程,什么时候使用多进程?
● 多线程
对于 I/O 密集型,因为需要大量的 IO 操作,在进行 IO 操作时候 CPU 处于空闲状态,那么可以交给其他的线程去执行
● 多进程
对于 CPU 密集型,他需要大量的 CPU 计算,那么可以充分利用多进程进行计算,提升效率(多核 CPU)
当需要并行计算的时候也可以使用多进程
如何查看死锁?判断发生了死锁?
● Jconsole:jdk 自带的图形化界面
● jstack:jps -ef | grep 进程 id
项目部署上去了,有用过日志吗?怎么在服务器查看日志?
1 2 3
| tail -f xxx.log # 动态查看日志 tail -10 xxx.log # 查看最后10行日志 tail -n 10 xxx.log # 查看最后10行日志
|
引申 :
如果需要查看某进程(例如需要查看 java 的进程)
如果需要不挂起运行,然后后台可以指定日志
1
| nohup /root/runoob.sh > runoob.log 2>&1 &
|
如需检查 CPU 使用 top 和 cat(特定路径)
1 2 3
| # 通过proc文件系统 cat /proc/cpuinfo #查看cpu信息 cat /proc/meminfo #查看memery信息
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| # top效果 top - 21:35:19 up 12 days, 2:19, 1 user, load average: 0.00, 0.00, 0.00 Tasks: 107 total, 1 running, 106 sleeping, 0 stopped, 0 zombie %Cpu(s): 0.3 us, 0.2 sy, 0.0 ni, 99.5 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st MiB Mem : 1890.1 total, 77.4 free, 1505.6 used, 307.2 buff/cache MiB Swap: 0.0 total, 0.0 free, 0.0 used. 231.7 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 7634 root 20 0 137292 21596 9016 S 0.7 1.1 97:19.68 AliYunDunMonito 2086 root 20 0 1287708 10156 7096 S 0.3 0.5 79:23.59 argusagent 7623 root 20 0 94708 8892 6384 S 0.3 0.5 58:19.71 AliYunDun 8415 root 20 0 689932 9512 6064 S 0.3 0.5 11:25.31 aliyun-service 8596 root 20 0 19432 2696 1740 S 0.3 0.1 2:17.24 assist_daemon 21434 root 20 0 3348728 885380 9064 S 0.3 45.7 26:24.20 java 32110 root 20 0 11844 3764 3104 R 0.3 0.2 0:00.01 top
|
内存使用 top free cat(特定路径)
1 2 3 4 5
| free -h #h:以人类可读的单位显示例如1G 23M root@xxxxxxxx:~# free -h total used free shared buff/cache available Mem: 1.8Gi 1.5Gi 77Mi 2.0Mi 307Mi 232Mi Swap: 0B 0B 0B
|
1 2 3 4
| free -m #m:以mb为单位显示 root@xxxxxxxx:~# free -m total used free shared buff/cache available Mem: 1890 1504 77 2 307 232
|
查看Java进程相关
查看java后端服务对应的进程ID
- jps -l
生成线程快照 并保存为文件
- jstack 进程id >dump_threads.txt