SpringBoot统一返回结果,注解分页,方法切面

github地址
jar包已上传至中央仓库,代码还有很多问题,后续再逐渐修复
引入方式

<dependency>
	<groupId>com.github.softwarevax</groupId>
	<artifactId>support-springboot-starter</artifactId>
	<version>0.0.3.RELEASE</version>
</dependency>

1、统一返回结果

统一返回结果,只针对数据接口,即添加@ResponseBody注解的接口,@ResponseBody的作用是将java对象转为json格式的数据。统一返回结果的好处是方便以后更换返回的实体,也不用每个返回结果,都用一个实体包装起来,先看功能实现效果。

1.1、返回字符串

字符串不做处理,也可以做处理,看具体需要,这里是做了处理的。

请求:GET http://localhost:8080/support/aaa/string/hello

 @ResponseBody
 @GetMapping("/string/hello")
 public String string() {
 	return "hello";
 }

返回JSON字符串

{"code":200,"data":"hello","flag":true,"message":"接口调用成功"}

1.2、返回指定的DTO

返回指定的DTO,不做处理

请求:GET http://localhost:8080/support/aaa/dto/hello

@ResponseBody
@GetMapping("/dto/hello")
public ResultDto<String> dto() {
	return ResultDto.successT("hello dto");
}

返回DTO实体

{
  "flag": true,
  "data": "hello dto",
  "message": "接口调用成功",
  "code": 200
}

1.3、返回普通实体

返回普通实体,自动加上包装实体,User是业务实体

请求:GET http://localhost:8080/support/aaa/object/hello

@ResponseBody
@GetMapping("/object/hello")
public User object() {
	User user = new User();
	user.setId("id");
	user.setUserName("object");
	user.setSex("man");
	return user;
}

返回DTO实体,自动包装

{
  "flag": true,
  "data": {
    "id": "id",
    "userName": "object",
    "sex": "man"
  },
  "message": "接口调用成功",
  "code": 200
}

1.4、返回程序异常

返回异常信息,如果设置了全局异常处理,会先进入全局异常处理,然后到切面

请求:GET http://localhost:8080/support/aaa/exception/hello

@ResponseBody
@GetMapping("/exception/hello")
public User exception() {
	User user = new User();
	user.setId("id");
	user.setUserName("object");
	user.setSex("man");
	Assert.isTrue(1 == 0, "1不等于0");
	return user;
}

返回1:有全局异常处理时

{
  "flag": false,
  "data": "1不等于0",
  "message": "接口调用失败",
  "code": 500
}

返回2:没有全局异常处理时

{
  "flag": true,
  "data": {
    "timestamp": "2022-07-07T06:09:19.973+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "1不等于0",
    "path": "/support/aaa/exception/hello"
  },
  "message": "接口调用成功",
  "code": 200
}

1.5、不需要处理返回结果

在接口上添加注解:@IgnoreResultWrapper即可

1.6、切面实现统一返回结果

对返回结果,默认是进行实体包装的,实现统一结果返回,实现接口ResponseBodyAdvice,并设置为切面即可完成,github地址

public class ResultAspect implements ResponseBodyAdvice<Object> {

    @Autowired
    private ResultConstant constant;

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        //判断是否有加自定义注解,有就跳过,不返回返回结果包装实体
        AnnotatedElement annotatedElement = methodParameter.getAnnotatedElement();
        IgnoreResultWrapper ignoreResultWrapper = AnnotationUtils.findAnnotation(annotatedElement, IgnoreResultWrapper.class);
        return ignoreResultWrapper == null;
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        // 可考虑使用对象池
        IResult result = BeanUtils.instantiateClass(constant.getWrapperImpl());
        if (o instanceof String) {
            // String要特殊处理
            return result.returnString(o);
        } else if(o.getClass() == LinkedHashMap.class && HttpServletUtils.getResponseStatus() >= 500) {
            // 异常时的处理,返回时出错,说明客户端请求正常,不会是4xx
            result.error((LinkedHashMap) o);
        } else if (IResult.class.isAssignableFrom(o.getClass())) {
            //如果是实现IResult的实体,则直接返回
            return o;
        }
        return result.returnDto(o);
    }
}

2、注解实现分页

注解分页是依赖PageHelper,故需要引入相关jar包,另外只用于Mybatis,github地址

2.1、不添加分页注解

请求:

POST http://localhost:8080/support/aaa/user/list?pageSize=1&pageNum=1
Content-Type: application/x-www-form-urlencoded

@ResponseBody
@ApiOperation("用户列表查询")
@PostMapping("/user/list")
public PageInfo<User> queryList(User user) {
List<User> list = userService.list();
	return new PageInfo<>(list);
}

返回

