spring cloud 和 spring boot 版本不对应时,会无法启动

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
org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:156) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:544) ~[spring-context-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:140) [spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at com.eureka.EurekaServerApplication.main(EurekaServerApplication.java:21) [classes/:na]
Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:126) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.<init>(TomcatWebServer.java:88) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:438) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:191) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:180) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:153) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
... 7 common frames omitted
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'traceFilterRegistration' defined in class path resource [org/springframework/cloud/netflix/eureka/server/EurekaServerAutoConfiguration.class]: Unsatisfied dependency expressed through method 'traceFilterRegistration' parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'javax.servlet.Filter' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Qualifier(value=httpTraceFilter)}
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:787) ~[spring-beans-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:528) ~[spring-beans-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1338) ~[spring-beans-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1177) ~[spring-beans-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:557) ~[spring-beans-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517) ~[spring-beans-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323) ~[spring-beans-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$171/204684384.getObject(Unknown Source) ~[na:na]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321) ~[spring-beans-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:207) ~[spring-beans-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:211) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:202) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addServletContextInitializerBeans(ServletContextInitializerBeans.java:96) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.<init>(ServletContextInitializerBeans.java:85) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.getServletContextInitializerBeans(ServletWebServerApplicationContext.java:253) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.selfInitialize(ServletWebServerApplicationContext.java:227) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext$$Lambda$386/305651902.onStartup(Unknown Source) ~[na:na]
at org.springframework.boot.web.embedded.tomcat.TomcatStarter.onStartup(TomcatStarter.java:53) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5135) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1384) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1374) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_25]
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134) ~[na:1.8.0_25]
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:909) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:841) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1384) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1374) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_25]
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134) ~[na:1.8.0_25]
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:909) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:262) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:421) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:930) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at org.apache.catalina.startup.Tomcat.start(Tomcat.java:459) ~[tomcat-embed-core-9.0.27.jar:9.0.27]
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:107) ~[spring-boot-2.2.1.RELEASE.jar:2.2.1.RELEASE]
... 12 common frames omitted
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'javax.servlet.Filter' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Qualifier(value=httpTraceFilter)}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1695) ~[spring-beans-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1253) ~[spring-beans-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1207) ~[spring-beans-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:874) ~[spring-beans-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:778) ~[spring-beans-5.2.1.RELEASE.jar:5.2.1.RELEASE]
... 54 common frames omitted

查看 spring 官方对应关系和官方文档

spring 官方对应关系:https://start.spring.io/actuator/info

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"Finchley.M2": "Spring Boot >=2.0.0.M3 and <2.0.0.M5",
"Finchley.M3": "Spring Boot >=2.0.0.M5 and <=2.0.0.M5",
"Finchley.M4": "Spring Boot >=2.0.0.M6 and <=2.0.0.M6",
"Finchley.M5": "Spring Boot >=2.0.0.M7 and <=2.0.0.M7",
"Finchley.M6": "Spring Boot >=2.0.0.RC1 and <=2.0.0.RC1",
"Finchley.M7": "Spring Boot >=2.0.0.RC2 and <=2.0.0.RC2",
"Finchley.M9": "Spring Boot >=2.0.0.RELEASE and <=2.0.0.RELEASE",
"Finchley.RC1": "Spring Boot >=2.0.1.RELEASE and <2.0.2.RELEASE",
"Finchley.RC2": "Spring Boot >=2.0.2.RELEASE and <2.0.3.RELEASE",
"Finchley.SR4": "Spring Boot >=2.0.3.RELEASE and <2.0.999.BUILD-SNAPSHOT",
"Finchley.BUILD-SNAPSHOT": "Spring Boot >=2.0.999.BUILD-SNAPSHOT and <2.1.0.M3",
"Greenwich.M1": "Spring Boot >=2.1.0.M3 and <2.1.0.RELEASE",
"Greenwich.SR6": "Spring Boot >=2.1.0.RELEASE and <2.1.16.BUILD-SNAPSHOT",
"Greenwich.BUILD-SNAPSHOT": "Spring Boot >=2.1.16.BUILD-SNAPSHOT and <2.2.0.M4",
"Hoxton.SR6": "Spring Boot >=2.2.0.M4 and <2.3.2.BUILD-SNAPSHOT",
"Hoxton.BUILD-SNAPSHOT": "Spring Boot >=2.3.2.BUILD-SNAPSHOT and <2.4.0.M1",
"2020.0.0-SNAPSHOT": "Spring Boot >=2.4.0.M1"

