一个请求是怎么达到后端的 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
}