{
  "flag": true,
  "data": {
    "total": 2,
    "list": [
      {
        "id": "1",
        "userName": "1",
        "sex": "1"
      },
      {
        "id": "2",
        "userName": "2",
        "sex": "2"
      }
    ],
    "pageNum": 1,
    "pageSize": 2,
    "size": 2,
    "startRow": 0,
    "endRow": 1,
    "pages": 1,
    "prePage": 0,
    "nextPage": 0,
    "isFirstPage": true,
    "isLastPage": true,
    "hasPreviousPage": false,
    "hasNextPage": false,
    "navigatePages": 8,
    "navigatepageNums": [
      1
    ],
    "navigateFirstPage": 1,
    "navigateLastPage": 1
  },
  "message": "接口调用成功",
  "code": 200
}

虽然返回的实体中有分页的相关属性,原因是返回实体是PageInfo,里面含有有些默认的属性,查看SQL日志,就会发现实际没有执行count查询,传入的pageSize是1,但是返会的仍然是两条

2.2、添加分页注解

请求:

POST http://localhost:8080/support/aaa/user/list?pageSize=1&pageNum=1
Content-Type: application/x-www-form-urlencoded

@Pagination
@ResponseBody
@ApiOperation("用户列表查询")
@PostMapping("/user/list")
public PageInfo<User> queryList(User user) {
    List<User> list = userService.list();
    return new PageInfo<>(list);
}

返回:

{
  "flag": true,
  "data": {
    "total": 2,
    "list": [
      {
        "id": "1",
        "userName": "1",
        "sex": "1"
      }
    ],
    "pageNum": 1,
    "pageSize": 1,
    "size": 1,
    "startRow": 1,
    "endRow": 1,
    "pages": 2,
    "prePage": 0,
    "nextPage": 2,
    "isFirstPage": true,
    "isLastPage": false,
    "hasPreviousPage": false,
    "hasNextPage": true,
    "navigatePages": 8,
    "navigatepageNums": [
      1,
      2
    ],
    "navigateFirstPage": 1,
    "navigateLastPage": 2
  },
  "message": "接口调用成功",
  "code": 200
}

2.3、分页和不分页同时支持

有些数据是比较少量的,但是在某些情况下,既想用作列表,又想作为下拉选择,注解加上属性skipIfMissing=true,在缺失pageSize和pageNum属性时,跳过分页功能查全部,也不会报错。

请求:

@Pagination(skipIfMissing = true)
@ResponseBody
@ApiOperation("用户列表查询")
@PostMapping("/user/list")
public PageInfo<User> queryList(User user) {
    List<User> list = userService.list();
    return new PageInfo<>(list);
}

2.4、注解Pagination的属性

如果系统的某些接口,页码字段名不是pageNum,如:pageNo,可以使用pageNum设置,pageSize同理,也可以全局设置属性名。orderBy可用于排序,详见2.5。maxPageSize是限制接口的最大请求数据量,当表中的数据很多时,接口设置pageSize很大,导致接口响应很慢,如果这种请求很多,甚至会影响整个系统,默认不做限制,可全局配置,也可以针对个别接口配置。pageSize超过maxPageSize时,会抛出异常。

@Inherited
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Pagination {

    /**
     * @return 页大小的参数名
     */
    String pageSize() default "";

    /**
     * @return 页码参数名
     */
    String pageNum() default "";

    /**
     * @return 排序参数名
     */
    String orderBy() default "";

    /**
     * @return 允许的最大页大小,小于或等于0时,不限制
     */
    int maxPageSize() default 0;

    /**
     * @return 如果没有拿到分页相关参数,是否跳过分页,默认不跳过
     * 跳过:即使没拿到,也不会报错,但分页不起效果,此时可当作列表查询使用
     * 不跳过:没拿到参数时,抛出异常
     */
    boolean skipIfMissing() default false;
}

2.5、实现列表字段排序

字段排序针对分页功能,如果分页功能没生效,排序也不会生效

请求:

POST http://localhost:8080/support/aaa/user/list?pageSize=1&pageNum=1&orderBy=userName desc
Content-Type: application/x-www-form-urlencoded

@Pagination(skipIfMissing = true)
@ResponseBody
@ApiOperation("用户列表查询")
@PostMapping("/user/list")
public PageInfo<User> queryList(User user) {
List<User> list = userService.list();
	return new PageInfo<>(list);
}

Mybatis的xml

<resultMap id="t_user" type="com.github.softwarevax.support.demo.entity.User">
	<id column="id" property="id"/>
	<result column="user_name" property="userName"/>
	<result column="sex" property="sex"/>
</resultMap>
    
<select id="list" resultMap="t_user">
    SELECT id, user_name, sex FROM t_user
</select>

