With 3个Web基础模块

以下内容纯属个人扯淡,仅供参考

目录

统一结果返回

全局异常处理

全局日志收集


 

参考:

Java项目构建基础:统一结果,统一异常,统一日志

统一结果返回

题外话:前后端分离是一种设计理念,数据传输格式一般都是json,因此统一一个规范的数据格式有利于双方代码约定。因此即便使用了Thymeleaf、Freemarker等框架,也尽量这样设计:提供单独的控制器方法仅用于返回视图,额外提供方法用于数据交互(但这样设计的话是无法实现纯粹的RestFul风格的)

参考:SpringBoot2.2.2.RELEASE+Thymeleaf

(1)统一数据格式

public class CommonResult<T> {
    private long code;
    private String message;
    private T data;
    protected CommonResult() {
    }
    protected CommonResult(long code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }
    public long getCode() {
        return code;
    }
    public void setCode(long code) {
        this.code = code;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}

这个CommonResult就是一个领域类,类似DTO。它定义了code、message、data三个属性,分别表示统一数据格式中的状态码、信息、数据,构造器和setter/getter是一个DTO必备的。

定义完上述DTO类后,我们如果要返回数据到前端,那么就像这样,示例

@GeiMapping("user/{userId}")
public CommonResult<User> getUser(@ParamVariable String userId){
    
    User user = userService.getOne(userId);
    if(data == null){
         return new CommonResult("500","操作失败",user);
    }
    return new CommonResult("200","操作成功",user);
}

扩展:泛型T。这里定义data是泛型,那么他能接收任意类型数据,这里用Object类型替换也是能接受任意类型的,但会有强制类型转换风险问题,这是运行时才会报出的问题,使用泛型可以在编译期就能发现类型转换问题。参考:java 泛型和object比较

(2)状态码、消息体枚举类

就功能而言是没问题的,我们在任何需要返回给前端数据的地方调用即可,但是就代码维护而言是有很多问题的:

1.硬编码
    "200"、"操作成功"等字面量不建议直接写在业务代码逻辑里(IDEA会提示你这是魔法值)
2.表意不够
    200本身是个无表意的量值,而若定义为private static final String SUCCESS = "200"才能表达这个200是成功的意思,代码
并非你自己看得懂就足够了
3.散乱、重构不友好
    RFC规范规定:第1位数字表示了5种响应状态:1=消息,2=成功,3=重定向,3=请求错误,5=服务器错误
    这样的设计下,最多支持10类大类,每个大类下最多100种具体情况,即共1000种具体响应码

当系统业务后期扩大到一定规模时,3位状态码已不足以支撑所有所有情况,3位数太少表意不够。
可以设计为5位,前2位表示大类情况,后3位表示具体情况,就共10000种具体响应码
那么,当你要用5位去重构替换3位时,你就不得不修改整个Web每个控制器方法返回处的代码
我们必须要知晓所有接口的返回值,才能知道整个系统返回给前端的code有哪些类型
如果能定义到一个类中,那么就显而易见的了
    

code字段对前端来说至关重要,它的值有限可知的,只能是500、400、401等等,前端的需要根据code的具体值去控制js逻辑,例如:200表示正常,那么就进行接下来的操作:从data中获取数据显示等等;500表示异常,此时需要取出msg值提示用户失败。因此code值间接控制着前端的代码执行方向,必须谨慎对待

msg字段是提示信息,本身也不是很重要。我们可以设计成半有限可知的:既有提供默认的值,如:操作成功、操作失败等等,也支持根据特定情况传入特定的提示进行选择,("用户名错误",这样的提示信息是特定业务下的返回信息)

因此,可以将code和msg设计到一个枚举类中,其中每个枚举实例中保存着那些有限可知的值。

public enum ResultCode {
    /**
     * 操作成功
     */
    SUCCESS(200, "操作成功"),
    /**
     * 操作失败,服务器内部错误
     */
    FAILED(500, "操作失败"),
    /**
     * 参数检验失败
     */
    VALIDATE_FAILED(400, "请求参数有误"),
    /**
     * 暂未登录或token已经过期
     */
    UNAUTHORIZED(401, "登录失败"),
    /**
     * 没有相关权限
     */
    FORBIDDEN(403, "没有相关权限"),
    /**
     * 未找到资源
     */
    NOT_FOUND(404,"未找到相关资源");

    private long code;

    private String message;

    private ResultCode(long code, String message) {
        this.code = code;
        this.message = message;
    }