官方文档:https://spring.io/projects/spring-cloud

目的

在调用接口时,要求必须有输入正确的图形验证码才能调用(防刷)。
但是,看一些代码中将这个功能和其它业务功能耦合在一起。每次有新的接口需要用时,又得重新复制一份,就想到值得优化重构。

思路

生成时,从全部字母和数字中随机获取 6 个字符。
接着借助 BufferedImage 和 ImageIO 工具生成字节码格式的图形验证码。
再将字节码形式的图形验证码转换成 base64 字符串,等待发给前端,前端接收后可以转换并显示出来(这样我们就可以统一用 json 的方式来和前端交互,返回格式是一样的;还有一种方式是通过回传 IMAGE 类型的方式,不过缺点是接口不统一,前端要单独处理)。

我们随机生成一个唯一标识(uniqueId),标识这次图形验证码请求,并将对应的验证码(graphValidateCode)存放到缓存(redis)中,并设置过期时间。
下一个请求需要带上这个唯一标识,和验证码。
结合过滤器,这样获取验证码的逻辑就可以和其它业务逻辑解耦了。

哪个接口需要有正确的图形验证码才放行,只需要配置上 api 即可。

过滤器配置

在过滤器中(GraphValidateCodeFilter)验证图形验证码,从缓存中获取,然后比对传入的验证码。
如果没传入参数,或者参数不对,就抛出错误,在统一管理错误的地方进行处理。

错误处理

一般情况,异常是在定义了 ControllerAdvice 注解 或者 RestControllerAdvice 注解的类中管理的。
但是过滤器中的异常是在 Servlet 之前调用的,ControllerAdvice 适用于 Controller。
不过可以换个思路,我们写一个类来继承 ErrorController ,重写 handleError 方法,在过滤器中异常发生时,最后也会来到这个 handleError 方法中,
我们在这里可以直接返回,也可以继续抛出异常,如果选择了继续抛出,接着就可以在 GlobalExceptionHandler 中进行捕获(统一出错出口),
这样就可以解决 filter 在 Servlet 之前调用问题。

参考资料:
How to ExceptionHandler of type ExpiredJwtException from JwtAuthenticationTokenFilter? · Issue #63 · szerhusenBC/jwt-spring-security-demo
It’s because the filter comes before Servlet is invoked. ControllerAdvice only applies to the Controller classes.
I’m also trying to find the solution to same problem. Been reading some stackoverflow articles, this one seems promising: https://stackoverflow.com/questions/34595605/how-to-manage-exceptions-thrown-in-filters-in-spring @josevlad
https://stackoverflow.com/a/50818385

最终效果

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
/*
* 先获取图形验证码
* http://localhost:8080/api/tool/getGraphValidateCode
* {
* "err_code": 200,
* "err_msg": "ok",
* "data": {
* "image": "iVBORw0KGgoAAAANSUhEUgAAAG4AAAAjCAIAAAD6/6geAAABWklEQVR42u2a0RaEIAhE+f8P3d9oX/alzXIYQLBDp8dCveE4YnIcn75dbmkEjbJRNsq+G2WjbJSNEnv0fGnfIh4WzWVpVzv2YVhxjPVKlHijQod7PUpt0+KOIwJldLuEml0fKIry17kaKNHels1KQkzc+6yiKZxevHuCX99FRHkPlAQUGuW0xTsV2ikr45yDi7jLet1R2AvWCS1DeXrFJToqzKko7YO9Nee+WQn6r61RTsblm/PGXQe3guO7HXAImRP8obulzNBmKP/SZF+U6kU8bgUnNn8gSpWWaVFqBW2RGTI2pLLKYJ+Nifm0MO5YGUIWGQtKdKeYVc4gJH+adEQlgtnGAB5zdVZqBY5zo3ZPyhWeV09wxOcXQanuQ1bBaheUU90wVVW9zvByYxpLy4NomQfHAd8yuhr0NKXyT+Jj0jPUb4xneon/GmKkxhhTvS4V+lGkUnoSTL8omDLPAIEmVgAAAABJRU5ErkJggg==",
* "graphValidateCode": "fHELQ",
* "uniqueId": "d476ef5e8d6b4e34a7dd5fde9fcd3c78"
* }
* }
*/