sql日志
在这里插入图片描述
说明:userName是实体中的属性,而SQL中排序是使用的是数据库字段user_name。

前端只对实体属性可见,对数据库字段不可见

1、结果使用resultMap映射,则直接取对应关系

2、结果使用resulttype映射,可以取别名,类似: select user_name userName from t_user。如果不取别名,就将驼峰转为下划线进行匹配

在进行联表查询是,使用这个会比较方便,PageHelper排序,需要设置表别名,不然无法知道字段是哪个表的

注意:当联表查询时,会存在有多个相同字段的情况,如表t1和t2都有字段name,但是在resultMap映射中,并没有列举出该字段,而前端又要对改字段进行排序,这时就无法知道需要对哪个表的name字段进行排序了。只要是resultMap有映射关系的,使用排序就不存在问题。

3、方法(含接口)动态切面

github地址

切面的用途有很多,比如系统鉴权、系统日志、方法耗时统计等等,但是平时见到的切面,大多是静态,写死在代码中的,同一套系统日志切面,每个应用使用时,都要复制一份。因为

@Pointcut(POINT_CUT_INCLUDE)
public void pointCut() {
}

这一串代码无法设置成配置的。特别是POINT_CUT_INCLUDE。实现DynamicMethodMatcherPointcut,可实现动态切面,里面有一些规则,根据具体情况而定。

3.1、配置切面

@Bean
@ConditionalOnProperty(value = "support.method.enable", havingValue = "true")
public DefaultPointcutAdvisor methodAdvisor() {
    Assert.hasText(methodConstant.getExpress(), "请配置切点表达式");
    MethodInterceptorAdvisor interceptor = new MethodInterceptorAdvisor();
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    // 读取配置文件的切面表达式
    pointcut.setExpression(methodConstant.getExpress());
    logger.info("method aop expression = {}", methodConstant.getExpress());
    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
    advisor.setPointcut(pointcut);
    advisor.setAdvice(interceptor);
    advisor.setOrder(methodConstant.getOrder());
    return advisor;
}
public interface MethodInterceptor extends Interceptor {
   Object invoke(MethodInvocation invocation) throws Throwable;
}

MethodInterceptorAdvisor需要实现接口MethodInterceptor,该接口只有一个待实现的方法invoke,在invoke中执行MethodInvocation.proceed()就是执行切点方法,也就是环绕通知,那在执行MethodInvocation.proceed()前的操作,就是前置通知,之后的操作就是后置通知。代码大致如下:

@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
    try {
    	// 前置通知
        advice.before(invocation);
        // 环绕通知
        Object ret = invocation.proceed();
        // 返回通知
        advice.afterReturn(ret, invocation);
        return ret;
    } catch (Exception e) {
    	// 异常通知
        advice.throwException(invocation, e);
        throw e;
    } finally {
    	// 后置通知
        advice.after(invocation);
    }
}

环绕通知需要创建代理对象,且一个方法存在于多个切面时,还涉及到代理对象的嵌套,好像有点类似于Mybatis的插件原理。此处使用其他4个通知类型,基本可以完成绝大部分的需要。

3.2、功能配置

#方法请求记录
support.method.enable=true
# 切面表达式,第一个星号:方法返回类型不限制,第二个星号:类名不限制,第三个星号:方法名不限制,符号".."表示包及其子包
support.method.express=execution(* com.github.softwarevax.support.demo..*.*(..))
# 自定义类,实现接口MethodInvokeNoticer可得到切面收集的一些数据
support.method.noticers=com.github.softwarevax.support.demo.custom.MyMethodListener
# 是否持久化,实现了相关类,开启后需要创建对应的表,数据虽然是异步处理的,但是是单个入库的,后期处理成批量的,如批量1000条,超多指定时间还没有1000条也要入库
support.method.persistence=true
# 用来标记操作名称的,默认取的swagger的注解ApiOperation,可自己实现,优先取注解中的属性value属性,value没有就取name属性
support.method.method-tag=io.swagger.annotations.ApiOperation
# 如果启用了持久化,每次启动时,是否重置表数据,默认不重置
support.method.reset-every-time=true

3.3、数据库表结构

数据持久化只是提供了默认的实现方式,如果有不符合要求的,可实现接口MethodInvokeNoticer,完成自定义处理,如参数值的长度超过4000,就会被截取。

-- method 所有切点方法,只在第一次进入时入库
DROP TABLE IF EXISTS `t_method`;
CREATE TABLE `t_method` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `application` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '应用名spring.application.name, 如果没设置,则取contextPath',
    `launch_time` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '启动时间,格式:yyyyMMddHHmmss',
    `expose` tinyint(1) DEFAULT NULL COMMENT '是否是接口',
    `method` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '方法简称',
    `method_tag` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '方法标记',
    `full_method_name` varchar(800) COLLATE utf8_bin DEFAULT NULL COMMENT '方法全称',
    `return_type` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '返回类型',
    `parameter` varchar(500) COLLATE utf8_bin DEFAULT NULL COMMENT '参数列表',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `lanuch_method` (`launch_time`,`full_method_name`)
) ENGINE=InnoDB AUTO_INCREMENT=131 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='应用方法';

