Springboot uses AOP aspects to uniformly process and query request bodies received in different interfaces to implement multi-condition paging queries.

Table of contents

Description of Requirement

Example of front-end ajax request calling query interface

Preparation

Introduce related dependencies

Entity class

controller layer

service layer interface

service layer implementation class

mapper layer

selectAll complex dynamic sql in mapper.xml

control layer aspect

Tool classMyUtils

General classDataVO

Send a request to view the response results

ajax request body

Response content

 The key - section enhancement

thoughts

Acknowledgments


Description of Requirement

In the effect I want to achieve, when the front-end calls the query interface, the request body carries the following data: the query condition field of the queried entity class (there may be multiple conditions or no query conditions), the variable of the paging query: page (Current page number), limit (limit number of items per page),

The backend needs to receive the content of the request body. It can determine which entity class to query based on different calling interfaces, create an entity class, and pass the corresponding entity class, page number, and item number to the service layer. The service layer passes the entity class to the mapper query. statement (dynamic sql is implemented in the mapper layer), and the pagehelper plug-in of mybatis is used to implement paging and return the data layer by layer to the front end.

Take my own project as an example

Example of front-end ajax request calling query interface

$.ajax({
    url: "http://127.0.0.1:8080/counter/select",
    method: "POST",
    headers: {
        "token": "myToken"
    },  //由于我的项目拦截器进行了token验证,所以请求头带一个token,没有进行token验证可不用写请求头
    data:JSON.stringify({
        "page":1,
        "limit":5,
        "id":"A0001"    //要查询的字段
    }),
    contentType: "application/json;charset=utf-8",
    success: function (response) {
        console.log(response.data)
    }
});

Preparation

Introduce related dependencies

The maven project is added in pom.xml

<!--spring-web依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring-aop依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--mybatis-plus依赖,由于我的项目中需要很多复杂sql,所以依旧是按照mybatis来写的,mp相对于mybatis只做增强不做改变,依旧可以按mybatis用法用-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>
<!--mybatis分页插件——pagehelper-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.6</version>
</dependency>
<!--mysql-->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>
<!--fastjson2,处理json数据-->
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.23</version>
</dependency>
<!--lombok注解-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

Entity class

package com.cns.coldstoragesys.bean;

import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.util.Date;

@Data
//@JsonInclude(JsonInclude.Include.NON_NULL)//删除返回前端时为null的字段
public class Counter {
    private String id;
    private Integer coldstorageId;
    private String type;
    private String state;
    private String pos;
    private Integer level;
    private String goodsId;
    /*
    添加临时字段,级联查询返回前端,方便数据表格获取关联数据
     */
    @TableField(exist=false)
    private String coldstorageName;
    @TableField(exist = false)
    private String video;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")//json日期格式转换
    @TableField(exist = false)
    private Date startTime;
    @TableField(exist = false)
    private String description;
    @TableField(exist = false)
    private Integer length;
    @TableField(exist = false)
    private Integer width;
    @TableField(exist = false)
    private Integer height;
}

controller layer

package com.cns.coldstoragesys.controller;

import com.cns.coldstoragesys.bean.Counter;
import com.cns.coldstoragesys.common.DataVO;
import com.cns.coldstoragesys.common.SysConstant;
import com.cns.coldstoragesys.service.CounterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/counter")
public class CounterController {
    @Autowired
    private CounterService counterService;
    @PostMapping("/select")
    public DataVO selectAll(@RequestBody Map<String,Object> param){
        return counterService.selectAll(
                (Counter) param.get(SysConstant.DEFAULT_BEAN_NAME),
                (Integer) param.get(SysConstant.DEFAULT_PAGE_NAME),
                (Integer) param.get(SysConstant.DEFAULT_LIMIT_NAME));
    }
}

service layer interface

package com.cns.coldstoragesys.service;

import com.cns.coldstoragesys.bean.Counter;
import com.cns.coldstoragesys.common.DataVO;

public interface CounterService {
    DataVO selectAll(Counter counter,Integer page,Integer limit);
}

service layer implementation class

package com.cns.coldstoragesys.service.impl;

import com.cns.coldstoragesys.bean.Counter;
import com.cns.coldstoragesys.common.DataVO;
import com.cns.coldstoragesys.common.SysConstant;
import com.cns.coldstoragesys.mapper.CounterMapper;
import com.cns.coldstoragesys.service.CounterService;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

