Java Spring Boot 基础

Java Spring Boot 基础与应用,其设计目的是用来简化新Spring应用的初始搭建以及开发过程

一个项目有多套前端目录配置

在原先staticstemplates目录前面加一级目录,默认为default,后面其他套前端页面根据需求增加比如default1default2等,很容易因为开发工具出现问题导致不生效需要注意。最好重新maven clean,maven install一下

freemarker的路径修改,key修改配置文件,也可以修改Java配置文件,只能选择一种。我选择的是修改Java配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class FreemarkerConfig {

@Bean
public FreeMarkerConfigurer freeMarkerConfigurer(ShiroTag shiroTag){
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/default/templates");
Map<String, Object> variables = new HashMap<>(1);
variables.put("shiro", shiroTag);
configurer.setFreemarkerVariables(variables);

Properties settings = new Properties();
settings.setProperty("default_encoding", "utf-8");
settings.setProperty("number_format", "0.##");
configurer.setFreemarkerSettings(settings);
return configurer;
}

}

修改静态文件的映射配置

1
2
3
4
5
6
7
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/statics/**").addResourceLocations("classpath:/default/statics/");
}
}

还有一种是对resources文件进行改名的,不建议使用,这个是改maven配置文件

1
2
3
4
5
6
7
8
<resources>
<resource>
<directory>src/main/res</directory>
<includes>
<include>**/*</include>
</includes>
</resource>
</resources>

多数据源配置

使用自定义数据源配置,取消默认数据源配置

通过切面对DRUID进行多数据源控制。还需要配置使用哪个配置,如果没有这么配置启动的时候会报循环引用错误org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerInvoker

1
2
3
4
5
6
7
8
sqlSessionFactory defined in class path resource [com/baomidou/mybatisplus/autoconfigure/MybatisPlusAutoConfiguration.class]
┌─────┐
| dataSource defined in class path resource [vip/infotech/test/common/datasource/DynamicDataSourceConfig.class]
↑ ↓
| firstDataSource defined in class path resource [vip/infotech/test/common/datasource/DynamicDataSourceConfig.class]
↑ ↓
| org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerInvoker
└─────┘

auto-configuration通过@EnableAutoConfiguration注解@SpringBootApplication包含该注解。@EnableAutoConfiguration注解开启了spring ApplicationContext的自动配置功能,它通过扫描classpath下的组件,满足不同Conditions的bean注册到容器中。当AutoConfiguration实现类打上@Configuration标签,可以作为spring配置类,当AutoConfiguration实现类打上@EnableConfigurationProperties标签,可以绑定自定义属性或者更多Conditional bean注册方法。在DataSourceAutoConfiguration中:

  1. DataSourceAutoConfiguration打上了@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })标签,这意味着只有当DataSource.class和EmbeddedDatabaseType.class出现在classpath时,DataSourceAutoConfiguration内的自动配置bean才可能被注册。
  2. 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
2
3
4
5
6
DataSourceInitializerInvoker(ObjectProvider<DataSource> dataSource, DataSourceProperties properties,
ApplicationContext applicationContext) {
this.dataSource = dataSource;
this.properties = properties;
this.applicationContext = applicationContext;
}

