写此篇博文目的有2个:
一、我想做一个最小化框架,包括配置、拦截器,工具包等等都一应具备,使开发可以直接开始编写业务代码,所以我的demo的名字叫做springboot-base。
二、这也是为了以后介绍springcloud做的一个铺垫。
该框架所有设置均来自本人工作实际开发经验总结出来的。好,废话不多说,我们开始吧!
一、springboot介绍
java一直给人很“重”的感觉,从ssh到s2sh到ssm,都离不开大量的xml文件以及搭建tomcat,所以springboot应运而生。springboot其实本质还是ssh(springmvc、spring、jpa(底层是hibernate)),它给这些框架赋予默认的配置(可手动修改),使得你的项目可以不需要任何xml就可以运行起来。当然它内嵌了tomcat,所以可以java -jar就可以运行,无需手动装tomcat。
二、自定义封装框架介绍
①对request请求,添加拦截器。继承webMvcConfiguerAdapter,将自定义的拦截器注入即可。
该拦截器主要5个功能:
1、如果有OpenInterface注解(自定义注解)则跳过拦截,主要用于第三方回调,或者接口直接调用。
2、获取前端app_name、app_version,这两个参数主要用于验证app版本是否需要更新,如果是web项目这两个参数可以不要,最后把他们写到MDC去(springboot默认logback,MDC是sle4j的一个记录线程数据工具,底层实现是用threadlocal)
3、添加追踪信息到MDC里去。traceId作用类似于springcloud的sleuth,可以在查询日志时候,以该关键字拉取某个请求全部日志。用uuid生成,保证每个请求的traceId不一样。
4、如果网页需要切换语言,把下面注释的部分打开就可以了,一个做语言国际化标准。
5、token校验,此处是自己写的token校验,如果大家用shiro或者spring security这个可以不用。主要功能还是判断是否登入,不需要该功能的可以删除。
总结:这里对请求做了格式要求。把基础信息写在header里,把token写在cookie里,这也是我一直以来做项目的习惯,大家有不同习惯可以自行更改。
@Configuration
public class GlobalRequestHandler extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestInterceptor());
}
public HandlerInterceptorAdapter requestInterceptor() {
return new HandlerInterceptorAdapter() {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//OpenInterface注释的接口不拦截
if (((HandlerMethod) handler).getMethod().isAnnotationPresent(OpenInterface.class)) {
return true;
}
interceptorHandler(request, handler);
return true;
}
};
}
private void interceptorHandler(HttpServletRequest request, Object handler) {
//获取请求信息
String appName = request.getHeader(Constants.APP_NAME);
String appVersion = request.getHeader(Constants.APP_VERSION);
//String lang = request.getHeader(Constants.LANG);
if (StringUtils.isEmpty(appName) || StringUtils.isEmpty(appVersion)) {
throw new SystemException(ExceptionCode.HANDLER_PARAM_ERROR.getCode(), "handler param is null!");
}
//清空线程缓存
MDC.clear();
//设置请求信息
MDC.put(Constants.APP_NAME, appName);
MDC.put(Constants.APP_VERSION, appVersion);
MDC.put("traceId", UUID.randomUUID().toString());
//语言国际化,有需要开启
//LocaleContextHolder.setLocale(StringUtils.isEmpty(lang) ? Constants.DEFAULT_LANG : Locale.forLanguageTag(lang));
//检查token,标记UncheckToken不检查token
if (!((HandlerMethod) handler).getMethod().isAnnotationPresent(UncheckToken.class)) {
String token = "";
token = Utils.getCookieByName(request.getCookies(), Constants.COOKILE_TOKEN);
//TODO 校验token步骤
//if (StringUtils.isEmpty(token) && ){
// throw new SystemException(ExceptionCode.NEED_LOGIN.getCode(), "token is over time");
//}
}
}
}
②封装response返回格式
此处修改了reponse返回格式。效果:不管返回的是对象还是string都统一格式化成code、data、msg。
这里之所以把springmvc的源码进行修改,是因为如果返回string类型,那么如果你把它格式化后会报错,原因是springmvc认为string类型应该用string信息解析器,但是你却把它封装成了个对象,导致无法解析。
@RestControllerAdvice
public class GlobalResponseHandler extends WebMvcRegistrationsAdapter{
/*
* WebMvcRegistrationsAdapter通过getRequestMappingHandlerAdapter获取RequestMappingHandlerAdapter。
* RequestMappingHandlerAdapter重写ReturnValueHandlers方法
* 传入的视图解析器是jackson
* 注:ReturnValueHandlers调用视图转换器,所以在此处替换returnValue
*/
@Override
public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
RequestMappingHandlerAdapter requestMappingHandlerAdapter = new RequestMappingHandlerAdapter();
List handlerList = new ArrayList<>();
handlerList.add(new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())){
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
super.handleReturnValue(packageResult(returnValue), returnType, mavContainer, webRequest);
}
});
requestMappingHandlerAdapter.setReturnValueHandlers(handlerList);
return requestMappingHandlerAdapter;
}
private Object packageResult(Object data){
Response response = new Response();
response.setData(data);
return response;
}
}
③全局异常拦截
作用和上面差不多,只是上面的是对response格式化,这里是全局异常拦截并且格式化。很简单,不讲解。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Throwable.class)
public Response handlerException(Throwable throwable) {
Response response = new Response();
response.setMeg(throwable.getMessage());
response.setCode(ExceptionCode.SYSTEM_ERROR.getCode());
return getError(throwable, response);
}
private Response getError(Throwable throwable, Response response){
if (throwable instanceof ServiceException){
response.setCode(((ServiceException)throwable).getCode());
}else if (throwable instanceof NoHandlerFoundException) {
response.setCode(ExceptionCode.NO_HANDLER_ERROR.getCode());
} else if (throwable instanceof HttpMessageNotReadableException) {
response.setCode(ExceptionCode.PARAM_TYPE_ERROR.getCode());
}
return response;
}
}
④日志(logback)
springboot默认是用logback,如果用log4j朋友也可以换,但是强烈建议你用logback。
我用的是logback.groovy,更多人用的是logback.xml。这两个可以相互转的,但是我喜欢groovy的简洁。这里唯一需要提的就是logstash是对接ELK的,所以FILE-INFO用该格式化,可以直接对接ELK。
def final LOCATION = "/data/log/app/"
def final SERVER_NAME = "base"
def final SAVE_TIME_RANGE = 7
String ENV = System.getProperty("env")
if (StringUtils.isEmpty(ENV)) {
appender('CONSOLE', ConsoleAppender) {
encoder(PatternLayoutEncoder) {
pattern = "%d{yyyy-MM-dd HH:mm:ss.SSS} %relative [%thread] %-5level %logger{36} %X{requestId} - %msg%n"
}
}
root(INFO, ['CONSOLE'])
} else {
appender('FILE-INFO', RollingFileAppender) {
rollingPolicy(TimeBasedRollingPolicy) {
fileNamePattern = String.format("%s%s/%s%s%s", LOCATION, SERVER_NAME, "logFile.", ENV, ".%d{yyyy-MM-dd}.log.gz")
maxHistory = SAVE_TIME_RANGE
}
encoder(LogstashEncoder) {
includeMdcKeyNames = ["traceId"]
}
}
root(INFO, ['FILE-INFO'])
}
注意点:要想运行该项目必须要装mysql,主要我集成了数据库,方便大家把框架拿过来改改名字就能直接用。端口默认是9000,数据库信息等都在application.yml里修改。
该项目还有很多我自己写的工具方法等等,在我日常工作中还将不断的添砖加瓦,谢谢!
github地址:https://github.com/wangsunqun/springboot-base.git