Java后端优雅的实现分页搜索排序-架构2

一个请求是怎么达到后端的 controller 的

公共的代码

复制代码
RequestHolder.java 主要是 ThreadLocal

@Slf4j
public class RequestHolder {

    private final static ThreadLocal<CommonRequest> REQUEST_COMMON_HOLDER = new ThreadLocal<>();

    private final static ThreadLocal<BaseSearchDTO> REQUEST_SEARCH_HOLDER = new ThreadLocal<>();

    private final static ThreadLocal<BaseOrderDTO> REQUEST_ORDER_HOLDER = new ThreadLocal<>();

    private final static ThreadLocal<Map<String, Object>> REQUEST_FILTER_MAP_HOLDER = new ThreadLocal<>();

    private final static ThreadLocal<Map<String, List<Object>>> REQUEST_FILTER_IN_HOLDER = new ThreadLocal<>();

    public static void addSearch(BaseSearchDTO baseSearchDTO) {
        REQUEST_SEARCH_HOLDER.set(baseSearchDTO);
    }
    public static void addSearch(String column, String keyword) {
        if (Objects.nonNull(REQUEST_SEARCH_HOLDER.get())) {
            return;
        }
        if (StringUtils.isBlank(keyword)) {
            return;
        }
        BaseSearchDTO d = new BaseSearchDTO();
        if (StringUtils.isNotBlank(column) && !isValid(column)){
            log.warn("[CHECK SQL INJECT ] the searchValue mybe is SQL Injection :{}",column);
            throw ClusterException.asException(ResultEnum.FILTER_OR_SEARCH_ERROR, String.format("搜索条件 '%s' 不合法!", column));
        }
        if (StringUtils.isNotBlank(keyword) && !isValid(keyword)){
            log.warn("[CHECK SQL INJECT ] the searchValue mybe is SQL Injection :{}",keyword);
            throw ClusterException.asException(ResultEnum.FILTER_OR_SEARCH_ERROR, String.format("搜索条件 '%s' 不合法!", keyword));
        }
        d.setSearchColumn(column);
        d.setSearchValue(keyword);
        REQUEST_SEARCH_HOLDER.set(d);
    }

    public static BaseSearchDTO getSearchable() {
        return REQUEST_SEARCH_HOLDER.get();
    }

    public static void removeSearchable() {
        REQUEST_SEARCH_HOLDER.remove();
    }

    public static void addOrder(BaseOrderDTO baseOrderDTO) {
        REQUEST_ORDER_HOLDER.set(baseOrderDTO);
    }
    public static void addOrder(String orderField, String orderBy) {
        if (Objects.nonNull(REQUEST_ORDER_HOLDER.get())) {
            return;
        }
        BaseOrderDTO d = new BaseOrderDTO();
        d.setOrderField(orderField);
        if (StringUtils.isNotBlank(orderField) && !isValid(orderField)){
            log.warn("[CHECK SQL INJECT ] the searchValue mybe is SQL Injection :{}",orderField);
            throw ClusterException.asException(ResultEnum.FILTER_OR_SEARCH_ERROR, String.format("搜索条件 '%s' 不合法!", orderField));
        }
        d.setOrderBy(orderBy);
        if (StringUtils.isEmpty(orderBy)) {
            d.setOrderBy("DESC");
        }
        REQUEST_ORDER_HOLDER.set(d);
    }

    public static BaseOrderDTO getOrderAble() {
        return REQUEST_ORDER_HOLDER.get();
    }

    public static void removeOrderAble() {
        REQUEST_ORDER_HOLDER.remove();
    }

    public static void addCommonRequest(CommonRequest commonRequest) {
        REQUEST_COMMON_HOLDER.set(commonRequest);
    }

    public static CommonRequest getCommonRequest() {
        return REQUEST_COMMON_HOLDER.get();
    }

    public static void removeCommonRequest() {
        REQUEST_COMMON_HOLDER.remove();
    }

    public static void addFilterMapRequest(Map<String, Object> filterMap) {
        REQUEST_FILTER_MAP_HOLDER.set(filterMap);
    }

    public static void addFilterInRequest(Map<String, List<Object>> filterMap) {
        REQUEST_FILTER_IN_HOLDER.set(filterMap);
    }