找到这个DataSourceInitializerInvoker是什么时候注入到IOC容器中的,因此我们找到了DataSourceAutoConfiguration,继而找到了DataSourceInitializationConfiguration这个配置类

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
/*
可以看到在这里通过EnableConfigurationProperties将DataSourceProperties注入到了Bean容器中
然后通过@Import将前面DataSourceConfiguration的内容去扫描添加到容器中
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {

@Configuration(proxyBeanMethods = false)
// EmbeddedDatabaseConfiguration生效条件1
@Conditional(EmbeddedDatabaseCondition.class)
// EmbeddedDatabaseConfiguration生效条件2,还没有生成DataSource、XADataSource类型的实例。XADataSource分布式事务的数据源
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
// 上面两个condition都满足的话,EmbeddedDataSourceConfiguration被加载
@Import(EmbeddedDataSourceConfiguration.class)
// 内置数据源配置类,加载H2,DERBY,HSQL这3种默认数据源
protected static class EmbeddedDatabaseConfiguration {

}

@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
// 上面两个condition都满足的话,import以下配置类
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
DataSourceJmxConfiguration.class })
// 加载池化的数据源,Hikari、Tomcat、Dbcp2、Generic都代表一种数据源
protected static class PooledDataSourceConfiguration {

}
}

abstract class DataSourceConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true)
static class Hikari {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
HikariDataSource dataSource(DataSourceProperties properties) {
HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}

}
}

static class EmbeddedDatabaseCondition extends SpringBootCondition {

private static final String DATASOURCE_URL_PROPERTY = "spring.datasource.url";

private final SpringBootCondition pooledCondition = new PooledDataSourceCondition();


public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("EmbeddedDataSource");
if (hasDataSourceUrlProperty(context)) {
return ConditionOutcome.noMatch(message.because(DATASOURCE_URL_PROPERTY + " is set"));
}
//PooledDataSourceConditions是否满足条件
if (anyMatches(context, metadata, this.pooledCondition)) {
return ConditionOutcome.noMatch(message.foundExactly("supported pooled data source"));
}
// com.zaxxer.hikari.HikariDataSource,org.apache.commons.dbcp2.BasicDataSource其中一个就表示满足条件
// classpath下有没有H2、DERBY、HSQL这3种类,如果有就返回
EmbeddedDatabaseType type = EmbeddedDatabaseConnection.get(context.getClassLoader()).getType();
if (type == null) {
return ConditionOutcome.noMatch(message.didNotFind("embedded database").atAll());
}
// 表示加载到了H2、DERBY、HSQL中的一种
return ConditionOutcome.match(message.found("embedded database").items(type));
}

private boolean hasDataSourceUrlProperty(ConditionContext context) {
Environment environment = context.getEnvironment();
if (environment.containsProperty(DATASOURCE_URL_PROPERTY)) {
try {
return StringUtils.hasText(environment.getProperty(DATASOURCE_URL_PROPERTY));
}
catch (IllegalArgumentException ex) {
// Ignore unresolvable placeholder errors
}
}
return false;
}

}


@Configuration(proxyBeanMethods = false)
@Import({ DataSourceInitializerInvoker.class, DataSourceInitializationConfiguration.Registrar.class })
class DataSourceInitializationConfiguration {

/**
* {@link ImportBeanDefinitionRegistrar} to register the
* {@link DataSourceInitializerPostProcessor} without causing early bean instantiation
* issues.
*/
static class Registrar implements ImportBeanDefinitionRegistrar {

private static final String BEAN_NAME = "dataSourceInitializerPostProcessor";

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
if (!registry.containsBeanDefinition(BEAN_NAME)) {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DataSourceInitializerPostProcessor.class);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
// We don't need this one to be post processed otherwise it can cause a
// cascade of bean instantiation that we would rather avoid.
beanDefinition.setSynthetic(true);
registry.registerBeanDefinition(BEAN_NAME, beanDefinition);
}
}

}

}

@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })。 不会执行里面代码,通过注解去掉DataSourceAutoConfiguration,启动时直接初始化加载用户定义的数据源。

解决方法:
可以dataSource脱离Spring控制也可以启动的时候排除DataSourceAutoConfiguration:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Import({DynamicDataSourceConfig.class})
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
public class TaskApplication extends SpringBootServletInitializer {

public static void main(String[] args) {
SpringApplication.run(TaskApplication.class, args);
}

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(AdminApplication.class);
}
}

JSON key 大写处理

处理Jason字符串中key为大写的数据,给实体类加上Json注解

1
2
@JsonProperty(value = "Code")
private int code;

表单验证

@Valid 注解

说明

注解 @Valid 的主要作用是用于数据效验,可以在定义的实体中的属性上,添加不同的注解来完成不同的校验规则

