springboot 简单实现 REST 框架
偶然看到了黄勇的从 MVC 到前后端分离这篇博客,感觉与自己已经做好的异曲同工,于是将项目中的稍加修改,拿来分享一下。
注意,请看完黄勇大神的博客先,当然,你流连忘返我也是很乐意的。
公共模块实现功能
- 统一响应(勇哥博客有写)
- 日志输出
- 异常处理
- 参数验证
- 解决跨域(听前端说vue不存在这个问题)
- 安全机制(登录后后台生成token返回给前端,自己也留一份,前端以后访问都带上)
- 对象序列化(springboot自己能搞定)
- 动态数据源
废话先说
首先说明,以下都是用springboot作为基础实现的功能,如果你对传统的框架理解得很清楚,那么随便看看想必是没问题的,如果你跟我一样是菜鸟,简易先找几篇博客看下springboot基础教程。
日志输出
我这里直接使用springboot内置的,总觉得官方的就是好的,虽然别人都喜欢自己另搞一套,但是我嫌麻烦。
日志代码:
@Aspect
@Component
public class LogAspect {
private final Logger logger = LoggerFactory.getLogger(LogAspect.class);
@Pointcut("execution(public * com.anso.outwork.*.controller.*.*(..))")
public void webLog() {
}
@Before("webLog()")
public void deBefore(JoinPoint joinPoint) throws Throwable {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 记录下请求内容
logger.info("URL" + Constant.LOG_TAG + request.getRequestURL().toString());
logger.info("HTTP_METHOD" + Constant.LOG_TAG + request.getMethod());
logger.info("IP" + Constant.LOG_TAG + request.getRemoteAddr());
logger.info("CLASS_METHOD" + Constant.LOG_TAG + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
logger.info("ARGS" + Constant.LOG_TAG + Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) throws Throwable {
// 处理完请求,返回内容
logger.info("方法的返回值" + Constant.LOG_TAG + ret);
}
//后置异常通知
@AfterThrowing(pointcut = "webLog()", throwing = "e")
public void throwss(JoinPoint jp, Exception e) {
logger.error("方法抛出异常" + Constant.LOG_TAG + e.getMessage());
}
}
我这里直接给所有的controller加了一个切面,这样每次有请求进来,都会记录到日志中去,方法抛异常了,也会记录下来。
日志输出到文件就,需要在配置文件application.properties中加logging.file=log/test.log
表示生成的日志是与当前项目同目录的log目录下的test.log文件。
异常处理
异常处理在全局拦截,与上面的日志类似,不过还是有小小区别。
另外,需要自定义一个日志类继承RuntimeException,下面贴下代码:
public class CustomException extends RuntimeException {
private int code;
private String message;
...省略setter、getter
}
@ControllerAdvice
@Component
public class GlobalExceptionHandler {
private final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(CustomException.class)
@ResponseBody
public Object baseExceptionHandler(HttpServletResponse response, CustomException e) {
logger.error(e.getMessage() + Constant.LOG_TAG, e);
return new Response(e.getCode(), e.getMessage());
}
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Object otherExceptionHandler(HttpServletResponse response, Exception e) {
Response res = null;
if (e instanceof org.springframework.web.servlet.NoHandlerFoundException) {
res = new Response(404, "找不到资源");
} else {
res = new Response(Response.SERVER_ERROR, e.getMessage());
}
logger.error(e.getMessage() + Constant.LOG_TAG, e);
return res;
}
@ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseBody
public Object handleValidationException(MethodArgumentNotValidException e) {
logger.error(Response.PARAM_VALID_ERROR_MSG + Constant.LOG_TAG);
//按需重新封装需要返回的错误信息
List<ArgumentInvalidResult> invalidArguments = new ArrayList<>();
//解析原错误信息,封装后返回,此处返回非法的字段名称,原始值,错误信息
for (FieldError error : e.getBindingResult().getFieldErrors()) {
ArgumentInvalidResult invalidArgument = new ArgumentInvalidResult();
invalidArgument.setDefaultMessage(error.getDefaultMessage());
invalidArgument.setField(error.getField());
invalidArgument.setRejectedValue(error.getRejectedValue());
invalidArguments.add(invalidArgument);
logger.error(invalidArgument.toString());
}
return new Response(Response.PARAM_VALID_ERROR, Response.PARAM_VALID_ERROR_MSG, invalidArguments);
}
}
这里一共拦截了三种异常:
- CustomException:自定义异常,一般是业务出错或者service中的错误。
- MethodArgumentNotValidException:参数检验抛出的异常,表示前端传过来的参数有问题。
Exception:上面两种之外的所有异常,一般是服务器运行出错。
加了ResponseBody,表示在这里直接给前端返回了,所以不仅打印了日志,还用Response将错误信息包装了起来,返回给前端。这样做的好处是,即使后端出错,前端接收到的响应,依旧是统一的。
参数验证
这里使用java自带的@Valid注解,代码如下:
@PutMapping("/app_version")
public Object update(@RequestParam @Valid AppVersion appVersion){
...
}
然后在实体类需要验证的属性上,加上@NotNull、@Max(…)、@Min(…)等就可以生效了。
前面说过,如果此时参数验证不通过,抛出的异常会由全局异常处理,返回结果示例:
{
"code": 406,
"message": "参数验证失败",
"data": [
{
"field": "ex",
"rejectedValue": null,
"defaultMessage": "不能为null"
}
]
}
动态数据源
这个其实不一定能用到,但是用到了就会很方便。
我用到的场景是,很多个需求方大概几十个吧,用同一套系统,除了数据库需要分开,其他的数据结构都是一样的。之前都是分开部署,发现极大地消耗了服务器内存,于是想到了用动态数据源来处理。
这样做的好处是不同的用户带着标识进来,我就切换数据源让他访问相对应的数据库,而不用像之前那样每个用户部署一套系统。
这样一来,最优的情况是只用部署一套系统,不过这样当然不行,至少部署几个做负载均衡还是必要的。
废话一大堆,下面看下代码:
@Configuration
public class DataSourceConfig {
@Value("${spring.datasource.config.path}")
private String databasePath;
@Bean
public DynamicDataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();//单例
loadDataSource(dynamicDataSource);
return dynamicDataSource;
}
public void loadDataSource(DynamicDataSource dynamicDataSource) {
Map<Object, Object> map = new HashMap<>();
try {
Properties prop = new Properties();
FileInputStream fis = new FileInputStream(ResourceUtils.getFile(databasePath));
prop.load(fis);
fis.close();
Set<Map.Entry<Object, Object>> set = prop.entrySet();
for (Map.Entry<Object, Object> entry : set) {
String dbConfig = entry.getValue().toString();
JSONObject jsonObject = JSON.parseObject(dbConfig);
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(jsonObject.getString("url"));
dataSource.setUsername(jsonObject.getString("username"));
dataSource.setPassword(jsonObject.getString("password"));
dataSource.setDriverClassName(jsonObject.getString("driverClass"));
map.put(jsonObject.getString("name"), dataSource);
if (jsonObject.getBoolean("default")) {
dynamicDataSource.setDefaultTargetDataSource(dataSource);
}
}
} catch (Exception e) {
e.printStackTrace();
}
dynamicDataSource.setTargetDataSources(map);
}
@Bean
public SqlSessionFactory sqlSessionFactory(
@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
try {
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/*.xml"));
Resource resource = new PathMatchingResourcePatternResolver()
.getResource("classpath:mapper/config/mybatis-config.xml");
bean.setConfigLocation(resource);
return bean.getObject();
} catch (Exception e) {
System.out.println("===========================================================================" + e.getMessage());
}
return null;
}
@Bean(name = "sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(
@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory)
throws Exception {
if (sqlSessionFactory == null) {
return null;
}
return new SqlSessionTemplate(sqlSessionFactory);
}
}
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static synchronized void setDBType(String dbType) {
contextHolder.set(dbType);
}
public static String getDBType() {
return contextHolder.get();
}
public static void clearDBType() {
contextHolder.remove();
}
}
public class DynamicDataSource extends AbstractRoutingDataSource {
private static DynamicDataSource instance;
private static byte[] lock = new byte[0];
private static Map<Object, Object> dataSourceMap = new HashMap<>();
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
dataSourceMap.putAll(targetDataSources);
super.afterPropertiesSet();// 必须添加该句,否则新添加数据源无法识别到
}
public Map<Object, Object> getDataSourceMap() {
return dataSourceMap;
}
public static synchronized DynamicDataSource getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new DynamicDataSource();
}
}
}
return instance;
}
//必须实现其方法
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDBType();
}
}
@Aspect
@Component
public class DynamicDBAspect {
private final Logger LOGGER = LoggerFactory.getLogger(DynamicDBAspect.class);
@Pointcut("execution(public * com.anso.outwork.*.controller.*.*(..))")//demo1包下所有类的所有方法
public void controllerPC() {
}
@Before("controllerPC()")
public void deBefore(JoinPoint joinPoint) throws Throwable {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//动态改变数据库
String projectName = request.getHeader(Constant.PROJECT_HEADER);
try {
LOGGER.info("请求的数据库projectName" + Constant.LOG_TAG + projectName);
DataSourceContextHolder.setDBType(projectName);
} catch (Exception e) {
LOGGER.error("请求数据库" + projectName + "出错", e.getMessage());
}
}
}
在DataSourceConfig中初始化自定义配置文件中的所有数据库配置,使用ThreadLocal做数据源切换,这样就不会出现并发的问题了。
最主要的是,用切面做切换,这样是不是方便极了。