    public static Map<String, Object> getFilterMapRequest() {
        Map<String, Object> map = REQUEST_FILTER_MAP_HOLDER.get();
        return Objects.isNull(map) ? new HashMap<>(0) : map;
    }

    public static Map<String, List<Object>> getFilterInRequest() {
        Map<String, List<Object>> map = REQUEST_FILTER_IN_HOLDER.get();
        return Objects.isNull(map) ? new HashMap<>(0) : map;
    }

    public static void removeFilterInRequest() {
        REQUEST_FILTER_IN_HOLDER.remove();
    }

    public static void removeFilterMapRequest() {
        REQUEST_FILTER_MAP_HOLDER.remove();
    }

    public static void addOrderCreateDesc() {
        BaseOrderDTO d = new BaseOrderDTO();
        d.setOrderField("id");
        d.setOrderBy("DESC");
        REQUEST_ORDER_HOLDER.set(d);
    }

    public static void addOrderCreateAsc() {
        BaseOrderDTO d = new BaseOrderDTO();
        d.setOrderField("id");
        d.setOrderBy("ASC");
        REQUEST_ORDER_HOLDER.set(d);
    }

    public static void addPageAbleDefault(int pageSize) {
        Page<Object> page = PageHelper.startPage(1, pageSize);
    }

    public static void addPageAble(int pageNumber, int pageSize) {
        Page<Object> page = PageHelper.startPage(pageNumber, pageSize);
    }

    public static void removeAllRequestHolder() {
        if (RequestHolder.getSearchable() != null) {
            removeSearchable();
        }
        if (RequestHolder.getOrderAble() != null) {
            REQUEST_ORDER_HOLDER.remove();
        }
        if (MapUtils.isNotEmpty(RequestHolder.getFilterMapRequest())) {
            REQUEST_FILTER_MAP_HOLDER.remove();
        }
        if (MapUtils.isNotEmpty(RequestHolder.getFilterInRequest())) {
            REQUEST_FILTER_IN_HOLDER.remove();
        }
        if (RequestHolder.getCommonRequest() != null) {
            REQUEST_COMMON_HOLDER.remove();
        }
    }

    private static boolean isValid(String input) {
        String regex = "^[a-zA-Z\\d_*%\\-\\.\\u4e00-\\u9fa5]+$";
        // 默认合法且小于32位
        return input.matches(regex) && input.length() <= 48;
    }
}

WebFilter

复制代码
@Slf4j
@WebFilter(filterName = "requestFilter", urlPatterns = {"/v2/*"})
public class RequestFilter implements Filter {
	