使用步骤

  1. Maven 引入相关依赖
  2. 自定义个异常类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class ParamErrorException extends RuntimeException {

    public ParamErrorException() {
    }

    public ParamErrorException(String msg) {
    super(msg);
    }

    }
  3. 自定义响应枚举类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public 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;
    }
    }
  4. 自定义响应对象类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Data
    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;
    }
    }
  5. 实体类中添加 @Valid 相关注解
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Data
    public class User {
    @NotBlank(message = "姓名不为空")
    private String username;
    @NotBlank(message = "密码不为空")
    private String password;
    @Valid
    @NotNull(message = "用户信息不能为空")
    private UserInfo userInfo;
    }
  6. Controller 中添加 @Valid 注解
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @RestController
    public class TestController {
    @Validated
    @GetMapping("/user/{username}")
    public ResponseResult findUserInfo(@PathVariable String username) {
    if (username == null || "".equals(username)) {
    throw new ParamErrorException("username 不能为空");
    }
    return new ResponseResult(ResultEnum.SUCCESS);
    }

    @PostMapping("/user")
    public ResponseResult save(@Valid @RequestBody User user) {
    return new ResponseResult(ResultEnum.SUCCESS);
    }
    }
  7. 全局异常处理类中处理 @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
    @Slf4j
    @RestControllerAdvice("xxx.xxx.xxx")
    public class GlobalExceptionHandler {

    /**
    * 忽略参数异常处理器
    *
    * @param e 忽略参数异常
    * @return ResponseResult
    */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public ResponseResult parameterMissingExceptionHandler(MissingServletRequestParameterException e) {
    log.error("", e);
    return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), "请求参数 " + e.getParameterName() + " 不能为空");
    }

    /**
    * 缺少请求体异常处理器
    *
    * @param e 缺少请求体异常
    * @return ResponseResult
    */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ResponseResult parameterBodyMissingExceptionHandler(HttpMessageNotReadableException e) {
    log.error("", e);
    return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), "参数体不能为空");
    }

    /**
    * 参数效验异常处理器
    *
    * @param e 参数验证异常
    * @return ResponseInfo
    */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    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
    */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler({ParamaErrorException.class})
    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);
    }

    }
  8. 启动类
  9. 测试

可能出现问题

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脚本。

解决方法

  1. 避免自己写的过滤器filter xss html sql等过滤器导致编码错误。或者把utf-8编码头给截取掉了。或者过滤截取的时候把标准json格式截取掉了一部分导致错误
  2. yml加上运行使用使用转义字符,与对应配置
    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    profiles:
    active: dev
    jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
    parser:
    allow_unquoted_control_chars: true
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Configuration
    public class JacksonConfig {
    @Bean
    @Primary
    @ConditionalOnMissingBean(ObjectMapper.class)
    public ObjectMapper getObjectMapper(Jackson2ObjectMapperBuilder builder) {
    ObjectMapper om = builder.build();
    return om;
    }
    }
  3. 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
    @Bean
    @Primary
    @ConditionalOnMissingBean(ObjectMapper.class)
    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>()
    {
    @Override
    public void serialize(Object o, JsonGenerator jsonGenerator,
    SerializerProvider serializerProvider)
    throws IOException
    {
    jsonGenerator.writeString("");
    }
    });
    return objectMapper;
    }
  4. 测试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
    @Data
    public class Tmp {
    @JsonProperty("tmp:id")
    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
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
public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
...
this.objectMapper.readValue(inputMessage.getBody(), javaType);
...
}
...
public final class ByteSourceJsonBootstrapper {
public JsonEncoding detectEncoding() throws IOException {
boolean foundEncoding = false;
int quad;
if (this.ensureLoaded(4)) {
quad = this._inputBuffer[this._inputPtr] << 24 | (this._inputBuffer[this._inputPtr + 1] & 255) << 16 | (this._inputBuffer[this._inputPtr + 2] & 255) << 8 | this._inputBuffer[this._inputPtr + 3] & 255;
if (this.handleBOM(quad)) {
foundEncoding = true;
} else if (this.checkUTF32(quad)) {
foundEncoding = true;
} else if (this.checkUTF16(quad >>> 16)) {
foundEncoding = true;
}
} else if (this.ensureLoaded(2)) {
quad = (this._inputBuffer[this._inputPtr] & 255) << 8 | this._inputBuffer[this._inputPtr + 1] & 255;
if (this.checkUTF16(quad)) {
foundEncoding = true;
}
}

JsonEncoding enc;
if (!foundEncoding) {
enc = JsonEncoding.UTF8; // 正常是会走到这一句,使用默认UTF-8
} else {
switch(this._bytesPerChar) {
case 1:
enc = JsonEncoding.UTF8;
break;
case 2:
enc = this._bigEndian ? JsonEncoding.UTF16_BE : JsonEncoding.UTF16_LE;
break;
case 3:
default:
throw new RuntimeException("Internal error");
case 4:
enc = this._bigEndian ? JsonEncoding.UTF32_BE : JsonEncoding.UTF32_LE;
}
}

this._context.setEncoding(enc);
return enc;
}
}

