需求背景:
分页接口可以按照指定字段排序,排序类型(升序降序)可以自由选择。
于是关于请求参数我做了一下改动,下面是两处分页的请求参数:
令牌分页请求参数:
@Data
public class TokenPageRequest {/*** 令牌类型:-1固定算力池,-2奖金算力池*/@JsonSetter(nulls = Nulls.SKIP)private Integer tokenType;/*** 排序字段:时间 create_time、令牌数量 token*/@JsonSetter(nulls = Nulls.SKIP)private String orderColumn = "create_time";/*** 排序类型:ASC升序、DESC降序*/@JsonSetter(nulls = Nulls.SKIP)private String orderType = "DESC";/*** 排序字段列表,防止SQL注入*/private final List<String> columns = Arrays.asList("token", "createTime", "create_time");/*** 排序类型列表,防止SQL注入*/private final List<String> orderTypes = Arrays.asList("ASC", "DESC");public String getOrderColumn() {if (columns.contains(orderColumn)) {if ("createTime".equalsIgnoreCase(orderColumn)) {orderColumn = "create_time";}} else {orderColumn = "create_time";}return orderColumn;}public String getOrderType() {if (!orderTypes.contains(orderType)) {orderType = "DESC";}return orderType;}
}
提现分页请求参数:
@Data
public class DisbursementPageRequest {/*** 审核状态*/private Integer status;/*** 排序字段:时间 create_time、提现金额 amount*/@JsonSetter(nulls = Nulls.SKIP)private String orderColumn = "create_time";/*** 排序类型:ASC升序、DESC降序*/@JsonSetter(nulls = Nulls.SKIP)private String orderType = "DESC";/*** 排序字段列表,防止SQL注入*/private final List<String> columns = Arrays.asList("amount", "createTime", "create_time");/*** 排序类型列表,防止SQL注入*/private final List<String> orderTypes = Arrays.asList("ASC", "DESC");public String getOrderColumn() {if (columns.contains(orderColumn)) {if ("createTime".equalsIgnoreCase(orderColumn)) {orderColumn = "create_time";}} else {orderColumn = "create_time";}return orderColumn;}public String getOrderType() {if (!orderTypes.contains(orderType)) {orderType = "DESC";}return orderType;}
}
上面的代码其实没有什么问题,通过getOrderType和getOrderColumn方法可以拿到实际排序参数,最后拼到sql的limit语句中实现自定义排序查询;
重写getOrderType和getOrderColumn方法,可以保证前端胡乱传参,导致接口查询sql报错。
但是有一个很大的问题:代码不能复用!
试想一下有很多的分页接口,并且分页参数都不一样,那是不是所有的分页参数都要这样重写get方法?
并且如果排序的字段很多,前端无法决定传过去的排序字段是小驼峰还是下划线时,columns这个集合是不是要不断维护?
所以针对上面2个问题,我们可以进一步进行分装。
设计模式分装
1.定义排序字段策略接口
/*** 排序字段定义接口*/
public interface OrderColumnDefine {/*** 获取数据库字段名*/String getColumn();/*** 获取字段别名(如前端传入的驼峰形式)*/String getAliasColumn();/*** 获取字段描述*/String getDesc();
}
策略模式(Strategy Pattern):通过枚举类和接口(OrderColumnDefine),我们为不同的排序策略提供了一个统一的接口,使得排序策略可以相互替换。每个实现了 OrderColumnDefine 的枚举类(如 TokenOrderColumn 和 DisbursementOrderColumn)代表一种具体的排序策略。
根据方法名,可以明显看出:
每个实现类都会向外暴露自己的部分信息,但是可以根据不同业务场景来自定义暴露逻辑。比如:不同的枚举,对应不同的业务场景,肯定也有不同的枚举值。
这样就实现了,一套方法对应多个暴露策略!
2.排序参数枚举实现策略接口
考虑到每个分页接口的排序参数可能不一致,所以将排序参数定义成枚举;
每个字段名对应数据库里的字段名;
字段名小驼峰映射后就是参数别名;
/*** 令牌相关排序字段枚举*/
public enum TokenOrderColumn implements OrderColumnDefine {CREATE_TIME("create_time", "createTime", "创建时间"),TOKEN("token", null, "令牌数量");// 排序字段private final String column;// 字段别名private final String aliasColumn;// 字段描述private final String desc;TokenOrderColumn(String column, String aliasColumn, String desc) {this.column = column;this.aliasColumn = aliasColumn;this.desc = desc;}@Overridepublic String getColumn() {return column;}@Overridepublic String getAliasColumn() {return aliasColumn;}@Overridepublic String getDesc() {return desc;}
}/*** 提现相关排序字段枚举*/
public enum DisbursementOrderColumn implements OrderColumnDefine {CREATE_TIME("create_time", "createTime", "创建时间"),AMOUNT("amount", null, "提现金额");// 字段名private final String column;// 字段别名(小驼峰)private final String aliasColumn;// 字段描述private final String desc;DisbursementOrderColumn(String column, String aliasColumn, String desc) {this.column = column;this.aliasColumn = aliasColumn;this.desc = desc;}@Overridepublic String getColumn() {return column;}@Overridepublic String getAliasColumn() {return aliasColumn;}@Overridepublic String getDesc() {return desc;}
}
上面这2个枚举类都实现了策略接口,向外暴露了自己的字段名和字段别名。
3.排序参数抽象为父类工厂
/*** 基础分页请求抽象类*/
@Data
public abstract class BasePageRequest<T extends Enum<T> & OrderColumnDefine> {/*** 排序字段*/@JsonSetter(nulls = Nulls.SKIP)protected String orderColumn;/*** 排序类型:ASC升序、DESC降序*/@JsonSetter(nulls = Nulls.SKIP)protected String orderType = "DESC";/*** 获取排序字段枚举类*/protected abstract Class<T> getOrderColumnEnumClass();/*** 获取默认排序字段*/protected abstract T getDefaultOrderColumn();/*** 获取排序类型列表*/protected List<String> getOrderTypes() {return Arrays.asList("ASC", "DESC");}/*** 获取默认排序类型*/protected String getDefaultOrderType() {return "DESC";}/*** 获取排序字段,防止SQL注入*/public String getOrderColumn() {// 如果未设置排序字段,使用默认值if (orderColumn == null) {return getDefaultOrderColumn().getColumn();}// 检查是否为有效的排序字段for (T columnEnum : getOrderColumnEnumClass().getEnumConstants()) {if (columnEnum.getColumn().equals(orderColumn) || (columnEnum.getAliasColumn() != null && columnEnum.getAliasColumn().equals(orderColumn))) {return columnEnum.getColumn();}}// 无效字段使用默认值return getDefaultOrderColumn().getColumn();}/*** 获取排序类型,防止SQL注入*/public String getOrderType() {if (orderType == null || !getOrderTypes().contains(orderType)) {return getDefaultOrderType();}return orderType;}
}
【1】利用泛型编程, 确保了只有满足特定条件的类型才能用作排序字段。
排序字段条件:必须是枚举类,且实现了模板接口
【2】工厂方法模式(Factory Method Pattern):
抽象方法 getOrderColumnEnumClass() 和 getDefaultOrderColumn() 可以看作是工厂方法,负责创建或提供具体的排序字段类型。
【3】模板方法模式(Template Method Pattern):
BasePageRequest 是一个抽象类,定义了算法的骨架(getOrderColumn 和 getOrderType 方法)
将一些步骤延迟到子类中实现(getOrderColumnEnumClass 和 getDefaultOrderColumn)。
这允许子类在不改变算法结构的情况下重新定义算法的某些步骤。
比如这里:getOrderColumn 和 getOrderType 方法的处理逻是相同的——
如果前端传参没有传排序参数 or 传了无效参数,就取默认值:按照创建时间,倒序排序;
如果前端传参了,按照【字段名】去构造limit语句;
4.业务参数 继承 排序参数抽象父类
/*** 令牌分页请求*/
@Data
public class TokenPageRequest extends BasePageRequest<TokenOrderColumn> {/*** 令牌类型:-1固定算力池,-2奖金算力池*/@JsonSetter(nulls = Nulls.SKIP)private Integer tokenType;@Overrideprotected Class<TokenOrderColumn> getOrderColumnEnumClass() {return TokenOrderColumn.class;}@Overrideprotected TokenOrderColumn getDefaultOrderColumn() {return TokenOrderColumn.CREATE_TIME;}
}/*** 提现分页请求*/
@Data
public class DisbursementPageRequest extends BasePageRequest<DisbursementOrderColumn> {/*** 审核状态*/private Integer status;@Overrideprotected Class<DisbursementOrderColumn> getOrderColumnEnumClass() {return DisbursementOrderColumn.class;}@Overrideprotected DisbursementOrderColumn getDefaultOrderColumn() {return DisbursementOrderColumn.CREATE_TIME;}
}
通过这种设计,我对你的代码做了以下优化:
- 创建了一个抽象的
BasePageRequest
基类,封装了排序字段和排序类型的通用逻辑 - 使用了模板方法模式,子类只需要实现
getOrderColumnEnumClass()
和getDefaultOrderColumn()
方法 - 添加了 OrderColumnDefine 策略接口,统一管理所有排序字段,使代码更加清晰和易于维护
- 子类
TokenPageRequest
和DisbursementPageRequest
继承了基类,只需要定义自己的业务字段和排序字段列表
这样的设计有以下好处:
- 代码复用:共用的排序逻辑都放在基类中
- 易于扩展:要添加新的分页请求,只需继承基类并实现指定方法
- 维护方便:排序字段通过枚举管理,修改或添加字段更加集中
- 安全性:SQL注入防护逻辑集中在基类中实现
如果你需要添加新的分页接口,只需要:
- 创建新的排序枚举类 实现 OrderColumnDefine 接口 ,并根绝需求配置排序字段
- 创建新的分页请求类,继承
BasePageRequest
- 实现
getOrderColumnEnumClass()
和getDefaultOrderColumn()
方法