This commit is contained in:
Jerry
2020-04-12 09:32:34 +08:00
parent be4a132ddf
commit c2a5f9d394
305 changed files with 33137 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.orange.admin</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>common-biz</artifactId>
<version>1.0.0</version>
<name>common-biz</name>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.orange.admin</groupId>
<artifactId>common-core</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,30 @@
package com.orange.admin.common.biz.advice;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Controller的环绕拦截类。
*
* @author Stephen.Liu
* @date 2020-04-11
*/
@ControllerAdvice
public class MyControllerAdvice {
/**
* 转换前端传入的日期变量参数为指定格式。
*
* @param binder 数据绑定参数。
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Date.class,
new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"), false));
}
}

View File

@@ -0,0 +1,58 @@
package com.orange.admin.common.biz.advice;
import lombok.extern.slf4j.Slf4j;
import com.orange.admin.common.core.constant.ErrorCodeEnum;
import com.orange.admin.common.core.exception.RedisCacheAccessException;
import com.orange.admin.common.core.object.ResponseResult;
import org.apache.ibatis.exceptions.PersistenceException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.PermissionDeniedDataAccessException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeoutException;
/**
* 业务层的异常处理类这里只是给出最通用的Exception的捕捉今后可以根据业务需要
* 用不同的函数,处理不同类型的异常。
*
* @author Stephen.Liu
* @date 2020-04-11
*/
@Slf4j
@RestControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(value = Exception.class)
public ResponseResult<?> exceptionHandle(Exception ex, HttpServletRequest request) {
log.error("Unhandled exception from URL [" + request.getRequestURI() + "]", ex);
return ResponseResult.error(ErrorCodeEnum.UNHANDLED_EXCEPTION);
}
@ExceptionHandler(value = DuplicateKeyException.class)
public ResponseResult<?> duplicateKeyExceptionHandle(Exception ex, HttpServletRequest request) {
log.error("DuplicateKeyException exception from URL [" + request.getRequestURI() + "]", ex);
return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY);
}
@ExceptionHandler(value = DataAccessException.class)
public ResponseResult<?> dataAccessExceptionHandle(Exception ex, HttpServletRequest request) {
log.error("DataAccessException exception from URL [" + request.getRequestURI() + "]", ex);
if (ex.getCause() instanceof PersistenceException
&& ex.getCause().getCause() instanceof PermissionDeniedDataAccessException) {
return ResponseResult.error(ErrorCodeEnum.DATA_PERM_ACCESS_FAILED);
}
return ResponseResult.error(ErrorCodeEnum.DATA_ACCESS_FAILED);
}
@ExceptionHandler(value = RedisCacheAccessException.class)
public ResponseResult<?> redisCacheAccessExceptionHandle(Exception ex, HttpServletRequest request) {
log.error("RedisCacheAccessException exception from URL [" + request.getRequestURI() + "]", ex);
if (ex.getCause() instanceof TimeoutException) {
return ResponseResult.error(ErrorCodeEnum.REDIS_CACHE_ACCESS_TIMEOUT);
}
return ResponseResult.error(ErrorCodeEnum.REDIS_CACHE_ACCESS_STATE_ERROR);
}
}

View File

@@ -0,0 +1,22 @@
package com.orange.admin.common.biz.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 应用程序自定义的通用属性配置文件。
*
* @author Stephen.Liu
* @date 2020-04-11
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "application.common-biz")
public class CommonBizConfig {
/**
* Snowflake计算主键Id时所需的WorkNode参数值。
*/
private Integer snowflakeWorkNode;
}

View File

@@ -0,0 +1,29 @@
package com.orange.admin.common.biz.config;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* DTA 对象数据格式转换器这里集成了alibaba的fastjson作为消息格式转换工具
*
* @author Stephen.Liu
* @date 2020-04-11
*/
@Configuration
public class FastjsonConfig {
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(
SerializerFeature.PrettyFormat, SerializerFeature.DisableCircularReferenceDetect);
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
fastConverter.setFastJsonConfig(fastJsonConfig);
return new HttpMessageConverters(fastConverter);
}
}

View File