/*
* 不填写时:
* http://localhost:8080/api/user/login
* {
* "err_code": 400,
* "err_msg": "缺少参数:uniqueId",
* "data": null
* }
*/

/*
* 错误时:
* http://localhost:8080/api/user/login?uniqueId=bbbb&graphValidateCode=aaaa
* {
* "err_code": 400,
* "err_msg": "输入的图形验证码不正确",
* "data": null
* }
*/

/*
* 正确时:
* http://localhost:8080/api/user/login?uniqueId=d476ef5e8d6b4e34a7dd5fde9fcd3c78&graphValidateCode=fHELQ
* {
* "err_code": 200,
* "err_msg": "ok",
* "data": "验证了图形验证码才能看到我:login"
* }
*/

参考资料

RuoYi-VUE

源代码

https://github.com/lyloou/spring-master/tree/master/spring-security-captcha

引入依赖

1
2
3
4
5
6
7
8
9
10
<!-- pom.xml -->
<!-- Logback -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>

添加配置

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
<!-- logback-spring.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<contextName>logback</contextName>
<!--输出到控制台 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n </pattern>
</encoder>
</appender>

<!--按天生成日志 -->
<appender name="logFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern> /logs/spring-lovelist/%d{yyyy-MM-dd}/%d{yyyy-MM-dd}.%i.log </FileNamePattern>
<maxFileSize>200MB</maxFileSize>
<maxHistory>100</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>

<root level="info">
<appender-ref ref="console" />
<appender-ref ref="logFile" />
</root>
</configuration>

1
2
3
4
5
6
7
8
9
    A                   B
/ \ /|\
C D E F G
/ \ / \
H I J K
/ \
L M
/ \
N O
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id  pid name age  email
1 0 A 18 A@baomidou.com
2 0 B 20 B@baomidou.com
3 1 C 28 C@baomidou.com
4 1 D 21 D@baomidou.com
5 2 E 24 E@baomidou.com
6 2 F 18 F@baomidou.com
7 2 G 10 G@baomidou.com
8 3 H 35 H@baomidou.com
9 3 I 21 I@baomidou.com
10 5 J 18 J@baomidou.com
11 5 K 23 K@baomidou.com
12 9 L 18 L@baomidou.com
13 9 M 33 M@baomidou.com
14 13 N 22 N@baomidou.com
15 13 O 11 O@baomidou.com

背景

节点和父节点之间只有 id 和 pid 上有关联;没有层级字段,没有当前所在的树高度字段,如何在不改变表的情况下,获取所有子节点?;

如果数据库可以重新设计的话,可以用下面的链接介绍的方式来设计:
在数据库中存储一棵树,实现无限级分类 - 个人文章 - SegmentFault 思否

思路

递归的方式

找到自己的子节点,再找子子节点,再找子子子节点…
这种方式虽能实现,但是如果节点关系复杂越来,因为要每次都要从 sql 中建立连接并查询,就会越来越慢
(递归的方式 200 多个后代要 2s 左右才能查出来; 在同样的数据下,用下面介绍的迭代的方式来查不到 100ms)。

迭代的方式

  1. 根据 id 查找后代节点时,拼成集合的方式 ids=set(id),根据 select id,pid from user where pid in (ids) 获取到所有的子节点(拼成实体对象:如具有 id 和 pid 属性的 ParentChild),加入到 ParentChild 列表中;
  2. 根据 1 获取的 id 拼成一组新的 ids 获取第二波数据的所有子节点;以此类推,直到 ids 为空为止,说明再无后代了;
  3. 根据 ParentChild 列表来构建 map 结构(其中的 key 是节点 ID,value 是直属子节点 ID 列表)
  4. 通过在第 3 步获取的 map 缓存,来进行更多的操作,如:查找直属子节点、 查找所有后代;

通过迭代的方式,一波一波地获取后代再数据库中进行查询,如果有 10 代,只需要从数据库中,查 10 次即可
(用递归的话,可能得成千上万次了),当然如果用户量特别多的时候,就不要用这种只有 id 和 pid 的方式了,得重新设计表了;
在数据库中存储一棵树,实现无限级分类 - 个人文章 - SegmentFault 思否