import java.util.List;
@Slf4j
@Service
@Transactional
public class CounterServiceImpl implements CounterService {
    @Autowired
    private CounterMapper counterMapper;
    @Override
    public DataVO selectAll(Counter counter,Integer page,Integer limit) {
        Page<Object> p = PageHelper.startPage(page,limit);
        try {
            List<Counter> counters = counterMapper.selectAll(counter);
            return new DataVO(SysConstant.CODE_SUCCESS,SysConstant.SELECT_SUCCESS,p.getTotal(),counters);
        } catch (Exception e) {
            log.error(e.toString());
            return new DataVO(SysConstant.CODE_ERROR,SysConstant.SELECT_ERROR);
        }
    }
}

mapper layer

package com.cns.coldstoragesys.mapper;

import com.cns.coldstoragesys.bean.Counter;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;
@Mapper
public interface CounterMapper {
    List<Counter> selectAll(Counter counter);
}

selectAll complex dynamic sql in mapper.xml

The reason why I wrote it so complicated is because the front-end data table also needs some data from the other three tables, so three connections were made. At the same time, the corresponding temporary fields were added to the entity class using the @TableField(exist=false) annotation, using sql The tag implements dynamic sql. If any field in the carried entity class is not empty, it means that it is a query condition.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cns.coldstoragesys.mapper.CounterMapper">
  <resultMap id="BaseResultMap" type="com.cns.coldstoragesys.bean.Counter">
    <id column="id" jdbcType="CHAR" property="id" />
    <result column="coldstorage_id" jdbcType="INTEGER" property="coldstorageId" />
    <result column="coldstorage_name" jdbcType="VARCHAR" property="coldstorageName" />
    <result column="type" jdbcType="CHAR" property="type" />
    <result column="state" jdbcType="VARCHAR" property="state" />
    <result column="pos" jdbcType="VARCHAR" property="pos" />
    <result column="level" jdbcType="INTEGER" property="level" />
    <result column="goods_id" jdbcType="CHAR" property="goodsId" />
    <result column="description" jdbcType="VARCHAR" property="description" />
    <result column="length" jdbcType="INTEGER" property="length" />
    <result column="width" jdbcType="INTEGER" property="width" />
    <result column="height" jdbcType="INTEGER" property="height" />
    <result column="video" jdbcType="VARCHAR" property="video" />
    <result column="start_time" jdbcType="TIMESTAMP" property="startTime"/>
  </resultMap>
  <select id="selectAll" parameterType="com.cns.coldstoragesys.bean.Counter" resultMap="BaseResultMap">
    select counter.id,counter.coldstorage_id,counter.type,counter.state,counter.pos,counter.`level`,counter.goods_id,
            type.description,type.length,type.width,type.height,
            record.video,record.start_time,
            cold.name as coldstorage_name
    from counter
    left join coldstorage as cold on cold.id=counter.coldstorage_id
    left join counter_type as type on type.id = counter.type
    left join record_access as record on record.start_time=(
      select MAX(record.start_time)
      from record_access as record
      where record.counter_id=counter.id
      group by record.counter_id
    )
    <where>
      <if test="null != coldstorageId and '' != coldstorageId">
        and counter.coldstorage_id=#{coldstorageId}
      </if>
      <if test="null != coldstorageName and '' != coldstorageName">
        and counter.coldstorage_id=(select id from coldstorage where name like "%${coldstorageName}%")
      </if>
      <if test="null != id and '' != id">
        and counter.`id`= #{id}
      </if>
      <if test="null != type and '' != type">
        and counter.`type` = #{type}
      </if>
      <if test="null != level and '' != level">
        and counter.`level` = #{level}
      </if>
      <if test="null != state and '' != state">
        and counter.`state` = #{state}
      </if>
      <if test="null != description and '' != description">
        and counter.type=(select id from counter_type where `description` like "%${description}%")
      </if>
    </where>
    order by counter.id asc
  </select>
</mapper>

control layer aspect

package com.cns.coldstoragesys.aspect;


import com.alibaba.fastjson2.JSON;
import com.cns.coldstoragesys.common.SysConstant;
import com.cns.coldstoragesys.util.MyUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.HashMap;
import java.util.Map;