	@Override
    public void init(FilterConfig filterConfig) {
	    //可以为空
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws BusinessException {
     	// 处理公共的逻辑
     	1. 解析请求头a:获取请求头 x-cloud-pin,确保当前请求人已经登录
     	2. 解析请求头b:获取请求头 X-Token,确保 token 合法,通常token 会包含当前请求人
     	3. 根据请求人的名字获取请求人的 pin或角色等信息
     	4.  把上述信息保存在 ThreadLocal 中,最后让请求继续过滤链
        filterChain.doFilter(new BodyHttpServletRequestWrapper((HttpServletRequest) servletRequest), servletResponse);

    }
    
    @Override
    public void destroy() {
	    //可以为空
    }
}

ControllerAspect

主要负责记录下日志和清空 ThreadLocal 数据

复制代码
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ControllerAspect {

    private static final Logger LOG = LoggerFactory.getLogger(("TRACE-INFO-LOGGER"));

    @Pointcut("execution(* com.demo.web.interfaces.controller..*(..))")
    public void controller() {
    }

    @AfterThrowing(pointcut = "controller()", throwing = "ex")
    public void doAfter(JoinPoint point, Exception ex) {
        CloudResult<?> result;
        if (ex instanceof ClusterException) {
            result = CloudResult.error(((ClusterException) ex).getCode(), ex.getMessage());
        } else {
            result = CloudResult.error(ResultEnum.INTERNAL_SERVER_ERROR.getCode(), ex.getMessage());
        }
        result.setRequestId(UserUtils.getRequestId());
        // 清空请求信息
        RequestHolder.removeAllRequestHolder();
        // 记录日志信息
        logger(System.currentTimeMillis(), point.getSignature().getDeclaringTypeName(), point.getArgs(), result);

    }

    @Around("controller()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {
        long startTime = System.currentTimeMillis();
        //outputRequestBody(ClusterUtils.getHttpServletRequest());
        Object[] reqParam = point.getArgs();
        Object result = point.proceed();

        // 清空请求信息,清空 ThreadLocal 中的内容
        RequestHolder.removeAllRequestHolder();
        // 记录日志信息
        logger(startTime, point.getSignature().getDeclaringTypeName(), reqParam, result);

        return result;
    }

    private static String logger(long startTime, String executeMethod, Object[] reqParam, Object resParam) {
        Map<String, Object> logMap = new HashMap<>(10);
        try {
            HttpServletRequest request = ClusterUtils.getHttpServletRequest();
            logMap.put("uri", request.getRequestURI());
            logMap.put("queryString", request.getQueryString());
            logMap.put("remoteAddr", request.getRemoteAddr());
            logMap.put("remotePort", String.valueOf(request.getRemotePort()));
            logMap.put("serverAddr", request.getLocalAddr());
            logMap.put("controller", executeMethod);
            logMap.put("realIp", getIp(request));
            logMap.put("requestStr", JacksonUtils.toJson(reqParam));
            // logMap.put("responseStr", JacksonUtils.toJson(resParam));
            logMap.put("runtime", System.currentTimeMillis() - startTime);
            logMap.put("requestId", UserUtils.getRequestId());
            String jsonData = JacksonUtils.toJson(logMap);
            LOG.info(jsonData);
            return jsonData;
        } catch (Exception e) {
            log.error("{} logger is exception " + e, executeMethod);
        }
        return null;
    }

    private static void outputRequestBody(HttpServletRequest request) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = null;
        try {
            InputStream inputStream = request.getInputStream();
            if (Objects.nonNull(inputStream)) {
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                char[] charBuffer = new char[128];
                int bytesRead;
                while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                    stringBuilder.append(charBuffer, 0, bytesRead);
                }
            }
        } finally {
            if (bufferedReader != null) {
                bufferedReader.close();
            }
        }
        LOG.info(stringBuilder.toString());
    }

    private static String getIp(HttpServletRequest request) {
        String unknown = "unknown";
        String ipAddress = request.getHeader("x-forwarded-for");
        if (StringUtils.isBlank(ipAddress) || unknown.equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("Proxy-Client-IP");
        }
        if (StringUtils.isBlank(ipAddress) || unknown.equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("WL-Proxy-Client-IP");
        }
        if (StringUtils.isBlank(ipAddress) || unknown.equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
            if ("127.0.0.1".equals(ipAddress) || "0:0:0:0:0:0:0:1".equals(ipAddress)) {
                try {
                    ipAddress = InetAddress.getLocalHost().getHostAddress();
                } catch (UnknownHostException e) {
                    ipAddress = "127.0.0.1";
                    log.error("format logger get ip is error " + e);
                }
            }
        }
        if (ipAddress != null && ipAddress.length() > 15) {
            if (ipAddress.indexOf(",") > 0) {
                ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
            }
        }
        return ipAddress;
    }
}

列表接口上的注解

分页、排序、过滤器,

把请求体中的排序、过滤器数据统一解析到 ThreadLocal 中

接口的请求体示例

复制代码
{
    "pageNumber": 1,
    "pageSize": 11,
    "columns":"dbName",
    "keyword":"rob",
    "orderFields":"createdDate",
    "orderBy":"DESC",
    "filters": [
        {
            "name": "dbType",
            "operator": "EQ",
            "values": [
                0
            ]
        }
        ,
        {
            "name": "favorite",
            "operator": "EQ",
            "values": [
                1
            ]
        }
    ]
}

注解 QueryFilter 及其切面 QueryFilterAspect

复制代码
QueryFilter.java:  自己定义的注解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface QueryFilter {

    /**
     * A single String role name or multiple comma-delimited role names required in order for the method invocation to
     * be allowed.
     * default Class<?> value() default Void.class;
     */
    Class<?> value();
}

复制代码
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@Slf4j
@Aspect
@Order(9997)
@Component
public class QueryFilterAspect {