@@ -0,0 +1,39 @@
package com.orange.admin.common.biz.config;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* tomcat配置对象。当前配置禁用了PUT和DELETE方法防止渗透攻击。
*
* @author Stephen.Liu
* @date 2020-04-11
*/
@Configuration
public class TomcatConfig {
@Bean
public TomcatServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.addContextCustomizers(context -> {
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
collection.addMethod("HEAD");
collection.addMethod("PUT");
collection.addMethod("PATCH");
collection.addMethod("DELETE");
collection.addMethod("TRACE");
collection.addMethod("COPY");
collection.addMethod("SEARCH");
collection.addMethod("PROPFIND");
securityConstraint.addCollection(collection);
context.addConstraint(securityConstraint);
});
return factory;
}
}

View File

@@ -0,0 +1,43 @@
package com.orange.admin.common.biz.constant;
import java.util.HashMap;
import java.util.Map;
public final class Subject {
/**
* 语文。
*/
public static final int CHINESE = 0;
/**
* 数学。
*/
public static final int MATH = 1;
/**
* 英语。
*/
public static final int ENGLISH = 2;
public static final Map<Object, String> DICT_MAP = new HashMap<>(3);
static {
DICT_MAP.put(CHINESE, "语文");
DICT_MAP.put(MATH, "数学");
DICT_MAP.put(ENGLISH, "英语");
}
/**
* 判断参数是否为当前常量字典的合法值。
*
* @param value 待验证的参数值。
* @return 合法返回true否则false。
*/
public static boolean isValid(int value) {
return DICT_MAP.containsKey(value);
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private Subject() {
}
}

View File

@@ -0,0 +1,38 @@
package com.orange.admin.common.biz.constant;
import java.util.HashMap;
import java.util.Map;
public final class YesNo {
/**
* 是。
*/
public static final int YES = 1;
/**
* 否。
*/
public static final int NO = 0;
public static final Map<Object, String> DICT_MAP = new HashMap<>(2);
static {
DICT_MAP.put(YES, "");
DICT_MAP.put(NO, "");
}
/**
* 判断参数是否为当前常量字典的合法值。
*
* @param value 待验证的参数值。
* @return 合法返回true否则false。
*/
public static boolean isValid(int value) {
return DICT_MAP.containsKey(value);
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private YesNo() {
}
}

View File

@@ -0,0 +1,59 @@
package com.orange.admin.common.biz.interceptor;
import com.orange.admin.common.core.object.GlobalThreadLocal;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 服务访问日志的拦截器,主要完成记录接口调用时长。
*
* @author Stephen.Liu
* @date 2020-04-11
*/
@Slf4j
public class AccessInterceptor implements HandlerInterceptor {
private static final ThreadLocal<Long> STARTTIME_THREADLOCAL = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
STARTTIME_THREADLOCAL.set(System.currentTimeMillis());
// 每次进入Controller接口之前均主动打开数据权限验证。
// 可以避免该Servlet线程在处理之前的请求时异常退出从而导致该状态数据没有被正常清除。
GlobalThreadLocal.setDataPerm(true);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
long startTime = STARTTIME_THREADLOCAL.get();
long elapse = System.currentTimeMillis() - startTime;
String urlPath = request.getRequestURI();
String controllerMethod = "Unknown";
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
String controllerName = hm.getBean().getClass().getName();
// 这里将cglib织入的部分去掉提升日志的可读性
int index = controllerName.indexOf("$$");
if (index > 0) {
controllerName = controllerName.substring(0, index);
}
controllerMethod = controllerName + "." + hm.getMethod().getName();
}
log.info("access: {} -- elapse {} ms -- {}.", controllerMethod, elapse, urlPath);
STARTTIME_THREADLOCAL.remove();
GlobalThreadLocal.clearDataPerm();
}
}

View File