JSON解析报错

  • 请求头与返回头:使用application/json(标准写法)而不是text/json(解析会报错)

应用

启动的时候加载字典信息到内存

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
@Component
public class CodeCache {
public static Map<String, CityEntity> cityMap = new HashMap<String, CityEntity>();

@Autowired
private CityDao cityDao;

public void load() {
List<CityEntity> cityList = cityDao.select();
for (CityEntity city : cityList) {
cityMap.put(city.getCode(), city);
}
}

@PostConstruct
public void init() {
// 启动加载cityMap
load()
}

@PreDestroy
public void destroy() {
//系统运行结束
}

@Scheduled(cron = "0 0 0/2 * * ?")
public void testOne() {
// 每2小时执行一次缓存
load();
}
}

使用
@Autowired
Map<String, CityEntity> cityMap;
// 或者
Map<String, CityEntity> cityMap = CodeCache.cityMap;

@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
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
public class ThreadPoolConfig {
// 核心线程池大小
private int corePoolSize = 50;

// 最大可创建的线程数
private int maxPoolSize = 200;

// 队列最大长度
private int queueCapacity = 1000;

// 线程池维护线程所允许的空闲时间
private int keepAliveSeconds = 300;

@Bean(name = "threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(maxPoolSize);
executor.setCorePoolSize(corePoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
// 线程池对拒绝任务(无线程可用)的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}


@Autowired
ThreadPoolTaskExecutor threadPoolTaskExecutor;


@Async
public void run() { // 可以是其他方法名,根据具体实现来设置
}

需要在test方法断点,dump查看内存中是否真的使用threadPoolTaskExecutor进行执行。如果配置错误可能会使用SimpleAsyncTaskExecutor执行,就不是真正线程池了,断点的时候Frames中可以看到:

1
2
3
4
run:26, TestTask (com.test.a.b.c)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)

使用场景

处理各种复杂任务,创建任务丢给线程池异步执行,还可以用于把复杂任务抽象分类成各种独立任务,根据消息触发事先判断需要执行哪种任务,丢给线程池执行减少当个业务类代码复杂度。减少业务代码复杂性业务代码只需要写业务任务就可以了。复用线程资源减少创建销毁线程的消耗。

线程池可能多个线程都需要操作同一个数据参数,但是这个数据参考可能只需要处理一次就可以了,这种情况可以考虑把处理操作提前到线程池之前的主线程处理,推迟到线程池之后是不行的,那样就还得判断线程池对应任务是否都执行完成。还有一种是主要线程把信息预先先获取出来传递给子线程使用,当参数传递进去子线程还少了从数据库等读取的操作。

可能需要保证线程组之间执行顺序就需要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
2
3
4
5
<!--dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency-->

log4j有安全漏洞

  1. 升级版本
  2. 改成logback
  3. 如果没有用到就移除掉吧
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
<!-- 一些参考:springboot默认是用logback,里面只引用到log4j2 api,没有core包实际上问题不大,需要用log4j2的话得用spring-boot-starter-log4j2。包冲突可以进行排除操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- 单独引入的可以这么改 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>

IDEA可以通过Maven→Show Denpendencies…查看相关依赖

扩展

不连接数据库启动?

对于需要用到数据库的项目,貌似不行的,没连接数据库工程启动不了。没办法初始化工作,项目也就没办法启动,数据库如果中途挂掉了,项目也得重新启动,这个是强关联的,没数据库业务都无法跑了。
其实druid默认是会无限重连的。

参考