    public long getCode() {
        return this.code;
    }

    
    public String getMessage() {
        return this.message;
    }

}

这里是定义一个普通的枚举类ResultCode,其中6个实例分别对应返回给前端的6种状态码和对应的默认提示信息,如果还有更多的情况,就直接在枚举类中添加对应实例即可

现在我们可以这样使用

@GeiMapping("user/{userId}")
public CommonResult<User> getUser(@ParamVariable String userId){
    
    User user = userService.getOne(userId);
    if(data == null){
        //默认提示消息
        return new CommonResult(FAILED.getCode(),SUCCESS.getMessage(),user);
        //如果是要自定义的提示消息
        //return new CommonResult(FAILED.getCode(),"用户不存在",user);
    }
    return new CommonResult(SUCCESS.getCode(),SUCCESS.getMessage(),user);
}

扩展:如果我们希望枚举实例拥有某种能力,通过这个能力能完成某件事情,比如:doEat(),每个枚举实例都需要有doEat()这样的能力,但是每个枚举实例执行eat的内容都不一样,可以用枚举类实现接口完成

public interface Eat {

    void doEat();
}

这个时候枚举类就需要有一个Eat类型的成员去完成这样的事情,并且每个枚举实例做的事情内容不一样

public enum ResultCode {
    
    SUCCESS(200, "操作成功",new Eat(){
        
        @Override
        public void doEat(){
            System.out.println("成功吃");
        }
    }),
    
    FAILED(500, "操作失败",new Eat(){

        @Override
        public void doEat(){
            System.out.println("失败吃");
        }
    });


    private long code;

    private String message;

    private Eat eat;

    private ResultCode(long code, String message,Eat eat) {
        this.code = code;
        this.message = message;
        this.eat = eat;
    }

    public long getCode() {
        return this.code;
    }

    
    public String getMessage() {
        return this.message;
    }

    public void doEat() {
        eat.doEat();
    }

}

这样,调用不同枚举实例去完成doEat事情时,它们都具备doEat()方法能力,但是有各自的实现

ResultCode.SUCCESS.doEat();
ResultCode.FAILED.doEat();

而当每个枚举实例在这样的方法内执行的代码逻辑是一样时,就可以将方法都抽离到公共接口中

public interface IErrorCode {

    /**
     * 获取code属性
     *
     * @return -
     */
    long getCode();

    /**
     * 获取Message属性
     *
     * @return -
     */
    String getMessage();
}

因此,最终枚举类被设计成这样

public enum ResultCode implements IErrorCode {
    /**
     * 操作成功
     */
    SUCCESS(200, "操作成功"),
    /**
     * 操作失败,服务器内部错误
     */
    FAILED(500, "操作失败"),
    /**
     * 参数检验失败
     */
    VALIDATE_FAILED(400, "请求参数有误"),
    /**
     * 暂未登录或token已经过期
     */
    UNAUTHORIZED(401, "登录失败"),
    /**
     * 没有相关权限
     */
    FORBIDDEN(403, "没有相关权限"),
    /**
     * 未找到资源
     */
    NOT_FOUND(404,"未找到相关资源");

    private long code;

    private String message;

    private ResultCode(long code, String message) {
        this.code = code;
        this.message = message;
    }

    @Override
    public long getCode() {
        return this.code;
    }

    @Override    
    public String getMessage() {
        return this.message;
    }

}

这实际是"面向接口"思想用在枚举类上,那么若有方法需要接收ResultCode枚举类作为形参时,我们就可以传递一个IErrorCode接口,而不是ResultCode类型

(3)统一结果生成工具类

上面的代码还是有一点冗余的。我们每次返回CommonResult对象,Controller控制器方法需要知道太多细节了:它需要知道CommonResult构造器的3个参数具体每个参数细节,例如:当用户不存在时,它应该传递一个null,而不是user=null。

return new CommonResult(FAILED.getCode(),"用户不存在",user);
return new CommonResult(SUCCESS.getCode(),SUCCESS.getMessage(),user);

因此,我们应该把这种细节交由另外一个类负责,控制器方法只需要选择方法。它可以选择不传递任何数据给CommonResult对象返回给前端,也可以传递数据、特定的提示信息

public class CommonResultUtil {

/**
     * 成功返回结果
     *
     * @param data 获取的数据
     */
    public static <T> CommonResult<T> success(T data) {
        return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
    }

    /**
     * 成功返回结果
     *
     * @param data 获取的数据
     * @param  message 提示信息
     */
    public static <T> CommonResult<T> success(T data, String message) {
        return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
    }