    @Around(value = "@annotation(QueryFilter)")
    public Object queryFilterAspect(ProceedingJoinPoint point) throws Throwable {

        HttpServletRequest request = DemoUtils.getHttpServletRequest();
        if (!isRequestJson(request)) {
            return point.proceed();
        }
        String body = getBodyByInputStream(request.getInputStream());
        log.info("############ get request body = {}", body);
        if (StringUtils.isBlank(body)) {
            return point.proceed();
        }
        Signature signature = point.getSignature();
        if(signature instanceof MethodSignature){
            MethodSignature methodSignature = (MethodSignature) signature;
            Method method = methodSignature.getMethod();
            QueryFilter annotation = method.getAnnotation(QueryFilter.class);
            // add filterGroup
            FilterGroupsRequest filterGroup = JacksonUtils.parse(body, FilterGroupsRequest.class);
            if (Objects.isNull(filterGroup)) {
                return point.proceed();
            }
            try {
                // 1. 设置分页
                DemoUtils.setPageable(filterGroup.getPageNumber(), filterGroup.getPageSize());
                // 2. 设置排序
                RequestHolder.addOrder(filterGroup.getOrderFields(), filterGroup.getOrderBy());
                // 3. 设置过滤器
                Class<?> clazz = annotation.value();
                Object dataObj = "java.lang.Void".equals(clazz.getName()) ? null : clazz.newInstance();
                RequestHolder.addFilterMapRequest(getFilterMap(dataObj, filterGroup));
            } catch (Exception e) {
                log.error("QueryFilterAspect get Request body is exception !" + e);
            }
        }

        return point.proceed();
    }

    private boolean isRequestJson(HttpServletRequest request) {
        if (Objects.isNull(request.getContentType())) {
            return false;
        }
        return request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) ||
                request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE);
    }

    private String getBodyByInputStream(InputStream inputStream) throws IOException {
        if (Objects.isNull(inputStream)) {
            return null;
        }
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        char[] charBuffer = new char[128];
        int bytesRead;
        while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
            stringBuilder.append(charBuffer, 0, bytesRead);
        }
        return stringBuilder.toString();
    }

    private Map<String, Object> getFilterMap(Object dataObj, FilterGroupsRequest request) throws Exception {
        if (Objects.isNull(request) || CollectionUtils.isEmpty(request.getFilters())) {
            return new HashMap<>((0));
        }
        for (FilterGroup filter : request.getFilters()) {
            if (Objects.isNull(filter)) {
                continue;
            }
            String name = filter.getName();
            List<Object> values = filter.getValues();
            String operator = StringUtils.isBlank(filter.getOperator()) ? "eq" : filter.getOperator();
            if (StringUtils.isBlank(name) || CollectionUtils.isEmpty(values) || Objects.isNull(values.get(0))) {
                continue;
            }
            // TODO 暂时只支持 EQ/LIKE,且只能有1个值
            if ("like".equalsIgnoreCase(operator)) {
                RequestHolder.addSearch(name, String.valueOf(values.get(0)));
            } else if ("eq".equalsIgnoreCase(operator)) {
                DemoUtils.setupDataObject(dataObj, name, values.get(0));
            }
        }
        return JacksonUtils.bean2Map(dataObj);
    }
}

复制代码
DemoUitls.java 设置分页

public static void setPageable(Integer pageNumber, Integer pageSize) {
        if (Objects.isNull(pageNumber) && Objects.isNull(pageSize)) {
            return;
        }
        if (Objects.nonNull(pageNumber) && pageNumber == -1) {
            return;
        }
        pageNumber = Objects.isNull(pageNumber) ? 1 : pageNumber;
        pageSize = Objects.isNull(pageSize) ? 10 : pageSize;
        pageSize = Math.min(pageSize, DemoConstants.PAGE_SIZE_MAX);
        PAGE_THREAD_LOCAL.set(PageHelper.startPage(pageNumber, pageSize));
    }

注解 Pageable 及其切面 PageAspect

复制代码
Pageable.java  自己定义的注解

