springboot uses the aop aspect to process and query the request body received in different interfaces in a unified manner, and realize multi-condition paging query

Directory

Description of Requirement

Front-end ajax request call query interface example

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 plane

Tool class MyUtils

General class DataVO

Send a request to view the response result

ajax request body

response content

The key – facet enhancement

feelings

thank you


Description of requirements

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 entity class to be queried (may have multiple conditions, or may not have query conditions), the variable of the page query: page (current page number), limit (limit number of entries per page),

The backend needs to receive the content of the request body. It can judge which entity class to query according to the different calling interfaces, create the entity class, and pass the corresponding entity class, page number, and item number to the service layer, and 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 call query interface

$.ajax({
    url: "http://127.0.0.1:8080/counter/select",
    method: "POST",
    headers: {
        "token": "myToken"
    }, //Because my project interceptor has performed token verification, so the request header carries a token, and there is no need to write the request header without token verification
    data:JSON. stringify({
        "page": 1,
        "limit": 5,
        "id":"A0001" //Field to query
    }),
    contentType: "application/json;charset=utf-8",
    success: function (response) {
        console. log(response. data)
    }
});

Preparation

The maven project is added in pom.xml

<!--spring-web dependency-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring-aop dependency-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--mybatis-plus dependency, because my project needs a lot of complex sql, so it is still written according to mybatis, mp is only enhanced and not changed compared to mybatis, and can still be used according to mybatis -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>
<!--mybatis paging plugin--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, process json data-->
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.23</version>
</dependency>
<!--lombok annotations-->
<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)//Delete fields that are null when returning to the front end
public class Counter {
    private String id;
    private Integer coldstorageId;
    private String type;
    private String state;
    private String pos;
    private Integer level;
    private String goodsId;
    /*
    Add temporary fields, cascade queries return to the front end, and facilitate data tables to obtain associated data
     */
    @TableField(exist=false)
    private String coldstorageName;
    @TableField(exist = false)
    private String video;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT + 8")//json date format conversion
    @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

I wrote it so complicated because the front-end data table also needs part of the data in the other three tables, so three connections are made, and the corresponding temporary fields are added with @TableField(exist=false) annotation in the entity class, using sql The tag implements dynamic sql. If any field in the carried entity class is not empty, it means 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

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 {
    //Specify the cut point as the selectAll method of all classes in the controller directory and the required parameter is Map<String, Object> param
    @Pointcut(value = "execution(* com.cns.coldstoragesys.controller..selectAll(..)) & amp; & amp; args(param)" ,argNames = "param")
    public void controllerPoint(Map<String, Object> param){}
    //Specify the entry point and parameter name of the surrounding enhanced aspect
    @Around(value = "controllerPoint(param) & amp; & amp; 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); //Get the page and limit used for paging in param and remove them, and the remaining key-value pairs in param are the conditions that need to be queried
        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()); //Get the fully qualified class name of the tool class
        Class<?> clazz = Class.forName(className); //Reflection mechanism creates class
        Object obj = JSON.parseObject(JSON.toJSONString(param), clazz); //Convert the remaining key-value pairs in the Map into corresponding types of json objects

        Map<String,Object> params=new HashMap<>(); //restore the last parameters that need to be returned, the parameters of the procceed method need an Object array,
        params.put(SysConstant.DEFAULT_BEAN_NAME, obj); //But the selectAll method in the controller layer has only one parameter,
        params.put(SysConstant.DEFAULT_PAGE_NAME,page); //If you directly put the key-value pair into the Object array, an abnormal number of parameters will be reported,
        params.put(SysConstant.DEFAULT_LIMIT_NAME,limit); //So here put the key-value pair into the Map, and then put the Map into the Object array

        return joinPoint.proceed(new Object[]{params}); //The parameter of the procceed method requires an Object array,
    }
}

tool class MyUtils

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 DataVO

package com.cns.coldstoragesys.common;

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

import java.util.List;
@Data
@JsonPropertyOrder({"code","msg","count","data"})//Specify the field order returned to the front end
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 constant SysConstant

package com.cns.coldstoragesys.common;