    /**
     * 默认失败返回结果
     * 使用{@link ResultCode#FAILED}
     */
    public static <T> CommonResult<T> failed() {
        return failed(ResultCode.FAILED);
    }

    /**
     * 失败返回结果
     * @param message 提示信息
     */
    public static <T> CommonResult<T> failed(String message) {
        return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null);
    }

    /**
     * 通用接口
     * @param errorCode 错误码
     */
    public static <T> CommonResult<T> failed(IErrorCode errorCode) {
        return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
    }

    /**
     * 参数验证失败返回结果
     */
    public static <T> CommonResult<T> validateFailed() {
        return failed(ResultCode.VALIDATE_FAILED);
    }

    /**
     * 参数验证失败返回结果
     * @param message 提示信息
     */
    public static <T> CommonResult<T> validateFailed(String message) {
        return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null);
    }

    /**
     * 未认证/登录的返回结果
     */
    public static <T> CommonResult<T> unAuthenticated(T data) {
        return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
    }

    /**
     * 未授权的返回结果
     */
    public static <T> CommonResult<T> unAuthorized(T data) {
        return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
    }
}

那么,现在调用方法就可以改成

return CommonResultUtil.success(user);
return CommonResultUtil.failed("用户不存在"); //这个字面量最好也定义一个static final来代替

疑问:分页对象怎么传递呢?:这里使用的是MybatisPlus。定义一个分页对象,再将分页对象传递给CommonResult即可,分页对象定义如下:

/**
 * 通用分页
 *
 * @date 17:23 2020/3/27
 * @author 
 **/
@Data
public class CommonPage<T> {

    /**
     * 当前页
     */
    private long pageNum;

    /**
     * 单页记录数
     */
    private long pageSize;

    /**
     * 总页数
     */
    private long totalPage;

    /**
     * 总记录数
     */
    private long count;

    /**
     * 数据集
     */
    private List<T> data;

    /**
     * 封装List数据到
     *
     * @date 17:33 2020/3/27
     * @author 李文龙
     * @param
     * @return
     **/
    public static <T> CommonPage<T> restPage(IPage<T> page) {
        CommonPage<T> result = new CommonPage<T>();
        result.setTotalPage(page.getPages());
        result.setPageNum(page.getCurrent());
        result.setPageSize(page.getSize());
        result.setCount(page.getTotal());
        result.setData(page.getRecords());
        return result;
    }
}

调用示例

@GetMapping("listAll")
public CommonResult<CommonPage<PctMenuVO>> listAll(
            @RequestParam Integer page,
            @RequestParam Integer limit,
            @RequestParam(value = "name", required = false) String name) {
    CommonPage<PctMenuVO> commonPage = pctMenuService.queryPctMenuByNameWithPaging(page, limit, name);
    return CommonResult.success(commonPage);
}

//PctMenuServiceImpl
public CommonPage<PctMenuVO> queryPctMenuByNameWithPaging(Integer current, Integer size, String name) {
    Page<PctMenuVO> page = new Page<>(current, size);
    IPage<PctMenuVO> data = baseMapper.queryPctMenuByNameWithPaging(page, name);
    return CommonPage.restPage(data);
}

//PctMenuMapper
Page<PctMenuVO> queryPctMenuByNameWithPaging(Page<PctMenuVO> page, @Param("name")String name);

//PctMenuMapper.xml
//<select id="queryPctMenuByNameWithPaging" //resultType="com.yihuacomputer.yhcloud.vo.PctMenuVO">
//    SELECT
//        pm.ID,pm.CODE,pm.NAME,pm.URL,
//        pm.TYPE,pm.PARENT_ID,pm.STATUS,pm.LAYOUT_EN,
//        pm.REMARK,pm.OPERATOR,pm.OPERATE_TIME
//    FROM PCT_MENU pm
//    WHERE 1=1
//    AND pm.STATUS != 3
//    <if test="name != null">
//        AND pm.NAME like '%${name}%'
//    </if>
//</select>

 

全局异常处理

1、系统异常设计

1)参数校验异常

参考:SpringBoot2.2.2.RELEASE+参数校验

其中Hibernate Validator所提供的参数校验注解(或者是自定义的参数校验注解),在参数校验功能中若校验失败,则会抛出

MethodArgumentNotValidException

2)自定义参数校验异常

注解型参数校验只是在Controlller层,其在于当校验失败时,利用AOP原理拦截控制器方法调用并抛出异常。但这种方式只适用于简单的参数校验,当校验逻辑复杂时、或希望在Service业务层进行更自由化的参数校验时,可以自定义一个异常类,然后在业务逻辑进行编码实现参数校验,主动抛出异常以终止方法执行(注意:这种方式可能很难去复用校验逻辑,它适合特定的场景)