@Target(value = {ElementType.TYPE,ElementType.FIELD,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Pageable {

}

复制代码
@Aspect
@Slf4j
@Component
@Order(9999)
@SuppressWarnings("unchecked")
public class PageAspect {

    private static final ThreadLocal<PageBean<Object>> PAGE_BEAN_THREAD_LOCAL = ThreadLocal.withInitial(PageBean::new);

    @Around(value = "@annotation(Pageable)")
    public Object pageableAspect(ProceedingJoinPoint point) throws Throwable {
        Object pageNumberObj = DemoUtils.getRequestValueByKey("pageNumber");
        Object pageSizeObj = DemoUtils.getRequestValueByKey("pageSize");
        if (Objects.isNull(pageNumberObj) && Objects.isNull(pageSizeObj)) {
            // 对 pageNumber 和 pageSize 不在请求URL 中会走到这里直接返回
            return point.proceed();
        }
        // 对 pageNumber 和 pageSize 在请求URL 中才会继续往下走
        try {
            int pageNumber = Integer.parseInt(pageNumberObj.toString());
            // pageSize小于0,则查询全部,无须分页
            if (pageNumber == -1) {
                return point.proceed();
            }
            int pageSize = Integer.parseInt(pageSizeObj.toString());
            if (pageSize <= 0) {
                throw DemoException.asException(ResultEnum.PARAM_ERROR);
            }
            pageSize = Math.min(pageSize, DemoConstants.PAGE_SIZE_MAX);
            DemoUtils.PAGE_THREAD_LOCAL.set(PageHelper.startPage(pageNumber, pageSize));
        } catch (Throwable throwable) {
            throw DemoException.asException(ResultEnum.PARAM_ERROR, "pageNumber or pageSize ");
        }
        return point.proceed();
    }
}

列表接口controller:

复制代码
@Pageable 
@QueryFilter(BookDO.class)
@RequestMapping(value = "/book", method = {RequestMethod.GET, RequestMethod.POST})
public CloudResult<PageBean<BookListVo>> describeBackupNodes(@RequestParam(value = "pageNumber", required = false) Integer pageNumber,
                                                                      @RequestParam(value = "pageSize", required = false) Integer pageSize,
                                                                      @RequestParam(value = "columns", required = false) String columns,
                                                                      @RequestParam(value = "keyword", required = false) String keyword,
                                                                      @RequestParam(value = "orderFields", required = false) String orderFields,
                                                                      @RequestParam(value = "orderBy", required = false) String orderBy) {

    // 从 ThreadLocal 中获取通过 QueryFilter 注解解析请求体写入ThreadLocal中的内容
    Map<String, Object> filtersMap = RequestHolder.getFilterMapRequest();
    List<BookInfo> bookInfos = bookService.findAndSearchByMap(filtersMap);

    if (CollectionUtils.isEmpty(backupInfos)) {
        return CloudResult.success(new PageBean<>(new ArrayList<>()));
    }
    return CloudResult.success(new PageBean<>(assembler.infoToNodeEntity(bookInfos)));
}

MyBatis PlugIn (可选)

这个比较晦涩,不好理解。

当然也可以在DAO层获取 ThreadLocal 中内容,通过 MyBatis 的ORM操作获取数据

复制代码
@Slf4j
@Component
@Intercepts({
        @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
@AutoConfigureBefore(PageHelperAutoConfiguration.class)
public class QueryPlugin implements Interceptor {
     	// 利用 ThreadLocal 中的搜索和排序中数据改写SQL,需要用到 Druid 改写 SQL
}

RestControllerAdvice 统一处理异常

参考 @RestControllerAdvice注解-架构1

相关推荐
甜鲸鱼1 小时前
【Spring Boot + OpenAPI 3】开箱即用的 API 文档方案(SpringDoc + Knife4j)
java·spring boot·后端
foxsen_xia1 小时前
go(基础10)——错误处理
开发语言·后端·golang
她说..1 小时前
在定义Java接口参数时,遇到整数类型,到底该用int还是Integer?
java·开发语言·java-ee·springboot
两广总督是码农1 小时前
IDEA-SpringBoot热部署
java·spring boot·intellij-idea
Evand J1 小时前
【PSINS进阶例程】雷达三维跟踪与EKF轨迹滤波。带坐标转换,观测为斜距、方向角、俯仰角。MATLAB编写,附下载链接
开发语言·matlab·psins·雷达观测
专业开发者1 小时前
Android 位置服务(LBS)客户支持指南
开发语言·php
MoFe11 小时前
【.net/.net core】【报错处理】另一个 SqlParameterCollection 中已包含 SqlParameter。
java·.net·.netcore
sang_xb1 小时前
深入解析 HashMap:从存储架构到性能优化
android·java·性能优化·架构
cws2004011 小时前
微软系统中AD域用户信息及状态报表命令介绍
开发语言·microsoft·php