03.
Spring Boot项目-02
本期分享内容:Spring Boot 搭建复杂的系统框架-02
本期邀请的是李熠老师(某大型互联网公司系统架构师)为我们分享《Spring Cloud快速入门》专栏。
Spring Cloud
Spring Boot项目
注入任何类
本节通过一个实际的例子来讲解如何注入一个普通类,并且说明这样做的好处。
假设一个需求是这样的:项目要求使用阿里云的 OSS 进行文件上传。
我们知道,一个项目一般会分为开发环境、测试环境和生产环境。OSS 文件上传一般有如下几个参数:appKey、appSecret、bucket、endpoint 等。不同环境的参数都可能不一样,这样便于区分。按照传统的做法,我们在代码里设置这些参数,这样做的话,每次发布不同的环境包都需要手动修改代码。
这个时候,我们就可以考虑将这些参数定义到配置文件里面,通过前面提到的 @Value
注解取出来,再通过 @Bean
将其定义为一个 Bean,这时我们只需要在需要使用的地方注入该 Bean 即可。
首先在 application.yml 加入如下内容:
oss:
appKey: 1
appSecret: 1
bucket: lynn
endPoint: https://www.aliyun.com
其次创建一个普通类:
public class Aliyun {
private String appKey;
private String appSecret;
private String bucket;
private String endPoint;
public static class Builder{
private String appKey;
private String appSecret;
private String bucket;
private String endPoint;
public Builder setAppKey(String appKey){
this.appKey = appKey;
return this;
}
public Builder setAppSecret(String appSecret){
this.appSecret = appSecret;
return this;
}
public Builder setBucket(String bucket){
this.bucket = bucket;
return this;
}
public Builder setEndPoint(String endPoint){
this.endPoint = endPoint;
return this;
}
public Aliyun build(){
return new Aliyun(this);
}
}
public static Builder options(){
return new Aliyun.Builder();
}
private Aliyun(Builder builder){
this.appKey = builder.appKey;
this.appSecret = builder.appSecret;
this.bucket = builder.bucket;
this.endPoint = builder.endPoint;
}
public String getAppKey() {
return appKey;
}
public String getAppSecret() {
return appSecret;
}
public String getBucket() {
return bucket;
}
public String getEndPoint() {
return endPoint;
}
}
然后在 @SpringBootConfiguration
注解的类添加如下代码:
@Value("${oss.appKey}")
private String appKey;
@Value("${oss.appSecret}")
private String appSecret;
@Value("${oss.bucket}")
private String bucket;
@Value("${oss.endPoint}")
private String endPoint;
@Bean
public Aliyun aliyun(){
return Aliyun.options()
.setAppKey(appKey)
.setAppSecret(appSecret)
.setBucket(bucket)
.setEndPoint(endPoint)
.build();
}
最后在需要的地方注入这个 Bean 即可:
@Autowired
private Aliyun aliyun;
以上代码其实并不完美,如果增加一个属性,就需要在 Aliyun 类新增一个字段,还需要在 Configuration 类里注入它,扩展性不好,那么我们有没有更好的方式呢?
答案是肯定的,我们可以利用 ConfigurationProperties 注解更轻松地将配置信息注入到实体中。
1.创建实体类 AliyunAuto,并编写以下代码:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "oss")
public class AliyunAuto {
private String appKey;
private String appSecret;
private String bucket;
private String endPoint;
public String getAppKey() {
return appKey;
}
public void setAppKey(String appKey) {
this.appKey = appKey;
}
public String getAppSecret() {
return appSecret;
}
public void setAppSecret(String appSecret) {
this.appSecret = appSecret;
}
public String getBucket() {
return bucket;
}
public void setBucket(String bucket) {
this.bucket = bucket;
}
public String getEndPoint() {
return endPoint;
}
public void setEndPoint(String endPoint) {
this.endPoint = endPoint;
}
@Override
public String toString() {
return "AliyunAuto{" +
"appKey='" + appKey + '\'' +
", appSecret='" + appSecret + '\'' +
", bucket='" + bucket + '\'' +
", endPoint='" + endPoint + '\'' +
'}';
}
}
其中,ConfigurationProperties 指定配置文件的前缀属性,实体具体字段和配置文件字段名一致,如在上述代码中,字段为 appKey,则自动获取 oss.appKey 的值,将其映射到 appKey 字段中,这样就完成了自动的注入。如果我们增加一个属性,则只需要修改 Bean 和配置文件即可,不用显示注入。
拦截器
我们在提供 API 的时候,经常需要对 API 进行统一的拦截,比如进行接口的安全性校验。
本节,我会讲解 Spring Boot 是如何进行拦截器设置的,请看接下来的代码。
创建一个拦截器类:ApiInterceptor,并实现 HandlerInterceptor 接口:
public class ApiInterceptor implements HandlerInterceptor {
//请求之前
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
System.out.println("进入拦截器");
return true;
}
//请求时
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
//请求完成
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
@SpringBootConfiguration
注解的类继承 WebMvcConfigurationSupport 类,并重写 addInterceptors 方法,将 ApiInterceptor 拦截器类添加进去,代码如下:
@SpringBootConfiguration
public class WebConfig extends WebMvcConfigurationSupport{
@Override
protected void addInterceptors(InterceptorRegistry registry) {
super.addInterceptors(registry);
registry.addInterceptor(new ApiInterceptor());
}
}
异常处理
我们在 Controller 里提供接口,通常需要捕捉异常,并进行友好提示,否则一旦出错,界面上就会显示报错信息,给用户一种不好的体验。最简单的做法就是每个方法都使用 try catch 进行捕捉,报错后,则在 catch 里面设置友好的报错提示。如果方法很多,每个都需要 try catch,代码会显得臃肿,写起来也比较麻烦。
我们可不可以提供一个公共的入口进行统一的异常处理呢?当然可以。方法很多,这里我们通过 Spring 的 AOP 特性就可以很方便的实现异常的统一处理。实现方法很简单,只需要在 Controller 类添加以下代码即可。
@ExceptionHandler
public String doError(Exception ex) throws Exception{
ex.printStackTrace();
return ex.getMessage();
}
其中,在 doError 方法上加入 @ExceptionHandler 注解即可,这样,接口发生异常会自动调用该方法。
这样,我们无需每个方法都添加 try catch,一旦报错,则会执行 handleThrowing 方法。
优雅的输入合法性校验
为了接口的健壮性,我们通常除了客户端进行输入合法性校验外,在 Controller 的方法里,我们也需要对参数进行合法性校验,传统的做法是每个方法的参数都做一遍判断,这种方式和上一节讲的异常处理一个道理,不太优雅,也不易维护。
其实,SpringMVC 提供了验证接口,下面请看代码:
@GetMapping("authorize")
public void authorize(@Valid AuthorizeIn authorize, BindingResult ret){
if(result.hasFieldErrors()){
List<FieldError> errorList = result.getFieldErrors();
//通过断言抛出参数不合法的异常
errorList.stream().forEach(item -> Assert.isTrue(false,item.getDefaultMessage()));
}
}
public class AuthorizeIn extends BaseModel{
@NotBlank(message = "缺少response_type参数")
private String responseType;
@NotBlank(message = "缺少client_id参数")
private String ClientId;
private String state;
@NotBlank(message = "缺少redirect_uri参数")
private String redirectUri;
public String getResponseType() {
return responseType;
}
public void setResponseType(String responseType) {
this.responseType = responseType;
}
public String getClientId() {
return ClientId;
}
public void setClientId(String clientId) {
ClientId = clientId;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getRedirectUri() {
return redirectUri;
}
public void setRedirectUri(String redirectUri) {
this.redirectUri = redirectUri;
}
}
我们再把验证的代码单独封装成方法:
protected void validate(BindingResult result){
if(result.hasFieldErrors()){
List<FieldError> errorList = result.getFieldErrors();
errorList.stream().forEach(item -> Assert.isTrue(false,item.getDefaultMessage()));
}
}
这样每次参数校验只需要调用 validate 方法就行了,我们可以看到代码的可读性也大大提高了。
接口版本控制
一个系统上线后会不断迭代更新,需求也会不断变化,有可能接口的参数也会发生变化,如果在原有的参数上直接修改,可能会影响线上系统的正常运行,这时我们就需要设置不同的版本,这样即使参数发生变化,由于老版本没有变化,因此不会影响上线系统的运行。
一般我们可以在地址上带上版本号,也可以在参数上带上版本号,还可以再 header 里带上版本号,这里我们在地址上带上版本号,大致的地址如:http://api.example.com/v1/test,其中,v1 即代表的是版本号。具体做法请看代码:
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
/**
* 标识版本号
* @return
*/
int value();
}
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
// 路径中版本的前缀, 这里用 /v[1-9]/的形式
private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)/");
private int apiVersion;
public ApiVersionCondition(int apiVersion){
this.apiVersion = apiVersion;
}
@Override
public ApiVersionCondition combine(ApiVersionCondition other) {
// 采用最后定义优先原则,则方法上的定义覆盖类上面的定义
return new ApiVersionCondition(other.getApiVersion());
}
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI());
if(m.find()){
Integer version = Integer.valueOf(m.group(1));
if(version >= this.apiVersion)
{
return this;
}
}
return null;
}
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
// 优先匹配最新的版本号
return other.getApiVersion() - this.apiVersion;
}
public int getApiVersion() {
return apiVersion;
}
}
public class CustomRequestMappingHandlerMapping extends
RequestMappingHandlerMapping {
@Override
protected RequestCondition<ApiVersionCondition> getCustomTypeCondition(Class<?> handlerType) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
return createCondition(apiVersion);
}
@Override
protected RequestCondition<ApiVersionCondition> getCustomMethodCondition(Method method) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
return createCondition(apiVersion);
}
private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion) {
return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());
}
}
@SpringBootConfiguration
public class WebConfig extends WebMvcConfigurationSupport {
@Bean
public AuthInterceptor interceptor(){
return new AuthInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor());
}
@Override
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();
handlerMapping.setOrder(0);
handlerMapping.setInterceptors(getInterceptors());
return handlerMapping;
}
}
Controller 类的接口定义如下:
@ApiVersion(1)
@RequestMapping("{version}/dd")
public class HelloController{}
这样我们就实现了版本控制,如果增加了一个版本,则创建一个新的 Controller,方法名一致,ApiVersion 设置为2,则地址中 v1 会找到 ApiVersion 为1的方法,v2 会找到 ApiVersion 为2的方法。
自定义 JSON 解析
Spring Boot 中 RestController 返回的字符串默认使用 Jackson 引擎,它也提供了工厂类,我们可以自定义 JSON 引擎,本节实例我们将 JSON 引擎替换为 fastJSON,首先需要引入 fastJSON:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
其次,在 WebConfig 类重写 configureMessageConverters 方法:
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
super.configureMessageConverters(converters);
/*
1.需要先定义一个 convert 转换消息的对象;
2.添加 fastjson 的配置信息,比如是否要格式化返回的 JSON 数据
3.在 convert 中添加配置信息
4.将 convert 添加到 converters 中
*/
//1.定义一个 convert 转换消息对象
FastJsonHttpMessageConverter fastConverter=new FastJsonHttpMessageConverter();
//2.添加 fastjson 的配置信息,比如是否要格式化返回 JSON 数据
FastJsonConfig fastJsonConfig=new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(
SerializerFeature.PrettyFormat
);
fastConverter.setFastJsonConfig(fastJsonConfig);
converters.add(fastConverter);
}