package com.yihuacomputer.yhcloud.common.exception;

import com.yihuacomputer.yhcloud.common.api.IErrorCode;

/**
 * @ClassName ExcelParseException
 * @Description Excel解析异常
 * @Author 罗新宇
 * @Date 2020/3/31 9:22
**/
public class ExcelParseException extends RuntimeException {

    private IErrorCode errorCode;

    public ExcelParseException(IErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }

    public ExcelParseException(String message) {
        super(message);
    }

    public ExcelParseException(Throwable cause) {
        super(cause);
    }

    public ExcelParseException(IErrorCode errorCode,String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public ExcelParseException(IErrorCode errorCode,String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;

    }

    public IErrorCode getErrorCode() {
        return errorCode;
    }
}

异常工具类

public class ExcelParseAsserts {

    public static void fail(String message){
        throw new ExcelParseException(message);
    }

    public static void fail(IErrorCode errorCode){
        throw new ExcelParseException(errorCode);
    }

    public static void fail(IErrorCode errorCode,String message) {
        throw new ExcelParseException(errorCode,message);
    }

    public static void fail(IErrorCode errorCode,String message,Throwable throwable){
        throw new ExcelParseException(errorCode,message,throwable);
    }
}

使用示例

 if(cell.getCellTypeEnum() == CellType.NUMERIC){
    //设置Y轴数值
    pctRecord.setYValue(cell.getNumericCellValue());
}else{
    //返回错误信息
    ExcelParseAsserts.fail(ResultCode.FAILED,"第"+r+"行第"+c+"列数据格式不正确,应为数字类型!");
}

2、控制器通知


/**
 * 全局异常处理器
 *
 * @date 16:49 2020/3/19
 * @author 
 **/
@ControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class GlobalExceptionHandler {
    private static final String UNKNOWN_EXCEPTION_MSG = "未知异常";

    /**
     * 其他未捕获的异常
     *
     * @date 9:47 2020/4/8
     * @author 李文龙
     * @param e:
     * @return
     **/
    @ResponseBody
    @ExceptionHandler(value = Exception.class)
    public CommonResult<String> handleUnKnownException(Exception e) {
        //TODO 目前是直接打印在控制台以便追踪问题,后期需要将异常跟踪信息备份到log文件中
        e.printStackTrace();

        String msg = e.getMessage();

        if (StringUtils.isEmpty(msg)) {
            return CommonResult.failed(msg);
        }

        return CommonResult.failed(UNKNOWN_EXCEPTION_MSG);
    }


    /**
     * 注解型:参数校验异常
     **/
    @ResponseBody
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public CommonResult<String> handleMethodArgumentNotValidException(
        MethodArgumentNotValidException e) {
        BindingResult result = e.getBindingResult();
        FieldError fieldError = result.getFieldError();

        if (fieldError != null) {
            return CommonResult.validateFailed(fieldError.getDefaultMessage());
        } else {
            return CommonResult.validateFailed();
        }
    }

    /**
     * 捕获表格解析异常
     */
    @ResponseBody
    @ExceptionHandler(value = ExcelParseException.class)
    public CommonResult<String> handleParamException(ExcelParseException e) {
        if (e.getMessage() != null) {
            return CommonResult.failed(e.getMessage());
        }

        return CommonResult.failed();
    }

}

疑问:多个异常处理器所处理的异常类型是有继承关系的,那如何判定使用哪个处理器呢?ExceptionHandler的执行顺序

通过查看源码:ExceptionHandlerMethodResolver#getMappedMethod方法可知,首先找到可以匹配的所有ExceptionHandler,然后对其进行排序,利用深度比较器算法(递归判断父类异常是否为目标异常)取深度最小的那个,也即匹配度最高的那个

结论:定义多个ExceptionHandler时,要注意其所处理的异常类的继承关系,这决定了处理优先级

全局日志收集

1、打印web层入口信息

2、日志文件收集

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Baa 一个简单高效的Go web开发框架。主要有路由、中间件,依赖注入和HTTP上下文构成。Baa 不使用 ``反射``和``正则``,没有魔法的实现。快速上手安装:go get -u gopkg.in/baa.v1示例:package main import (     "gopkg.in/baa.v1" ) func main() {     app := baa.New()     app.Get("/", func(c *baa.Context) {         c.String(200, "Hello World!")     })     app.Run(":1323") }特性支持静态路由、参数路由、组路由(前缀路由/命名空间)和路由命名路由支持链式操作路由支持文件/目录服务支持中间件和链式操作支持依赖注入*支持JSON/JSONP/XML/HTML格式输出统一的HTTP错误处理统一的日志处理支持任意更换模板引擎(实现baa.Renderer接口即可)中间件[gzip](https://github.com/baa-middleware/gzip)[logger](https://github.com/baa-middleware/logger)[recovery](https://github.com/baa-middleware/recovery)[session](https://github.com/baa-middleware/session)组件(DI)[cache](https://github.com/go-baa/cache)[render](https://github.com/go-baa/render)性能测试和快速的Echo框架对比 [Echo](https://github.com/labstack/echo)> 注意:[Echo](https://github.com/labstack/echo) 在V2版本中使用了fasthttp,我们这里使用 [Echo V1](https://github.com/labstack/echo/releases/tag/v1.4) 测试。路由测试使用 [go-http-routing-benchmark] (https://github.com/safeie/go-http-routing-benchmark) 测试, 2016-02-27 更新.[GitHub API](http://developer.github.com/v3)> Baa的路由性能非常接近 Echo.BenchmarkBaa_GithubAll             30000     50984 ns/op       0 B/op       0 allocs/op BenchmarkBeego_GithubAll            3000    478556 ns/op    6496 B/op     203 allocs/op BenchmarkEcho_GithubAll           30000     47121 ns/op       0 B/op       0 allocs/op BenchmarkGin_GithubAll             30000     41004 ns/op       0 B/op       0 allocs/op BenchmarkGocraftWeb_GithubAll      3000    450709 ns/op  131656 B/op    1686 allocs/op BenchmarkGorillaMux_GithubAll       200   6591485 ns/op  154880 B/op    2469 allocs/op BenchmarkMacaron_GithubAll          2000    679559 ns/op  201140 B/op    1803 allocs/op BenchmarkMartini_GithubAll           300   5680389 ns/op  228216 B/op    2483 allocs/op BenchmarkRevel_GithubAll            1000   1413894 ns/op  337424 B/op    5512 allocs/opHTTP测试Baa:package main import ( "github.com/baa-middleware/logger" "github.com/baa-middleware/recovery" "gopkg.in/baa.v1" ) func hello(c *baa.Context) { c.String(200, "Hello, World!\n") } func main() { b := baa.New() b.Use(logger.Logger()) b.Use(recovery.Recovery()) b.Get("/", hello) b.Run(":8001") }Echo:package main import ( "github.com/labstack/echo" mw "github.com/labstack/echo/middleware" ) // Handler func hello(c *echo.Context) error { return c.String(200, "Hello, World!\n") } func main() { // Echo instance e := echo.New() // Middleware e.Use(mw.Logger()) // Routes e.Get("/", hello) // Start server e.Run(":8001") }测试结果:> Baa 在http中的表现还稍稍比 Echo 好一些。Baa:$ wrk -t 10 -c 100 -d 30 http://127.0.0.1:8001/ Running 30s test @ http://127.0.0.1:8001/   10 threads and 100 connections   Thread Stats   Avg      Stdev     Max    /- Stdev     Latency     1.92ms    1.43ms  55.26ms   90.86%     Req/Sec     5.46k   257.26     6.08k    88.30%   1629324 requests in 30.00s, 203.55MB read Requests/sec:  54304.14 Transfer/sec:      6.78MB Echo:$ wrk -t 10 -c 100 -d 30 http://127.0.0.1:8001/ Running 30s test @ http://127.0.0.1:8001/   10 threads and 100 connections   Thread Stats   Avg      Stdev     Max    /- Stdev     Latency     2.83ms    3.76ms  98.38ms   90.20%     Req/Sec     4.79k     0.88k   45.22k    96.27%   1431144 requests in 30.10s, 178.79MB read Requests/sec:  47548.11 Transfer/sec:      5.94MB案例目前使用在 健康一线 的私有项目中。手册[godoc](http://godoc.org/github.com/go-baa/baa)贡献Baa的灵感来自 [beego](https://github.com/astaxie/beego) [echo](https://github.com/labstack/echo) [macaron](https://github.com/go-macaron/macaron)- [safeie](https://github.com/safeie)、[micate](https://github.com/micate) - Author- [betty](https://github.com/betty3039) - Language Consultant- [Contributors](https://github.com/go-baa/baa/graphs/contributors)LicenseThis project is under the MIT License (MIT) See the [LICENSE](https://raw.githubusercontent.com/go-baa/baa/master/LICENSE) file for the full license text. 标签:Web框架
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值