如何查找直属子节点

根据节点 id,直接从 map 缓存中获取即可;

如何查找所有后代

第 1 步: 建 1 个 set 集合,用来存储所有的后代;
第 2 步: 根据节点 id,从 map 中先找到直属子节点列表,加入到 set 中;
第 3 步: 再根据第 2 步得到的子节点列表,遍历这些子节点,从 map 中获取这些子节点的子节点(递归到第 2 步)

代码实现

service 层实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

@Override
public List<User> listDirectChild(Integer userId) {
AbsUserTreeHelper helper = newUserTreeHelper(userId);
return getUsers(helper.getDirectChildIds(userId));
}

private AbsUserTreeHelper newUserTreeHelper(Integer userId) {
checkUserId(userId);
AbsUserTreeHelper helper = new UserTreeHelper(this.baseMapper);
helper.init(userId);
return helper;
}

private void checkUserId(Integer userId) {
if (userId == null) {
throw new ParamException("参数无效:userId为" + userId);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class UserTreeHelper extends AbsUserTreeHelper {
UserMapper userMapper;

public UserTreeHelper(UserMapper userMapper) {
this.userMapper = userMapper;
}

@Override
protected List<ParentChild> listParentChild(List<Integer> userIdList) {
return this.userMapper.listParentChild(userIdList);
}
}

mapper 层实现

1
2
3
4
public interface UserMapper extends BaseMapper<User> {

List<ParentChild> listParentChild(@Param("userIdList") List<Integer> userIdList);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lyloou.demo.mapper.UserMapper">

<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.lyloou.demo.model.User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
<result column="email" property="email"/>
</resultMap>

<select id="listParentChild" resultType="com.lyloou.demo.model.ParentChild">
select id as childId, pid as parentId from t_user where pid in
<foreach item="item" index="index" collection="userIdList"
open="(" separator="," close=")">
#{item}
</foreach>
</select>

</mapper>

UserTreeHelper 源码

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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// UserTreeHelper.java

/**
* 获取用户的直接下属,或全部下属
*/
public class UserTreeHelper {

private Map<Integer, List<Integer>> parentToChildrenMap;

/**
* 获取map中的所有用户ID
*
* @return 用户ID集合
*/
public Set<Integer> getAllIds() {
checkInit();
Set<Integer> allIds = new HashSet<>();
Set<Integer> keySet = parentToChildrenMap.keySet();
for (Integer key : keySet) {
allIds.add(key);
allIds.addAll(getAllChildIds(key));
}
return allIds;
}

/**
* 获取树上的全部用户ID列表(不包含自己)
*
* @param userId 用户ID
* @return 全部用户ID
*/
public Set<Integer> getAllChildIds(Integer userId) {
checkInit();
return getAllChildIds(userId, null);
}

private void checkInit() {
if (parentToChildrenMap == null) {
throw new RuntimeException("没有调用初始化方法,请调用init方法初始化");
}
}

private Set<Integer> getAllChildIds(Integer userId, Set<Integer> idSet) {
if (idSet == null) {
idSet = new HashSet<>();
}
List<Integer> idList = parentToChildrenMap.getOrDefault(userId, Collections.emptyList());
if (idList.isEmpty()) {
return idSet;
}
idSet.addAll(idList);
for (Integer id : idList) {
getAllChildIds(id, idSet);
}
return idSet;
}

/**
* 获取直属用户ID列表 (不包含自己)
*
* @param userId 用户的ID
* @return 直属用户ID列表
*/
public Set<Integer> getDirectChildIds(Integer userId) {
checkInit();
return new HashSet<>(parentToChildrenMap.getOrDefault(userId, new ArrayList<>()));
}


/**
* 根据用户ID来初始化 Helper
*
* @param userId 用户ID
*/
public void init(Integer userId) {
init(Collections.singletonList(userId));
}

/**
* 把一组用户和它们的后代缓存起来
* 后面可以通过map.get(userId) 的方式获取userId对应的直接子节点
*
* @param userIdList 用户列表
*/
public void init(List<Integer> userIdList) {
parentToChildrenMap = new HashMap<>();
iterateUser(userIdList);
}

/**
* 通过迭代的方式重新实现,一波一波地获取,而不是一个一个地获取
*
* @param userIdList 用户列表
*/
private void iterateUser(List<Integer> userIdList) {
Set<ParentChild> allParentChildren = new HashSet<>();
List<ParentChild> parentChildren = listParentChild(userIdList);
while (!parentChildren.isEmpty()) {
allParentChildren.addAll(parentChildren);

// 获取这一波的id
List<Integer> newRoundIds = parentChildren.stream().map(ParentChild::getChildId).collect(Collectors.toList());
parentChildren = listParentChild(newRoundIds);
}

for (ParentChild parentChild : allParentChildren) {
List<Integer> oldList = parentToChildrenMap.getOrDefault(parentChild.getParentId(), new ArrayList<>());
oldList.add(parentChild.getChildId());
parentToChildrenMap.put(parentChild.getParentId(), oldList);
}

}

public List<ParentChild> listParentChild(List<Integer> userIdList){
//伪代码 List<ParentChild> list = select id,pid from user where pid in (userIdList);
}

}

// ParentChild.java
@Data
public class ParentChild {
private Integer id;
private Integer pid;
}

// Main.java
public class Main{
public static void main(String[] args) {
UserTreeHelper helper = new UserTreeHelper();
helper.init(1);
List<Integer> ids = helper.getAllChildIds(1);
System.out.println(ids);
}
}

更多可查看源码实现

https://github.com/lyloou/spring-boot-web/blob/v1.1.0/src/main/java/com/lyloou/demo/service/helper/AbsUserTreeHelper.java

返回图片思路 1

Spring MVC: How to return image in @ResponseBody? - Stack Overflow

1
2
3
4
5
6
@ResponseBody
@RequestMapping(value = "/photo2", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE)
public byte[] testphoto() throws IOException {
InputStream in = servletContext.getResourceAsStream("/images/no_image.jpg");
return IOUtils.toByteArray(in);
}
1
2
3
4
5
6
7
8
9
// JavaScript
export function getcaptcha(params) {
return request({
url: '/api/user/getcaptcha',
method: 'get',
responseType: 'blob',
params,
});
}

返回图片思路 2

将图片作为 base64 字符串返回,前端渲染 base64 即可。

1
2
3
4
5
6
7
8
// 引入 hutool 的验证码功能
@Override
public String getCaptcha(String captchaKey) {
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(160, 80);
String captchaCode = lineCaptcha.getCode();
codeCache.set(captchaKey, captchaCode, Duration.ofMinutes(5).getSeconds());
return lineCaptcha.getImageBase64Data();
}

spring 异步处理上传的文件时,自动删除问题

UML 类图符号 各种关系说明以及举例

  • 依赖(Dependency):虚线箭头表示
  • 关联(Association):实线箭头表示
  • 聚合(Aggregation):带空心菱形头表示

    特征:属于是关联的特殊情况,体现部分-整体关系,是一种弱拥有关系;整体和部分可以有不一样的生命周期;是一种弱关联;

  • 组合(Composition):带实心菱形头的实线表示

    特征:属于是关联的特殊情况,也体现了体现部分-整体关系,是一种强“拥有关系”;整体与部分有相同的生命周期,是一种强关联;

  • 泛化(Generalization):带空心箭头的实线线表示 // 类继承类
  • 实现(Realization):空心箭头和虚线表示 // 类实现接口

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

/**
* 给定一个单链表 L:L0→L1→…→Ln-1→Ln ,
* 将其重新排列后变为: L0→Ln→L1→Ln-1→L2→Ln-2→…
* 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
* 示例 1:
* 给定链表 1->2->3->4, 重新排列为 1->4->2->3.
* 示例 2:
* 给定链表 1->2->3->4->5, 重新排列为 1->5->2->4->3.
*/
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
Node firstNode = createNode(4);
System.out.println("origin:" + firstNode);
resortNode(firstNode);
System.out.println("resorted:" + firstNode);

firstNode = createNode(5);
System.out.println("origin:" + firstNode);
resortNode(firstNode);
System.out.println("resorted:" + firstNode);
}

private static void resortNode(Node node) {
List<Node> list = new ArrayList<>();
while (node != null) {
list.add(node);
node = node.next;
}

for (int i = 0; i < list.size(); i++) {
Node head = list.get(i);
Node tail = list.get(list.size() - i - 1);
if (head == tail) {
head.next = null;
break;
}
if (head.next == tail) {
tail.next = null;
break;
}
tail.next = head.next;
head.next = tail;
}
}

private static Node createNode(int num) {
Node tail = new Node(num, null);
for (int i = num - 1; i > 0; i--) {
tail = new Node(i, tail);
}
return tail;
}

static class Node {
public Node(int value, Node next) {
this.value = value;
this.next = next;
}

int value;
Node next;

@Override
public String toString() {
return value + (next == null ? "" : ("->" + next));
}
}
}
/*
origin:1->2->3->4
resorted:1->4->2->3
origin:1->2->3->4->5
resorted:1->5->2->4->3
*/

委托教程

关于委托的教程,具体参考上面链接即可,下面直接给出SharedPreferences封装的代码。

SharedPreference 封装

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
class DefaultSPreference<T>(key: String, defaultValue: T) :
SPreference<T>("default", key, defaultValue)

open class SPreference<T>(
private val spName: String,
private val key: String,
private val defaultValue: T
) : ReadWriteProperty<Any?, T> {
val prefs: SharedPreferences by lazy {
App.instance.getSharedPreferences(spName, Context.MODE_PRIVATE)
}

override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return findPreference(key, defaultValue)
}

override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
putPreference(key, value)
}