@Aspect
@Component
public class ControllerAspect {
    //指定切点为controller目录中所有类的selectAll方法并且要求携带的参数是Map<String,Object> param
    @Pointcut(value = "execution(* com.cns.coldstoragesys.controller..selectAll(..)) && args(param)" ,argNames = "param")
    public void controllerPoint(Map<String, Object> param){}
    //指定环绕增强切面的切入点和参数名
    @Around(value = "controllerPoint(param) && args(..)",argNames= "joinPoint,param")
    public Object changeParam(ProceedingJoinPoint joinPoint,@RequestBody Map<String, Object> param) throws Throwable {
        Integer page = (Integer) param.get(SysConstant.DEFAULT_PAGE_NAME);  //获得param中用于分页的page和limit后将其移除,剩余在param中的键值对即为需要查询的条件
        param.remove(SysConstant.DEFAULT_PAGE_NAME);
        Integer limit = (Integer) param.get(SysConstant.DEFAULT_LIMIT_NAME);
        param.remove(SysConstant.DEFAULT_LIMIT_NAME);

        String className = MyUtils.getClassName(joinPoint.getTarget().getClass().getName());    //工具类获取全限定类名
        Class<?> clazz = Class.forName(className);  //反射机制创建类
        Object obj = JSON.parseObject(JSON.toJSONString(param), clazz); //将Map中剩余的键值对转为对应类型的json对象

        Map<String,Object> params=new HashMap<>();          //重新存放最后需要新返回的参数,procceed方法的参数需要一个Object数组,
        params.put(SysConstant.DEFAULT_BEAN_NAME,obj);      //但是controller层中的selectAll方法又只有一个参数,
        params.put(SysConstant.DEFAULT_PAGE_NAME,page);     //如果直接将键值对放到Object数组中将会报参数个数异常,
        params.put(SysConstant.DEFAULT_LIMIT_NAME,limit);   //所以这里将键值对放到Map中,再将Map放到Object数组中

        return joinPoint.proceed(new Object[]{params}); //procceed方法的参数需要一个Object数组,
    }
}

Tool classMyUtils

package com.cns.coldstoragesys.util;

import com.cns.coldstoragesys.common.SysConstant;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class MyUtils {
    public static String getClassName(String fullClassName) {
        Pattern pattern = Pattern.compile("\\.(\\w+)Controller$");
        Matcher matcher = pattern.matcher(fullClassName);
        if (matcher.find()) {
            return SysConstant.DEFAULT_BEAN_PATH+matcher.group(1);
        }
        return null;
    }
}

General classDataVO

package com.cns.coldstoragesys.common;

import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.Data;

import java.util.List;
@Data
@JsonPropertyOrder({"code","msg","count","data"})//指定返回给前端的字段顺序
public class DataVO<T> {
    private Integer code;
    private String msg;
    private Long count;
    private List<T> data;
    public DataVO() {
    }
    public DataVO(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    public DataVO(Integer code, String msg, Long count, List<T> data) {
        this.code = code;
        this.msg = msg;
        this.count = count;
        this.data = data;
    }

}

System constantSysConstant

package com.cns.coldstoragesys.common;

public interface SysConstant {
    Integer CODE_SUCCESS=0;     //操作成功
    Integer CODE_ERROR=1;       //操作失败
    String DEFAULT_BEAN_PATH="com.cns.coldstoragesys.bean.";
    String DEFAULT_PAGE_NAME="page";    //默认传递指定页数的变量名称,因为前端传来的数据是放在请求体中的,controller层接口通过Map<String,Object>接收,需要通过key取值
    Integer DEFAULT_PAGE=1;     //默认页数
    String DEFAULT_LIMIT_NAME="limit";
    Integer DEFAULT_LIMIT=10;   //默认条数
    String DEFAULT_BEAN_NAME="bean";
    Long REDIS_OVERDUE_TIME=30*24*60*60L;
    String DEFAULT_TOKEN_ISSUER="Yan";
    String DEFAULT_TOKEN_AUDIENCE="Huang";
    String SELECT_SUCCESS="查询成功";
    String SELECT_ERROR="查询失败";
    String ADD_SUCCESS="添加成功";
    String ADD_ERROR="添加失败";
    String DELETE_SUCCESS="删除成功";
    String DELETE_ERROR="删除失败";
    String UPDATE_SUCCESS="修改成功";
    String UPDATE_ERROR="修改失败";
    String NULL_VALUE="主键不存在";
    String REPEAT_VALUE="主键重复";
    String LOGIN_SUCCESS="登陆成功";
    String LOGIN_ERROR="登陆失败";
    String UNKNOW_ERROR="未知错误";
}

Send a request to view the response results

ajax request body

Response content