@@ -0,0 +1,234 @@
package com.orange.admin.common.biz.interceptor;
import cn.hutool.core.convert.Convert;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.orange.admin.common.core.annotation.MyRequestBody;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpMethod;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.util.*;
/**
* MyRequestBody解析器
* 解决的问题:
* 1、单个字符串等包装类型都要写一个对象才可以用@RequestBody接收
* 2、多个对象需要封装到一个对象里才可以用@RequestBody接收。
*
* @author Stephen.Liu
* @date 2020-04-11
*/
public class MyRequestArgumentResolver implements HandlerMethodArgumentResolver {
private static final String JSONBODY_ATTRIBUTE = "MY_REQUEST_BODY_ATTRIBUTE_XX";
private static Set<Class<?>> classSet = new HashSet<>();
static {
classSet.add(Integer.class);
classSet.add(Long.class);
classSet.add(Short.class);
classSet.add(Float.class);
classSet.add(Double.class);
classSet.add(Boolean.class);
classSet.add(Byte.class);
classSet.add(Character.class);
}
/**
* 设置支持的方法参数类型
*
* @param parameter 方法参数
* @return 支持的类型
*/
@Override
public boolean supportsParameter(@NonNull MethodParameter parameter) {
return parameter.hasParameterAnnotation(MyRequestBody.class);
}
/**
* 参数解析利用fastjson
* 注意非基本类型返回null会报空指针异常要通过反射或者JSON工具类创建一个空对象
*/
@SuppressWarnings("unchecked")
@Override
public Object resolveArgument(
@NonNull MethodParameter parameter,
ModelAndViewContainer mavContainer,
@NonNull NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
String contentType = servletRequest.getContentType();
if (!HttpMethod.POST.name().equals(servletRequest.getMethod())) {
throw new IllegalArgumentException("Only POST method can be applied @MyRequestBody annotation");
}
if (!StringUtils.containsIgnoreCase(contentType, "application/json")) {
throw new IllegalArgumentException(
"Only application/json Content-Type can be applied @MyRequestBody annotation");
}
// 根据@MyRequestBody注解value作为json解析的key
MyRequestBody parameterAnnotation = parameter.getParameterAnnotation(MyRequestBody.class);
JSONObject jsonObject = getRequestBody(webRequest);
if (jsonObject == null) {
if (parameterAnnotation.required()) {
throw new IllegalArgumentException("Request Body is EMPTY!");
}
return null;
}
String key = parameterAnnotation.value();
if (StringUtils.isBlank(key)) {
key = parameter.getParameterName();
}
Object value = jsonObject.get(key);
if (value == null) {
if (parameterAnnotation.required()) {
throw new IllegalArgumentException(String.format("Required parameter %s is not present!", key));
}
return null;
}
// 获取参数类型。
Class<?> parameterType = parameter.getParameterType();
//基本类型
if (parameterType.isPrimitive()) {
return parsePrimitive(parameterType.getName(), value);
}
// 基本类型包装类
if (isBasicDataTypes(parameterType)) {
return parseBasicTypeWrapper(parameterType, value);
// 字符串类型
} else if (parameterType == String.class) {
return value.toString();
}
if (value instanceof JSONArray) {
Object o;
if (!parameterType.equals(List.class)) {
o = parameterType.newInstance();
parameterType = (Class<?>) ((ParameterizedType)
parameterType.getGenericSuperclass()).getActualTypeArguments()[0];
} else {
parameterType = parameterAnnotation.elementType();
if (parameterType.equals(Class.class)) {
throw new IllegalArgumentException(
String.format("List Type parameter %s MUST have elementType!", key));
}
o = new LinkedList<>();
}
if (!(o instanceof List)) {
throw new IllegalArgumentException(String.format("Required parameter %s is List!", key));
}
((List<Object>) o).addAll(((JSONArray) value).toJavaList(parameterType));
return o;
}
// 其他复杂对象
return JSONObject.toJavaObject((JSONObject) value, parameterType);
}
/**
* 基本类型解析
*/
private Object parsePrimitive(String parameterTypeName, Object value) {
final String booleanTypeName = "boolean";
if (booleanTypeName.equals(parameterTypeName)) {
return Boolean.valueOf(value.toString());
}
final String intTypeName = "int";
if (intTypeName.equals(parameterTypeName)) {
return Integer.valueOf(value.toString());
}
final String charTypeName = "char";
if (charTypeName.equals(parameterTypeName)) {
return value.toString().charAt(0);
}
final String shortTypeName = "short";
if (shortTypeName.equals(parameterTypeName)) {
return Short.valueOf(value.toString());
}
final String longTypeName = "long";
if (longTypeName.equals(parameterTypeName)) {
return Long.valueOf(value.toString());
}
final String floatTypeName = "float";
if (floatTypeName.equals(parameterTypeName)) {
return Float.valueOf(value.toString());
}
final String doubleTypeName = "double";
if (doubleTypeName.equals(parameterTypeName)) {
return Double.valueOf(value.toString());
}
final String byteTypeName = "byte";
if (byteTypeName.equals(parameterTypeName)) {
return Byte.valueOf(value.toString());
}
return null;
}
/**
* 基本类型包装类解析
*/
private Object parseBasicTypeWrapper(Class<?> parameterType, Object value) {
if (Number.class.isAssignableFrom(parameterType)) {
if (value instanceof String) {
return Convert.convert(parameterType, value);
}
Number number = (Number) value;
if (parameterType == Integer.class) {
return number.intValue();
} else if (parameterType == Short.class) {
return number.shortValue();
} else if (parameterType == Long.class) {
return number.longValue();
} else if (parameterType == Float.class) {
return number.floatValue();
} else if (parameterType == Double.class) {
return number.doubleValue();
} else if (parameterType == Byte.class) {
return number.byteValue();
}
} else if (parameterType == Boolean.class) {
return value.toString();
} else if (parameterType == Character.class) {
return value.toString().charAt(0);
}
return null;
}
/**
* 判断是否为基本数据类型包装类
*/
private boolean isBasicDataTypes(Class<?> clazz) {
return classSet.contains(clazz);
}
/**
* 获取请求体JSON字符串
*/
private JSONObject getRequestBody(NativeWebRequest webRequest) {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
// 有就直接获取
JSONObject jsonObject = (JSONObject) webRequest.getAttribute(JSONBODY_ATTRIBUTE, NativeWebRequest.SCOPE_REQUEST);
// 没有就从请求中读取
if (jsonObject == null) {
try {
String jsonBody = IOUtils.toString(servletRequest.getReader());
jsonObject = JSON.parseObject(jsonBody);
if (jsonObject != null) {
webRequest.setAttribute(JSONBODY_ATTRIBUTE, jsonObject, NativeWebRequest.SCOPE_REQUEST);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return jsonObject;
}
}

View File

@@ -0,0 +1,27 @@
package com.orange.admin.common.biz.listener;
import com.orange.admin.common.core.base.service.BaseDictService;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 应用程序启动后的事件监听对象。主要负责加载Model之间的字典关联和一对一关联所对应的Service结构关系。
*
* @author Stephen.Liu
* @date 2020-04-11
*/
@Component
public class LoadCachedDataListener implements ApplicationListener<ApplicationReadyEvent> {
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
Map<String, BaseDictService> serviceMap =
applicationReadyEvent.getApplicationContext().getBeansOfType(BaseDictService.class);
for (Map.Entry<String, BaseDictService> e : serviceMap.entrySet()) {
e.getValue().loadCachedData();
}
}
}

View File

@@ -0,0 +1,27 @@
package com.orange.admin.common.biz.listener;
import com.orange.admin.common.core.base.service.BaseService;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 应用程序启动后的事件监听对象。主要负责加载Model之间的字典关联和一对一关联所对应的Service结构关系。
*
* @author Stephen.Liu
* @date 2020-04-11
*/
@Component
public class LoadServiceRelationListener implements ApplicationListener<ApplicationReadyEvent> {
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
Map<String, BaseService> serviceMap =
applicationReadyEvent.getApplicationContext().getBeansOfType(BaseService.class);
for (Map.Entry<String, BaseService> e : serviceMap.entrySet()) {
e.getValue().loadRelationStruct();
}
}
}

View File

@@ -0,0 +1,51 @@
package com.orange.admin.common.biz.util;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import com.orange.admin.common.biz.config.CommonBizConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* 全局共享的snowflake计算工具类。
* 基于Hutool中的对应工具类实现同时读取系统的配置参数并初始化该对象。如果今后
* 升级为基于分布式Id生成服务的实现时仅需修改内部实现外部业务方法不会受到任何影响。
*
* @author Stephen.Liu
* @date 2020-04-11
*/
@Component
public class BasicIdGenerator {
@Autowired
private CommonBizConfig config;
private Snowflake snowflake;
@PostConstruct
private void init() {
snowflake = IdUtil.createSnowflake(config.getSnowflakeWorkNode(), 0);
}
/**
* 获取基于Snowflake算法的数值型Id。
* 由于底层实现为synchronized方法因此计算过程串行化且线程安全。
*
* @return 计算后的全局唯一Id。
*/
public long nextLongId() {
return this.snowflake.nextId();
}
/**
* 获取基于Snowflake算法的字符串Id。
* 由于底层实现为synchronized方法因此计算过程串行化且线程安全。
*
* @return 计算后的全局唯一Id。
*/
public String nextStringId() {
return this.snowflake.nextIdStr();
}
}