- 发表于
SpringBoot
- Authors
- 作者
- Masachi Zhang
- @MasachiZhang
Spring Boot
Spring maven包相关
spring-boot-starter-web: SpringMVC相关依赖,Server容器采用Tomcat
spring-boot-autoconfigure:SpringBoot自动化配置
spring-boot-starter-data-redis: Redis Lettuce 相关
spring-boot-starter-data-jpa: JPA相关 由于国内用的人不多并且用不好 因此国内很少使用JPA 一般使用Mybatis 或者 Mybatis-plus
spring-boot-starter-validation:提供验证等方法
自用root pom 文件可见 链接
1.1. SpringBoot 注解相关
通过在类上添加 @Configuration 注解,声明这是一个 Spring 配置类。 通过在方法上添加 @Bean 注解,声明该方法创建一个 Spring Bean。
@ConditionalOn 相关注解是条件类注解 表示当某条件符合的时候才生效
AutoConfiguration通过SpringFactoriesLoader 读取spring.factories注册
@Profile注解 通过pom中的profile来判定是否注册当前bean 一般来说用不上
1.1.1. 自定义注解
/**
* @description: 自定义注解, 用于控制层统一处理封装结果, 异常, 日志和告警功能;
* 同时集成了@RestController注解功能,后续接口开发只需使用@LoggableRestController注解用于Controller上
**/
//集成@RestController
@RestController
//适用于类
@Target({ElementType.TYPE})
//只在运行时保留
@Retention(RetentionPolicy.RUNTIME)
//被javadoc记录
@Documented
public @interface LoggableRestController {
@AliasFor(
annotation = RestController.class
)
String value() default "";
}
@Aspect
@Log4j2
@Component
@DependsOn("springBeanUtils")
public class LoggableRestControllerHelper {
@Autowired(required = false)
private HttpServletRequest request;
/**
* 通過注解定义切点
*/
@Pointcut("@within(LoggableRestController)")
public void pointCut() {
}
/**
* <p>使用AOP统一封装controller返回结果</p>
*
* @param point the date to format, not null
* @return the object
* @throws Throwable 系统异常
*/
@ResponseBody
@Around("pointCut() && @within(LoggableRestController)")
public Object around(ProceedingJoinPoint point) throws Throwable {
Object respondData = point.proceed();
// If the API return a RespVO object,
// Return it directly here to avoid encode the result.
// Currently, this is a work around for /version API
if (respondData instanceof RespVO) {
String json = JSONObject.toJSONString(respondData, SerializerFeature.DisableCircularReferenceDetect);
return json;
}
RespVO res = RespVO.success(respondData);
return JSONObject.toJSONString(res);
}
}
这里的自定义注解 target表示用在什么类型之上,type类型包含class interface 等,然后使用Spring AOP切片 通过注解切分 随后在切片位置获取信息之后包装返回
1.2. 热部署
某些脚本语言可使用热部署,例如PHP,只需要把代码文件放到nginx中即可完成部署,Java相关的需要编译成class文件才能给到执行,因此热部署方式目前由idea提供插件实现,或者采用spring-boot-devtools来实现,但是spring-boot-devtools不是热部署 而是快速重启
1.3. Lombok
常用的注解如下
注解 | 描述 |
---|---|
@Data | 1.为所有属性,添加 @Getter、@ToString、@EqualsAndHashCode 注解的效果 2.为非 final 修饰的属性,添加 @Setter 注解的效果 3.为 final 修改的属性,添加 @RequiredArgsConstructor 注解的效果 |
@Getter | 添加在类或者属性上,设定get方法 |
@Setter | 添加在类或者属性上,设定set方法 |
@Builder | 添加在类上,生成构造者模式builder类 |
@Accessors | chain = true 链式编程 |
@Synchronized | 添加在方法上 增加同步锁 |
@AllArgsConstructor、@RequiredArgsConstructor、@NoArgsConstructor | 添加在类上,生成constructor方法 |
Log相关:@Log4j,@Log4j2等 | 添加在类上,支持日志收集 |
@SneakyThrows | 添加在方法上,设定try catch |
@NonNull | 添加在方法参数 类属性上,校验是否null |
1.4. MapStruct
MapStruct可用BeaaUtils 等其他方式替代 此处仅作记录 按照项目以及个人习惯使用 对象转换或者复制方法 MapStruct 是用于生成类型安全的 Bean 映射类的 Java 注解处理器。
你所要做的就是定义一个映射器接口,声明任何需要映射的方法。在编译过程中,MapStruct 将生成该接口的实现。此实现使用纯 Java 的方法调用源对象和目标对象之间进行映射,并非 Java 反射机制。
与手工编写映射代码相比,MapStruct 通过生成冗长且容易出错的代码来节省时间。在配置方法的约定之后,MapStruct 使用了合理的默认值,但在配置或实现特殊行为时将不再适用。
与动态映射框架相比,MapStruct 具有以下优点:
使用纯 Java 方法代替 Java 反射机制快速执行。 编译时类型安全:只能映射彼此的对象和属性,不能映射一个 Order 实体到一个 Customer DTO 中等等。 如果无法映射实体或属性,则在编译时清除错误报告。
使用方法:创建两个实体类,然后对于这两个实体类间新建一个convert 接口 convert接口上添加@Mapper注解,此注解会自动生成相关实体类转换的implements方法 接口样例:
@Mapper // <1>
public interface UserConvert {
UserConvert INSTANCE = Mappers.getMapper(UserConvert.class); // <2>
UserBO convert(UserDO userDO);
}
@Mapping注解可用于convert相关方法上 用于当实体类key不相等时使用 @Mapping(source="source", target="target")
1.5. 拦截器
拦截器一般用作access_token验证、用户信息获取之类情景下,通过对相关请求进行拦截,获取request header中一些信息来反查当前信息所对应的用户是否有权限操作,同时一般将用户信息存放至request attributes或者ThreadLocal中用作后续业务中使用,
// HandlerInterceptor.java
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
PreHandle中return true 表示当前拦截器允许相关请求通过,反之return false afterCompletion 实现请求完成之后的处理逻辑 诸如 释放资源、记录请求时间等操作 现在很少使用
多个 HandlerInterceptor 们,可以组成一个 Chain 拦截器链。那么,整个执行的过程,就变成:
- 首先,按照 HandlerInterceptor 链的正序,执行 #preHandle(...) 方法。
- 然后,执行 handler 的逻辑处理。
- 之后,按照 HandlerInterceptor 链的倒序,执行 #postHandle(...) 方法。
- 最后,按照 HandlerInterceptor 链的倒序,执行 #afterCompletion(...) 方法。
当出现异常时,任意一个interceptor抛出的Exception不影响其他interceptor的执行
WebFlux
1.6.WebFlux 响应式编程 类似RxJava的通知订阅模式
对于现有的Servlet模式改动在于Controller层以及Filter层 Filter层改动较大 Controller层改动较小
同样webflux也可对返回值进行包装 因此Controller层的返回值 可仍旧沿用Servlet下的返回相关业务实体对象
样例Controller:
@GetMapping("/list")
public Flux<UserVO> list() {
// 查询列表
List<UserVO> result = new ArrayList<>();
result.add(new UserVO().setId(1).setUsername("yudaoyuanma"));
result.add(new UserVO().setId(2).setUsername("woshiyutou"));
result.add(new UserVO().setId(3).setUsername("chifanshuijiao"));
// 返回列表
return Flux.fromIterable(result);
}
/**
* 获得指定用户编号的用户
*
* @param id 用户编号
* @return 用户
*/
@GetMapping("/get")
public Mono<UserVO> get(@RequestParam("id") Integer id) {
// 查询用户
UserVO user = new UserVO().setId(id).setUsername("username:" + id);
// 返回
return Mono.just(user);
}
上述样例中 返回列表使用Flux.fromInerable包装 对象使用Mono包装
// GlobalResponseBodyHandler.java
public class GlobalResponseBodyHandler extends ResponseBodyResultHandler {
private static Logger LOGGER = LoggerFactory.getLogger(GlobalResponseBodyHandler.class);
private static MethodParameter METHOD_PARAMETER_MONO_COMMON_RESULT;
private static final CommonResult COMMON_RESULT_SUCCESS = CommonResult.success(null);
static {
try {
// <1> 获得 METHOD_PARAMETER_MONO_COMMON_RESULT 。其中 -1 表示 `#methodForParams()` 方法的返回值
METHOD_PARAMETER_MONO_COMMON_RESULT = new MethodParameter(
GlobalResponseBodyHandler.class.getDeclaredMethod("methodForParams"), -1);
} catch (NoSuchMethodException e) {
LOGGER.error("[static][获取 METHOD_PARAMETER_MONO_COMMON_RESULT 时,找不都方法");
throw new RuntimeException(e);
}
}
public GlobalResponseBodyHandler(List<HttpMessageWriter<?>> writers, RequestedContentTypeResolver resolver) {
super(writers, resolver);
}
public GlobalResponseBodyHandler(List<HttpMessageWriter<?>> writers, RequestedContentTypeResolver resolver, ReactiveAdapterRegistry registry) {
super(writers, resolver, registry);
}
@Override
@SuppressWarnings("unchecked")
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
Object returnValue = result.getReturnValue();
Object body;
// <1.1> 处理返回结果为 Mono 的情况
if (returnValue instanceof Mono) {
body = ((Mono<Object>) result.getReturnValue())
.map((Function<Object, Object>) GlobalResponseBodyHandler::wrapCommonResult)
.defaultIfEmpty(COMMON_RESULT_SUCCESS);
// <1.2> 处理返回结果为 Flux 的情况
} else if (returnValue instanceof Flux) {
body = ((Flux<Object>) result.getReturnValue())
.collectList()
.map((Function<Object, Object>) GlobalResponseBodyHandler::wrapCommonResult)
.defaultIfEmpty(COMMON_RESULT_SUCCESS);
// <1.3> 处理结果为其它类型
} else {
body = wrapCommonResult(returnValue);
}
// <2>
return writeBody(body, METHOD_PARAMETER_MONO_COMMON_RESULT, exchange);
}
private static Mono<CommonResult> methodForParams() {
return null;
}
private static CommonResult<?> wrapCommonResult(Object body) {
// 如果已经是 CommonResult 类型,则直接返回
if (body instanceof CommonResult) {
return (CommonResult<?>) body;
}
// 如果不是,则包装成 CommonResult 类型
return CommonResult.success(body);
}
}
个人不建议在生产环境使用webflux 由于与Servlet 相差较大 请评估后使用
1.6.1. WebFlux Filter
SpringMVC中可实现HandlerInterceptor接口来拦截请求,在WebFlux中 可实现WebFilter来实现相同的逻辑
// DemoWebFilterWebFilter.java
/**
* Contract for interception-style, chained processing of Web requests that may
* be used to implement cross-cutting, application-agnostic requirements such
* as security, timeouts, and others.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public interface WebFilter {
/**
* Process the Web request and (optionally) delegate to the next
* {@code WebFilter} through the given {@link WebFilterChain}.
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return {@code Mono<Void>} to indicate when request processing is complete
*/
Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain);
}
样例WebFilter
@Component
@Order(1)
public class DemoWebFilter implements WebFilter {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
// <1> 继续执行请求
return webFilterChain.filter(serverWebExchange)
.doOnSuccess(new Consumer<Void>() { // <2> 执行成功后回调
@Override
public void accept(Void aVoid) {
logger.info("[accept][执行成功]");
}
});
}
}
Mono存在doOnSuccess方法表示执行成功之后 Mono还有诸如doOnError等方法 具体请参照Reactor Mono
个人建议不采用上述的样例Filter中filter写法,建议下面的写法
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 如果是 CORS 跨域请求
if (CorsUtils.isCorsRequest(request)) {
// 获得该接口的 CORS 跨域请求的配置
CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(exchange);
if (corsConfiguration != null) {
// 执行 CORS 跨域请求的处理,
boolean isValid = this.processor.process(corsConfiguration, exchange);
// !isValid 表示,如果跨域请求的校验不通过
// CorsUtils.isPreFlightRequest(request) 表示,是 OPTIONS “预检请求”
if (!isValid || CorsUtils.isPreFlightRequest(request)) {
// 直接返回空的 Mono ,不进行后续的处理
return Mono.empty();
}
}
}
// 继续过滤器的处理
return chain.filter(exchange);
}
上述样例在相对于SpringMVC Filter return false 的时候返回一个空的Mono来终止后续Filter执行,在当前Filter 通过时返回chain.filter(exchange)来继续执行剩下的Filter
1.6.2. R2DBC
R2DBC(响应式的关系数据库连接),是一种将响应式 API 引入 SQL 数据库的尝试。
此处记录的是 jasync-sql
- 基于 Netty 实现
- 异步、高性能、可靠的 PostgreSQL、MySQL 的驱动
- 使用 Kotlin 语言编写
// DatabaseConfiguration.java
@Configuration
@EnableTransactionManagement // 开启事务的支持
public class DatabaseConfiguration {
@Bean
public ConnectionFactory connectionFactory(R2dbcProperties properties) throws URISyntaxException {
// 从 R2dbcProperties 中,解析出 host、port、database
URI uri = new URI(properties.getUrl());
String host = uri.getHost();
int port = uri.getPort();
String database = uri.getPath().substring(1); // 去掉首位的 / 斜杠
// 创建 jasync Configuration 配置配置对象
com.github.jasync.sql.db.Configuration configuration = new com.github.jasync.sql.db.Configuration(
properties.getUsername(), host, port, properties.getPassword(), database);
// 创建 JasyncConnectionFactory 对象
return new JasyncConnectionFactory(new MySQLConnectionFactory(configuration));
}
@Bean
public ReactiveTransactionManager transactionManager(R2dbcProperties properties) throws URISyntaxException {
return new R2dbcTransactionManager(this.connectionFactory(properties));
}
}
1.7. 分布式Session
多机部署的时候会出现用户登录到一台服务器上 之后tomcat检测cookie值不存在,然后生成一个sessionId并回写给Client,随后Client在另一台机器上登录,被nginx路由至另一台Tomcat,然而cookie所带的sessionId无法在这台Tomcat上找到对应session,这里又被创建了一个session,两台Tomcat需要做Session一致性,有以下方案:
- Tomcat做集群 session复制
- Nginx session 黏连
- Session存放至redis、mysql等数据库中
此处建议将Session放到redis中 然后redis做集群配置 使session同步
// SessionConfiguration.java
@Configuration
@EnableRedisHttpSession // 自动化配置 Spring Session 使用 Redis 作为数据源
public class SessionConfiguration {
/**
* 创建 {@link RedisOperationsSessionRepository} 使用的 RedisSerializer Bean 。
*
* 具体可以看看 {@link RedisHttpSessionConfiguration#setDefaultRedisSerializer(RedisSerializer)} 方法,
* 它会引入名字为 "springSessionDefaultRedisSerializer" 的 Bean 。
*
* @return RedisSerializer Bean
*/
@Bean(name = "springSessionDefaultRedisSerializer")
public RedisSerializer springSessionDefaultRedisSerializer() {
return RedisSerializer.json();
}
}
随后可直接调用 session.setAttribute(key, value); 来设定session 每一个 Session 对应 Redis 两个 key-value 键值对。 开头:以 spring:session 开头,可以通过 @EnableRedisHttpSession 注解的 redisNamespace 属性配置。
结尾:以对应 Session 的 sessionid 结尾。
中间:中间分别是 "session"、"expirations"、sessions:expires 。一般情况下,我们只需要关注中间为 "session" 的 key-value 键值对即可,它负责真正存储 Session 数据。
对于中间为 "sessions:expires" 和 "expirations" 的两个来说,主要为了实现主动删除 Redis 过期的 Session 会话,解决 Redis 惰性删除的“问题”。
"spring:session:expirations:{时间戳}" ,是为了获得每分钟需要过期的 sessionid 集合,即 {时间戳} 是每分钟的时间戳。
1.7.1. Spring Security + Spring Session (TODO: 详细解释)
Spring Security与 redis配置方式几乎相同 ,Spring Security在集成之后 redis中的session相关的字段被保护起来
此处关于Session 不再过多展开,现在一版使用OAuth 2 以及JWT来做鉴权
1.8 JWT
1.9 Validation
我个人建议 数据校验前端 后端都做一份。然后一般用javax.validation包来做数据校验,有以下几个注解:
- @NotBlank :只能用于字符串不为 null ,并且字符串 #trim() 以后 length 要大于 0 。
- @NotEmpty :集合对象的元素不为 0 ,即集合不为空,也可以用于字符串不为 null 。
- @NotNull :不能为 null 。
- @Null :必须为 null 。
- @DecimalMax(value) :被注释的元素必须是一个数字,其值必须小于等于指定的最大值。
- @DecimalMin(value) :被注释的元素必须是一个数字,其值必须大于等于指定的最小值。
- @Digits(integer, fraction) :被注释的元素必须是一个数字,其值必须在可接受的范围内。
- @Positive :判断正数。
- @PositiveOrZero :判断正数或 0 。
- @Max(value) :该字段的值只能小于或等于该值。
- @Min(value) :该字段的值只能大于或等于该值。
- @Negative :判断负数。
- @NegativeOrZero :判断负数或 0 。
- Boolean 值检查
- @AssertFalse :被注释的元素必须为 true 。
- @AssertTrue :被注释的元素必须为 false 。
- @Size(max, min) :检查该字段的 size 是否在 min 和 max 之间,可以是字符串、数组、集合、Map 等。
- @Future :被注释的元素必须是一个将来的日期。
- @FutureOrPresent :判断日期是否是将来或现在日期。
- @Past :检查该字段的日期是在过去。
- @PastOrPresent :判断日期是否是过去或现在日期。
- @Email :被注释的元素必须是电子邮箱地址。
- @Pattern(value) :被注释的元素必须符合指定的正则表达式。
@Valid 注解,是 Bean Validation 所定义,可以添加在普通方法、构造方法、方法参数、方法返回、成员变量上,表示它们需要进行约束校验。
@Validated 注解,是 Spring Validation 锁定义,可以添加在类、方法参数、普通方法上,表示它们需要进行约束校验。同时,@Validated 有 value 属性,支持分组校验。
绝大多数场景下,我们使用 @Validated 注解即可。而在有嵌套校验的场景,我们使用 @Valid 注解添加到成员属性上。
对于这类型错误的提示消息可参照以下代码:
// GlobalExceptionHandler.java
@ResponseBody
@ExceptionHandler(value = BindException.class)
public CommonResult bindExceptionHandler(HttpServletRequest req, BindException ex) {
logger.debug("[bindExceptionHandler]", ex);
// 拼接错误
StringBuilder detailMessage = new StringBuilder();
for (ObjectError objectError : ex.getAllErrors()) {
// 使用 ; 分隔多个错误
if (detailMessage.length() > 0) {
detailMessage.append(";");
}
// 拼接内容到其中
detailMessage.append(objectError.getDefaultMessage());
}
// 包装 CommonResult 结果
return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
}