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
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
@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;

参考