目录
需求描述
前端ajax请求调用查询接口示例
准备工作
引入相关依赖
实体类
controller层
service层接口
service层实现类
mapper层
mapper.xml中的selectAll复杂动态sql
控制层切面
工具类MyUtils
通用类DataVO
发送请求查看响应结果
ajax请求体
响应内容
关键——切面增强
感想
致谢
需求描述
在我想要实现的效果中,前端调用查询接口时,请求体中携带以下数据:所查询实体类的查询条件字段(可能有多个条件,也可能没有查询条件)、分页查询的变量:page(当前页码)、limit(每页限制条数),
后端需要接收请求体的内容,可以根据调用接口不同判断是查询哪个实体类,创建出实体类,将对应的实体类和页码、条数传给servic层,service层传实体类给mapper的查询语句(mapper层中实现动态sql),使用mybatis的pagehelper插件实现分页将数据层层返回给前端。
以我自己的项目为例
前端ajax请求调用查询接口示例
$.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)}
});
准备工作
引入相关依赖
maven项目在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>
实体类
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层
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 {@Autowiredprivate 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层接口
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层实现类
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 {@Autowiredprivate CounterMapper counterMapper;@Overridepublic 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层
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);
}
mapper.xml中的selectAll复杂动态sql
我写这么复杂是因为前端这个数据表格还需要其他三个表中的部分数据,所以进行了三个连接,同时实体类中用 @TableField(exist=false)注解添加了相应的临时字段,使用sql标签实现动态sql,如果携带的实体类中哪个字段不为空则说明其是查询条件。
<?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_namefrom counterleft join coldstorage as cold on cold.id=counter.coldstorage_idleft join counter_type as type on type.id = counter.typeleft join record_access as record on record.start_time=(select MAX(record.start_time)from record_access as recordwhere record.counter_id=counter.idgroup 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>
控制层切面
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数组,}
}
工具类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;}
}
通用类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"})//指定返回给前端的字段顺序
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;}}
系统常量SysConstant
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="未知错误";
}
发送请求查看响应结果
ajax请求体
响应内容
关键——切面增强
在这个切面中,使用@Around注解,其切点定义为controller包下所有不同实体类的controller层类中的selectAll方法,并且这个方法要带有一个Map<String,Object>类型的参数param,即如切点表达式
@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数组,}
}
这里用到的MyUtil工具类获取全限定类名方法,用正则表达式提取出xxxController对应的实体类名,加上系统常量中定义的Bean包位置,即就是全限定类名
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;
}
在changeParam这个环绕增强方法中,将param中分页相关的两个变量page和limit拿到,将其从map移除,那么map中就只剩下我们需要查询的字段了,
可以通过joinPoint获取当前加强目标方法的controller层类名,比如当前对counter进行查询,那就是CounterController,如果是查询User,那就是UserController,因为我们结构规范,那么我们可以通过这个类名获取实体类的全限定名称,比如com.cns.bean.
获取到全限定类名后,通过反射机制创建类,
通过com.alibaba.fastjson2.JSON的方法将map转为对应实体类,因为这里的具体类我们不能确定,所以用Object接收,由controller层再强制转换成所需实体类,就实现效果了
因为是通用的切面,所以这个切面可以对多个controller进行增强,我有很多实体类的controller需要这样的加强
假如接口需要查询其他实体类,只需要换个接口一样能实现增强。
感想
虽然可以直接在Controller层中对传来的requestBody进行处理实现动态sql分页查询,但是这样很多个controller都需要多一段这样的重复性质代码,所以我就想放到切面中减少代码重复,这样controller还是只需要写一行,优雅美观
致谢
感谢ChatGPT,我有实现这个效果的想法,但其实自己并不能很好进行具体实现,很多bug是由询问ChatGPT才恍然大悟的