public interface SysConstant {
    Integer CODE_SUCCESS=0; //The operation is successful
    Integer CODE_ERROR=1; //operation failed
    String DEFAULT_BEAN_PATH="com.cns.coldstoragesys.bean.";
    String DEFAULT_PAGE_NAME="page"; //By default, the variable name of the specified page number is passed, because the data from the front end is placed in the request body, and the controller layer interface is received through Map<String, Object>, which needs to be retrieved through the key value
    Integer DEFAULT_PAGE=1; //Default page number
    String DEFAULT_LIMIT_NAME="limit";
    Integer DEFAULT_LIMIT=10; //The default number
    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="Query succeeded";
    String SELECT_ERROR="Query failed";
    String ADD_SUCCESS="Added successfully";
    String ADD_ERROR="Add failed";
    String DELETE_SUCCESS="Delete successfully";
    String DELETE_ERROR="Delete failed";
    String UPDATE_SUCCESS="Modification succeeded";
    String UPDATE_ERROR="Modification failed";
    String NULL_VALUE="Primary key does not exist";
    String REPEAT_VALUE="Primary key repeats";
    String LOGIN_SUCCESS="Login successful";
    String LOGIN_ERROR="Login failed";
    String UNKNOW_ERROR="Unknown error";
}

Send a request to view the response result

ajax request body

Response content

The key – aspect enhancement

In this aspect, the @Around annotation is used, and its cut 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, that is pointcut expression

@Pointcut(value = “execution(* com.cns.coldstoragesys.controller..selectAll(..)) & amp; & amp; args(param)” ,argNames = “param”)

@Aspect
@Component
public class ControllerAspect {
    //Specify the cut point as the selectAll method of all classes in the controller directory and the required parameter is Map<String, Object> param
    @Pointcut(value = "execution(* com.cns.coldstoragesys.controller..selectAll(..)) & amp; & amp; args(param)" ,argNames = "param")
    public void controllerPoint(Map<String, Object> param){}
    //Specify the entry point and parameter name of the surrounding enhanced aspect
    @Around(value = "controllerPoint(param) & amp; & amp; 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); //Get the page and limit used for paging in param and remove them, and the remaining key-value pairs in param are the conditions that need to be queried
        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()); //Get the fully qualified class name of the tool class
        Class<?> clazz = Class.forName(className); //Reflection mechanism creates class
        Object obj = JSON.parseObject(JSON.toJSONString(param), clazz); //Convert the remaining key-value pairs in the Map into corresponding types of json objects

        Map<String,Object> params=new HashMap<>(); //restore the last parameters that need to be returned, the parameters of the procceed method need an Object array,
        params.put(SysConstant.DEFAULT_BEAN_NAME, obj); //But the selectAll method in the controller layer has only one parameter,
        params.put(SysConstant.DEFAULT_PAGE_NAME,page); //If you directly put the key-value pair into the Object array, an abnormal number of parameters will be reported,
        params.put(SysConstant.DEFAULT_LIMIT_NAME,limit); //So here put the key-value pair into the Map, and then put the Map into the Object array

        return joinPoint.proceed(new Object[]{params}); //The parameter of the procceed method requires an Object array,
    }
}

The MyUtil tool class used here obtains the fully qualified class name method, and uses regular expressions to extract the entity class name corresponding to xxxController, plus the location of the Bean package defined in the system constant, that 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 surround enhancement method of changeParam, the two variables page and limit related to paging in param are obtained and removed from the map, then only the fields we need to query are left in the map.

The class name of the controller layer of the current enhanced target method can be obtained through joinPoint. For example, if the counter is currently queried, it is CounterController. If the user is queried, it is UserController. Because of our standardized structure, we can use this class name to obtain the entity class. 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, so we use Object to receive it, and then the controller layer is forced to convert it into the required entity class, and the effect is achieved.

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

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

Feelings

Although the incoming requestBody can be processed directly in the Controller layer to implement dynamic sql paging query, but such a lot of controllers need an extra section of such repetitive code, so I want to put it in the section to reduce code duplication, so that the controller is still Only need to write one line, elegant and beautiful

Thanks

Thanks to ChatGPT, I have the idea to achieve this effect, but in fact I can’t implement it very well. Many bugs are realized by asking ChatGPT