每日一课 | Spring Boot 搭建复杂的系统框架-02

03.

Spring Boot项目-02

大家好,我是营长,上期给大家分享——Spring Boot 搭建复杂的系统框架-01

本期分享内容:Spring Boot 搭建复杂的系统框架-02

本期邀请的是李熠老师(某大型互联网公司系统架构师)为我们分享《Spring Cloud快速入门》专栏。

Spring  Cloud

Spring Boot项目

注入任何类

本节通过一个实际的例子来讲解如何注入一个普通类,并且说明这样做的好处。

假设一个需求是这样的:项目要求使用阿里云的 OSS 进行文件上传。

我们知道,一个项目一般会分为开发环境、测试环境和生产环境。OSS 文件上传一般有如下几个参数:appKey、appSecret、bucket、endpoint 等。不同环境的参数都可能不一样,这样便于区分。按照传统的做法,我们在代码里设置这些参数,这样做的话,每次发布不同的环境包都需要手动修改代码。

这个时候,我们就可以考虑将这些参数定义到配置文件里面,通过前面提到的 @Value 注解取出来,再通过 @Bean 将其定义为一个 Bean,这时我们只需要在需要使用的地方注入该 Bean 即可。

首先在 application.yml 加入如下内容:

oss:appKey: 1appSecret: 1bucket: lynnendPoint: 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;@Beanpublic Aliyun aliyun(){return Aliyun.options().setAppKey(appKey).setAppSecret(appSecret).setBucket(bucket).setEndPoint(endPoint).build();}

最后在需要的地方注入这个 Bean 即可:

    @Autowiredprivate 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;}@Overridepublic 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 {//请求之前@Overridepublic boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {System.out.println("进入拦截器");return true;}//请求时@Overridepublic void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {}//请求完成@Overridepublic void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {}
}

@SpringBootConfiguration 注解的类继承 WebMvcConfigurationSupport 类,并重写 addInterceptors 方法,将 ApiInterceptor 拦截器类添加进去,代码如下:

@SpringBootConfiguration
public class WebConfig extends WebMvcConfigurationSupport{@Overrideprotected void addInterceptors(InterceptorRegistry registry) {super.addInterceptors(registry);registry.addInterceptor(new ApiInterceptor());}
}

异常处理

我们在 Controller 里提供接口,通常需要捕捉异常,并进行友好提示,否则一旦出错,界面上就会显示报错信息,给用户一种不好的体验。最简单的做法就是每个方法都使用 try catch 进行捕捉,报错后,则在 catch 里面设置友好的报错提示。如果方法很多,每个都需要 try catch,代码会显得臃肿,写起来也比较麻烦。

我们可不可以提供一个公共的入口进行统一的异常处理呢?当然可以。方法很多,这里我们通过 Spring 的 AOP 特性就可以很方便的实现异常的统一处理。实现方法很简单,只需要在 Controller 类添加以下代码即可。

    @ExceptionHandlerpublic 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 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 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 {// 路径中版本的前缀, 这里用 /v[1-9]/的形式private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)/");private int apiVersion;public ApiVersionCondition(int apiVersion){this.apiVersion = apiVersion;}@Overridepublic ApiVersionCondition combine(ApiVersionCondition other) {// 采用最后定义优先原则,则方法上的定义覆盖类上面的定义return new ApiVersionCondition(other.getApiVersion());}@Overridepublic 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;}@Overridepublic int compareTo(ApiVersionCondition other, HttpServletRequest request) {// 优先匹配最新的版本号return other.getApiVersion() - this.apiVersion;}public int getApiVersion() {return apiVersion;}
}
public class CustomRequestMappingHandlerMapping extendsRequestMappingHandlerMapping {@Overrideprotected RequestCondition getCustomTypeCondition(Class handlerType) {ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);return createCondition(apiVersion);}@Overrideprotected RequestCondition getCustomMethodCondition(Method method) {ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);return createCondition(apiVersion);}private RequestCondition createCondition(ApiVersion apiVersion) {return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());}
}
@SpringBootConfiguration
public class WebConfig extends WebMvcConfigurationSupport {@Beanpublic AuthInterceptor interceptor(){return new AuthInterceptor();}@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new AuthInterceptor());}@Override@Beanpublic 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:

com.alibabafastjson${fastjson.version}

其次,在 WebConfig 类重写 configureMessageConverters 方法:

@Overridepublic void configureMessageConverters(List> 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);}

今日内容有get吗,欢迎各位留言讨论!

下期预告:Spring Boot 启动原理-01

以上专栏均来自CSDN GitChat专栏《Spring Cloud快速入门》,作者李熠,专栏详情可识别下方二维码查看哦!

了解更多详情

可识别下方二维码


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部