 The key - section enhancement

In this aspect, the @Around annotation is used, and the pointcut point is defined as the selectAll method in the controller layer class of all different entity classes under the controller package, and this method must have a parameter param of type Map<String, Object>, that is tangency point expression

@Pointcut(value = "execution(* com.cns.coldstoragesys.controller..selectAll(..)) && args(param)" ,argNames = "param")

@Aspect
@Component
public class ControllerAspect {
    //指定切点为controller目录中所有类的selectAll方法并且要求携带的参数是Map<String,Object> param
    @Pointcut(value = "execution(* com.cns.coldstoragesys.controller..selectAll(..)) && args(param)" ,argNames = "param")
    public void controllerPoint(Map<String, Object> param){}
    //指定环绕增强切面的切入点和参数名
    @Around(value = "controllerPoint(param) && args(..)",argNames= "joinPoint,param")
    public Object changeParam(ProceedingJoinPoint joinPoint,@RequestBody Map<String, Object> param) throws Throwable {
        Integer page = (Integer) param.get(SysConstant.DEFAULT_PAGE_NAME);  //获得param中用于分页的page和limit后将其移除,剩余在param中的键值对即为需要查询的条件
        param.remove(SysConstant.DEFAULT_PAGE_NAME);
        Integer limit = (Integer) param.get(SysConstant.DEFAULT_LIMIT_NAME);
        param.remove(SysConstant.DEFAULT_LIMIT_NAME);

        String className = MyUtils.getClassName(joinPoint.getTarget().getClass().getName());    //工具类获取全限定类名
        Class<?> clazz = Class.forName(className);  //反射机制创建类
        Object obj = JSON.parseObject(JSON.toJSONString(param), clazz); //将Map中剩余的键值对转为对应类型的json对象

        Map<String,Object> params=new HashMap<>();          //重新存放最后需要新返回的参数,procceed方法的参数需要一个Object数组,
        params.put(SysConstant.DEFAULT_BEAN_NAME,obj);      //但是controller层中的selectAll方法又只有一个参数,
        params.put(SysConstant.DEFAULT_PAGE_NAME,page);     //如果直接将键值对放到Object数组中将会报参数个数异常,
        params.put(SysConstant.DEFAULT_LIMIT_NAME,limit);   //所以这里将键值对放到Map中,再将Map放到Object数组中

        return joinPoint.proceed(new Object[]{params}); //procceed方法的参数需要一个Object数组,
    }
}

The MyUtil tool class used here obtains the fully qualified class name method. It uses regular expressions to extract the entity class name corresponding to xxxController, plus the Bean package location defined in the system constant, which is the fully qualified class name.

public static String getClassName(String fullClassName) {
    Pattern pattern = Pattern.compile("\\.(\\w+)Controller$");
    Matcher matcher = pattern.matcher(fullClassName);
    if (matcher.find()) {
        return SysConstant.DEFAULT_BEAN_PATH+matcher.group(1);
    }
    return null;
}

In the changeParam surround enhancement method, get the two variables page and limit related to paging in param, and remove them from the map. Then only the fields we need to query are left in the map.

You can use joinPoint to get the controller layer class name of the current enhanced target method. For example, if you are currently querying counter, it is CounterController. If you are querying User, it is UserController. Because our structure is standardized, we can get the entity class through this class name. Fully qualified name, such as com.cns.bean.

After obtaining the fully qualified class name, create the class through the reflection mechanism.

Convert the map to the corresponding entity class through the method of com.alibaba.fastjson2.JSON. Because we cannot determine the specific class here, we use Object to receive it, and then the controller layer forces it into the required entity class to achieve the effect.

Because it is a universal aspect, this aspect can enhance multiple controllers. I have many entity class controllers that need such enhancements.

If the interface needs to query other entity classes, you only need to change the interface to achieve enhancement.

thoughts

Although the requestBody passed in can be processed directly in the Controller layer to implement dynamic SQL paging query, many controllers need one more piece of such repetitive code, so I want to put it in the aspect to reduce code duplication, so that the controller can still Just write one line, elegant and beautiful

Acknowledgments

Thanks to ChatGPT, I have the idea to achieve this effect, but in fact I am not very good at implementing it. Many bugs were discovered by asking ChatGPT.

Guess you like

Origin blog.csdn.net/m0_54250110/article/details/129749589