private fun <T> findPreference(name: String, default: T): T = with(prefs) {
val res: Any = when (default) {
is Long -> getLong(name, default)
is String -> getString(name, default) ?: default
is Int -> getInt(name, default)
is Boolean -> getBoolean(name, default)
is Float -> getFloat(name, default)
else -> throw IllegalArgumentException("This type can not be resolved in SpPreferences")
}
@Suppress("UNCHECKED_CAST")
res as T
}

private fun <U> putPreference(name: String, value: U) = with(prefs.edit()) {
when (value) {
is Long -> putLong(name, value)
is String -> putString(name, value)
is Int -> putInt(name, value)
is Boolean -> putBoolean(name, value)
is Float -> putFloat(name, value)
else -> throw IllegalArgumentException("This type can not be saved into SpPreferences")
}.apply()
}
}

fun <T> SPreference<T>.remove(key: String) {
prefs.edit().remove(key).apply()
}

fun <T> SPreference<T>.clear() {
prefs.edit().clear().apply()
}

用法

1
var a: String by DefaultSPreference(Key.SCHEDULE_ITEM_A.name, "")

通过上面的委托,给 a 赋值就直接保存到 SharedPreferences 文件中了。

1
a = "your value here"

当获取 a 的值时,实际调用的就是 SharedPreferences 中key对应的值

