springboot简单实现REST 框架

springboot 简单实现 REST 框架

偶然看到了黄勇的从 MVC 到前后端分离这篇博客,感觉与自己已经做好的异曲同工,于是将项目中的稍加修改,拿来分享一下。

注意,请看完黄勇大神的博客先,当然,你流连忘返我也是很乐意的。

公共模块实现功能

  1. 统一响应(勇哥博客有写)
  2. 日志输出
  3. 异常处理
  4. 参数验证
  5. 解决跨域(听前端说vue不存在这个问题)
  6. 安全机制(登录后后台生成token返回给前端,自己也留一份,前端以后访问都带上)
  7. 对象序列化(springboot自己能搞定)
  8. 动态数据源

废话先说

首先说明,以下都是用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);
    }
}

这里一共拦截了三种异常:

  1. CustomException:自定义异常,一般是业务出错或者service中的错误。
  2. MethodArgumentNotValidException:参数检验抛出的异常,表示前端传过来的参数有问题。
  3. 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做数据源切换,这样就不会出现并发的问题了。

最主要的是,用切面做切换,这样是不是方便极了。

猜你喜欢

转载自blog.csdn.net/m0_37659871/article/details/81087877