Java Spring Boot 基础与应用,其设计目的是用来简化新Spring应用的初始搭建以及开发过程
一个项目有多套前端目录配置
在原先statics
与templates
目录前面加一级目录,默认为default
,后面其他套前端页面根据需求增加比如default1
,default2
等,很容易因为开发工具出现问题导致不生效需要注意。最好重新maven clean,maven install一下
freemarker的路径修改,key修改配置文件,也可以修改Java配置文件,只能选择一种。我选择的是修改Java配置文件
1 |
|
修改静态文件的映射配置
1 |
|
还有一种是对resources文件进行改名的,不建议使用,这个是改maven配置文件
1 | <resources> |
多数据源配置
使用自定义数据源配置,取消默认数据源配置
通过切面对DRUID进行多数据源控制。还需要配置使用哪个配置,如果没有这么配置启动的时候会报循环引用错误org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerInvoker
1 | sqlSessionFactory defined in class path resource [com/baomidou/mybatisplus/autoconfigure/MybatisPlusAutoConfiguration.class] |
auto-configuration通过@EnableAutoConfiguration注解@SpringBootApplication包含该注解。@EnableAutoConfiguration注解开启了spring ApplicationContext的自动配置功能,它通过扫描classpath下的组件,满足不同Conditions的bean注册到容器中。当AutoConfiguration实现类打上@Configuration标签,可以作为spring配置类,当AutoConfiguration实现类打上@EnableConfigurationProperties标签,可以绑定自定义属性或者更多Conditional bean注册方法。在DataSourceAutoConfiguration中:
- DataSourceAutoConfiguration打上了@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })标签,这意味着只有当DataSource.class和EmbeddedDatabaseType.class出现在classpath时,DataSourceAutoConfiguration内的自动配置bean才可能被注册。
- DataSourceAutoConfiguration打上了@EnableConfigurationProperties(DataSourceProperties.class)标签,意味着配置文件中的属性和DataSourceProperties类自动绑定了。
配置文件中spring:datasource开头的属性将自动绑定到DataSourceProperties对象上,其他注解,如@ConditionalOnMissingBean, @ConditionalOnClass and @ConditionalOnProperty等,标识只要条件满足,bean definition将注册到ApplicationContext中。
spring boot 会默认加载org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
DataSourceAutoConfiguration类使用了@Configuration注解向spring注入了一个dataSource bean。可能是使用的时候又去创建了一个dataSource,dataSource创建,需要依赖DataSourceInitializerInvoker,DataSourceInitializerInvoker创建需要依赖dataSource发现dataSource正在创建,循环依赖。
1 | DataSourceInitializerInvoker(ObjectProvider<DataSource> dataSource, DataSourceProperties properties, |
找到这个DataSourceInitializerInvoker是什么时候注入到IOC容器中的,因此我们找到了DataSourceAutoConfiguration,继而找到了DataSourceInitializationConfiguration这个配置类
1 | /* |
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
。 不会执行里面代码,通过注解去掉DataSourceAutoConfiguration
,启动时直接初始化加载用户定义的数据源。
解决方法:
可以dataSource脱离Spring控制也可以启动的时候排除DataSourceAutoConfiguration:
1 |
|
JSON key 大写处理
处理Jason字符串中key为大写的数据,给实体类加上Json注解
1 |
|
表单验证
@Valid 注解
说明
注解 @Valid 的主要作用是用于数据效验,可以在定义的实体中的属性上,添加不同的注解来完成不同的校验规则
使用步骤
- Maven 引入相关依赖
- 自定义个异常类
1
2
3
4
5
6
7
8
9
10public class ParamErrorException extends RuntimeException {
public ParamErrorException() {
}
public ParamErrorException(String msg) {
super(msg);
}
} - 自定义响应枚举类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public enum ResultEnum {
SUCCESS(1000, "请求成功"), // 0
PARAMETER_ERROR(1001, "请求参数有误!"),
UNKNOWN_ERROR(9999, "未知的错误!");
private Integer code;
private String msg;
ResultEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
} - 自定义响应对象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ResponseResult {
private Integer code;
private String msg;
public ResponseResult(){
}
public ResponseResult(ResultEnum resultEnum){
this.code = resultEnum.getCode();
this.msg = resultEnum.getMsg();
}
public ResponseResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
} - 实体类中添加 @Valid 相关注解
1
2
3
4
5
6
7
8
9
10
public class User {
private String username;
private String password;
private UserInfo userInfo;
} - Controller 中添加 @Valid 注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestController {
public ResponseResult findUserInfo( String username){
if (username == null || "".equals(username)) {
throw new ParamErrorException("username 不能为空");
}
return new ResponseResult(ResultEnum.SUCCESS);
}
public ResponseResult save( User user){
return new ResponseResult(ResultEnum.SUCCESS);
}
} - 全局异常处理类中处理 @Valid 抛出的异常
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
public class GlobalExceptionHandler {
/**
* 忽略参数异常处理器
*
* @param e 忽略参数异常
* @return ResponseResult
*/
public ResponseResult parameterMissingExceptionHandler(MissingServletRequestParameterException e) {
log.error("", e);
return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), "请求参数 " + e.getParameterName() + " 不能为空");
}
/**
* 缺少请求体异常处理器
*
* @param e 缺少请求体异常
* @return ResponseResult
*/
public ResponseResult parameterBodyMissingExceptionHandler(HttpMessageNotReadableException e) {
log.error("", e);
return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), "参数体不能为空");
}
/**
* 参数效验异常处理器
*
* @param e 参数验证异常
* @return ResponseInfo
*/
public ResponseResult parameterExceptionHandler(MethodArgumentNotValidException e) {
log.error("", e);
// 获取异常信息
BindingResult exceptions = e.getBindingResult();
// 判断异常中是否有错误信息,如果存在就使用异常中的消息,否则使用默认消息
if (exceptions.hasErrors()) {
List<ObjectError> errors = exceptions.getAllErrors();
if (!errors.isEmpty()) {
// 这里列出了全部错误参数,按正常逻辑,只需要第一条错误即可
FieldError fieldError = (FieldError) errors.get(0);
return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), fieldError.getDefaultMessage());
}
}
return new ResponseResult(ResultEnum.PARAMETER_ERROR);
}
/**
* 自定义参数错误异常处理器
*
* @param e 自定义参数
* @return ResponseInfo
*/
public ResponseResult paramExceptionHandler(ParamErrorException e) {
log.error("", e);
// 判断异常中是否有错误信息,如果存在就使用异常中的消息,否则使用默认消息
if (!StringUtils.isEmpty(e.getMessage())) {
return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), e.getMessage());
}
return new ResponseResult(ResultEnum.PARAMETER_ERROR);
}
} - 启动类
- 测试
可能出现问题
The valid characters are defined in RFC 7230 and RFC 3986
1)使用Tomcat7.0.69之前的版本
2)对url的特殊字符进行转义(推荐)。浏览器端处理encodeURI()
可以用于整个路径或者encodeURIComponent()
用于参数因为会把/也进行转码
3)修改tomcat配置文件
JSON parse error: Invalid UTF-8 start byte 0xa1; nested exception is com.fasterxml.jackson.core.JsonParseException: Invalid UTF-8 start byte 0xa1
Unrecognized token 'c': was expecting ('true', 'false' or 'null')
格式出错了
对于特殊字符前端需要进行转义,避免直接执行html,或者js脚本。
解决方法
- 避免自己写的过滤器
filter
xss html sql等过滤器导致编码错误。或者把utf-8编码头给截取掉了。或者过滤截取的时候把标准json格式截取掉了一部分导致错误 - yml加上运行使用使用转义字符,与对应配置
1
2
3
4
5
6
7
8spring:
profiles:
active: dev
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
parser:
allow_unquoted_control_chars: true1
2
3
4
5
6
7
8
9
10
public class JacksonConfig {
public ObjectMapper getObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper om = builder.build();
return om;
}
} - java配置
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
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder)
{
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
// 通过该方法对mapper对象进行设置,所有序列化的对象都将按改规则进行系列化
// Include.Include.ALWAYS 默认
// Include.NON_DEFAULT 属性为默认值不序列化
// Include.NON_EMPTY 属性为 空("") 或者为 NULL 都不序列化,则返回的json是没有这个字段的。这样对移动端会更省流量
// Include.NON_NULL 属性为NULL 不序列化
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 允许出现特殊字符和转义符
objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
// 允许出现单引号
objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
// 字段保留,将null值转为""
objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>()
{
public void serialize(Object o, JsonGenerator jsonGenerator,
SerializerProvider serializerProvider)
throws IOException
{
jsonGenerator.writeString("");
}
});
return objectMapper;
} - 测试demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Tmp {
private String id;
private String name;
}
public class Test {
public static void main(String args[]) {
ObjectMapper mapper = new ObjectMapper();
mapper.disable(SerializationFeature.WRITE_NULL_MAP_VALUES);
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.enable(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS);
try {
Tmp tmp = mapper.readValue("{\"tmp:id\":\"ab>c\",\"name\":null}", Tmp.class);
System.out.println(tmp.getId());
System.out.println(tmp.getName());
} catch (IOException e) {
e.printStackTrace();
}
}
}
错误位置代码:
1 | public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> { |
JSON解析报错
- 请求头与返回头:使用application/json(标准写法)而不是text/json(解析会报错)
应用
启动的时候加载字典信息到内存
1 |
|
@Autowired
@Autowired(required=true):当使用@Autowired注解的时候,其实默认就是@Autowired(required=true),表示注入的时候,该bean必须存在,否则就会注入失败
@Autowired(required=false):表示忽略当前要注入的bean,如果有直接注入,没有跳过,不会报错
在注入的过程中,扫描到公共方法中要注入的bean,并未找到,强行注入就会注入失败。我们又不能单独的去除改方法,所以我们采取的思想就是有bean就注入,没有就不注入。解决办法就是@Autowired(required=false)。
容器的启动顺序:
先加载父容器(spring),后加载子容器(springmvc)。所以在Controller里面注入service时,父容器中的bean已经初始化完毕,所以正常注入。
在父子容器中,父容器元素对子容器可见,子容器对父容器的元素不可见。所以父容器中不能获取子容器的元素,但是子容器可以获取父容器的元素。
当前容器均可获取当前容器中的元素,也就是说在service中可以注入其他service。但是,当前容器不可以注入自己。
启动初始化
ApplicationRunner或者CommandLineRunner
线程池
线程池拒绝策略CallerRunsPolicy,当线程数线程池的最大线程数并且阻塞队列已满的情况下,后到的数据会执行拒绝策略,让调用线程(提交任务的线程)直接执行此任务,导致数据处理顺序不一致,当然多线程本来顺序就不应该一致,一致就是顺序执行了。
建议使用自定义线程池:
实现接口AsyncConfigurer 或者 继承AsyncConfigurerSupport 或者 配置自定义的TaskExecutor
配置自定义的TaskExecutor可以定义多个线程池名称:如果定义了多个就得指定线程池名称@Async("threadPoolTaskExecutor")
,不然会使用系统默认的SimpleAsyncTaskExecutor,如果只定义了一个线程池就会使用新定义的线程池替换系统默认的线程池。具体细节可以看源码。
线程池配置使用关键点
1 | public class ThreadPoolConfig { |
需要在test方法断点,dump查看内存中是否真的使用threadPoolTaskExecutor进行执行。如果配置错误可能会使用SimpleAsyncTaskExecutor执行,就不是真正线程池了,断点的时候Frames中可以看到:
1 | run:26, TestTask (com.test.a.b.c) |
使用场景
处理各种复杂任务,创建任务丢给线程池异步执行,还可以用于把复杂任务抽象分类成各种独立任务,根据消息触发事先判断需要执行哪种任务,丢给线程池执行减少当个业务类代码复杂度。减少业务代码复杂性业务代码只需要写业务任务就可以了。复用线程资源减少创建销毁线程的消耗。
线程池可能多个线程都需要操作同一个数据参数,但是这个数据参考可能只需要处理一次就可以了,这种情况可以考虑把处理操作提前到线程池之前的主线程处理,推迟到线程池之后是不行的,那样就还得判断线程池对应任务是否都执行完成。还有一种是主要线程把信息预先先获取出来传递给子线程使用,当参数传递进去子线程还少了从数据库等读取的操作。
可能需要保证线程组之间执行顺序就需要CountDownLatch和CyclicBarrier,也就算监工线程等各个子线程都countdown了再执行后面语句继续分配任务执行。
注入带参数的构造函数
@Configuration+ @Bean注解来实现注入
或者在需要用的地方直接用springBeanUtils.getBeanName("xxxx"); applicationContext.getBean(name, requiredType);
也是不错的替代方案。在父类注入参数的话还是要比较谨慎,大概率子类需要的对象是不一样的,不一定是父类注入的那个类,更多的情况是不同子类需要根据具体需求注入不同对象进行操作比其他如XXservice、redisUtils等,当然如果是各个子类都有用到的参数还是可以考虑在父类注入的,如果各个方法都封装到service就是子类去调用具体的service就行其实不需要注入比如redis工具类具体实现在service中实现。
文件监听
Apache io 没有使用inotify其实文件监听大多数情况不会成为性能瓶颈,所以用不用操作体统实现差别不大。保证旧数据被处理可以再启动一个线程池处理,也可以弄两个队列一个放旧文件,一个放最新监听到的文件,还可以结合CountDownLatch。保证顺序可以通过多种方式实现
问题
spring boot从redis取缓存发生java.lang.ClassCastException异常
使用的类加载器不一样。主要原因是pom文件中引入了DevTools配置。 当你使用DevTools进行缓存时,需要了解这一限制。 当对象序列化到缓存中时,应用程序类加载器是C1。然后,更改一些代码或者配置后,devtools会自动重新启动上下文并创建一个新的类加载器C2。所以当你通过redis操作获取缓存反序列化的时候应用的类加载器是C2,虽然包名及其来类名完全一致,但是序列化与反序列化是通过不同的类加载器加载则在JVM中它们也不是同一个类。如果缓存库没有考虑上下文类加载器,那么这个对象会附加错误的类加载器 ,也就是我们常见的类强制转换异常(ClassCastException)。
将devtools热部署注释掉
1 | <!--dependency> |
log4j有安全漏洞
- 升级版本
- 改成logback
- 如果没有用到就移除掉吧
1 | <!-- 一些参考:springboot默认是用logback,里面只引用到log4j2 api,没有core包实际上问题不大,需要用log4j2的话得用spring-boot-starter-log4j2。包冲突可以进行排除操作 --> |
IDEA可以通过Maven→Show Denpendencies…查看相关依赖
扩展
不连接数据库启动?
对于需要用到数据库的项目,貌似不行的,没连接数据库工程启动不了。没办法初始化工作,项目也就没办法启动,数据库如果中途挂掉了,项目也得重新启动,这个是强关联的,没数据库业务都无法跑了。
其实druid默认是会无限重连的。
参考
- SpringBoot 中使用 @Valid 注解 + Exception 全局处理器优雅处理参数验证
- Spring Boot Admin服务监控
- springboot 整合线程池(通俗易懂)
- springboot+线程池使用
- 理解 spring-boot-starter-parent
- 都在建议你不要直接使用 @Async 注解,为什么?
- Spring使用ThreadPoolTaskExecutor自定义线程池及实现异步调用
- SpringBoot - 使用ExecutorService线程池执行异步任务教程(Runnable任务为例)
- spring boot 定义线程池多线程执行任务
- 线程通信——CountDownLatch和CyclicBarrier
- Java多线程–等待所有子线程执行完的五种方法
- @Autowired 警告 Field injection is not recommended Spring @Autowired注入
- Field injection is not recommended 依赖注入方式
- 你还在用@Autowired吗
- @Autowired报错原因分析和4种解决方案!
- 一文讲尽门面日志slf4j和log4j、log4j2、logback依赖jar引用关系
- maven dependency全局排除
- Spring Boot(十)Logback和Log4j2集成与日志发展史
- Springboot2.0 配置Log4j的Slf4j冲突问题
- 解决Spring Boot Druid 在项目启动时无限次重连数据库
- java.lang.IllegalStateException: Duplicate key异常问题