上一章已经把整个框架做了介绍,同时也略带了后续的微服务生态图。这章将从框架的搭建开始说起,spring-boot和spring-cloud对于看这篇文章的jr吗都不陌生,我们就从它们开始。
是一个空的maven项目,在pom文件中定义spring-boot/clould和其他依赖的版本:spring-boot版本是:2.1.3.RELEASE
spring-cloud:版本是Greenwich.SR2;在父pom中以import的方式申明。
这个包相当简单,把Spring-boot的相关依赖统一放在这边管理,为什么这么做呢?之前在core包中集成大量的spring-boot包,导致pom文件相当臃肿不好管理;单独拎出来,会让整个pom变得很清晰,后续所有spring-boot的升级,新增依赖都只要在这个maven模块维护即可,很多人会有疑问这样做是不是有点多余,我的观点是,如果我们在这样的细节上做一些小的调整,能为整个框架维护和扩展带来一些好处的话是值得的,这种好处看似没啥,实际上渗透在每个框架开发人员中(自行体会吧)。这个模块不会对外暴露,只会在core中引用。ok,接下啦看下pom文件的引用:引入了spring-boot的相关依赖
fw-parent
com.mars.fw
1.0.0-SNAPSHOT
4.0.0
com.mars.fw
FW-BOOT
1.0.0-SNAPSHOT
jar
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-jdbc
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-tomcat
compile
org.apache.tomcat.embed
tomcat-embed-jasper
compile
org.springframework.boot
spring-boot-devtools
true
org.springframework.boot
spring-boot-starter-logging
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-aop
org.springframework.boot
spring-boot-configuration-processor
true
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-autoconfigure
这个和FW-BOOT一样把spring-cloud的相关配置依赖抽取出来,在core中引用,目的是一样的;看下pom文件的引用:注意在这里面我引入了spring-cloud-starter-alibaba-nacos-config和spring-cloud-starter-alibaba-nacos-discovery,这是因为服务的注册中心我使用了nacos 而没有使用尤里卡;两种我都使用过,nacos在可视化界面和配置文件管理方面我认为不错,实践下来很刚。nacos的安装部署使用会写专门的章节来介绍使用。
fw-parent
com.mars.fw
1.0.0-SNAPSHOT
4.0.0
com.mars.fw
FW-CLOUD
1.0.0-SNAPSHOT
jar
MARS-FW-CLOUD-STARTER
dengjinde
[email protected]
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.cloud
spring-cloud-starter-alibaba-nacos-config
org.springframework.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.cloud
spring-cloud-starter-openfeign
2.1.1.RELEASE
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
2.1.1.RELEASE
这个是核心,接下来围绕这个详细来讲解实践过程,没有很复杂难懂的东西。
从上面的目录结构来看:我把core分成了web模块和其他模块。先来讲下web模块:我的定义是所有有关提供API和WEB应用相关的操作都放在这个包中;我们来思考下对于一个web的应用需要具备哪些基础功能(实际上是对springMVC的封装和增强)。
要支持spring-web相关的功能首先要进行web相关的配置,这个大家都清楚了我们新建一个类来实现这个接口进行配置。
这里面主要处理了这么几个问题:
1.前端浏览器ajax的跨域问题
2.静态资源路径设置问题
3.接下来会讲到的统一参数返回的处理就是这个方法:
package com.mars.fw;
import com.mars.fw.web.reponse.handler.MarsRespValueHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.DeferredResultMethodReturnValueHandler;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
/**
* @Author King
* @Date 2020-04-20
*/
@Configuration
@EnableAspectJAutoProxy
@EnableAutoConfiguration
public class MarsWebMvcConfigure implements WebMvcConfigurer {
/**
* 实例请求映射处理适配器
*/
@Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
/**
* 解决跨域问题
*
*
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
private CorsConfiguration addcorsConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
List<String> list = new ArrayList<>();
list.add("*");
corsConfiguration.setAllowedOrigins(list);
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", addcorsConfig());
return new CorsFilter(source);
}
/**
* mvc 静态资源路径访问权限配置
* 例如:registry.addResourceHandler("/upload/**").addResourceLocations("classpath:/upload/");
* /upload/ 路径下的静态资源可以访问
*
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//所有目录都可以访问
registry.addResourceHandler("/static/**").
addResourceLocations("file:/usr/share/nginx/images/");
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
/**
* @PostConstruct 非静态的void()方法 尽量少在复杂的逻辑方法上注解这个 影响启动速度
* Tomcat6.x 以上
* 服务器加载Servlet的时候运行,并且只会被服务器执行一次
*
* 服务器加载Servlet的时候运行 执行实例请求映射处理适配器下自定义的参数拦截统一处理 返回标准数据结构
*/
@PostConstruct
public void initResponseValue() {
final List<HandlerMethodReturnValueHandler> originalHandlers = new ArrayList<>(requestMappingHandlerAdapter.getReturnValueHandlers());
RequestResponseBodyMethodProcessor requestResponseBodyMethodProcessor = null;
for (int i = 0; i < originalHandlers.size(); i++) {
final HandlerMethodReturnValueHandler valueHandler = originalHandlers.get(i);
if (RequestResponseBodyMethodProcessor.class.isAssignableFrom(valueHandler.getClass())) {
requestResponseBodyMethodProcessor = (RequestResponseBodyMethodProcessor) valueHandler;
break;
}
}
MarsRespValueHandler marsRespValueHandler = new MarsRespValueHandler(requestResponseBodyMethodProcessor);
final int deferredPos = obtainValueHandlerPosition(originalHandlers, DeferredResultMethodReturnValueHandler.class);
originalHandlers.add(deferredPos + 1, marsRespValueHandler);
requestMappingHandlerAdapter.setReturnValueHandlers(originalHandlers);
}
private int obtainValueHandlerPosition(final List<HandlerMethodReturnValueHandler> originalHandlers, Class<?> handlerClass) {
for (int i = 0; i < originalHandlers.size(); i++) {
final HandlerMethodReturnValueHandler valueHandler = originalHandlers.get(i);
if (handlerClass.isAssignableFrom(valueHandler.getClass())) {
return i;
}
}
return -1;
}
}
首先我们希望我们的API接口返回的数据格式是统一的,这边就设计到SpringMVC里面怎么封装统一的数据格式,原理这边不深讲。首先我们先定义系统中的统一返回码和统一返回数据格式:
定义一个IRCode的接口用来给业务层做自定义实现,同时定义一个King类来实现框架中包含的code和message;
package com.mars.fw.web.reponse;
/**
* @Author King
* @create 2020/4/20 11:43
*/
public interface IRCode {
String message();
int code();
}
package com.mars.fw.web.reponse;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import java.io.Serializable;
/**
* @Author King
* @create 2020/4/20 11:24
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class King<T> implements Serializable {
private static final long serialVersionUID = -2466174531203439862L;
public static final Integer SUCCESS_CODE = 1000;
public static final String SUCCESS_MSG = "操作成功";
private int code;
private String msg;
private T data;
public King() {
this(SUCCESS_CODE, SUCCESS_MSG, null);
}
public King(IRCode irCode) {
this(irCode.code(), irCode.message(), null);
}
public King(IRCode irCode, T data) {
this(irCode.code(), irCode.message(), data);
}
public King(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public static <T> King<T> success(IRCode irCode, T data) {
return new King(KingCode.SUCCESS, data);
}
public static <T> King<T> success(String message) {
return new King(KingCode.SUCCESS, message);
}
public static <T> King<T> fail(IRCode irCode, T data) {
return new King(KingCode.FAULT, data);
}
public static <T> King<T> fail(String message) {
return new King(KingCode.FAULT, message);
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
除此之外我们还需定义一个系统枚举的code:KingCode
package com.mars.fw.web.reponse;
/**
* @Author King
* @create 2020/4/20 11:52
*/
public enum KingCode implements IRCode {
/**
* 状态码定义
*/
FAULT(9999, "失败"),
SUCCESS(10000, "成功"),
LOGIN_SUCCESS(10001, "登录成功"),
LOGOUT_SUCCESS(10002, "登录失败"),
PASS_WRONG(10002, "密码错误"),
USER_NOT_FIND(10003, "用户不存在"),
CAPTCHA_TIMEOUT(10004, "验证码失效"),
CAPTCHA_ERROR(10005, "验证码错误"),
LOGIN_LOCK(10006, "该账号已被关闭"),
SMS_EXCEPTION(10007, "短信登录异常"),
TOKEN_NOT_EMPTY(90000, "token不能为空"),
TOKEN_ERROR(90001, "token校验失败"),
TOKEN_EXCEPTION(90002, "token校验异常"),
URL_EXPIRED(90003, "访问的URL过期"),
DEFAULT_EXCEPTION(99999, "系统异常");
final int code;
final String message;
@Override
public int code() {
return this.code;
}
@Override
public String message() {
return this.message;
}
private KingCode(final int code, final String message) {
this.code = code;
this.message = message;
}
}
写到这边我发现这些不用贴上代码只讲核心实现就好,这些直接到源码去看。
核心的实现在我们自定义一个MarsRespValueHandler,这个类是实现了spring-web包中的HandlerMethodReturnValueHandler。这个handler是spring提供给我们的,用来处理controller层方法返回的结果处理器。我们可以重写里面的方法来得到我们需要的数据格式。代码里面我有详细的注释,handleReturnValue 是我们处理的关键,我们把我们处理后的值重新放回spring-web response的链路中也就是this.requestResponseBodyMethodProcessor.handleReturnValue。
package com.mars.fw.web.reponse.handler;
import com.mars.fw.web.reponse.King;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
/**
* @description: mvc 返回值统一处理 统一处理成Response实体结构 标准输出到前端
* @author:dengjinde
* @date:2020/4/20
*/
public class MarsRespValueHandler implements HandlerMethodReturnValueHandler {
private RequestResponseBodyMethodProcessor requestResponseBodyMethodProcessor;
/**
* 构造函数
*
* @param requestResponseBodyMethodProcessor
*/
public MarsRespValueHandler(RequestResponseBodyMethodProcessor requestResponseBodyMethodProcessor) {
this.requestResponseBodyMethodProcessor = requestResponseBodyMethodProcessor;
}
/**
* 当返回值为 true的时候才会开启自定义的handler
*
* @param returnType
* @return
*/
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class));
}
/**
* 自定义handler逻辑 当返回值为空的时候自动返回标准格式
* 这边的逻辑可以根据需要扩展
*
* @param returnValue
* @param returnType
* @param mavContainer
* @param webRequest
* @throws Exception
*/
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
King response = null;
if (!King.class.isAssignableFrom(returnType.getParameterType())) {
response = new King(King.SUCCESS_CODE, "success", returnValue);
} else {
response = (King) returnValue;
}
this.requestResponseBodyMethodProcessor.handleReturnValue(response, returnType, mavContainer, webRequest);
}
}
定义好这个handler后在前面的MarsWebMvcConfigure中,在指定的位置织入我们定义好的handler这样就可以实现自定义的数据处理并返回了。我们可以很自由的在这里面定义我们想要的数据格式,做更多的事情。实现的细节可以看源码。
有了统一的数据格式返回,我们常常会遇到一些异常,直接返回异常肯定是不可取的。所以我们需要对异常统一处理,这边只讲核心实现,细节看源码。实现的原理是利用spring-web提供的@ControllerAdvice controller增强器实现的。
package com.mars.fw.web.exception.advice;
import com.mars.fw.web.exception.KingException;
import com.mars.fw.web.reponse.ExceptionCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.regex.Pattern;
/**
* 统一系统异常处理
*
* @Author King
* @create 2020/4/20 15:25
*/
@Slf4j
@ControllerAdvice
public class KingExceptionHandlerAdvice {
/**
* 统一异常处理
*
* @param request
* @param response
* @param ex
* @return
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public Object controllerExceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception ex) {
String message = ExceptionCode.DEFAULT_EXCEPTION.message();
if (KingException.class.isAssignableFrom(ex.getClass())) {
KingException kingException = (KingException) ex;
printExceptionLog(kingException, ex);
return kingException.transferResponse();
}
if (ex instanceof MissingServletRequestParameterException) {
MissingServletRequestParameterException exception = (MissingServletRequestParameterException) ex;
message = "必填 " + exception.getParameterType() + "类型参数 '" + exception.getParameterName() + "' 不存在";
} else if (ex instanceof EmptyResultDataAccessException) {
EmptyResultDataAccessException exception = (EmptyResultDataAccessException) ex;
String pattern = "No class.*with.*exists!.*";
boolean isMatch = Pattern.matches(pattern, ex.getMessage());
if (isMatch) {
message = "数据不存在,请确认";
}
} else {
if (null != ex.getMessage()) {
message = "系统异常,联系管理员";
}
}
KingException kingException = new KingException(ExceptionCode.DEFAULT_EXCEPTION, message);
printExceptionLog(kingException, ex);
if (log.isDebugEnabled()) {
log.debug(ex.getMessage(), ex.getStackTrace());
}
return kingException.transferResponse();
}
/**
* 错误日志
*
* @param kingException
*/
private void printExceptionLog(KingException kingException, Exception ex) {
if (kingException.getCause() == null) {
log.error("######ErrorCode: {},message: {}#######", kingException.getCode().code(), kingException.getMessage());
} else {
log.error("######ErrorCode: {},message: {}#######", kingException.getCode().code(), kingException.getMessage(), kingException);
}
log.error("######ErrorCode: {},message: {}#######", kingException.getCode().code(), kingException.getMessage(), ex);
}
}
基于此我们根据团队自定义的异常,根据ExceptionHandler处理指定异常。
API文档是很重要的,统一的API文档更为重要,不要研发们另外维护一套文档我觉得是更重要的。所以在线文档就显得很迫切,这边采用的是swagger2的升级版:knife4j-spring-ui
首先在pom中引入包:
<!--框架其他核心功能依赖的第三方包-swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-ui</artifactId>
</dependency>
<!--框架其他核心功能依赖的第三方包-swagger -->
package com.mars.fw.web.doc;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* @author King
* @description swagger相关配置类
* @date 2020/4/20
*/
@EnableSwagger2
@Configuration
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(true)
.select()
.apis(RequestHandlerSelectors.basePackage("com.mars"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("King 自定义 DOC 文档")
.description("king-swagger-api")
.termsOfServiceUrl("http://localhost:9098")
.version("v0.1")
.build();
}
}
在微服务中现在几乎都是无状态的,与往常的session不同;更多的是使用token来做用户的信息认证,那么接口的用户信息的全局读取我们就要做一些处理。实现的方式也很多,我这边使用的是利用ThreadLocal,来做全局信息的保存。接口授权后,会将用户等全局信息保存到ThreadLocal中;ThreadLocal我们知道它是线程的副本,意味着每个线程都持有,是隔离的,在线程的生命周期中的任何地方都能取到。基于这些就能满足我们的需要,其实spring的事务,也是利用ThreaLocal的特性,当然有人可能会考虑到ThreadLocal的内存泄露问题,不过只要在实现的时候注意就可以避免这个问题;这边不做详细阐述,后续会在单独章节讲解这个。ok,我们来看下实现:
首先先定义一个全局的GlobalEntry 作为数据存储的载体,里面可以根据需要在自己的团队中定义;然后定义一个上下文KingContext,作为数据的管理类;紧接着在定义一个KingContextProvider接口,里面是可以定义Entry的获取方法,由业务侧根据需要做具体实现,这边主要是想约束GlobalEntry获取的实现 因为GlobalEntry是比较敏感或者说能影响全局的数据。
package com.mars.fw.web.context;
import com.mars.fw.web.exception.KingException;
import org.apache.commons.lang.StringUtils;
/**
* 封装会话上下文 利用的是ThreadLocal的特性
* 这个是使用来管理接口生命周期内的全局变量的 最多的应用场景是在 用户信息的存储
*
* 这边的封装想尽量简化调用者的复杂度
*
* @Author King
* @create 2020/4/21 17:27
*/
public class KingContext {
private static ThreadLocal<GlobalEntry> CONTEXT = new ThreadLocal<GlobalEntry>();
public static void setContext(GlobalEntry context) {
CONTEXT.set(context);
}
public static void removeContext() {
CONTEXT.remove();
}
/**
* 获取用户ID
*
* @return
*/
public static Long getUserId() {
GlobalEntry entry = CONTEXT.get();
Long userId = null == entry ? null : entry.getUserId();
if (null == userId) {
throw new KingException("请先登录");
}
return userId;
}
/**
* 获取用户Code
*
* @return
*/
public static String getUserCode() {
GlobalEntry entry = CONTEXT.get();
String userCode = null == entry ? null : entry.getUserCode();
if (StringUtils.isBlank(userCode)) {
throw new KingException("请先登录");
}
return userCode;
}
/**
* 获取token
*
* @return
*/
public static String getToken() {
GlobalEntry entry = CONTEXT.get();
String token = null == entry ? null : entry.getToken();
if (org.springframework.util.StringUtils.isEmpty(token)) {
throw new KingException("请先登录");
}
return token;
}
/**
* 获取全局存储的信息
*
* @return
*/
public static Object getObject() {
GlobalEntry entry = CONTEXT.get();
Object object = null == entry ? null : entry.getObject();
if (org.springframework.util.StringUtils.isEmpty(object)) {
throw new KingException("会话上下文没有设置");
}
return object;
}
}
ok,这张的篇幅有点长,其他内容放到下章接着讲。
源码地址:码云源码地址
上一章
下一章