-- t_method_invoke 方法调用记录,每次调用都会记录
DROP TABLE IF EXISTS `t_method_invoke`;
CREATE TABLE `t_method_invoke` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `session_id` varchar(40) COLLATE utf8_bin DEFAULT NULL COMMENT '会话id',
   `invoke_id` bigint(20) DEFAULT NULL COMMENT '调用id,同一个请求,该值相同',
   `method_id` int(11) DEFAULT NULL COMMENT '方法id,见表t_method',
   `expose` tinyint(1) DEFAULT NULL COMMENT '是否是接口',
   `parameter_val` varchar(4000) COLLATE utf8_bin DEFAULT NULL COMMENT '参数值,多个参数会转为json字符串,超过4000字符,会被截取',
   `return_val` varchar(4000) COLLATE utf8_bin DEFAULT NULL COMMENT '同参数值',
   `start_time` datetime DEFAULT NULL COMMENT '方法开始时间',
   `elapsed_time` int(11) DEFAULT NULL COMMENT '运行时长,单位:毫秒',
   `create_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
   PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=77 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='应用方法调用';

-- t_method_interface接口方法,如果方法是接口方法,特别处理,在第一次进入时处理
DROP TABLE IF EXISTS `t_method_interface`;
CREATE TABLE `t_method_interface` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `method_id` int(11) DEFAULT NULL COMMENT '方法id',
  `method` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '请求方式,get,post。。。,如果有支持多个,逗号分割',
  `mappings` varchar(500) COLLATE utf8_bin DEFAULT NULL COMMENT '请求路径,不含contextPath,如果支持多个,逗号分割',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='应用接口方法';

-- t_method_interface_invoke 类似于t_method_invoke,不过是接口调用
DROP TABLE IF EXISTS `t_method_interface_invoke`;
CREATE TABLE `t_method_interface_invoke` (
 `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
 `invoke_id` int(11) DEFAULT NULL COMMENT 't_method_invoke.id',
 `scheme` varchar(40) COLLATE utf8_bin DEFAULT NULL COMMENT '协议',
 `method` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '方法,get,post。。。',
 `remote_addr` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '请求端地址',
 `headers` varchar(2000) COLLATE utf8_bin DEFAULT NULL COMMENT '请求头,超过2000被截取',
 `payload` varchar(4000) COLLATE utf8_bin DEFAULT NULL COMMENT '静荷载,超过4000被截取',
 `response_status` int(11) DEFAULT NULL COMMENT '响应码',
 `response_body` varchar(4000) COLLATE utf8_bin DEFAULT NULL COMMENT '响应体',
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='应用接口方法调用';

3.4、表数据截图

t_method表数据,配置method_tag可用于记录操作日志,若service或mapper的方法不方便使用swagger的注解,可自定义注解,含有value和name属性记录操作名即可。
在这里插入图片描述

t_method_invoke表数据,sessionId是会话id,invokeId是一次请求的id,controller–>service–>mapper
在这里插入图片描述

t_method_interface表数据
在这里插入图片描述

t_method_interface_invoke表数据
在这里插入图片描述

3.5、自定义MethodInvokeNoticer

MyMethodListener不需要放入到Spring容器中,将MyMethodListener全类名配置成属性support.method.noticers值即可

public class MyMethodListener implements MethodInvokeNoticer {

    private Logger logger = LoggerFactory.getLogger(MyMethodListener.class);

    @Override
    public void callBack(InvokeMethod method) {
        logger.info("方法[{}]耗时{}毫秒", method.getFullMethodName(), method.getElapsedTime());
    }
}

日志:

[2022-07-07 17:30:10] [c.g.s.s.d.c.MyMethodListener] [task-2] [INFO ] 方法[com.github.softwarevax.support.demo.service.impl.UserServiceImpl.list()]耗时257毫秒
[2022-07-07 17:30:10] [c.g.s.s.d.c.MyMethodListener] [task-3] [INFO ] 方法[com.github.softwarevax.support.demo.controller.TestController.queryList(com.github.softwarevax.support.demo.entity.User)]耗时514毫秒
[2022-07-07 17:30:10] [c.g.s.s.d.c.MyMethodListener] [task-1] [INFO ] 方法[com.github.softwarevax.support.demo.mapper.UserMapper.list()]耗时254毫秒
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值