1
toast(a)

如果使用了 data-binding,可以直接把 a,传递给 EditText
当页面加载后, EditText 中的值就会被 a 填充;
EditText 中的文字变化时,就会存到 SharedPreferences 中。

1
2
3
4
5
6
7
<EditText
android:id="@+id/editTextA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:padding="10dp"
android:text="@={data.a}"/>

更多

可以在flow项目中查看更多细节 https://github.com/lyloou/flow

参考资料:《kotlin for android developers》中文版——泛型 preference 委托

方法 1:排除冲突的依赖

1
2
# 查看 app 模块所依赖的项目(通过 grep 来过滤冲突的模块名称)
./gradlew -q app:dependencies | grep commons-codec
1
2
3
4
5
// 针对某个冲突模块排除
api("com.afollestad.material-dialogs:core:0.9.5.0") {
exclude group: 'com.android.support', module: 'support-v13'
exclude group: 'com.android.support', module: 'support-vector-drawable'
}
1
2
3
4
5
6
7
8
9
10
11
12
// 重置所有相同模块的版本号
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
def requested = details.requested
if (requested.group == 'com.android.support') {
if (!requested.name.startsWith("multidex")) {
details.useVersion '28.0.0'
}
}
}
}

方法 2:使用 androidx

如果是 com.android.support 库冲突可以使用 androidx 来解决。

具体查看 迁移到 AndroidX  |  Android 开发者  |  Android Developers

参考资料

0%