commit:添加单体工程开源版本

This commit is contained in:
Jerry
2020-09-25 10:45:02 +08:00
parent 28ea2247f6
commit 53270e70f8
354 changed files with 40271 additions and 9 deletions

View File

@@ -0,0 +1,110 @@
<?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">
<parent>
<groupId>com.orange.demo</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-core</artifactId>
<version>1.0.0</version>
<name>common-core</name>
<packaging>jar</packaging>
<dependencies>
<!-- 常用工具 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>${commons-collections4.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>${common-csv.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
<dependency>
<groupId>cn.jimmyshi</groupId>
<artifactId>bean-query</artifactId>
<version>${bean.query.version}</version>
</dependency>
<!-- poi相关工具包 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi-ooxml.version}</version>
</dependency>
<!-- 数据库访问层相关工具 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>${mybatis-mapper.version}</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,30 @@
package com.orange.demo.common.core.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 Jerry
* @date 2020-09-25
*/
@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,128 @@
package com.orange.demo.common.core.advice;
import com.orange.demo.common.core.exception.InvalidClassFieldException;
import com.orange.demo.common.core.exception.InvalidDataFieldException;
import com.orange.demo.common.core.exception.InvalidDataModelException;
import com.orange.demo.common.core.constant.ErrorCodeEnum;
import com.orange.demo.common.core.exception.RedisCacheAccessException;
import com.orange.demo.common.core.object.ResponseResult;
import lombok.extern.slf4j.Slf4j;
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 Jerry
* @date 2020-09-25
*/
@Slf4j
@RestControllerAdvice("com.orange.demo")
public class MyExceptionHandler {
/**
* 通用异常处理方法。
*
* @param ex 异常对象。
* @param request http请求。
* @return 应答对象。
*/
@ExceptionHandler(value = Exception.class)
public ResponseResult<Void> exceptionHandle(Exception ex, HttpServletRequest request) {
log.error("Unhandled exception from URL [" + request.getRequestURI() + "]", ex);
return ResponseResult.error(ErrorCodeEnum.UNHANDLED_EXCEPTION);
}
/**
* 无效的实体对象异常。
*
* @param ex 异常对象。
* @param request http请求。
* @return 应答对象。
*/
@ExceptionHandler(value = InvalidDataModelException.class)
public ResponseResult<Void> invalidDataModelExceptionHandle(Exception ex, HttpServletRequest request) {
log.error("InvalidDataModelException exception from URL [" + request.getRequestURI() + "]", ex);
return ResponseResult.error(ErrorCodeEnum.INVALID_DATA_MODEL);
}
/**
* 无效的实体对象字段异常。
*
* @param ex 异常对象。
* @param request http请求。
* @return 应答对象。
*/
@ExceptionHandler(value = InvalidDataFieldException.class)
public ResponseResult<Void> invalidDataFieldExceptionHandle(Exception ex, HttpServletRequest request) {
log.error("InvalidDataFieldException exception from URL [" + request.getRequestURI() + "]", ex);
return ResponseResult.error(ErrorCodeEnum.INVALID_DATA_FIELD);
}
/**
* 无效类字段异常。
*
* @param ex 异常对象。
* @param request http请求。
* @return 应答对象。
*/
@ExceptionHandler(value = InvalidClassFieldException.class)
public ResponseResult<Void> invalidClassFieldExceptionHandle(Exception ex, HttpServletRequest request) {
log.error("InvalidClassFieldException exception from URL [" + request.getRequestURI() + "]", ex);
return ResponseResult.error(ErrorCodeEnum.INVALID_CLASS_FIELD);
}
/**
* 重复键异常处理方法。
*
* @param ex 异常对象。
* @param request http请求。
* @return 应答对象。
*/
@ExceptionHandler(value = DuplicateKeyException.class)
public ResponseResult<Void> duplicateKeyExceptionHandle(Exception ex, HttpServletRequest request) {
log.error("DuplicateKeyException exception from URL [" + request.getRequestURI() + "]", ex);
return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY);
}
/**
* 数据访问失败异常处理方法。
*
* @param ex 异常对象。
* @param request http请求。
* @return 应答对象。
*/
@ExceptionHandler(value = DataAccessException.class)
public ResponseResult<Void> 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);
}
/**
* Redis缓存访问异常处理方法。
*
* @param ex 异常对象。
* @param request http请求。
* @return 应答对象。
*/
@ExceptionHandler(value = RedisCacheAccessException.class)
public ResponseResult<Void> 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,16 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.*;
/**
* 主要用于标记逻辑删除字段。
*
* @author Jerry
* @date 2020-09-25
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DeletedFlagColumn {
}

View File

@@ -0,0 +1,16 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.*;
/**
* 主要用于标记更新字段。
*
* @author Jerry
* @date 2020-09-25
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JobUpdateTimeColumn {
}

View File

@@ -0,0 +1,21 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.*;
/**
* 主要用于标记Service所依赖的数据源类型。
*
* @author Jerry
* @date 2020-09-25
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyDataSource {
/**
* 标注的数据源类型
* @return 当前标注的数据源类型。
*/
int value();
}

View File

@@ -0,0 +1,31 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记Controller中的方法参数参数解析器会根据该注解将请求中的JSON数据映射到参数中的绑定字段。
*
* @author Jerry
* @date 2020-09-25
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRequestBody {
/**
* 是否必须出现的参数。
*/
boolean required() default false;
/**
* 解析时用到的JSON的key。
*/
String value() default "";
/**
* 集合元素的ClassType。只有在接口参数为List<E>的时候需要把E的class传入。
* 缺省值Class.class表示没有设置。
*/
Class<?> elementType() default Class.class;
}

View File

@@ -0,0 +1,15 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.*;
/**
* 主要用于标记无需Token验证的接口
*
* @author Jerry
* @date 2020-09-25
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoAuthInterface {
}

View File

@@ -0,0 +1,29 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.*;
/**
* 标识Model和常量字典之间的关联关系。
*
* @author Jerry
* @date 2020-09-25
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RelationConstDict {
/**
* 当前对象的关联Id字段名称。
*
* @return 当前对象的关联Id字段名称。
*/
String masterIdField();
/**
* 被关联的常量字典的Class对象。
*
* @return 关联的常量字典的Class对象。
*/
Class<?> constantDictClass();
}

View File

@@ -0,0 +1,60 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.*;
/**
* 标识Model之间的字典关联关系。
*
* @author Jerry
* @date 2020-09-25
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RelationDict {
/**
* 当前对象的关联Id字段名称。
*
* @return 当前对象的关联Id字段名称。
*/
String masterIdField();
/**
* 被关联Model对象的Class对象。
*
* @return 被关联Model对象的Class对象。
*/
Class<?> slaveModelClass();
/**
* 被关联Model对象的关联Id字段名称。
*
* @return 被关联Model对象的关联Id字段名称。
*/
String slaveIdField();
/**
* 被关联Model对象的关联Name字段名称。
*
* @return 被关联Model对象的关联Name字段名称。
*/
String slaveNameField();
/**
* 被关联的本地Service对象名称。
*
* @return 被关联的本地Service对象名称。
*/
String slaveServiceName();
/**
* 在同一个实体对象中,如果有一对一关联和字典关联,都是基于相同的主表字段,并关联到
* 相同关联表的同一关联字段时,可以在字典关联的注解中引用被一对一注解标准的对象属性。
* 从而在数据整合时,当前字典的数据可以直接取自"equalOneToOneRelationField"指定
* 的字段,从而避免一次没必要的数据库查询操作,提升了加载显示的效率。
*
* @return 与该字典字段引用关系完全相同的一对一关联属性名称。
*/
String equalOneToOneRelationField() default "";
}

View File

@@ -0,0 +1,36 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.*;
/**
* 标注多对多的Model关系。
*
* @author Jerry
* @date 2020-09-25
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RelationManyToMany {
/**
* 多对多中间表的Mapper对象名称。
*
* @return 被关联的本地Service对象名称。
*/
String relationMapperName();
/**
* 多对多关联表Model对象的Class对象。
*
* @return 被关联Model对象的Class对象。
*/
Class<?> relationModelClass();
/**
* 多对多关联表Model对象中与主表关联的Id字段名称。
*
* @return 被关联Model对象的关联Id字段名称。
*/
String relationMasterIdField();
}

View File

@@ -0,0 +1,85 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.*;
/**
* 主要用于多对多的Model关系。标注通过从表关联字段或者关联表关联字段计算主表聚合计算字段的规则。
*
* @author Jerry
* @date 2020-09-25
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RelationManyToManyAggregation {
/**
* 当前对象的关联Id字段名称。
*
* @return 当前对象的关联Id字段名称。
*/
String masterIdField();
/**
* 被关联的本地Service对象名称。
*
* @return 被关联的本地Service对象名称。
*/
String slaveServiceName();
/**
* 多对多从表Model对象的Class对象。
*
* @return 被关联Model对象的Class对象。
*/
Class<?> slaveModelClass();
/**
* 多对多从表Model对象的关联Id字段名称。
*
* @return 被关联Model对象的关联Id字段名称。
*/
String slaveIdField();
/**
* 多对多关联表Model对象的Class对象。
*
* @return 被关联Model对象的Class对象。
*/
Class<?> relationModelClass();
/**
* 多对多关联表Model对象中与主表关联的Id字段名称。
*
* @return 被关联Model对象的关联Id字段名称。
*/
String relationMasterIdField();
/**
* 多对多关联表Model对象中与从表关联的Id字段名称。
*
* @return 被关联Model对象的关联Id字段名称。
*/
String relationSlaveIdField();
/**
* 聚合计算所在的Model。
*
* @return 聚合计算所在Model的Class。
*/
Class<?> aggregationModelClass();
/**
* 聚合类型。具体数值参考AggregationType对象。
*
* @return 聚合类型。
*/
int aggregationType();
/**
* 聚合计算所在Model的字段名称。
*
* @return 聚合计算所在Model的字段名称。
*/
String aggregationField();
}

View File

@@ -0,0 +1,57 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.*;
/**
* 主要用于一对多的Model关系。标注通过从表关联字段计算主表聚合计算字段的规则。
*
* @author Jerry
* @date 2020-09-25
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RelationOneToManyAggregation {
/**
* 当前对象的关联Id字段名称。
*
* @return 当前对象的关联Id字段名称。
*/
String masterIdField();
/**
* 被关联的本地Service对象名称。
*
* @return 被关联的本地Service对象名称。
*/
String slaveServiceName();
/**
* 被关联Model对象的Class对象。
*
* @return 被关联Model对象的Class对象。
*/
Class<?> slaveModelClass();
/**
* 被关联Model对象的关联Id字段名称。
*
* @return 被关联Model对象的关联Id字段名称。
*/
String slaveIdField();
/**
* 被关联Model对象中参与计算的聚合类型。具体数值参考AggregationType对象。
*
* @return 被关联Model对象中参与计算的聚合类型。
*/
int aggregationType();
/**
* 被关联Model对象中参与聚合计算的字段名称。
*
* @return 被关联Model对象中参与计算字段的名称。
*/
String aggregationField();
}

View File

@@ -0,0 +1,50 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.*;
/**
* 标识Model之间的一对一关联关系。
*
* @author Jerry
* @date 2020-09-25
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RelationOneToOne {
/**
* 当前对象的关联Id字段名称。
*
* @return 当前对象的关联Id字段名称。
*/
String masterIdField();
/**
* 被关联Model对象的Class对象。
*
* @return 被关联Model对象的Class对象。
*/
Class<?> slaveModelClass();
/**
* 被关联Model对象的关联Id字段名称。
*
* @return 被关联Model对象的关联Id字段名称。
*/
String slaveIdField();
/**
* 被关联的本地Service对象名称。
*
* @return 被关联的本地Service对象名称。
*/
String slaveServiceName();
/**
* 在一对一关联时,是否加载从表的字典关联。
*
* @return 是否加载从表的字典关联。true关联false则只返回从表自身数据。
*/
boolean loadSlaveDict() default true;
}

View File

@@ -0,0 +1,84 @@
package com.orange.demo.common.core.aop;
import com.alibaba.fastjson.JSON;
import com.orange.demo.common.core.constant.ApplicationConstant;
import com.orange.demo.common.core.util.MyCommonUtil;
import lombok.extern.slf4j.Slf4j;
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.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
/**
* 记录接口的链路traceId、请求参数、应答数据、错误信息和调用时长。
*
* @author Jerry
* @date 2020-09-25
*/
@Aspect
@Component
@Order(1)
@Slf4j
public class AccessLogAspect {
/**
* 所有controller方法。
*/
@Pointcut("execution(public * com.orange.demo..controller..*(..))")
public void controllerPointCut() {
// 空注释避免sonar警告
}
@Around("controllerPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
// 请求流水号
String traceId = MyCommonUtil.generateUuid();
HttpServletResponse response =
((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getResponse();
response.setHeader(ApplicationConstant.HTTP_HEADER_TRACE_ID, traceId);
MDC.put(ApplicationConstant.HTTP_HEADER_TRACE_ID, traceId);
long start = System.currentTimeMillis();
// 获取方法参数
List<Object> httpReqArgs = new ArrayList<>();
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Object[] args = joinPoint.getArgs();
for (Object object : args) {
if (!(object instanceof HttpServletRequest)
&& !(object instanceof HttpServletResponse)
&& !(object instanceof MultipartFile)) {
httpReqArgs.add(object);
}
}
String url = request.getRequestURI();
String params = JSON.toJSONString(httpReqArgs);
log.info("开始请求traceId={}, url={}, reqData={}", traceId, url, params);
Object result = null;
try {
// 调用原来的方法
result = joinPoint.proceed();
} catch (Exception e) {
log.error("请求报错traceId={}, url={}, reqData={}, error={}", traceId, url, params, e.getMessage());
throw e;
} finally {
// 获取应答报文及接口处理耗时
String respData = result == null ? null : JSON.toJSONString(result);
log.info("请求完成, traceId={}, url={}elapse={}ms, respData={}",
traceId, url, (System.currentTimeMillis() - start), respData);
}
return result;
}
}

View File

@@ -0,0 +1,90 @@
package com.orange.demo.common.core.base.dao;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import tk.mybatis.mapper.additional.insert.InsertListMapper;
import tk.mybatis.mapper.annotation.RegisterMapper;
import tk.mybatis.mapper.common.Mapper;
import java.util.List;
import java.util.Map;
/**
* 数据访问对象的基类。
*
* @param <M> 主Model实体对象。
* @author Jerry
* @date 2020-09-25
*/
@RegisterMapper
public interface BaseDaoMapper<M> extends Mapper<M>, InsertListMapper<M> {
/**
* 根据指定的表名、显示字段列表、过滤条件字符串和分组字段,返回聚合计算后的查询结果。
*
* @param selectTable 表名称。
* @param selectFields 返回字段列表,逗号分隔。
* @param whereClause SQL常量形式的条件从句。
* @param groupBy 分组字段列表,逗号分隔。
* @return 对象可选字段Map列表。
*/
@Select("<script>"
+ "SELECT ${selectFields} FROM ${selectTable}"
+ "<where>"
+ " <if test=\"whereClause != null and whereClause != ''\">"
+ " AND ${whereClause}"
+ " </if>"
+ "</where>"
+ "<if test=\"groupBy != null and groupBy != ''\">"
+ " GROUP BY ${groupBy}"
+ "</if>"
+ "</script>")
List<Map<String, Object>> getGroupedListByCondition(
@Param("selectTable") String selectTable,
@Param("selectFields") String selectFields,
@Param("whereClause") String whereClause,
@Param("groupBy") String groupBy);
/**
* 根据指定的表名、显示字段列表、过滤条件字符串和排序字符串,返回查询结果。
*
* @param selectTable 表名称。
* @param selectFields 选择的字段列表。
* @param whereClause 过滤字符串。
* @param orderBy 排序字符串。
* @return 查询结果。
*/
@Select("<script>"
+ "SELECT ${selectFields} FROM ${selectTable}"
+ "<where>"
+ " <if test=\"whereClause != null and whereClause != ''\">"
+ " AND ${whereClause}"
+ " </if>"
+ "</where>"
+ "<if test=\"orderBy != null and orderBy != ''\">"
+ " ORDER BY ${orderBy}"
+ "</if>"
+ "</script>")
List<Map<String, Object>> getListByCondition(
@Param("selectTable") String selectTable,
@Param("selectFields") String selectFields,
@Param("whereClause") String whereClause,
@Param("orderBy") String orderBy);
/**
* 用指定过滤条件,计算记录数量。
*
* @param selectTable 表名称。
* @param whereClause 过滤字符串。
* @return 返回过滤后的数据数量。
*/
@Select("<script>"
+ "SELECT COUNT(1) FROM ${selectTable}"
+ "<where>"
+ " <if test=\"whereClause != null and whereClause != ''\">"
+ " AND ${whereClause}"
+ " </if>"
+ "</where>"
+ "</script>")
int getCountByCondition(@Param("selectTable") String selectTable, @Param("whereClause") String whereClause);
}

View File

@@ -0,0 +1,124 @@
package com.orange.demo.common.core.base.mapper;
import cn.hutool.core.bean.BeanUtil;
import org.apache.commons.collections4.CollectionUtils;
import java.util.*;
import java.util.stream.Collectors;
/**
* Model对象到Domain类型对象的相互转换。实现类通常声明在Model实体类中。
*
* @param <D> Domain域对象类型。
* @param <M> Model实体对象类型。
* @author Jerry
* @date 2020-09-25
*/
public interface BaseModelMapper<D, M> {
/**
* 转换Model实体对象到Domain域对象。
*
* @param model Model实体对象。
* @return Domain域对象。
*/
D fromModel(M model);
/**
* 转换Model实体对象列表到Domain域对象列表。
*
* @param modelList Model实体对象列表。
* @return Domain域对象列表。
*/
List<D> fromModelList(List<M> modelList);
/**
* 转换Domain域对象到Model实体对象。
*
* @param domain Domain域对象。
* @return Model实体对象。
*/
M toModel(D domain);
/**
* 转换Domain域对象列表到Model实体对象列表。
*
* @param domainList Domain域对象列表。
* @return Model实体对象列表。
*/
List<M> toModelList(List<D> domainList);
/**
* 转换bean到map
*
* @param bean bean对象。
* @param ignoreNullValue 值为null的字段是否转换到Map。
* @param <T> bean类型。
* @return 转换后的map对象。
*/
default <T> Map<String, Object> beanToMap(T bean, boolean ignoreNullValue) {
return BeanUtil.beanToMap(bean, false, ignoreNullValue);
}
/**
* 转换bean集合到map集合
*
* @param dataList bean对象集合。
* @param ignoreNullValue 值为null的字段是否转换到Map。
* @param <T> bean类型。
* @return 转换后的map对象集合。
*/
default <T> List<Map<String, Object>> beanToMap(List<T> dataList, boolean ignoreNullValue) {
if (CollectionUtils.isEmpty(dataList)) {
return new LinkedList<>();
}
return dataList.stream()
.map(o -> BeanUtil.beanToMap(o, false, ignoreNullValue))
.collect(Collectors.toList());
}
/**
* 转换map到bean。
*
* @param map map对象。
* @param beanClazz bean的Class对象。
* @param <T> bean类型。
* @return 转换后的bean对象。
*/
default <T> T mapToBean(Map<String, Object> map, Class<T> beanClazz) {
return BeanUtil.mapToBean(map, beanClazz, true);
}
/**
* 转换map集合到bean集合。
*
* @param mapList map对象集合。
* @param beanClazz bean的Class对象。
* @param <T> bean类型。
* @return 转换后的bean对象集合。
*/
default <T> List<T> mapToBean(List<Map<String, Object>> mapList, Class<T> beanClazz) {
if (CollectionUtils.isEmpty(mapList)) {
return new LinkedList<>();
}
return mapList.stream()
.map(m -> BeanUtil.mapToBean(m, beanClazz, true))
.collect(Collectors.toList());
}
/**
* 对于Map字段到Map字段的映射场景MapStruct会根据方法签名自动选择该函数
* 作为对象copy的函数。由于该函数是直接返回的因此没有对象copy效率更高。
* 如果没有该函数MapStruct会生成如下代码
* Map<String, Object> map = courseDto.getTeacherIdDictMap();
* if ( map != null ) {
* course.setTeacherIdDictMap( new HashMap<String, Object>( map ) );
* }
*
* @param map map对象。
* @return 直接返回的map。
*/
default Map<String, Object> mapToMap(Map<String, Object> map) {
return map;
}
}

View File

@@ -0,0 +1,58 @@
package com.orange.demo.common.core.base.mapper;
import java.util.List;
/**
* 哑元占位对象。Model实体对象和Domain域对象相同的场景下使用。
* 由于没有实际的数据转换,因此同时保证了代码统一和执行效率。
*
* @param <M> 数据类型。
* @author Jerry
* @date 2020-09-25
*/
public class DummyModelMapper<M> implements BaseModelMapper<M, M> {
/**
* 不转换直接返回。
*
* @param model Model实体对象。
* @return Domain域对象。
*/
@Override
public M fromModel(M model) {
return model;
}
/**
* 不转换直接返回。
*
* @param modelList Model实体对象列表。
* @return Domain域对象列表。
*/
@Override
public List<M> fromModelList(List<M> modelList) {
return modelList;
}
/**
* 不转换直接返回。
*
* @param domain Domain域对象。
* @return Model实体对象。
*/
@Override
public M toModel(M domain) {
return domain;
}
/**
* 不转换直接返回。
*
* @param domainList Domain域对象列表。
* @return Model实体对象列表。
*/
@Override
public List<M> toModelList(List<M> domainList) {
return domainList;
}
}

View File

@@ -0,0 +1,149 @@
package com.orange.demo.common.core.base.service;
import com.orange.demo.common.core.cache.DictionaryCache;
import com.orange.demo.common.core.constant.GlobalDeletedFlag;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import tk.mybatis.mapper.entity.Example;
import java.util.Set;
import java.util.List;
/**
* 带有缓存功能的字典Service基类需要留意的是由于缓存基于Key/Value方式存储
* 目前仅支持基于主键字段的缓存查找,其他条件的查找仍然从数据源获取。
*
* @param <M> Model实体对象的类型。
* @param <K> Model对象主键的类型。
* @author Jerry
* @date 2020-09-25
*/
@Slf4j
public abstract class BaseDictService<M, K> extends BaseService<M, K> {
/**
* 缓存池对象。
*/
protected DictionaryCache<K, M> dictionaryCache;
/**
* 构造函数使用缺省缓存池对象。
*/
public BaseDictService() {
super();
}
/**
* 是否在服务启动的时候加载。子类可以重载该方法并在需要的时候手工调用loadCachedData加载数据。
*
* @return true表示启动即可加载数据false需要手动调用loadCachedData进行加载。
*/
public boolean loadOnStartup() {
return true;
}
/**
* 在系统启动时加载全部数据到内存缓存的key只能为映射表的主键。
*/
public void loadCachedData() {
if (loadOnStartup()) {
reloadCachedData(false);
}
}
/**
* 重新加载数据库中所有当前表数据到系统内存。
*
* @param force true则强制刷新如果false当缓存中存在数据时不刷新。
*/
public void reloadCachedData(boolean force) {
// 在非强制刷新情况下。
// 先行判断缓存中是否存在数据,如果有就不加载了。
if (!force && dictionaryCache.getCount() > 0) {
return;
}
List<M> allList = super.getAllList();
dictionaryCache.reload(allList, force);
}
/**
* 直接从缓存池中获取主键Id关联的数据。
*
* @param id 主键Id。
* @return 主键关联的数据不存在返回null。
*/
@Override
public M getById(K id) {
return dictionaryCache.get(id);
}
/**
* 直接从缓存池中获取所有数据。
*
* @return 返回所有数据。
*/
@Override
public List<M> getAllList() {
return dictionaryCache.getAll();
}
/**
* 直接从缓存池中返回符合主键 in (idValues) 条件的所有数据。
*
* @param idValues 主键值列表。
* @return 检索后的数据列表。
*/
@Override
public List<M> getInList(Set<K> idValues) {
return dictionaryCache.getInList(idValues);
}
/**
* 返回符合 inFilterField in (inFilterValues) 条件的所有数据。蜀国property是主键则从缓存中读取。
*
* @param inFilterField 参与(In-list)过滤的Java字段。
* @param inFilterValues 参与(In-list)过滤的Java字段值集合。
* @return 检索后的数据列表。
*/
@Override
@SuppressWarnings("unchecked")
public <T> List<M> getInList(String inFilterField, Set<T> inFilterValues) {
if (inFilterField.equals(this.idFieldName)) {
return this.getInList((Set<K>) inFilterValues);
}
return this.getInList(inFilterField, inFilterValues, (String) null);
}
/**
* 判断参数值列表中的所有数据是否全部存在。另外keyName字段在数据表中必须是唯一键值否则返回结果会出现误判。
*
* @param inFilterField 待校验的数据字段这里使用Java对象中的属性如courseId而不是数据字段名course_id。
* @param inFilterValues 数据值集合。
* @return 全部存在返回true否则false。
*/
@Override
@SuppressWarnings("unchecked")
public <T> boolean existUniqueKeyList(String inFilterField, Set<T> inFilterValues) {
if (CollectionUtils.isEmpty(inFilterValues)) {
return false;
}
if (inFilterField.equals(this.idFieldName)) {
List<M> dataList = dictionaryCache.getInList((Set<K>) inFilterValues);
return dataList.size() == inFilterValues.size();
}
Example e = this.makeDefaultInListExample(inFilterField, inFilterValues, null);
if (deletedFlagFieldName != null) {
e.and().andEqualTo(deletedFlagFieldName, GlobalDeletedFlag.NORMAL);
}
return mapper().selectCountByExample(e) == inFilterValues.size();
}
/**
* 获取缓存中的数据数量。
*
* @return 缓存中的数据总量。
*/
public int getCachedCount() {
return dictionaryCache.getCount();
}
}

View File

@@ -0,0 +1,90 @@
package com.orange.demo.common.core.cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* 使用Caffeine作为本地缓存库
*
* @author Jerry
* @date 2020-09-25
*/
@Configuration
@EnableCaching
public class CacheConfig {
private static final int DEFAULT_MAXSIZE = 10000;
private static final int DEFAULT_TTL = 3600;
/**
* 定义cache名称、超时时长秒、最大个数
* 每个cache缺省3600秒过期最大个数1000
*/
public enum CacheEnum {
/**
* 专门存储用户权限的缓存。
*/
USER_PERMISSION_CACHE(1800, 10000),
/**
* session下上传文件名的缓存(时间是24小时)。
*/
UPLOAD_FILENAME_CACHE(86400, 20000),
/**
* 缺省全局缓存(时间是24小时)。
*/
GLOBAL_CACHE(86400, 20000);
CacheEnum() {
}
CacheEnum(int ttl, int maxSize) {
this.ttl = ttl;
this.maxSize = maxSize;
}
/**
* 缓存的最大数量。
*/
private int maxSize = DEFAULT_MAXSIZE;
/**
* 缓存的时长(单位:秒)
*/
private int ttl = DEFAULT_TTL;
public int getMaxSize() {
return maxSize;
}
public int getTtl() {
return ttl;
}
}
/**
* 初始化缓存配置。
*/
@Bean
public CacheManager cacheManager() {
SimpleCacheManager manager = new SimpleCacheManager();
//把各个cache注册到cacheManager中CaffeineCache实现了org.springframework.cache.Cache接口
ArrayList<CaffeineCache> caches = new ArrayList<>();
for (CacheEnum c : CacheEnum.values()) {
caches.add(new CaffeineCache(c.name(),
Caffeine.newBuilder().recordStats()
.expireAfterAccess(c.getTtl(), TimeUnit.SECONDS)
.maximumSize(c.getMaxSize())
.build())
);
}
manager.setCaches(caches);
return manager;
}
}

View File

@@ -0,0 +1,88 @@
package com.orange.demo.common.core.cache;
import java.util.List;
import java.util.Set;
/**
* 主要用于完整缓存字典表数据的接口对象。
*
* @param <K> 字典表主键类型。
* @param <V> 字典表对象类型。
* @author Jerry
* @date 2020-09-25
*/
public interface DictionaryCache<K, V> {
/**
* 按照数据插入的顺序返回全部字典对象的列表。
*
* @return 全部字段数据列表。
*/
List<V> getAll();
/**
* 获取缓存中与键列表对应的对象列表。
*
* @param keys 主键集合。
* @return 对象列表。
*/
List<V> getInList(Set<K> keys);
/**
* 将参数List中的数据保存到缓存中同时保证getAll返回的数据列表与参数列表中数据项的顺序保持一致。
*
* @param dataList 待缓存的数据列表。
*/
void putAll(List<V> dataList);
/**
* 重新加载先清空原有数据在执行putAll的操作。
*
* @param dataList 待缓存的数据列表。
* @param force true则强制刷新如果false当缓存中存在数据时不刷新。
*/
void reload(List<V> dataList, boolean force);
/**
* 从缓存中获取指定的数据。
*
* @param key 数据的key。
* @return 获取到的数据如果没有返回null。
*/
V get(K key);
/**
* 将数据存入缓存。
*
* @param key 通常为字典数据的主键。
* @param object 字典数据对象。
*/
void put(K key, V object);
/**
* 获取缓存中数据条目的数量。
*
* @return 返回缓存的数据数量。
*/
int getCount();
/**
* 删除缓存中指定的键。
*
* @param key 待删除数据的主键。
* @return 返回被删除的对象如果主键不存在返回null。
*/
V invalidate(K key);
/**
* 删除缓存中,参数列表中包含的键。
*
* @param keys 待删除数据的主键集合。
*/
void invalidateSet(Set<K> keys);
/**
* 清空缓存。
*/
void invalidateAll();
}

View File

@@ -0,0 +1,176 @@
package com.orange.demo.common.core.cache;
import java.util.*;
import java.util.function.Function;
/**
* 字典数据内存缓存对象。
*
* @param <K> 字典表主键类型。
* @param <V> 字典表对象类型。
* @author Jerry
* @date 2020-09-25
*/
public class MapDictionaryCache<K, V> implements DictionaryCache<K, V> {
/**
* 存储字典数据的Map。
*/
protected LinkedHashMap<K, V> dataMap = new LinkedHashMap<>();
/**
* 获取字典主键数据的函数对象。
*/
protected Function<V, K> idGetter;
/**
* 当前对象的构造器函数。
*
* @param idGetter 获取当前类主键字段值的函数对象。
* @param <K> 字典主键类型。
* @param <V> 字典对象类型
* @return 实例化后的字典内存缓存对象。
*/
public static <K, V> MapDictionaryCache<K, V> create(Function<V, K> idGetter) {
if (idGetter == null) {
throw new IllegalArgumentException("IdGetter can't be NULL.");
}
return new MapDictionaryCache<>(idGetter);
}
/**
* 构造函数。
*
* @param idGetter 主键Id的获取函数对象。
*/
public MapDictionaryCache(Function<V, K> idGetter) {
this.idGetter = idGetter;
}
/**
* 按照数据插入的顺序返回全部字典对象的列表。
*
* @return 全部字段数据列表。
*/
@Override
public synchronized List<V> getAll() {
List<V> resultList = new LinkedList<>();
for (Map.Entry<K, V> entry : dataMap.entrySet()) {
resultList.add(entry.getValue());
}
return resultList;
}
/**
* 获取缓存中与键列表对应的对象列表。
*
* @param keys 主键集合。
* @return 对象列表。
*/
@Override
public synchronized List<V> getInList(Set<K> keys) {
List<V> resultList = new LinkedList<>();
keys.forEach(key -> {
V object = dataMap.get(key);
if (object != null) {
resultList.add(object);
}
});
return resultList;
}
/**
* 将参数List中的数据保存到缓存中同时保证getAll返回的数据列表与参数列表中数据项的顺序保持一致。
*
* @param dataList 待缓存的数据列表。
*/
@Override
public synchronized void putAll(List<V> dataList) {
if (dataList == null) {
return;
}
dataList.forEach(dataObj -> {
K id = idGetter.apply(dataObj);
dataMap.put(id, dataObj);
});
}
/**
* 重新加载先清空原有数据在执行putAll的操作。
*
* @param dataList 待缓存的数据列表。
* @param force true则强制刷新如果false当缓存中存在数据时不刷新。
*/
@Override
public synchronized void reload(List<V> dataList, boolean force) {
if (!force && this.getCount() > 0) {
return;
}
this.invalidateAll();
this.putAll(dataList);
}
/**
* 从缓存中获取指定的数据。
*
* @param id 数据的key。
* @return 获取到的数据如果没有返回null。
*/
@Override
public synchronized V get(K id) {
return id == null ? null : dataMap.get(id);
}
/**
* 将数据存入缓存。
*
* @param id 通常为字典数据的主键。
* @param object 字典数据对象。
*/
@Override
public synchronized void put(K id, V object) {
dataMap.put(id, object);
}
/**
* 获取缓存中数据条目的数量。
*
* @return 返回缓存的数据数量。
*/
@Override
public synchronized int getCount() {
return dataMap.size();
}
/**
* 删除缓存中指定的键。
*
* @param id 待删除数据的主键。
* @return 返回被删除的对象如果主键不存在返回null。
*/
@Override
public synchronized V invalidate(K id) {
return id == null ? null : dataMap.remove(id);
}
/**
* 删除缓存中,参数列表中包含的键。
*
* @param keys 待删除数据的主键集合。
*/
@Override
public synchronized void invalidateSet(Set<K> keys) {
keys.forEach(id -> {
if (id != null) {
dataMap.remove(id);
}
});
}
/**
* 清空缓存。
*/
@Override
public synchronized void invalidateAll() {
dataMap.clear();
}
}

View File

@@ -0,0 +1,142 @@
package com.orange.demo.common.core.cache;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import java.util.*;
import java.util.function.Function;
/**
* 树形字典数据内存缓存对象。
*
* @param <K> 字典表主键类型。
* @param <V> 字典表对象类型。
* @author Jerry
* @date 2020-09-25
*/
public class MapTreeDictionaryCache<K, V> extends MapDictionaryCache<K, V> {
/**
* 树形数据存储对象。
*/
private final Multimap<K, V> allTreeMap = LinkedHashMultimap.create();
/**
* 获取字典父主键数据的函数对象。
*/
protected Function<V, K> parentIdGetter;
/**
* 当前对象的构造器函数。
*
* @param idGetter 获取当前类主键字段值的函数对象。
* @param parentIdGetter 获取当前类父主键字段值的函数对象。
* @param <K> 字典主键类型。
* @param <V> 字典对象类型
* @return 实例化后的树形字典内存缓存对象。
*/
public static <K, V> MapTreeDictionaryCache<K, V> create(Function<V, K> idGetter, Function<V, K> parentIdGetter) {
if (idGetter == null) {
throw new IllegalArgumentException("IdGetter can't be NULL.");
}
if (parentIdGetter == null) {
throw new IllegalArgumentException("ParentIdGetter can't be NULL.");
}
return new MapTreeDictionaryCache<>(idGetter, parentIdGetter);
}
/**
* 构造函数。
*
* @param idGetter 获取当前类主键字段值的函数对象。
* @param parentIdGetter 获取当前类父主键字段值的函数对象。
*/
public MapTreeDictionaryCache(Function<V, K> idGetter, Function<V, K> parentIdGetter) {
super(idGetter);
this.parentIdGetter = parentIdGetter;
}
/**
* 获取该父主键的子数据列表。
*
* @param parentId 父主键Id。
* @return 子数据列表。
*/
public synchronized List<V> getListByParentId(K parentId) {
return new LinkedList<>(allTreeMap.get(parentId));
}
/**
* 将参数List中的数据保存到缓存中同时保证getAll返回的数据列表与参数列表中数据项的顺序保持一致。
*
* @param dataList 待缓存的数据列表。
*/
@Override
public synchronized void putAll(List<V> dataList) {
if (dataList == null) {
return;
}
super.putAll(dataList);
dataList.forEach(data -> {
K parentId = parentIdGetter.apply(data);
allTreeMap.remove(parentId, data);
allTreeMap.put(parentId, data);
});
}
/**
* 将数据存入缓存。
*
* @param id 通常为字典数据的主键。
* @param data 字典数据对象。
*/
@Override
public synchronized void put(K id, V data) {
super.put(id, data);
K parentId = parentIdGetter.apply(data);
allTreeMap.remove(parentId, data);
allTreeMap.put(parentId, data);
}
/**
* 删除缓存中指定的键。
*
* @param id 待删除数据的主键。
* @return 返回被删除的对象如果主键不存在返回null。
*/
@Override
public synchronized V invalidate(K id) {
V v = super.invalidate(id);
if (v != null) {
K parentId = parentIdGetter.apply(v);
allTreeMap.remove(parentId, v);
}
return v;
}
/**
* 删除缓存中,参数列表中包含的键。
*
* @param keys 待删除数据的主键集合。
*/
@Override
public synchronized void invalidateSet(Set<K> keys) {
keys.forEach(id -> {
if (id != null) {
V data = dataMap.remove(id);
if (data != null) {
K parentId = parentIdGetter.apply(data);
allTreeMap.remove(parentId, data);
}
}
});
}
/**
* 清空缓存。
*/
@Override
public synchronized void invalidateAll() {
super.invalidateAll();
allTreeMap.clear();
}
}

View File

@@ -0,0 +1,97 @@
package com.orange.demo.common.core.cache;
import com.orange.demo.common.core.object.TokenData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.Set;
/**
* Session数据缓存辅助类。
*
* @author Jerry
* @date 2020-09-25
*/
@SuppressWarnings("unchecked")
@Component
public class SessionCacheHelper {
@Autowired
private CacheManager cacheManager;
/**
* 缓存当前session内上传过的文件名。
*
* @param filename 通常是本地存储的文件名,而不是上传时的原始文件名。
*/
public void putSessionUploadFile(String filename) {
if (filename != null) {
Set<String> sessionUploadFileSet = null;
Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name());
Cache.ValueWrapper valueWrapper = cache.get(TokenData.takeFromRequest().getSessionId());
if (valueWrapper != null) {
sessionUploadFileSet = (Set<String>) valueWrapper.get();
}
if (sessionUploadFileSet == null) {
sessionUploadFileSet = new HashSet<>();
}
sessionUploadFileSet.add(filename);
cache.put(TokenData.takeFromRequest().getSessionId(), sessionUploadFileSet);
}
}
/**
* 判断参数中的文件名是否有当前session上传。
*
* @param filename 通常是本地存储的文件名,而不是上传时的原始文件名。
* @return true表示该文件是由当前session上传并存储在本地的否则false。
*/
public boolean existSessionUploadFile(String filename) {
if (filename == null) {
return false;
}
Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name());
Cache.ValueWrapper valueWrapper = cache.get(TokenData.takeFromRequest().getSessionId());
if (valueWrapper == null) {
return false;
}
return ((Set<String>) valueWrapper.get()).contains(filename);
}
/**
* 存放session的Token数据。
*
* @param sessionId 当前会话的SessionId。
* @param tokenData 当前会话的JWT Token对象。
*/
public void putTokenData(String sessionId, TokenData tokenData) {
if (sessionId == null || tokenData == null) {
return;
}
Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.GLOBAL_CACHE.name());
cache.put(sessionId, tokenData);
}
/**
* 获取session的JWT Token对象。
*
* @param sessionId 当前会话的SessionId。
* @return 当前会话的JWT Token对象。
*/
public TokenData getTokenData(String sessionId) {
Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.GLOBAL_CACHE.name());
return cache.get(sessionId, TokenData.class);
}
/**
* 清除当前session的所有缓存数据。
*/
public void removeAllSessionCache() {
for (CacheConfig.CacheEnum c : CacheConfig.CacheEnum.values()) {
cacheManager.getCache(c.name()).clear();
}
}
}

View File

@@ -0,0 +1,67 @@
package com.orange.demo.common.core.config;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import com.orange.demo.common.core.interceptor.MyRequestArgumentResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* 所有的项目拦截器、参数解析器、消息对象转换器都在这里集中配置。
*
* @author Jerry
* @date 2020-09-25
*/
@Configuration
public class CommonWebMvcConfig implements WebMvcConfigurer {
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
// 添加MyRequestBody参数解析器
argumentResolvers.add(new MyRequestArgumentResolver());
}
@Bean
public HttpMessageConverter<String> responseBodyConverter() {
return new StringHttpMessageConverter(StandardCharsets.UTF_8);
}
@Bean
public FastJsonHttpMessageConverter fastJsonHttpMessageConverters() {
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.add(MediaType.APPLICATION_JSON);
supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
fastConverter.setSupportedMediaTypes(supportedMediaTypes);
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(
SerializerFeature.PrettyFormat,
SerializerFeature.DisableCircularReferenceDetect,
SerializerFeature.IgnoreNonFieldGetter);
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
fastConverter.setFastJsonConfig(fastJsonConfig);
return fastConverter;
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(responseBodyConverter());
converters.add(fastJsonHttpMessageConverters());
}
}

View File

@@ -0,0 +1,20 @@
package com.orange.demo.common.core.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* 目前用于用户密码加密UAA接入应用客户端的client_secret加密。
*
* @author Jerry
* @date 2020-09-25
*/
@Configuration
public class EncryptConfig {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,64 @@
package com.orange.demo.common.core.config;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* RestTemplate连接池配置对象。
*
* @author Jerry
* @date 2020-09-25
*/
@Configuration
public class RestTemplateConfig {
private static final int MAX_TOTAL_CONNECTION = 50;
private static final int MAX_CONNECTION_PER_ROUTE = 20;
private static final int CONNECTION_TIMEOUT = 20000;
private static final int READ_TIMEOUT = 30000;
@Bean
@ConditionalOnMissingBean({RestOperations.class, RestTemplate.class})
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate(createFactory());
List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
messageConverters.removeIf(
c -> c instanceof StringHttpMessageConverter || c instanceof MappingJackson2HttpMessageConverter);
messageConverters.add(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
messageConverters.add(new FastJsonHttpMessageConverter());
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
// 防止400+和500等错误被直接抛出异常这里避开了缺省处理方式所有的错误均交给业务代码处理。
}
});
return restTemplate;
}
private ClientHttpRequestFactory createFactory() {
HttpClient httpClient = HttpClientBuilder.create()
.setMaxConnTotal(MAX_TOTAL_CONNECTION)
.setMaxConnPerRoute(MAX_CONNECTION_PER_ROUTE)
.build();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
factory.setReadTimeout(READ_TIMEOUT);
factory.setConnectTimeout(CONNECTION_TIMEOUT);
return factory;
}
}

View File

@@ -0,0 +1,39 @@
package com.orange.demo.common.core.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 Jerry
* @date 2020-09-25
*/
@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,81 @@
package com.orange.demo.common.core.constant;
import java.util.HashMap;
import java.util.Map;
/**
* 聚合计算的常量类型对象。
*
* @author Jerry
* @date 2020-09-25
*/
public final class AggregationType {
/**
* sum 计数
*/
public static final int SUM = 0;
/**
* count 汇总
*/
public static final int COUNT = 1;
/**
* average 平均值
*/
public static final int AVG = 2;
/**
* min 最小值
*/
public static final int MIN = 3;
/**
* max 最大值
*/
public static final int MAX = 4;
private static final Map<Object, String> DICT_MAP = new HashMap<>(5);
static {
DICT_MAP.put(0, "累计总和");
DICT_MAP.put(1, "数量总和");
DICT_MAP.put(2, "平均值");
DICT_MAP.put(3, "最小值");
DICT_MAP.put(4, "最大值");
}
/**
* 判断参数是否为当前常量字典的合法值。
*
* @param value 待验证的参数值。
* @return 合法返回true否则false。
*/
public static boolean isValid(Integer value) {
return value != null && DICT_MAP.containsKey(value);
}
/**
* 获取与SQL对应的聚合函数字符串名称。
*
* @return 聚合函数名称。
*/
public static String getAggregationFunction(Integer aggregationType) {
switch (aggregationType) {
case COUNT:
return "COUNT";
case AVG:
return "AVG";
case SUM:
return "SUM";
case MAX:
return "MAX";
case MIN:
return "MIN";
default:
throw new IllegalArgumentException("无效的聚合类型!");
}
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private AggregationType() {
}
}

View File

@@ -0,0 +1,55 @@
package com.orange.demo.common.core.constant;
/**
* 应用程序的常量声明对象。
*
* @author Jerry
* @date 2020-09-25
*/
public final class ApplicationConstant {
/**
* 图片文件上传的父目录。
*/
public static final String UPLOAD_IMAGE_PARENT_PATH = "image";
/**
* 附件文件上传的父目录。
*/
public static final String UPLOAD_ATTACHMENT_PARENT_PATH = "attachment";
/**
* CSV文件扩展名。
*/
public static final String CSV_EXT = "csv";
/**
* XLSX文件扩展名。
*/
public static final String XLSX_EXT = "xlsx";
/**
* 统计分类计算时,按天聚合计算的常量值。(前端在MyOrderParam和MyGroupParam中传给后台)
*/
public static final String DAY_AGGREGATION = "day";
/**
* 统计分类计算时,按月聚合计算的常量值。(前端在MyOrderParam和MyGroupParam中传给后台)
*/
public static final String MONTH_AGGREGATION = "month";
/**
* 统计分类计算时,按年聚合计算的常量值。(前端在MyOrderParam和MyGroupParam中传给后台)
*/
public static final String YEAR_AGGREGATION = "year";
/**
* 请求头跟踪id名。
*/
public static final String HTTP_HEADER_TRACE_ID = "traceId";
/**
* 重要说明:该值为项目生成后的缺省密钥,仅为使用户可以快速上手并跑通流程。
* 在实际的应用中,一定要为不同的项目或服务,自行生成公钥和私钥,并将 PRIVATE_KEY 的引用改为服务的配置项。
* 密钥的生成方式可通过执行common.core.util.RsaUtil类的main函数动态生成。
*/
public static final String PRIVATE_KEY =
"MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKkLhAydtOtA4WuIkkIIUVaGWu4ElOEAQF9GTulHHWOwCHI1UvcKolvS1G+mdsKcmGtEAQ92AUde/kDRGu8Wn7kLDtCgUfo72soHz7Qfv5pVB4ohMxQd/9cxeKjKbDoirhB9Z3xGF20zUozp4ZPLxpTtI7azr0xzUtd5+D/HfLDrAgMBAAECgYEApESZhDz4YyeAJiPnpJ06lS8oS2VOWzsIUs0av5uoloeoHXtt7Lx7u2kroHeNrl3Hy2yg7ypH4dgQkGHin3VHrVAgjG3TxhgBXIqqntzzk2AGJKBeIIkRX86uTvtKZyp3flUgcwcGmpepAHS1V1DPY3aVYvbcqAmoL6DX6VYN0NECQQDQUitMdC76lEtAr5/ywS0nrZJDo6U7eQ7ywx/eiJ+YmrSye8oorlAj1VBWG+Cl6jdHOHtTQyYv/tu71fjzQiJTAkEAz7wb47/vcSUpNWQxItFpXz0o6rbJh71xmShn1AKP7XptOVZGlW9QRYEzHabV9m/DHqI00cMGhHrWZAhCiTkUCQJAFsJjaJ7o4weAkTieyO7B+CvGZw1h5/V55Jvcx3s1tH5yb22G0Jr6tm9/r2isSnQkReutzZLwgR3e886UvD7lcQJAAUcD2OOuQkDbPwPNtYwaHMbQgJj9JkOI9kskUE5vuiMdltOr/XFAyhygRtdmy2wmhAK1VnDfkmL6/IR8fEGImQJABOB0KCalb0M8CPnqqHzozrD8gPObnIIr4aVvLIPATN2g7MM2N6F7JbI4RZFiKa92LV6bhQCY8OvHi5K2cgFpbw==";
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private ApplicationConstant() {
}
}

View File

@@ -0,0 +1,78 @@
package com.orange.demo.common.core.constant;
/**
* 返回应答中的错误代码和错误信息。
*
* @author Jerry
* @date 2020-09-25
*/
public enum ErrorCodeEnum {
/**
* 没有错误
*/
NO_ERROR("没有错误"),
/**
* 未处理的异常!
*/
UNHANDLED_EXCEPTION("未处理的异常!"),
ARGUMENT_NULL_EXIST("数据验证失败,接口调用参数存在空值,请核对!"),
ARGUMENT_PK_ID_NULL("数据验证失败接口调用主键Id参数为空请核对"),
INVALID_ARGUMENT_FORMAT("数据验证失败,不合法的参数格式,请核对!"),
INVALID_STATUS_ARGUMENT("数据验证失败,无效的状态参数值,请核对!"),
INVALID_UPLOAD_FILE_ARGUMENT("数据验证失败,上传文件参数错误,请核对!"),
INVALID_UPLOAD_FILE_IOERROR("上传文件写入失败,请联系管理员!"),
UNAUTHORIZED_LOGIN("当前用户尚未登录或登录已超时,请重新登录!"),
UNAUTHORIZED_USER_PERMISSION("权限验证失败,当前用户不能访问该接口,请核对!"),
NO_ACCESS_PERMISSION("当前用户没有访问权限,请核对!"),
NO_OPERATION_PERMISSION("当前用户没有操作权限,请核对!"),
PASSWORD_ERR("密码错误,请重试!"),
INVALID_USERNAME_PASSWORD("用户名或密码错误,请重试!"),
INVALID_ACCESS_TOKEN("无效的用户访问令牌!"),
INVALID_USER_STATUS("用户状态错误,请刷新后重试!"),
HAS_CHILDREN_DATA("数据验证失败,子数据存在,请刷新后重试!"),
DATA_VALIDATAED_FAILED("数据验证失败,请核对!"),
UPLOAD_FILE_FAILED("文件上传失败,请联系管理员!"),
DATA_SAVE_FAILED("数据保存失败,请联系管理员!"),
DATA_ACCESS_FAILED("数据访问失败,请联系管理员!"),
DATA_PERM_ACCESS_FAILED("数据访问失败,您没有该页面的数据访问权限!"),
DUPLICATED_UNIQUE_KEY("数据保存失败,存在重复数据,请核对!"),
DATA_NOT_EXIST("数据不存在,请刷新后重试!"),
DATA_PARENT_LEVEL_ID_NOT_EXIST("数据验证失败父级别关联Id不存在请刷新后重试"),
DATA_PARENT_ID_NOT_EXIST("数据验证失败ParentId不存在请核对"),
INVALID_RELATED_RECORD_ID("数据验证失败,关联数据并不存在,请刷新后重试!"),
INVALID_DATA_MODEL("数据验证失败,无效的数据实体对象!"),
INVALID_DATA_FIELD("数据验证失败,无效的数据实体对象字段!"),
INVALID_CLASS_FIELD("数据验证失败,无效的类对象字段!"),
SERVER_INTERNAL_ERROR("服务器内部错误,请联系管理员!"),
REDIS_CACHE_ACCESS_TIMEOUT("Redis缓存数据访问超时请刷新后重试"),
REDIS_CACHE_ACCESS_STATE_ERROR("Redis缓存数据访问状态错误请刷新后重试");
// 下面的枚举值为特定枚举值,即开发者可以根据自己的项目需求定义更多的非通用枚举值
/**
* 构造函数。
*
* @param errorMessage 错误消息。
*/
ErrorCodeEnum(String errorMessage) {
this.errorMessage = errorMessage;
}
/**
* 错误信息。
*/
private String errorMessage;
/**
* 获取错误信息。
*
* @return 错误信息。
*/
public String getErrorMessage() {
return errorMessage;
}
}

View File

@@ -0,0 +1,25 @@
package com.orange.demo.common.core.constant;
/**
* 数据记录逻辑删除标记常量。
*
* @author Jerry
* @date 2020-09-25
*/
public final class GlobalDeletedFlag {
/**
* 表示数据表记录已经删除
*/
public static final int DELETED = -1;
/**
* 数据记录正常
*/
public static final int NORMAL = 1;
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private GlobalDeletedFlag() {
}
}

View File

@@ -0,0 +1,26 @@
package com.orange.demo.common.core.exception;
/**
* 数据验证失败的自定义异常。
*
* @author Jerry
* @date 2020-09-25
*/
public class DataValidationException extends RuntimeException {
/**
* 构造函数。
*/
public DataValidationException() {
}
/**
* 构造函数。
*
* @param msg 错误信息。
*/
public DataValidationException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,30 @@
package com.orange.demo.common.core.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 无效的类对象字段的自定义异常。
*
* @author Jerry
* @date 2020-09-25
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class InvalidClassFieldException extends RuntimeException {
private final String className;
private final String fieldName;
/**
* 构造函数。
*
* @param className 对象名。
* @param fieldName 字段名。
*/
public InvalidClassFieldException(String className, String fieldName) {
super("Invalid FieldName [" + fieldName + "] in Class [" + className + "].");
this.className = className;
this.fieldName = fieldName;
}
}

View File

@@ -0,0 +1,30 @@
package com.orange.demo.common.core.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 无效的实体对象字段的自定义异常。
*
* @author Jerry
* @date 2020-09-25
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class InvalidDataFieldException extends RuntimeException {
private final String modelName;
private final String fieldName;
/**
* 构造函数。
*
* @param modelName 实体对象名。
* @param fieldName 字段名。
*/
public InvalidDataFieldException(String modelName, String fieldName) {
super("Invalid FieldName [" + fieldName + "] in Model Class [" + modelName + "].");
this.modelName = modelName;
this.fieldName = fieldName;
}
}

View File

@@ -0,0 +1,27 @@
package com.orange.demo.common.core.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 无效的实体对象的自定义异常。
*
* @author Jerry
* @date 2020-09-25
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class InvalidDataModelException extends RuntimeException {
private final String modelName;
/**
* 构造函数。
*
* @param modelName 实体对象名。
*/
public InvalidDataModelException(String modelName) {
super("Invalid Model Class [" + modelName + "].");
this.modelName = modelName;
}
}

View File

@@ -0,0 +1,36 @@
package com.orange.demo.common.core.exception;
/**
* 自定义的运行时异常,在需要抛出运行时异常时,可使用该异常。
* NOTE主要是为了避免SonarQube进行代码质量扫描时给出警告。
*
* @author Jerry
* @date 2020-09-25
*/
public class MyRuntimeException extends RuntimeException {
/**
* 构造函数。
*/
public MyRuntimeException() {
}
/**
* 构造函数。
*
* @param throwable 引发异常对象。
*/
public MyRuntimeException(Throwable throwable) {
super(throwable);
}
/**
* 构造函数。
*
* @param msg 错误信息。
*/
public MyRuntimeException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,26 @@
package com.orange.demo.common.core.exception;
/**
* 没有数据被修改的自定义异常。
*
* @author Jerry
* @date 2020-09-25
*/
public class NoDataAffectException extends RuntimeException {
/**
* 构造函数。
*/
public NoDataAffectException() {
}
/**
* 构造函数。
*
* @param msg 错误信息。
*/
public NoDataAffectException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,26 @@
package com.orange.demo.common.core.exception;
/**
* 没有数据访问权限的自定义异常。
*
* @author Jerry
* @date 2020-09-25
*/
public class NoDataPermException extends RuntimeException {
/**
* 构造函数。
*/
public NoDataPermException() {
}
/**
* 构造函数。
*
* @param msg 错误信息。
*/
public NoDataPermException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,20 @@
package com.orange.demo.common.core.exception;
/**
* Redis缓存访问失败。比如获取分布式数据锁超时、等待线程中断等。
*
* @author Jerry
* @date 2020-09-25
*/
public class RedisCacheAccessException extends RuntimeException {
/**
* 构造函数。
*
* @param msg 错误信息。
* @param cause 原始异常。
*/
public RedisCacheAccessException(String msg, Throwable cause) {
super(msg, cause);
}
}

View File

@@ -0,0 +1,226 @@
package com.orange.demo.common.core.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.demo.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.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
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 Jerry
* @date 2020-09-25
*/
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工具类创建一个空对象。
*/
@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, MediaType.APPLICATION_JSON_VALUE)) {
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) {
return parseArray(parameterType, parameterAnnotation.elementType(), key, value);
}
// 其他复杂对象
return JSON.toJavaObject((JSONObject) value, parameterType);
}
@SuppressWarnings("unchecked")
private Object parseArray(Class<?> parameterType, Class<?> elementType, String key, Object value)
throws IllegalAccessException, InstantiationException {
Object o;
if (!parameterType.equals(List.class)) {
o = parameterType.newInstance();
parameterType = (Class<?>) ((ParameterizedType)
parameterType.getGenericSuperclass()).getActualTypeArguments()[0];
} else {
parameterType = 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;
}
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);
}
private JSONObject getRequestBody(NativeWebRequest webRequest) throws IOException {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
// 有就直接获取
JSONObject jsonObject = (JSONObject) webRequest.getAttribute(JSONBODY_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
// 没有就从请求中读取
if (jsonObject == null) {
String jsonBody = IOUtils.toString(servletRequest.getReader());
jsonObject = JSON.parseObject(jsonBody);
if (jsonObject != null) {
webRequest.setAttribute(JSONBODY_ATTRIBUTE, jsonObject, RequestAttributes.SCOPE_REQUEST);
}
}
return jsonObject;
}
}

View File

@@ -0,0 +1,28 @@
package com.orange.demo.common.core.listener;
import com.orange.demo.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 Jerry
* @date 2020-09-25
*/
@Component
public class LoadCachedDataListener implements ApplicationListener<ApplicationReadyEvent> {
@SuppressWarnings("all")
@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,28 @@
package com.orange.demo.common.core.listener;
import com.orange.demo.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 Jerry
* @date 2020-09-25
*/
@Component
public class LoadServiceRelationListener implements ApplicationListener<ApplicationReadyEvent> {
@SuppressWarnings("all")
@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,87 @@
package com.orange.demo.common.core.object;
import com.alibaba.fastjson.JSONObject;
import lombok.Data;
/**
* 接口数据验证结果对象。主要是Service类使用。
* 同时为了提升效率减少查询次数可以根据具体的需求将部分验证关联对象存入data字段以供Controller使用。
*
* @author Jerry
* @date 2020-09-25
*/
@Data
public class CallResult {
/**
* 为了优化性能,所有没有携带数据的正确结果,均可用该对象表示。
*/
private static final CallResult OK = new CallResult();
/**
* 是否成功标记。
*/
private boolean success = true;
/**
* 错误信息描述。
*/
private String errorMessage = null;
/**
* 在验证同时,仍然需要附加的关联数据对象。
*/
private JSONObject data;
/**
* 创建验证结果对象。
*
* @param errorMessage 错误描述信息。
* @return 如果参数为空,表示成功,否则返回代码错误信息的错误对象实例。
*/
public static CallResult create(String errorMessage) {
return errorMessage == null ? ok() : error(errorMessage);
}
/**
* 创建验证结果对象。
*
* @param errorMessage 错误描述信息。
* @param data 附带的数据对象。
* @return 如果参数为空,表示成功,否则返回代码错误信息的错误对象实例。
*/
public static CallResult create(String errorMessage, JSONObject data) {
return errorMessage == null ? ok(data) : error(errorMessage);
}
/**
* 创建表示验证成功的对象实例。
*
* @return 验证成功对象实例。
*/
public static CallResult ok() {
return OK;
}
/**
* 创建表示验证成功的对象实例。
*
* @param data 附带的数据对象。
* @return 验证成功对象实例。
*/
public static CallResult ok(JSONObject data) {
CallResult result = new CallResult();
result.data = data;
return result;
}
/**
* 创建表示验证失败的对象实例。
*
* @param errorMessage 错误描述。
* @return 验证失败对象实例。
*/
public static CallResult error(String errorMessage) {
CallResult result = new CallResult();
result.success = false;
result.errorMessage = errorMessage;
return result;
}
}

View File

@@ -0,0 +1,24 @@
package com.orange.demo.common.core.object;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* Mybatis Mapper.xml中所需的分组条件对象。
*
* @author Jerry
* @date 2020-09-25
*/
@Data
@AllArgsConstructor
public class MyGroupCriteria {
/**
* GROUP BY 从句后面的参数。
*/
private String groupBy;
/**
* SELECT 从句后面的分组显示字段。
*/
private String groupSelect;
}

View File

@@ -0,0 +1,168 @@
package com.orange.demo.common.core.object;
import cn.hutool.core.util.ReflectUtil;
import com.orange.demo.common.core.constant.ApplicationConstant;
import com.orange.demo.common.core.exception.InvalidClassFieldException;
import com.orange.demo.common.core.exception.InvalidDataFieldException;
import com.orange.demo.common.core.exception.InvalidDataModelException;
import com.orange.demo.common.core.util.MyModelUtil;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* 查询分组参数请求对象。
*
* @author Jerry
* @date 2020-09-25
*/
@EqualsAndHashCode(callSuper = true)
@Slf4j
@Data
public class MyGroupParam extends ArrayList<MyGroupParam.GroupInfo> {
/**
* SQL语句的SELECT LIST中分组字段的返回字段名称列表。
*/
private List<String> selectGroupFieldList;
/**
* 分组参数解析后构建的SQL语句中所需的分组数据如GROUP BY的字段列表和SELECT LIST中的分组字段显示列表。
*/
private MyGroupCriteria groupCriteria;
/**
* 基于分组参数对象中的数据构建SQL中select list和group by从句可以直接使用的分组对象。
*
* @param groupParam 分组参数对象。
* @param modelClazz 查询表对应的主对象的Class。
* @return SQL中所需的GROUP对象。详见MyGroupCriteria类定义。
*/
public static MyGroupParam buildGroupBy(MyGroupParam groupParam, Class<?> modelClazz) {
if (groupParam == null) {
return null;
}
if (modelClazz == null) {
throw new IllegalArgumentException("modelClazz Argument can't be NULL");
}
groupParam.selectGroupFieldList = new LinkedList<>();
StringBuilder groupByBuilder = new StringBuilder(128);
StringBuilder groupSelectBuilder = new StringBuilder(128);
int i = 0;
for (GroupInfo groupInfo : groupParam) {
GroupBaseData groupBaseData = parseGroupBaseData(groupInfo, modelClazz);
if (StringUtils.isBlank(groupBaseData.tableName)) {
throw new InvalidDataModelException(groupBaseData.modelName);
}
if (StringUtils.isBlank(groupBaseData.columnName)) {
throw new InvalidDataFieldException(groupBaseData.modelName, groupBaseData.fieldName);
}
processGroupInfo(groupInfo, groupBaseData, groupByBuilder, groupSelectBuilder);
String aliasName = StringUtils.isBlank(groupInfo.aliasName) ? groupInfo.fieldName : groupInfo.aliasName;
// selectGroupFieldList中的元素目前只是被export操作使用。会根据集合中的元素名称匹配导出表头。
groupParam.selectGroupFieldList.add(aliasName);
if (++i < groupParam.size()) {
groupByBuilder.append(", ");
groupSelectBuilder.append(", ");
}
}
groupParam.groupCriteria = new MyGroupCriteria(groupByBuilder.toString(), groupSelectBuilder.toString());
return groupParam;
}
private static GroupBaseData parseGroupBaseData(GroupInfo groupInfo, Class<?> modelClazz) {
GroupBaseData baseData = new GroupBaseData();
String[] stringArray = StringUtils.split(groupInfo.fieldName,'.');
if (stringArray.length == 1) {
baseData.modelName = modelClazz.getSimpleName();
baseData.fieldName = groupInfo.fieldName;
baseData.tableName = MyModelUtil.mapToTableName(modelClazz);
baseData.columnName = MyModelUtil.mapToColumnName(groupInfo.fieldName, modelClazz);
} else {
Field field = ReflectUtil.getField(modelClazz, stringArray[0]);
if (field == null) {
throw new InvalidClassFieldException(modelClazz.getSimpleName(), stringArray[0]);
}
Class<?> fieldClazz = field.getType();
baseData.modelName = fieldClazz.getSimpleName();
baseData.fieldName = stringArray[1];
baseData.tableName = MyModelUtil.mapToTableName(fieldClazz);
baseData.columnName = MyModelUtil.mapToColumnName(baseData.fieldName, fieldClazz);
}
return baseData;
}
private static void processGroupInfo(
GroupInfo groupInfo,
GroupBaseData baseData,
StringBuilder groupByBuilder,
StringBuilder groupSelectBuilder) {
String tableName = baseData.tableName;
String columnName = baseData.columnName;
if (StringUtils.isNotBlank(groupInfo.dateAggregateBy)) {
groupByBuilder.append("DATE_FORMAT(").append(tableName).append(".").append(columnName);
groupSelectBuilder.append("DATE_FORMAT(").append(tableName).append(".").append(columnName);
if (ApplicationConstant.DAY_AGGREGATION.equals(groupInfo.dateAggregateBy)) {
groupByBuilder.append(", '%Y-%m-%d')");
groupSelectBuilder.append(", '%Y-%m-%d')");
} else if (ApplicationConstant.MONTH_AGGREGATION.equals(groupInfo.dateAggregateBy)) {
groupByBuilder.append(", '%Y-%m-01')");
groupSelectBuilder.append(", '%Y-%m-01')");
} else if (ApplicationConstant.YEAR_AGGREGATION.equals(groupInfo.dateAggregateBy)) {
groupByBuilder.append(", '%Y-01-01')");
groupSelectBuilder.append(", '%Y-01-01')");
} else {
throw new IllegalArgumentException("Illegal DATE_FORMAT for GROUP ID list.");
}
if (StringUtils.isNotBlank(groupInfo.aliasName)) {
groupSelectBuilder.append(" ").append(groupInfo.aliasName);
} else {
groupSelectBuilder.append(" ").append(columnName);
}
} else {
groupByBuilder.append(tableName).append(".").append(columnName);
groupSelectBuilder.append(tableName).append(".").append(columnName);
if (StringUtils.isNotBlank(groupInfo.aliasName)) {
groupSelectBuilder.append(" ").append(groupInfo.aliasName);
}
}
}
/**
* 分组信息对象。
*/
@Data
public static class GroupInfo {
/**
* Java对象的字段名。目前主要包含三种格式
* 1. 简单的属性名称如userId将会直接映射到与其关联的数据库字段。表名为当前ModelClazz所对应的表名。
* 映射结果或为 my_main_table.user_id
* 2. 一对一关联表属性如user.userId这里将先获取user属性的对象类型并映射到对应的表名后面的userId为
* user所在实体的属性。映射结果或为my_sys_user.user_id
*/
private String fieldName;
/**
* SQL语句的Select List中分组字段的别名。如果别名为NULL直接取fieldName。
*/
private String aliasName;
/**
* 如果该值不为NULL则会对分组字段进行DATE_FORMAT函数的计算并根据具体的值将日期数据截取到指定的位。
* day: 表示按照天聚合将会截取到天。DATE_FORMAT(columnName, '%Y-%m-%d')
* month: 表示按照月聚合将会截取到月。DATE_FORMAT(columnName, '%Y-%m-01')
* year: 表示按照年聚合将会截取到年。DATE_FORMAT(columnName, '%Y-01-01')
*/
private String dateAggregateBy;
}
private static class GroupBaseData {
private String modelName;
private String fieldName;
private String tableName;
private String columnName;
}
}

View File

@@ -0,0 +1,258 @@
package com.orange.demo.common.core.object;
import cn.hutool.core.util.ReflectUtil;
import com.orange.demo.common.core.constant.ApplicationConstant;
import com.orange.demo.common.core.exception.InvalidClassFieldException;
import com.orange.demo.common.core.exception.InvalidDataFieldException;
import com.orange.demo.common.core.exception.InvalidDataModelException;
import com.orange.demo.common.core.util.MyModelUtil;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Field;
import java.util.*;
/**
* Controller参数中的排序请求对象。
*
* @author Jerry
* @date 2020-09-25
*/
@EqualsAndHashCode(callSuper = true)
@Slf4j
@Data
public class MyOrderParam extends ArrayList<MyOrderParam.OrderInfo> {
private static final String DICT_MAP = "DictMap.";
/**
* 基于排序对象中的JSON数据构建SQL中order by从句可以直接使用的排序字符串。
*
* @param orderParam 排序参数对象。
* @param modelClazz 查询主表对应的主对象的Class。
* @return SQL中order by从句可以直接使用的排序字符串。
*/
public static String buildOrderBy(MyOrderParam orderParam, Class<?> modelClazz) {
if (orderParam == null) {
return null;
}
if (modelClazz == null) {
throw new IllegalArgumentException(
"modelClazz Argument in MyOrderParam.buildOrderBy can't be NULL");
}
int i = 0;
StringBuilder orderBy = new StringBuilder(128);
for (OrderInfo orderInfo : orderParam) {
OrderBaseData orderBaseData = parseOrderBaseData(orderInfo, modelClazz);
if (StringUtils.isBlank(orderBaseData.tableName)) {
throw new InvalidDataModelException(orderBaseData.modelName);
}
if (StringUtils.isBlank(orderBaseData.columnName)) {
throw new InvalidDataFieldException(orderBaseData.modelName, orderBaseData.fieldName);
}
processOrderInfo(orderInfo, orderBaseData, orderBy);
if (++i < orderParam.size()) {
orderBy.append(", ");
}
}
return orderBy.toString();
}
private static void processOrderInfo(
OrderInfo orderInfo, OrderBaseData orderBaseData, StringBuilder orderByBuilder) {
if (StringUtils.isNotBlank(orderInfo.dateAggregateBy)) {
orderByBuilder.append("DATE_FORMAT(")
.append(orderBaseData.tableName).append(".").append(orderBaseData.columnName);
if (ApplicationConstant.DAY_AGGREGATION.equals(orderInfo.dateAggregateBy)) {
orderByBuilder.append(", '%Y-%m-%d')");
} else if (ApplicationConstant.MONTH_AGGREGATION.equals(orderInfo.dateAggregateBy)) {
orderByBuilder.append(", '%Y-%m-01')");
} else if (ApplicationConstant.YEAR_AGGREGATION.equals(orderInfo.dateAggregateBy)) {
orderByBuilder.append(", '%Y-01-01')");
} else {
throw new IllegalArgumentException("Illegal DATE_FORMAT for GROUP ID list.");
}
} else {
orderByBuilder.append(orderBaseData.tableName).append(".").append(orderBaseData.columnName);
}
if (orderInfo.asc != null && !orderInfo.asc) {
orderByBuilder.append(" DESC");
}
}
private static OrderBaseData parseOrderBaseData(OrderInfo orderInfo, Class<?> modelClazz) {
OrderBaseData orderBaseData = new OrderBaseData();
orderBaseData.fieldName = StringUtils.substringBefore(orderInfo.fieldName, DICT_MAP);
String[] stringArray = StringUtils.split(orderBaseData.fieldName, '.');
if (stringArray.length == 1) {
orderBaseData.modelName = modelClazz.getSimpleName();
orderBaseData.tableName = MyModelUtil.mapToTableName(modelClazz);
orderBaseData.columnName = MyModelUtil.mapToColumnName(orderBaseData.fieldName, modelClazz);
} else {
Field field = ReflectUtil.getField(modelClazz, stringArray[0]);
if (field == null) {
throw new InvalidClassFieldException(modelClazz.getSimpleName(), stringArray[0]);
}
Class<?> fieldClazz = field.getType();
orderBaseData.modelName = fieldClazz.getSimpleName();
orderBaseData.fieldName = stringArray[1];
orderBaseData.tableName = MyModelUtil.mapToTableName(fieldClazz);
orderBaseData.columnName = MyModelUtil.mapToColumnName(orderBaseData.fieldName, fieldClazz);
}
return orderBaseData;
}
/**
* 在排序列表中,可能存在基于指定表字段的排序,该函数将获取指定表的所有排序字段。
* 返回的字符串可直接用于SQL中的ORDER BY从句。
*
* @param orderParam 排序参数对象。
* @param modelClazz 查询主表对应的主对象的Class。
* @param relationModelName 与关联表对应的Model的名称如my_course_paper表应对的Java对象CoursePaper。
* 如果该值为null或空字符串则获取所有主表的排序字段。
* @return 返回的是表字段而非Java对象的属性多个字段之间逗号分隔。
*/
public static String getOrderClauseByModelName(
MyOrderParam orderParam, Class<?> modelClazz, String relationModelName) {
if (orderParam == null) {
return null;
}
if (modelClazz == null) {
throw new IllegalArgumentException(
"modelClazz Argument in MyOrderParam.getOrderClauseByModelName can't be NULL");
}
List<String> fieldNameList = new LinkedList<>();
String prefix = null;
if (StringUtils.isNotBlank(relationModelName)) {
prefix = relationModelName + ".";
}
for (OrderInfo orderInfo : orderParam) {
OrderBaseData baseData = parseOrderBaseData(orderInfo, modelClazz, prefix, relationModelName);
if (baseData != null) {
fieldNameList.add(makeOrderBy(baseData, orderInfo.asc));
}
}
return StringUtils.join(fieldNameList, ", ");
}
private static OrderBaseData parseOrderBaseData(
OrderInfo orderInfo, Class<?> modelClazz, String prefix, String relationModelName) {
OrderBaseData baseData = null;
String fieldName = StringUtils.substringBefore(orderInfo.fieldName, DICT_MAP);
if (prefix != null) {
if (fieldName.startsWith(prefix)) {
baseData = new OrderBaseData();
Field field = ReflectUtil.getField(modelClazz, relationModelName);
if (field == null) {
throw new InvalidClassFieldException(modelClazz.getSimpleName(), relationModelName);
}
Class<?> fieldClazz = field.getType();
baseData.modelName = fieldClazz.getSimpleName();
baseData.fieldName = StringUtils.removeStart(fieldName, prefix);
baseData.tableName = MyModelUtil.mapToTableName(fieldClazz);
baseData.columnName = MyModelUtil.mapToColumnName(fieldName, fieldClazz);
}
} else {
String dotLimitor = ".";
if (!fieldName.contains(dotLimitor)) {
baseData = new OrderBaseData();
baseData.modelName = modelClazz.getSimpleName();
baseData.tableName = MyModelUtil.mapToTableName(modelClazz);
baseData.columnName = MyModelUtil.mapToColumnName(fieldName, modelClazz);
}
}
return baseData;
}
private static String makeOrderBy(OrderBaseData baseData, Boolean asc) {
if (StringUtils.isBlank(baseData.tableName)) {
throw new InvalidDataModelException(baseData.modelName);
}
if (StringUtils.isBlank(baseData.columnName)) {
throw new InvalidDataFieldException(baseData.modelName, baseData.fieldName);
}
StringBuilder orderBy = new StringBuilder(128);
orderBy.append(baseData.tableName).append(".").append(baseData.columnName);
if (asc != null && !asc) {
orderBy.append(" DESC");
}
return orderBy.toString();
}
/**
* 在排序列表中,可能存在基于指定表字段的排序,该函数将删除指定表的所有排序字段。
*
* @param orderParam 排序参数对象。
* @param modelClazz 查询主表对应的主对象的Class。
* @param relationModelName 与关联表对应的Model的名称如my_course_paper表应对的Java对象CoursePaper。
* 如果该值为null或空字符串则获取所有主表的排序字段。
*/
public static void removeOrderClauseByModelName(
MyOrderParam orderParam, Class<?> modelClazz, String relationModelName) {
if (orderParam == null) {
return;
}
if (modelClazz == null) {
throw new IllegalArgumentException(
"modelClazz Argument in MyOrderParam.removeOrderClauseByModelName can't be NULL");
}
List<Integer> fieldIndexList = new LinkedList<>();
String prefix = null;
if (StringUtils.isNotBlank(relationModelName)) {
prefix = relationModelName + ".";
}
int i = 0;
for (OrderInfo orderInfo : orderParam) {
String fieldName = StringUtils.substringBefore(orderInfo.fieldName, DICT_MAP);
if (prefix != null) {
if (fieldName.startsWith(prefix)) {
fieldIndexList.add(i);
}
} else {
if (!fieldName.contains(".")) {
fieldIndexList.add(i);
}
}
++i;
}
for (int index : fieldIndexList) {
orderParam.remove(index);
}
}
/**
* 排序信息对象。
*/
@Data
public static class OrderInfo {
/**
* Java对象的字段名。目前主要包含三种格式
* 1. 简单的属性名称如userId将会直接映射到与其关联的数据库字段。表名为当前ModelClazz所对应的表名。
* 映射结果或为 my_main_table.user_id
* 2. 字典属性名称如userIdDictMap.id由于仅仅支持字典中Id数据的排序所以直接截取DictMap之前的字符串userId作为排序属性。
* 表名为当前ModelClazz所对应的表名。映射结果或为 my_main_table.user_id
* 3. 一对一关联表属性如user.userId这里将先获取user属性的对象类型并映射到对应的表名后面的userId为
* user所在实体的属性。映射结果或为my_sys_user.user_id
*/
private String fieldName;
/**
* 排序方向。true为升序否则降序。
*/
private Boolean asc = true;
/**
* 如果该值不为NULL则会对日期型排序字段进行DATE_FORMAT函数的计算并根据具体的值将日期数据截取到指定的位。
* day: 表示按照天聚合将会截取到天。DATE_FORMAT(columnName, '%Y-%m-%d')
* month: 表示按照月聚合将会截取到月。DATE_FORMAT(columnName, '%Y-%m-01')
* year: 表示按照年聚合将会截取到年。DATE_FORMAT(columnName, '%Y-01-01')
*/
private String dateAggregateBy;
}
private static class OrderBaseData {
private String modelName;
private String fieldName;
private String tableName;
private String columnName;
}
}

View File

@@ -0,0 +1,58 @@
package com.orange.demo.common.core.object;
import lombok.Getter;
/**
* Controller参数中的分页请求对象
*
* @author Jerry
* @date 2020-09-25
*/
@Getter
public class MyPageParam {
public static final int DEFAULT_PAGE_NUM = 1;
public static final int DEFAULT_PAGE_SIZE = 10;
public static final int DEFAULT_MAX_SIZE = 100;
/**
* 分页号码从1开始计数。
*/
private Integer pageNum;
/**
* 每页大小。
*/
private Integer pageSize;
/**
* 设置当前分页页号。
*
* @param pageNum 页号,如果传入非法值,则使用缺省值。
*/
public void setPageNum(Integer pageNum) {
if (pageNum == null) {
return;
}
if (pageNum <= 0) {
pageNum = DEFAULT_PAGE_NUM;
}
this.pageNum = pageNum;
}
/**
* 设置分页的大小。
*
* @param pageSize 分页大小,如果传入非法值,则使用缺省值。
*/
public void setPageSize(Integer pageSize) {
if (pageSize == null) {
return;
}
if (pageSize <= 0 || pageSize > DEFAULT_MAX_SIZE) {
pageSize = DEFAULT_PAGE_SIZE;
}
this.pageSize = pageSize;
}
}

View File

@@ -0,0 +1,84 @@
package com.orange.demo.common.core.object;
import lombok.Builder;
import lombok.Data;
/**
* 实体对象数据组装参数构建器。
* BaseService中的实体对象数据组装函数会根据该参数对象进行数据组装。
*
* @author Jerry
* @date 2020-09-25
*/
@Data
@Builder
public class MyRelationParam {
/**
* 是否组装字典关联的标记。
* 组装RelationDict和RelationConstDict注解标记的字段。
*/
private boolean buildDict;
/**
* 是否组装一对一关联的标记。
* 组装RelationOneToOne注解标记的字段。
*/
private boolean buildOneToOne;
/**
* 在组装一对一关联的同时,是否继续关联从表中的字典。
* 从表中RelationDict和RelationConstDict注解标记的字段。
* 该字段为true时无需设置buildOneToOne了。
*/
private boolean buildOneToOneWithDict;
/**
* 是否组装主表对多对多中间表关联的标记。
* 组装RelationManyToMany注解标记的字段。
*/
private boolean buildRelationManyToMany;
/**
* 是否组装聚合计算关联的标记。
* 组装RelationOneToManyAggregation和RelationManyToManyAggregation注解标记的字段。
*/
private boolean buildRelationAggregation;
/**
* 便捷方法,返回仅做字典关联的参数对象。
*
* @return 返回仅做字典关联的参数对象。
*/
public static MyRelationParam dictOnly() {
return MyRelationParam.builder().buildDict(true).build();
}
/**
* 便捷方法,返回仅做字典关联、一对一从表及其字典和聚合计算的参数对象。
* NOTE: 对于一对多和多对多,这种从表数据是列表结果的关联,均不返回。
*
* @return 返回仅做字典关联、一对一从表及其字典和聚合计算的参数对象。
*/
public static MyRelationParam normal() {
return MyRelationParam.builder()
.buildDict(true)
.buildOneToOneWithDict(true)
.buildRelationAggregation(true)
.build();
}
/**
* 便捷方法,返回全部关联的参数对象。
*
* @return 返回全部关联的参数对象。
*/
public static MyRelationParam full() {
return MyRelationParam.builder()
.buildDict(true)
.buildOneToOneWithDict(true)
.buildRelationAggregation(true)
.buildRelationManyToMany(true)
.build();
}
}

View File

@@ -0,0 +1,308 @@
package com.orange.demo.common.core.object;
import cn.hutool.core.util.ReflectUtil;
import com.alibaba.fastjson.annotation.JSONField;
import com.orange.demo.common.core.exception.InvalidDataFieldException;
import com.orange.demo.common.core.exception.InvalidDataModelException;
import com.orange.demo.common.core.util.MyModelUtil;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import java.util.Collection;
import java.util.List;
/**
* Where中的条件语句。
*
* @author Jerry
* @date 2020-09-25
*/
@Slf4j
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyWhereCriteria {
/**
* 等于
*/
public static final int OPERATOR_EQUAL = 0;
/**
* 不等于
*/
public static final int OPERATOR_NOT_EQUAL = 1;
/**
* 大于等于
*/
public static final int OPERATOR_GE = 2;
/**
* 大于
*/
public static final int OPERATOR_GT = 3;
/**
* 小于等于
*/
public static final int OPERATOR_LE = 4;
/**
* 小于
*/
public static final int OPERATOR_LT = 5;
/**
* LIKE
*/
public static final int OPERATOR_LIKE = 6;
/**
* NOT NULL
*/
public static final int OPERATOR_NOT_NULL = 7;
/**
* IS NULL
*/
public static final int OPERATOR_IS_NULL = 8;
/**
* IN
*/
public static final int OPERATOR_IN = 9;
/**
* 参与过滤的实体对象的Class。
*/
@JSONField(serialize = false)
private Class<?> modelClazz;
/**
* Java属性名称。
*/
private String fieldName;
/**
* 操作符类型,取值范围见上面的常量值。
*/
private Integer operatorType;
/**
* 条件数据值。
*/
private Object value;
/**
* 设置条件值。
*
* @param fieldName 条件所属的实体对象的字段名。
* @param operatorType 条件操作符。具体值可参考当前对象的静态变量。
* @param value 条件过滤值。
* @return 验证结果对象,如果有错误将会返回具体的错误信息。
*/
public CallResult setCriteria(String fieldName, Integer operatorType, Object value) {
this.operatorType = operatorType;
this.fieldName = fieldName;
this.value = value;
return doVerify();
}
/**
* 设置条件值。
*
* @param modelClazz 数据表对应实体对象的Class.
* @param fieldName 条件所属的实体对象的字段名。
* @param operatorType 条件操作符。具体值可参考当前对象的静态变量。
* @param value 条件过滤值。
* @return 验证结果对象,如果有错误将会返回具体的错误信息。
*/
public CallResult setCriteria(Class<?> modelClazz, String fieldName, Integer operatorType, Object value) {
this.modelClazz = modelClazz;
this.operatorType = operatorType;
this.fieldName = fieldName;
this.value = value;
return doVerify();
}
/**
* 在执行该函数之前,该对象的所有数据均已经赋值完毕。
* 该函数主要验证操作符字段和条件值字段对应关系的合法性。
*
* @return 验证结果对象,如果有错误将会返回具体的错误信息。
*/
public CallResult doVerify() {
if (fieldName == null) {
return CallResult.error("过滤字段名称 [fieldName] 不能为空!");
}
if (modelClazz != null && ReflectUtil.getField(modelClazz, fieldName) == null) {
return CallResult.error(
"过滤字段 [" + fieldName + "] 在实体对象 [" + modelClazz.getSimpleName() + "] 中并不存在!");
}
if (!checkOperatorType()) {
return CallResult.error("无效的操作符类型 [" + operatorType + "]!");
}
// 其他操作符必须包含value值
if (operatorType != OPERATOR_IS_NULL && operatorType != OPERATOR_NOT_NULL && value == null) {
String operatorString = this.getOperatorString();
return CallResult.error("操作符 [" + operatorString + "] 的条件值不能为空!");
}
if (this.operatorType == OPERATOR_IN) {
if (!(value instanceof Collection)) {
return CallResult.error("操作符 [IN] 的条件值必须为集合对象!");
}
if (CollectionUtils.isEmpty((Collection<?>) value)) {
return CallResult.error("操作符 [IN] 的条件值不能为空!");
}
}
return CallResult.ok();
}
/**
* 判断操作符类型是否合法。
*
* @return 合法返回true否则false。
*/
public boolean checkOperatorType() {
return operatorType != null
&& (operatorType >= OPERATOR_EQUAL && operatorType <= OPERATOR_IN);
}
/**
* 获取操作符的字符串形式。
*
* @return 操作符的字符串。
*/
public String getOperatorString() {
switch (operatorType) {
case OPERATOR_EQUAL:
return " = ";
case OPERATOR_NOT_EQUAL:
return " != ";
case OPERATOR_GE:
return " &gt;= ";
case OPERATOR_GT:
return " &gt; ";
case OPERATOR_LE:
return " &lt;= ";
case OPERATOR_LT:
return " &lt; ";
case OPERATOR_LIKE:
return " LIKE ";
case OPERATOR_NOT_NULL:
return " IS NOT NULL ";
case OPERATOR_IS_NULL:
return " IS NULL ";
case OPERATOR_IN:
return " IN ";
default:
return null;
}
}
/**
* 获取组装后的SQL Where从句如 table_name.column_name = 'value'。
* 与查询数据表对应的实体对象Class为当前对象的modelClazz字段。
*
* @exception InvalidDataFieldException selectFieldList中存在非法实体字段时抛出该异常。
* @return 组装后的SQL条件从句。
*/
public String makeCriteriaString() {
return makeCriteriaString(this.modelClazz);
}
/**
* 获取组装后的SQL Where从句如 table_name.column_name = 'value'。
*
* @param modelClazz 与查询数据表对应的实体对象的Class。
* @exception InvalidDataFieldException selectFieldList中存在非法实体字段时抛出该异常。
* @exception InvalidDataModelException 参数modelClazz没有对应的table抛出该异常。
* @return 组装后的SQL条件从句。
*/
public String makeCriteriaString(Class<?> modelClazz) {
if (modelClazz == null) {
throw new IllegalArgumentException("ModelClazz argument can't be NULL.");
}
Tuple2<String, Integer> fieldInfo = MyModelUtil.mapToColumnInfo(fieldName, modelClazz);
if (fieldInfo == null) {
throw new InvalidDataFieldException(modelClazz.getSimpleName(), fieldName);
}
String tableName = MyModelUtil.mapToTableName(modelClazz);
if (tableName == null) {
throw new InvalidDataModelException(modelClazz.getSimpleName());
}
return this.buildClauseString(tableName, fieldInfo.getFirst(), fieldInfo.getSecond());
}
/**
* 获取组装后的SQL Where从句。如 table_name.column_name = 'value'。
*
* @param criteriaList 条件列表,所有条件直接目前仅支持 AND 的关系。
* @exception InvalidDataFieldException selectFieldList中存在非法实体字段时抛出该异常。
* @return 组装后的SQL条件从句。
*/
public static String makeCriteriaString(List<MyWhereCriteria> criteriaList) {
return makeCriteriaString(criteriaList, null);
}
/**
* 获取组装后的SQL Where从句。如 table_name.column_name = 'value'。
*
* @param criteriaList 条件列表,所有条件直接目前仅支持 AND 的关系。
* @param modelClazz 与数据表对应的实体对象的Class。
* 如果不为NULL实体对象Class使用该值否则使用每个MyWhereCriteria自身的modelClazz。
* @exception InvalidDataFieldException selectFieldList中存在非法实体字段时抛出该异常。
* @return 组装后的SQL条件从句。
*/
public static String makeCriteriaString(List<MyWhereCriteria> criteriaList, Class<?> modelClazz) {
if (CollectionUtils.isEmpty(criteriaList)) {
return null;
}
StringBuilder sb = new StringBuilder(256);
int i = 0;
for (MyWhereCriteria whereCriteria : criteriaList) {
Class<?> clazz = modelClazz;
if (clazz == null) {
clazz = whereCriteria.modelClazz;
}
if (i++ != 0) {
sb.append(" AND ");
}
String criteriaString = whereCriteria.makeCriteriaString(clazz);
sb.append(criteriaString);
}
return sb.length() == 0 ? null : sb.toString();
}
private String buildClauseString(String tableName, String columnName, Integer columnType) {
StringBuilder sb = new StringBuilder(64);
sb.append(tableName).append(".").append(columnName).append(getOperatorString());
if (operatorType == OPERATOR_IN) {
Collection<?> filterValues = (Collection<?>) value;
sb.append("(");
int i = 0;
for (Object filterValue : filterValues) {
if (columnType.equals(MyModelUtil.NUMERIC_FIELD_TYPE)) {
sb.append(filterValue);
} else {
sb.append("'").append(filterValue).append("'");
}
if (i++ != filterValues.size() - 1) {
sb.append(", ");
}
}
sb.append(")");
return sb.toString();
}
if (value != null) {
if (columnType.equals(MyModelUtil.NUMERIC_FIELD_TYPE)) {
sb.append(value);
} else {
sb.append("'").append(value).append("'");
}
}
return sb.toString();
}
}

View File

@@ -0,0 +1,157 @@
package com.orange.demo.common.core.object;
import com.orange.demo.common.core.constant.ErrorCodeEnum;
import lombok.Data;
/**
* 接口返回对象
*
* @author Jerry
* @date 2020-09-25
*/
@Data
public class ResponseResult<T> {
/**
* 为了优化性能,所有没有携带数据的正确结果,均可用该对象表示。
*/
private static final ResponseResult<Void> OK = new ResponseResult<>();
/**
* 是否成功标记。
*/
private boolean success = true;
/**
* 错误码。
*/
private String errorCode = "NO-ERROR";
/**
* 错误信息描述。
*/
private String errorMessage = "NO-MESSAGE";
/**
* 实际数据。
*/
private T data = null;
/**
* 根据参数errorCodeEnum的枚举值判断创建成功对象还是错误对象。
* 如果返回错误对象errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和 getErrorMessage()。
*
* @param errorCodeEnum 错误码枚举
* @return 返回创建的ResponseResult实例对象
*/
public static ResponseResult<Void> create(ErrorCodeEnum errorCodeEnum) {
return create(errorCodeEnum, errorCodeEnum.getErrorMessage());
}
/**
* 根据参数errorCodeEnum的枚举值判断创建成功对象还是错误对象。
* 如果返回错误对象errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。
*
* @param errorCodeEnum 错误码枚举。
* @param errorMessage 如果该参数为null错误信息取自errorCodeEnum参数内置的errorMessage否则使用当前参数。
* @return 返回创建的ResponseResult实例对象
*/
public static ResponseResult<Void> create(ErrorCodeEnum errorCodeEnum, String errorMessage) {
errorMessage = errorMessage != null ? errorMessage : errorCodeEnum.getErrorMessage();
return errorCodeEnum == ErrorCodeEnum.NO_ERROR ? success() : error(errorCodeEnum.name(), errorMessage);
}
/**
* 根据参数errorCode是否为空判断创建成功对象还是错误对象。
* 如果返回错误对象errorCode 和 errorMessage 分别取自于参数 errorCode 和参数 errorMessage。
*
* @param errorCode 自定义的错误码
* @param errorMessage 自定义的错误信息
* @return 返回创建的ResponseResult实例对象
*/
public static ResponseResult<Void> create(String errorCode, String errorMessage) {
return errorCode == null ? success() : error(errorCode, errorMessage);
}
/**
* 创建成功对象。
* 如果需要绑定返回数据可以在实例化后调用setDataObject方法。
*
* @return 返回创建的ResponseResult实例对象
*/
public static ResponseResult<Void> success() {
return OK;
}
/**
* 创建带有返回数据的成功对象。
*
* @param data 返回的数据对象
* @return 返回创建的ResponseResult实例对象
*/
public static <T> ResponseResult<T> success(T data) {
ResponseResult<T> resp = new ResponseResult<>();
resp.data = data;
return resp;
}
/**
* 创建错误对象。
* 如果返回错误对象errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和 getErrorMessage()。
*
* @param errorCodeEnum 错误码枚举
* @return 返回创建的ResponseResult实例对象
*/
public static <T> ResponseResult<T> error(ErrorCodeEnum errorCodeEnum) {
return error(errorCodeEnum.name(), errorCodeEnum.getErrorMessage());
}
/**
* 创建错误对象。
* 如果返回错误对象errorCode 和 errorMessage 分别取自于参数 errorCodeEnum 的 name() 和参数 errorMessage。
*
* @param errorCodeEnum 错误码枚举
* @param errorMessage 自定义的错误信息
* @return 返回创建的ResponseResult实例对象
*/
public static <T> ResponseResult<T> error(ErrorCodeEnum errorCodeEnum, String errorMessage) {
return error(errorCodeEnum.name(), errorMessage);
}
/**
* 创建错误对象。
* 如果返回错误对象errorCode 和 errorMessage 分别取自于参数 errorCode 和参数 errorMessage。
*
* @param errorCode 自定义的错误码
* @param errorMessage 自定义的错误信息
* @return 返回创建的ResponseResult实例对象
*/
public static <T> ResponseResult<T> error(String errorCode, String errorMessage) {
return new ResponseResult<>(false, errorCode, errorMessage);
}
/**
* 根据参数的errorCode和errorMessage创建新的错误应答对象。
*
* @param errorCause 导致错误原因的应答对象。
* @return 返回创建的ResponseResult实例对象。
*/
public static <T, E> ResponseResult<T> errorFrom(ResponseResult<E> errorCause) {
return error(errorCause.errorCode, errorCause.getErrorMessage());
}
/**
* 是否成功。
*
* @return true成功否则false。
*/
public boolean isSuccess() {
return success;
}
private ResponseResult() {
}
private ResponseResult(boolean success, String errorCode, String errorMessage) {
this.success = success;
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
}

View File

@@ -0,0 +1,59 @@
package com.orange.demo.common.core.object;
import com.orange.demo.common.core.util.ContextUtil;
import lombok.Data;
import lombok.ToString;
import javax.servlet.http.HttpServletRequest;
/**
* 基于Jwt用于前后端传递的令牌对象。
*
* @author Jerry
* @date 2020-09-25
*/
@Data
@ToString
public class TokenData {
/**
* 在HTTP Request对象中的属性键。
*/
public static final String REQUEST_ATTRIBUTE_NAME = "tokenData";
/**
* 用户Id。
*/
private Long userId;
/**
* 是否为超级管理员。
*/
private Boolean isAdmin;
/**
* 用户显示名称。
*/
private String showName;
/**
* 标识不同登录的会话Id。
*/
private String sessionId;
/**
* 将令牌对象添加到Http请求对象。
*
* @param tokenData 令牌对象。
*/
public static void addToRequest(TokenData tokenData) {
HttpServletRequest request = ContextUtil.getHttpRequest();
request.setAttribute(TokenData.REQUEST_ATTRIBUTE_NAME, tokenData);
}
/**
* 从Http Request对象中获取令牌对象。
*
* @return 令牌对象。
*/
public static TokenData takeFromRequest() {
HttpServletRequest request = ContextUtil.getHttpRequest();
return (TokenData) request.getAttribute(REQUEST_ATTRIBUTE_NAME);
}
}

View File

@@ -0,0 +1,50 @@
package com.orange.demo.common.core.object;
/**
* 二元组对象。主要用于可以一次返回多个结果的场景,同时还能避免强制转换。
*
* @author Jerry
* @date 2020-09-25
*/
public class Tuple2<T1, T2> {
/**
* 第一个变量。
*/
private final T1 first;
/**
* 第二个变量。
*/
private final T2 second;
/**
* 构造函数。
*
* @param first 第一个变量。
* @param second 第二个变量。
*/
public Tuple2(T1 first, T2 second) {
this.first = first;
this.second = second;
}
/**
* 获取第一个变量。
*
* @return 返回第一个变量。
*/
public T1 getFirst() {
return first;
}
/**
* 获取第二个变量。
*
* @return 返回第二个变量。
*/
public T2 getSecond() {
return second;
}
}

View File

@@ -0,0 +1,58 @@
package com.orange.demo.common.core.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.framework.AopProxy;
import org.springframework.aop.support.AopUtils;
import java.lang.reflect.Field;
/**
* 获取JDK动态代理/CGLIB代理对象代理的目标对象的工具类。
*
* @author Jerry
* @date 2020-09-25
*/
@Slf4j
public class AopTargetUtil {
/**
* 获取参数对象代理的目标对象。
*
* @param proxy 代理对象
* @return 代理的目标对象。
*/
public static Object getTarget(Object proxy) {
if (!AopUtils.isAopProxy(proxy)) {
return proxy;
}
try {
if (AopUtils.isJdkDynamicProxy(proxy)) {
return getJdkDynamicProxyTargetObject(proxy);
} else {
return getCglibProxyTargetObject(proxy);
}
} catch (Exception e) {
log.error("Failed to call getJdkDynamicProxyTargetObject or getCglibProxyTargetObject", e);
return null;
}
}
private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
h.setAccessible(true);
Object dynamicAdvisedInterceptor = h.get(proxy);
Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
advised.setAccessible(true);
return ((AdvisedSupport) advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
}
private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
h.setAccessible(true);
AopProxy aopProxy = (AopProxy) h.get(proxy);
Field advised = aopProxy.getClass().getDeclaredField("advised");
advised.setAccessible(true);
return ((AdvisedSupport) advised.get(aopProxy)).getTargetSource().getTarget();
}
}

View File

@@ -0,0 +1,74 @@
package com.orange.demo.common.core.util;
import com.orange.demo.common.core.exception.MyRuntimeException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
/**
* Spring 系统启动应用感知对象主要用于获取Spring Bean的上下文对象后续的代码中可以直接查找系统中加载的Bean对象。
*
* @author Jerry
* @date 2020-09-25
*/
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext applicationContext;
/**
* Spring 启动的过程中会自动调用,并将应用上下文对象赋值进来。
*
* @param applicationContext 应用上下文对象可通过该对象查找Spring中已经加载的Bean。
*/
@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) {
doSetApplicationContext(applicationContext);
}
/**
* 获取应用上下文对象。
*
* @return 应用上下文。
*/
public static ApplicationContext getApplicationContext() {
assertApplicationContext();
return applicationContext;
}
/**
* 根据BeanName获取Bean对象。
*
* @param beanName Bean名称。
* @param <T> 返回的Bean类型。
* @return Bean对象。
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String beanName) {
assertApplicationContext();
return (T) applicationContext.getBean(beanName);
}
/**
* 根据Bean的ClassType获取Bean对象。
*
* @param beanType Bean的Class类型。。
* @param <T> 返回的Bean类型。
* @return Bean对象。
*/
public static <T> T getBean(Class<T> beanType) {
assertApplicationContext();
return applicationContext.getBean(beanType);
}
private static void assertApplicationContext() {
if (ApplicationContextHolder.applicationContext == null) {
throw new MyRuntimeException("applicaitonContext属性为null,请检查是否注入了ApplicationContextHolder!");
}
}
private static void doSetApplicationContext(ApplicationContext applicationContext) {
ApplicationContextHolder.applicationContext = applicationContext;
}
}

View File

@@ -0,0 +1,49 @@
package com.orange.demo.common.core.util;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 获取Servlet HttpRequest和HttpResponse的工具类。
*
* @author Jerry
* @date 2020-09-25
*/
public class ContextUtil {
/**
* 判断当前是否处于HttpServletRequest上下文环境。
*
* @return 是返回true否则false。
*/
public static boolean hasRequestContext() {
return RequestContextHolder.getRequestAttributes() != null;
}
/**
* 获取Servlet请求上下文的HttpRequest对象。
*
* @return 请求上下文中的HttpRequest对象。
*/
public static HttpServletRequest getHttpRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
/**
* 获取Servlet请求上下文的HttpResponse对象。
*
* @return 请求上下文中的HttpResponse对象。
*/
public static HttpServletResponse getHttpResponse() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private ContextUtil() {
}
}

View File

@@ -0,0 +1,95 @@
package com.orange.demo.common.core.util;
import cn.hutool.core.io.IoUtil;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import cn.jimmyshi.beanquery.BeanQuery;
import com.orange.demo.common.core.constant.ApplicationConstant;
import com.orange.demo.common.core.exception.MyRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.io.FilenameUtils;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* 导出工具类目前支持xlsx和csv两种类型。
*
* @author Jerry
* @date 2020-09-25
*/
@Slf4j
public class ExportUtil {
/**
* 数据导出。目前仅支持xlsx和csv。
*
* @param dataList 导出数据列表。
* @param selectFieldMap 导出的数据字段key为对象字段名称value为中文标题名称。
* @param filename 导出文件名。
* @param <T> 数据对象类型。
* @throws IOException 文件操作失败。
*/
public static <T> void doExport(
Collection<T> dataList, Map<String, String> selectFieldMap, String filename) throws IOException {
if (CollectionUtils.isEmpty(dataList)) {
return;
}
StringBuilder sb = new StringBuilder(128);
for (Map.Entry<String, String> e : selectFieldMap.entrySet()) {
sb.append(e.getKey()).append(" as ").append(e.getValue()).append(", ");
}
// 去掉末尾的逗号
String selectFieldString = sb.substring(0, sb.length() - 2);
// 写出数据到xcel格式的输出流
List<Map<String, Object>> resultList = BeanQuery.select(selectFieldString).executeFrom(dataList);
// 构建HTTP输出流参数
HttpServletResponse response = ContextUtil.getHttpResponse();
response.setHeader("content-type", "application/octet-stream");
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + filename);
if (ApplicationConstant.XLSX_EXT.equals(FilenameUtils.getExtension(filename))) {
ServletOutputStream out = response.getOutputStream();
ExcelWriter writer = ExcelUtil.getWriter(true);
writer.setRowHeight(-1, 30);
writer.setColumnWidth(-1, 30);
writer.setColumnWidth(1, 20);
writer.write(resultList);
writer.flush(out);
writer.close();
IoUtil.close(out);
} else if (ApplicationConstant.CSV_EXT.equals(FilenameUtils.getExtension(filename))) {
Collection<String> headerList = selectFieldMap.values();
String[] headerArray = new String[headerList.size()];
headerList.toArray(headerArray);
CSVFormat format = CSVFormat.DEFAULT.withHeader(headerArray);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
try (Writer out = response.getWriter(); CSVPrinter printer = new CSVPrinter(out, format)) {
for (Map<String, Object> o : resultList) {
for (Map.Entry<String, Object> entry : o.entrySet()) {
printer.print(entry.getValue());
}
printer.println();
}
printer.flush();
} catch (Exception e) {
log.error("Failed to call ExportUtil.doExport", e);
}
} else {
throw new MyRuntimeException("不支持的导出文件类型!");
}
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private ExportUtil() {
}
}

View File

@@ -0,0 +1,214 @@
package com.orange.demo.common.core.util;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.poi.excel.ExcelReader;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.sax.Excel07SaxReader;
import com.orange.demo.common.core.constant.ApplicationConstant;
import com.orange.demo.common.core.exception.MyRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.*;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;
/**
* 导入工具类目前支持xlsx和csv两种类型。
*
* @author Jerry
* @date 2020-09-25
*/
@Slf4j
public class ImportUtil {
private static final String IMPORT_EXCEPTION_ERROR = "Failed to call ImportUtil.doImport";
private static final String UNSUPPORT_FILE_EXT_ERROR = "不支持的导入文件类型!";
/**
* 同步导入方式。
*
* @param filename 导入文件名。
* @return 导入数据列表。
*/
public static List<Map<String, Object>> doImport(String filename) {
if (ApplicationConstant.XLSX_EXT.equals(FilenameUtils.getExtension(filename))) {
try (ExcelReader reader = ExcelUtil.getReader(filename)) {
return reader.readAll();
}
} else if (ApplicationConstant.CSV_EXT.equals(FilenameUtils.getExtension(filename))) {
List<Map<String, Object>> resultList = new LinkedList<>();
try (FileReader reader = new FileReader(filename)) {
CSVParser parser = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(reader);
Map<String, Integer> headerMap = parser.getHeaderMap();
for (CSVRecord record : parser) {
Map<String, Object> rowMap = new LinkedHashMap<>();
for (final Map.Entry<String, Integer> header : headerMap.entrySet()) {
int col = header.getValue();
if (col < record.size()) {
rowMap.put(header.getKey(), record.get(col));
}
}
resultList.add(rowMap);
}
} catch (Exception e) {
log.error(IMPORT_EXCEPTION_ERROR, e);
}
return resultList;
}
throw new MyRuntimeException(UNSUPPORT_FILE_EXT_ERROR);
}
/**
* 异步导入方式即SAX导入方式。
*
* @param filename 导入文件名。
* @param importer 异步导入处理接口。
* @throws IOException 文件处理异常。
*/
public static <T> void doImport(String filename, BaseImporter<T> importer) throws IOException {
if (ApplicationConstant.XLSX_EXT.equals(FilenameUtils.getExtension(filename))) {
Excel07SaxReader reader = new MyExcel07SaxReader<>(importer);
try (InputStream in = new FileInputStream(filename)) {
reader.read(in, 0);
}
} else if (ApplicationConstant.CSV_EXT.equals(FilenameUtils.getExtension(filename))) {
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
int rowIndex = 0;
do {
String rowData = reader.readLine();
if (StringUtils.isBlank(rowData)) {
importer.doImport(-1, null);
break;
}
String[] dataArray = StringUtils.split(rowData, ",");
importer.doImport(rowIndex++, Arrays.asList(dataArray));
} while (!importer.doInterrupt());
} catch (Exception e) {
log.error(IMPORT_EXCEPTION_ERROR, e);
}
}
throw new MyRuntimeException(UNSUPPORT_FILE_EXT_ERROR);
}
/**
* 异步导入抽象类。
*
* @param <T> 导入数据对象类型。
*/
public abstract static class BaseImporter<T> {
private Class<T> beanType;
private List<T> batchRowList = new LinkedList<>();
private int batchSize;
private Field[] fieldArray = null;
private Map<String, String> headerColumnMap;
public BaseImporter(int batchSize, Class<T> beanType, Map<String, String> headerColumnMap) {
if (batchSize <= 0) {
batchSize = 100;
}
this.batchSize = batchSize;
this.beanType = beanType;
this.headerColumnMap = headerColumnMap;
}
/**
* 导入操作执行函数。
*
* @param rowIndex 当前行号。
* @param row 当前行数据列表对象。
*/
public void doImport(int rowIndex, List<Object> row) {
if (row == null) {
doProcess(batchRowList);
doFinish();
batchRowList.clear();
return;
}
if (rowIndex <= 0) {
fieldArray = new Field[row.size()];
List<String> headerList = row.stream().map(Object::toString).collect(Collectors.toList());
List<String> columnList = new ArrayList<>(row.size());
for (String headerName : headerList) {
String columnName = headerColumnMap.get(headerName);
if (columnName != null) {
columnList.add(columnName);
}
}
columnList.stream()
.map(columnName -> ReflectUtil.getField(beanType, columnName))
.collect(Collectors.toList())
.toArray(fieldArray);
return;
}
T data;
try {
data = beanType.newInstance();
for (int i = 0; i < row.size(); i++) {
Object value = row.get(i);
Field field = fieldArray[i];
if (field != null) {
ReflectUtil.setFieldValue(data, field, value);
}
}
batchRowList.add(data);
} catch (Exception e) {
log.error(IMPORT_EXCEPTION_ERROR, e);
}
if (rowIndex % batchSize == 0) {
doProcess(batchRowList);
batchRowList.clear();
}
}
/**
* 数据处理进行中回调模板函数。
*
* @param batchRowList 一批数据行。
*/
public abstract void doProcess(List<T> batchRowList);
/**
* 数据处理完毕回调模板函数。
*/
public abstract void doFinish();
/**
* 数据处理终端标记模板函数。
* @return 是否中断。true则中断后面的处理。
*/
public abstract boolean doInterrupt();
}
static class MyExcel07SaxReader<T> extends Excel07SaxReader {
private BaseImporter<T> importer;
MyExcel07SaxReader(BaseImporter<T> importer) {
super((sheetIndex, rowIndex, rowList) -> importer.doImport(rowIndex, rowList));
this.importer = importer;
}
@Override
public void endElement(String uri, String localName, String qName) {
super.endElement(uri, localName, qName);
if (importer.doInterrupt()) {
throw new MyRuntimeException();
}
}
@Override
public void endDocument() {
importer.doImport(-1, null);
}
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private ImportUtil() {
}
}

View File

@@ -0,0 +1,101 @@
package com.orange.demo.common.core.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
/**
* Ip工具类。
*
* @author Jerry
* @date 2020-09-25
*/
@Slf4j
public class IpUtil {
private static final String UNKNOWN = "unknown";
/**
* 通过Servlet的HttpRequest对象获取Ip地址。
*
* @param request HttpRequest对象。
* @return 本次请求的Ip地址。
*/
public static String getRemoteIpAddress(HttpServletRequest request) {
String ip = null;
//X-Forwarded-ForSquid 服务代理
String ipAddresses = request.getHeader("X-Forwarded-For");
if (StringUtils.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) {
//Proxy-Client-IPapache 服务代理
ipAddresses = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) {
//WL-Proxy-Client-IPweblogic 服务代理
ipAddresses = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) {
//HTTP_CLIENT_IP有些代理服务器
ipAddresses = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) {
//X-Real-IPnginx服务代理
ipAddresses = request.getHeader("X-Real-IP");
}
//有些网络通过多层代理那么获取到的ip就会有多个一般都是通过逗号,分割开来并且第一个ip为客户端的真实IP
if (StringUtils.isNotBlank(ipAddresses)) {
ip = ipAddresses.split(",")[0];
}
//还是不能获取到最后再通过request.getRemoteAddr();获取
if (StringUtils.isBlank(ipAddresses) || UNKNOWN.equalsIgnoreCase(ipAddresses)) {
ip = request.getRemoteAddr();
}
return ip;
}
public static String getFirstLocalIpAddress() {
String ip;
try {
List<String> ipList = getHostAddress();
// default the first
ip = (!ipList.isEmpty()) ? ipList.get(0) : "";
} catch (Exception ex) {
ip = "";
log.error("Failed to call ", ex);
}
return ip;
}
private static List<String> getHostAddress() throws SocketException {
List<String> ipList = new ArrayList<>(5);
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface ni = interfaces.nextElement();
Enumeration<InetAddress> allAddress = ni.getInetAddresses();
while (allAddress.hasMoreElements()) {
InetAddress address = allAddress.nextElement();
// skip the IPv6 addr
// skip the IPv6 addr
if (address.isLoopbackAddress() || address instanceof Inet6Address) {
continue;
}
String hostAddress = address.getHostAddress();
ipList.add(hostAddress);
}
}
return ipList;
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private IpUtil() {
}
}

View File

@@ -0,0 +1,110 @@
package com.orange.demo.common.core.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.Map;
/**
* 基于JWT的Token生成工具类
*
* @author Jerry
* @date 2020-09-25
*/
@Slf4j
public class JwtUtil {
private static final String TOKEN_PREFIX = "Bearer:";
private static final String CLAIM_KEY_CREATEDTIME = "CreatedTime";
/**
* Token缺省过期时间是30分钟
*/
private static final Long TOKEN_EXPIRATION = 1800000L;
/**
* 缺省情况下Token会每5分钟被刷新一次
*/
private static final Long REFRESH_TOKEN_INTERVAL = 300000L;
/**
* 生成加密后的JWT令牌生成的结果中包含令牌前缀如"Bearer "
*
* @param claims 令牌中携带的数据
* @param expirationMillisecond 过期的毫秒数
* @return 生成后的令牌信息
*/
public static String generateToken(Map<String, Object> claims, long expirationMillisecond, String signingKey) {
// 自动添加token的创建时间
long createTime = System.currentTimeMillis();
claims.put(CLAIM_KEY_CREATEDTIME, createTime);
String token = Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(createTime + expirationMillisecond))
.signWith(SignatureAlgorithm.HS512, signingKey)
.compact();
return TOKEN_PREFIX + token;
}
/**
* 生成加密后的JWT令牌生成的结果中包含令牌前缀如"Bearer "
*
* @param claims 令牌中携带的数据
* @return 生成后的令牌信息
*/
public static String generateToken(Map<String, Object> claims, String signingKey) {
return generateToken(claims, TOKEN_EXPIRATION, signingKey);
}
/**
* 获取token中的数据对象
*
* @param token 令牌信息(需要包含令牌前缀,如"Bearer:")
* @return 令牌中的数据对象解析视频返回null。
*/
public static Claims parseToken(String token, String signingKey) {
if (token == null || !token.startsWith(TOKEN_PREFIX)) {
return null;
}
String tokenKey = token.substring(TOKEN_PREFIX.length());
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(signingKey).parseClaimsJws(tokenKey).getBody();
} catch (Exception e) {
log.error("Token Expired", e);
}
return claims;
}
/**
* 判断令牌是否过期
*
* @param claims 令牌解密后的Map对象。
* @return true 过期否则false。
*/
public static boolean isNullOrExpired(Claims claims) {
return claims == null || claims.getExpiration().before(new Date());
}
/**
* 判断解密后的Token payload是否需要被强制刷新如果需要,则调用generateToken方法重新生成Token。
*
* @param claims Token解密后payload数据
* @return true 需要刷新否则false
*/
public static boolean needToRefresh(Claims claims) {
if (claims == null) {
return false;
}
Long createTime = (Long) claims.get(CLAIM_KEY_CREATEDTIME);
return createTime == null || System.currentTimeMillis() - createTime > REFRESH_TOKEN_INTERVAL;
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private JwtUtil() {
}
}

View File

@@ -0,0 +1,33 @@
package com.orange.demo.common.core.util;
/**
* 拼接日志消息的工具类。
* 主要目标是,尽量保证日志输出的统一性,同时也可以有效减少与日志信息相关的常量字符串,
* 提高代码的规范度和可维护性。
*
* @author Jerry
* @date 2020-09-25
*/
public class LogMessageUtil {
/**
* RPC调用错误格式。
*/
private static final String RPC_ERROR_MSG_FORMAT = "RPC Failed with Error message [%s]";
/**
* 组装RPC调用的错误信息。
*
* @param errorMsg 具体的错误信息。
* @return 格式化后的错误信息。
*/
public static String makeRpcError(String errorMsg) {
return String.format(RPC_ERROR_MSG_FORMAT, errorMsg);
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private LogMessageUtil() {
}
}

View File

@@ -0,0 +1,144 @@
package com.orange.demo.common.core.util;
import cn.hutool.crypto.digest.DigestUtil;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import java.util.UUID;
/**
* 脚手架中常用的基本工具方法集合,一般而言工程内部使用的方法。
*
* @author Jerry
* @date 2020-09-25
*/
public class MyCommonUtil {
private static Validator validator;
static {
validator = Validation.buildDefaultValidatorFactory().getValidator();
}
/**
* 创建uuid。
*
* @return 返回uuid。
*/
public static String generateUuid() {
return UUID.randomUUID().toString().replace("-", "");
}
/**
* 对用户密码进行加盐后加密。
*
* @param password 明文密码。
* @param passwordSalt 盐值。
* @return 加密后的密码。
*/
public static String encrptedPassword(String password, String passwordSalt) {
return DigestUtil.md5Hex(password + passwordSalt);
}
/**
* 这个方法一般用于Controller对于入口参数的基本验证。
* 对于字符串如果为空字符串也将视为Blank同时返回true。
*
* @param objs 一组参数。
* @return 返回是否存在null或空字符串的参数。
*/
public static boolean existBlankArgument(Object...objs) {
for (Object obj : objs) {
if (MyCommonUtil.isBlankOrNull(obj)) {
return true;
}
}
return false;
}
/**
* 结果和 existBlankArgument 相反。
*
* @param objs 一组参数。
* @return 返回是否存在null或空字符串的参数。
*/
public static boolean existNotBlankArgument(Object...objs) {
for (Object obj : objs) {
if (!MyCommonUtil.isBlankOrNull(obj)) {
return true;
}
}
return false;
}
/**
* 验证参数是否为空。
*
* @param obj 待判断的参数。
* @return 空或者null返回true否则false。
*/
public static boolean isBlankOrNull(Object obj) {
if (obj instanceof Collection) {
return CollectionUtils.isEmpty((Collection<?>) obj);
}
return obj == null || (obj instanceof CharSequence && StringUtils.isBlank((CharSequence) obj));
}
/**
* 验证参数是否为非空。
*
* @param obj 待判断的参数。
* @return 空或者null返回false否则true。
*/
public static boolean isNotBlankOrNull(Object obj) {
return !isBlankOrNull(obj);
}
/**
* 判断模型对象是否通过校验,没有通过返回具体的校验错误信息。
*
* @param model 带校验的model。
* @param groups Validate绑定的校验组。
* @return 没有错误返回null否则返回具体的错误信息。
*/
public static <T> String getModelValidationError(T model, Class<?>...groups) {
Set<ConstraintViolation<T>> constraintViolations = validator.validate(model, groups);
if (!constraintViolations.isEmpty()) {
Iterator<ConstraintViolation<T>> it = constraintViolations.iterator();
ConstraintViolation<T> constraint = it.next();
return constraint.getMessage();
}
return null;
}
/**
* 拼接参数中的字符串列表,用指定分隔符进行分割,同时每个字符串对象用单引号括起来。
*
* @param dataList 字符串集合。
* @param separator 分隔符。
* @return 拼接后的字符串。
*/
public static String joinString(Collection<String> dataList, final char separator) {
int index = 0;
StringBuilder sb = new StringBuilder(128);
for (String data : dataList) {
sb.append("'").append(data).append("'");
if (index++ != dataList.size() - 1) {
sb.append(separator);
}
}
return sb.toString();
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private MyCommonUtil() {
}
}

View File

@@ -0,0 +1,181 @@
package com.orange.demo.common.core.util;
import com.orange.demo.common.core.object.Tuple2;
import org.apache.commons.lang3.time.DateUtils;
import org.joda.time.DateTime;
import org.joda.time.Period;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.Date;
import static org.joda.time.PeriodType.days;
/**
* 日期工具类主要封装了部分joda-time中的方法让很多代码一行完成同时统一了日期到字符串的pattern格式。
*
* @author Jerry
* @date 2020-09-25
*/
public class MyDateUtil {
/**
* 统一的日期pattern今后可以根据自己的需求去修改。
*/
public static final String COMMON_DATE_FORMAT = "yyyy-MM-dd";
/**
* 统一的日期时间pattern今后可以根据自己的需求去修改。
*/
public static final String COMMON_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";
/**
* 缺省日期格式化器,提前获取提升运行时效率。
*/
private static final DateTimeFormatter DATE_PARSE_FORMATTER =
DateTimeFormat.forPattern(MyDateUtil.COMMON_DATE_FORMAT);
/**
* 缺省日期时间格式化器,提前获取提升运行时效率。
*/
private static final DateTimeFormatter DATETIME_PARSE_FORMATTER =
DateTimeFormat.forPattern(MyDateUtil.COMMON_DATETIME_FORMAT);
/**
* 获取一天的开始时间的字符串格式如2019-08-03 00:00:00.000。
*
* @param dateTime 待格式化的日期时间对象。
* @return 格式化后的字符串。
*/
public static String getBeginTimeOfDay(DateTime dateTime) {
return dateTime.withTimeAtStartOfDay().toString(COMMON_DATETIME_FORMAT);
}
/**
* 获取一天的结束时间的字符串格式如2019-08-03 23:59:59.999。
*
* @param dateTime 待格式化的日期时间对象。
* @return 格式化后的字符串。
*/
public static String getEndTimeOfDay(DateTime dateTime) {
return dateTime.withTime(23, 59, 59, 999).toString(COMMON_DATETIME_FORMAT);
}
/**
* 获取一天中的开始时间和结束时间的字符串格式如2019-08-03 00:00:00.000 和 2019-08-03 23:59:59.999。
*
* @param dateTime 待格式化的日期时间对象。
* @return 包含格式后字符串的二元组对象。
*/
public static Tuple2<String, String> getDateTimeRangeOfDay(DateTime dateTime) {
return new Tuple2<>(getBeginTimeOfDay(dateTime), getEndTimeOfDay(dateTime));
}
/**
* 获取本月第一天的日期格式。如2019-08-01。
*
* @param dateTime 待格式化的日期对象。
* @return 格式化后的字符串。
*/
public static String getBeginDateOfMonth(DateTime dateTime) {
return dateTime.withDayOfMonth(1).toString(COMMON_DATE_FORMAT);
}
/**
* 获取本月第一天的日期格式。如2019-08-01。
*
* @param dateString 待格式化的日期字符串对象。
* @return 格式化后的字符串。
*/
public static String getBeginDateOfMonth(String dateString) {
DateTime dateTime = toDate(dateString);
return dateTime.withDayOfMonth(1).toString(COMMON_DATE_FORMAT);
}
/**
* 计算指定日期距离今天相差的天数。
*
* @param dateTime 待格式化的日期时间对象。
* @return 相差天数。
*/
public static int getDayDiffToNow(DateTime dateTime) {
return new Period(dateTime, new DateTime(), days()).getDays();
}
/**
* 将日期对象格式化为缺省的字符串格式。
*
* @param dateTime 待格式化的日期对象。
* @return 格式化后的字符串。
*/
public static String toDateString(DateTime dateTime) {
return dateTime.toString(COMMON_DATE_FORMAT);
}
/**
* 将日期时间对象格式化为缺省的字符串格式。
*
* @param dateTime 待格式化的日期对象。
* @return 格式化后的字符串。
*/
public static String toDateTimeString(DateTime dateTime) {
return dateTime.toString(COMMON_DATETIME_FORMAT);
}
/**
* 将缺省格式的日期字符串解析为日期对象。
*
* @param dateString 待解析的字符串。
* @return 解析后的日期对象。
*/
public static DateTime toDate(String dateString) {
return DATE_PARSE_FORMATTER.parseDateTime(dateString);
}
/**
* 将缺省格式的日期字符串解析为日期对象。
*
* @param dateTimeString 待解析的字符串。
* @return 解析后的日期对象。
*/
public static DateTime toDateTime(String dateTimeString) {
return DATETIME_PARSE_FORMATTER.parseDateTime(dateTimeString);
}
/**
* 截取时间到天。如2019-10-03 01:20:30 转换为 2019-10-03 00:00:00。
* 由于没有字符串的中间转换,因此效率更高。
*
* @param date 待截取日期对象。
* @return 转换后日期对象。
*/
public static Date truncateToDay(Date date) {
return DateUtils.truncate(date, Calendar.DAY_OF_MONTH);
}
/**
* 截取时间到月。如2019-10-03 01:20:30 转换为 2019-10-01 00:00:00。
* 由于没有字符串的中间转换,因此效率更高。
*
* @param date 待截取日期对象。
* @return 转换后日期对象。
*/
public static Date truncateToMonth(Date date) {
return DateUtils.truncate(date, Calendar.MONTH);
}
/**
* 截取时间到年。如2019-10-03 01:20:30 转换为 2019-01-01 00:00:00。
* 由于没有字符串的中间转换,因此效率更高。
*
* @param date 待截取日期对象。
* @return 转换后日期对象。
*/
public static Date truncateToYear(Date date) {
return DateUtils.truncate(date, Calendar.YEAR);
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private MyDateUtil() {
}
}

View File

@@ -0,0 +1,437 @@
package com.orange.demo.common.core.util;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ReflectUtil;
import com.orange.demo.common.core.annotation.RelationConstDict;
import com.orange.demo.common.core.annotation.RelationDict;
import com.orange.demo.common.core.annotation.RelationOneToOne;
import com.orange.demo.common.core.exception.MyRuntimeException;
import com.orange.demo.common.core.object.Tuple2;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import tk.mybatis.mapper.entity.Example;
import javax.persistence.Column;
import javax.persistence.Table;
import javax.persistence.Transient;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 负责Model数据操作、类型转换和关系关联等行为的工具类。
*
* @author Jerry
* @date 2020-09-25
*/
@Slf4j
public class MyModelUtil {
/**
* 数值型字段。
*/
public static final Integer NUMERIC_FIELD_TYPE = 0;
/**
* 字符型字段。
*/
public static final Integer STRING_FIELD_TYPE = 1;
/**
* 日期型字段。
*/
public static final Integer DATE_FIELD_TYPE = 2;
/**
* mapToColumnName和mapToColumnInfo使用的缓存。
*/
private static Map<String, Tuple2<String, Integer>> cachedColumnInfoMap = new ConcurrentHashMap<>();
/**
* 映射Model对象的字段反射对象获取与该字段对应的数据库列名称。
*
* @param field 字段反射对象。
* @param modelClazz Model对象的Class类。
* @return 该字段所对应的数据表列名称。
*/
public static String mapToColumnName(Field field, Class<?> modelClazz) {
return mapToColumnName(field.getName(), modelClazz);
}
/**
* 映射Model对象的字段名称获取与该字段对应的数据库列名称。
*
* @param fieldName 字段名称。
* @param modelClazz Model对象的Class类。
* @return 该字段所对应的数据表列名称。
*/
public static String mapToColumnName(String fieldName, Class<?> modelClazz) {
Tuple2<String, Integer> columnInfo = mapToColumnInfo(fieldName, modelClazz);
return columnInfo == null ? null : columnInfo.getFirst();
}
/**
* 映射Model对象的字段名称获取与该字段对应的数据库列名称和字段类型。
*
* @param fieldName 字段名称。
* @param modelClazz Model对象的Class类。
* @return 该字段所对应的数据表列名称和Java字段类型。
*/
public static Tuple2<String, Integer> mapToColumnInfo(String fieldName, Class<?> modelClazz) {
if (StringUtils.isBlank(fieldName)) {
return null;
}
StringBuilder sb = new StringBuilder(128);
sb.append(modelClazz.getName()).append("-#-").append(fieldName);
Tuple2<String, Integer> columnInfo = cachedColumnInfoMap.get(sb.toString());
if (columnInfo == null) {
Field field = ReflectUtil.getField(modelClazz, fieldName);
if (field == null) {
return null;
}
Column c = field.getAnnotation(Column.class);
String typeName = field.getType().getSimpleName();
String columnName = c == null ? fieldName : c.name();
// 这里缺省情况下都是按照整型去处理,因为他覆盖太多的类型了。
// 如Integer/Long/Double/BigDecimal可根据实际情况完善和扩充。
Integer type = NUMERIC_FIELD_TYPE;
if (String.class.getSimpleName().equals(typeName)) {
type = STRING_FIELD_TYPE;
} else if (Date.class.getSimpleName().equals(typeName)) {
type = DATE_FIELD_TYPE;
}
columnInfo = new Tuple2<>(columnName, type);
cachedColumnInfoMap.put(sb.toString(), columnInfo);
}
return columnInfo;
}
/**
* 映射Model主对象的Class名称到Model所对应的表名称。
*
* @param modelClazz Model主对象的Class。
* @return Model对象对应的数据表名称。
*/
public static String mapToTableName(Class<?> modelClazz) {
Table t = modelClazz.getAnnotation(Table.class);
return t == null ? null : t.name();
}
/**
* 主Model类型中遍历所有包含RelationConstDict注解的字段并将关联的静态字典中的数据
* 填充到thisModel对象的被注解字段中。
*
* @param thisClazz 主对象的Class对象。
* @param thisModel 主对象。
* @param <T> 主表对象类型。
*/
@SuppressWarnings("unchecked")
public static <T> void makeConstDictRelation(Class<T> thisClazz, T thisModel) {
if (thisModel == null) {
return;
}
Field[] fields = ReflectUtil.getFields(thisClazz);
for (Field field : fields) {
// 这里不做任何空值判断,从而让配置错误在调试期间即可抛出
Field thisTargetField = ReflectUtil.getField(thisClazz, field.getName());
RelationConstDict r = thisTargetField.getAnnotation(RelationConstDict.class);
if (r == null) {
continue;
}
Field dictMapField = ReflectUtil.getField(r.constantDictClass(), "DICT_MAP");
Map<Object, String> dictMap = (Map<Object, String>) ReflectUtil.getFieldValue(thisClazz, dictMapField);
Object id = ReflectUtil.getFieldValue(thisModel, r.masterIdField());
if (id != null) {
String name = dictMap.get(id);
if (name != null) {
Map<String, Object> m = new HashMap<>(2);
m.put("id", id);
m.put("name", name);
ReflectUtil.setFieldValue(thisModel, thisTargetField, m);
}
}
}
}
/**
* 在主Model类型中根据thisRelationField字段的RelationDict注解参数将被关联对象thatModel中的数据
* 关联到thisModel对象的thisRelationField字段中。
*
* @param thisClazz 主对象的Class对象。
* @param thisModel 主对象。
* @param thatModel 字典关联对象。
* @param thisRelationField 主表对象中保存被关联对象的字段名称。
* @param <T> 主表对象类型。
* @param <R> 从表对象类型。
*/
public static <T, R> void makeDictRelation(
Class<T> thisClazz, T thisModel, R thatModel, String thisRelationField) {
if (thatModel == null || thisModel == null) {
return;
}
// 这里不做任何空值判断,从而让配置错误在调试期间即可抛出
Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField);
RelationDict r = thisTargetField.getAnnotation(RelationDict.class);
Class<?> thatClass = r.slaveModelClass();
Field slaveIdField = ReflectUtil.getField(thatClass, r.slaveIdField());
Field slaveNameField = ReflectUtil.getField(thatClass, r.slaveNameField());
Map<String, Object> m = new HashMap<>(2);
m.put("id", ReflectUtil.getFieldValue(thatModel, slaveIdField));
m.put("name", ReflectUtil.getFieldValue(thatModel, slaveNameField));
ReflectUtil.setFieldValue(thisModel, thisTargetField, m);
}
/**
* 在主Model类型中根据thisRelationField字段的RelationDict注解参数将被关联对象集合thatModelList中的数据
* 逐个关联到thisModelList每一个元素的thisRelationField字段中。
*
* @param thisClazz 主对象的Class对象。
* @param thisModelList 主对象列表。
* @param thatModelList 字典关联对象列表集合。
* @param thisRelationField 主表对象中保存被关联对象的字段名称。
* @param <T> 主表对象类型。
* @param <R> 从表对象类型。
*/
public static <T, R> void makeDictRelation(
Class<T> thisClazz, List<T> thisModelList, List<R> thatModelList, String thisRelationField) {
if (CollectionUtils.isEmpty(thatModelList)
|| CollectionUtils.isEmpty(thisModelList)) {
return;
}
// 这里不做任何空值判断,从而让配置错误在调试期间即可抛出
Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField);
RelationDict r = thisTargetField.getAnnotation(RelationDict.class);
Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField());
Class<?> thatClass = r.slaveModelClass();
Field slaveIdField = ReflectUtil.getField(thatClass, r.slaveIdField());
Field slaveNameField = ReflectUtil.getField(thatClass, r.slaveNameField());
Map<Object, R> thatMap = new HashMap<>(20);
thatModelList.forEach(thatModel -> {
Object id = ReflectUtil.getFieldValue(thatModel, slaveIdField);
thatMap.put(id, thatModel);
});
thisModelList.forEach(thisModel -> {
if (thisModel != null) {
Object id = ReflectUtil.getFieldValue(thisModel, masterIdField);
R thatModel = thatMap.get(id);
if (thatModel != null) {
Map<String, Object> m = new HashMap<>(4);
m.put("id", id);
m.put("name", ReflectUtil.getFieldValue(thatModel, slaveNameField));
ReflectUtil.setFieldValue(thisModel, thisTargetField, m);
}
}
});
}
/**
* 在主Model类型中根据thisRelationField字段的RelationDict注解参数将被关联对象集合thatModelMap中的数据
* 逐个关联到thisModelList每一个元素的thisRelationField字段中。
* 该函数之所以使用Map主要出于性能优化考虑在连续使用thatModelMap进行关联时有效的避免了从多次从List转换到Map的过程。
*
* @param thisClazz 主对象的Class对象。
* @param thisModelList 主对象列表。
* @param thatMadelMap 字典关联对象映射集合。
* @param thisRelationField 主表对象中保存被关联对象的字段名称。
* @param <T> 主表对象类型。
* @param <R> 从表对象类型。
*/
public static <T, R> void makeDictRelation(
Class<T> thisClazz, List<T> thisModelList, Map<Object, R> thatMadelMap, String thisRelationField) {
if (MapUtils.isEmpty(thatMadelMap)
|| CollectionUtils.isEmpty(thisModelList)) {
return;
}
// 这里不做任何空值判断,从而让配置错误在调试期间即可抛出
Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField);
RelationDict r = thisTargetField.getAnnotation(RelationDict.class);
Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField());
Class<?> thatClass = r.slaveModelClass();
Field slaveNameField = ReflectUtil.getField(thatClass, r.slaveNameField());
thisModelList.forEach(thisModel -> {
if (thisModel != null) {
Object id = ReflectUtil.getFieldValue(thisModel, masterIdField);
R thatModel = thatMadelMap.get(id);
if (thatModel != null) {
Map<String, Object> m = new HashMap<>(4);
m.put("id", id);
m.put("name", ReflectUtil.getFieldValue(thatModel, slaveNameField));
ReflectUtil.setFieldValue(thisModel, thisTargetField, m);
}
}
});
}
/**
* 在主Model类型中根据thisRelationField字段的RelationOneToOne注解参数将被关联对象列表thatModelList中的数据
* 逐个关联到thisModelList每一个元素的thisRelationField字段中。
*
* @param thisClazz 主对象的Class对象。
* @param thisModelList 主对象列表。
* @param thatModelList 一对一关联对象列表。
* @param thisRelationField 主表对象中保存被关联对象的字段名称。
* @param <T> 主表对象类型。
* @param <R> 从表对象类型。
*/
public static <T, R> void makeOneToOneRelation(
Class<T> thisClazz, List<T> thisModelList, List<R> thatModelList, String thisRelationField) {
if (CollectionUtils.isEmpty(thatModelList)
|| CollectionUtils.isEmpty(thisModelList)) {
return;
}
// 这里不做任何空值判断,从而让配置错误在调试期间即可抛出
Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField);
RelationOneToOne r = thisTargetField.getAnnotation(RelationOneToOne.class);
Field masterIdField = ReflectUtil.getField(thisClazz, r.masterIdField());
Class<?> thatClass = r.slaveModelClass();
Field slaveIdField = ReflectUtil.getField(thatClass, r.slaveIdField());
Map<Object, R> thatMap = new HashMap<>(20);
thatModelList.forEach(thatModel -> {
Object id = ReflectUtil.getFieldValue(thatModel, slaveIdField);
thatMap.put(id, thatModel);
});
// 判断放在循环的外部,提升一点儿效率。
if (thisTargetField.getType().equals(Map.class)) {
thisModelList.forEach(thisModel -> {
Object id = ReflectUtil.getFieldValue(thisModel, masterIdField);
R thatModel = thatMap.get(id);
if (thatModel != null) {
ReflectUtil.setFieldValue(thisModel, thisTargetField, BeanUtil.beanToMap(thatModel));
}
});
} else {
thisModelList.forEach(thisModel -> {
Object id = ReflectUtil.getFieldValue(thisModel, masterIdField);
R thatModel = thatMap.get(id);
if (thatModel != null) {
ReflectUtil.setFieldValue(thisModel, thisTargetField, thatModel);
}
});
}
}
/**
* 根据主对象和关联对象各自的关联Id函数将主对象列表和关联对象列表中的数据关联到一起并将关联对象
* 设置到主对象的指定关联字段中。
* NOTE: 用于主对象关联字段中没有包含RelationOneToOne注解的场景。
*
* @param thisClazz 主对象的Class对象。
* @param thisModelList 主对象列表。
* @param thisIdGetterFunc 主对象Id的Getter函数。
* @param thatModelList 关联对象列表。
* @param thatIdGetterFunc 关联对象Id的Getter函数。
* @param thisRelationField 主对象中保存被关联对象的字段名称。
* @param <T> 主表对象类型。
* @param <R> 从表对象类型。
*/
public static <T, R> void makeOneToOneRelation(
Class<T> thisClazz,
List<T> thisModelList,
Function<T, Object> thisIdGetterFunc,
List<R> thatModelList,
Function<R, Object> thatIdGetterFunc,
String thisRelationField) {
makeOneToOneRelation(thisClazz, thisModelList,
thisIdGetterFunc, thatModelList, thatIdGetterFunc, thisRelationField, false);
}
/**
* 根据主对象和关联对象各自的关联Id函数将主对象列表和关联对象列表中的数据关联到一起并将关联对象
* 设置到主对象的指定关联字段中。
* NOTE: 用于主对象关联字段中没有包含RelationOneToOne注解的场景。
*
* @param thisClazz 主对象的Class对象。
* @param thisModelList 主对象列表。
* @param thisIdGetterFunc 主对象Id的Getter函数。
* @param thatModelList 关联对象列表。
* @param thatIdGetterFunc 关联对象Id的Getter函数。
* @param thisRelationField 主对象中保存被关联对象的字段名称。
* @param orderByThatList 如果为true则按照ThatModelList的顺序输出。同时thisModelList被排序后的新列表替换。
* @param <T> 主表对象类型。
* @param <R> 从表对象类型。
*/
public static <T, R> void makeOneToOneRelation(
Class<T> thisClazz,
List<T> thisModelList,
Function<T, Object> thisIdGetterFunc,
List<R> thatModelList,
Function<R, Object> thatIdGetterFunc,
String thisRelationField,
boolean orderByThatList) {
Field thisTargetField = ReflectUtil.getField(thisClazz, thisRelationField);
boolean isMap = thisTargetField.getType().equals(Map.class);
if (orderByThatList) {
List<T> newThisModelList = new LinkedList<>();
Map<Object, ? extends T> thisModelMap =
thisModelList.stream().collect(Collectors.toMap(thisIdGetterFunc, c -> c));
thatModelList.forEach(thatModel -> {
Object thatId = thatIdGetterFunc.apply(thatModel);
T thisModel = thisModelMap.get(thatId);
if (thisModel != null) {
ReflectUtil.setFieldValue(thisModel, thisTargetField, normalize(isMap, thatModel));
newThisModelList.add(thisModel);
}
});
thisModelList.clear();
thisModelList.addAll(newThisModelList);
} else {
Map<Object, R> thatMadelMap =
thatModelList.stream().collect(Collectors.toMap(thatIdGetterFunc, c -> c));
thisModelList.forEach(thisModel -> {
Object thisId = thisIdGetterFunc.apply(thisModel);
R thatModel = thatMadelMap.get(thisId);
if (thatModel != null) {
ReflectUtil.setFieldValue(thisModel, thisTargetField, normalize(isMap, thatModel));
}
});
}
}
private static <M> Object normalize(boolean isMap, M model) {
return isMap ? BeanUtil.beanToMap(model) : model;
}
/**
* 转换过滤对象到与其等效的Example对象。
*
* @param filterModel 过滤对象。
* @param modelClass 过滤对象的Class对象。
* @param <T> 过滤对象类型。
* @return 转换后的Example对象。
*/
public static <T> Example convertFilterModelToExample(T filterModel, Class<T> modelClass) {
if (filterModel == null) {
return null;
}
Example e = new Example(modelClass);
Example.Criteria c = e.createCriteria();
Field[] fields = ReflectUtil.getFields(modelClass);
for (Field field : fields) {
if (field.getAnnotation(Transient.class) != null) {
continue;
}
int modifiers = field.getModifiers();
// transient类型的字段不能作为查询条件
if ((modifiers & 128) == 0) {
ReflectUtil.setAccessible(field);
try {
Object o = field.get(filterModel);
if (o != null) {
c.andEqualTo(field.getName(), field.get(filterModel));
}
} catch (IllegalAccessException ex) {
log.error("Failed to call reflection code.", ex);
throw new MyRuntimeException(ex);
}
}
}
return e;
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private MyModelUtil() {
}
}

View File

@@ -0,0 +1,72 @@
package com.orange.demo.common.core.util;
import cn.jimmyshi.beanquery.BeanQuery;
import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.Page;
import java.util.List;
/**
* 生成带有分页信息的数据列表
*
* @author Jerry
* @date 2020-09-25
*/
public class MyPageUtil {
private static final String DATA_LIST_LITERAL = "dataList";
private static final String TOTAL_COUNT_LITERAL = "totalCount";
/**
* 用户构建带有分页信息的数据列表。
*
* @param dataList 数据列表该参数必须是调用PageMethod.startPage之后立即执行mybatis查询操作的结果集。
* @param includeFields 结果集中需要返回到前端的字段,多个字段之间逗号分隔。
* @return 返回只是包含includeFields字段的数据列表以及结果集TotalCount。
*/
public static <T> JSONObject makeResponseData(List<T> dataList, String includeFields) {
JSONObject pageData = new JSONObject();
pageData.put(DATA_LIST_LITERAL, BeanQuery.select(includeFields).from(dataList).execute());
if (dataList instanceof Page) {
pageData.put(TOTAL_COUNT_LITERAL, ((Page<?>)dataList).getTotal());
}
return pageData;
}
/**
* 用户构建带有分页信息的数据列表。
*
* @param dataList 数据列表该参数必须是调用PageMethod.startPage之后立即执行mybatis查询操作的结果集。
* @return 返回结果集和TotalCount。
*/
public static <T> JSONObject makeResponseData(List<T> dataList) {
JSONObject pageData = new JSONObject();
pageData.put(DATA_LIST_LITERAL, dataList);
if (dataList instanceof Page) {
pageData.put(TOTAL_COUNT_LITERAL, ((Page<?>)dataList).getTotal());
}
return pageData;
}
/**
* 用户构建带有分页信息的数据列表。
*
* @param dataList 数据列表该参数必须是调用PageMethod.startPage之后立即执行mybatis查询操作的结果集。
* @param totalCount 总数量。
* @return 返回结果集和TotalCount。
*/
public static <T> JSONObject makeResponseData(List<T> dataList, Long totalCount) {
JSONObject pageData = new JSONObject();
pageData.put(DATA_LIST_LITERAL, dataList);
if (totalCount != null) {
pageData.put(TOTAL_COUNT_LITERAL, totalCount);
}
return pageData;
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private MyPageUtil() {
}
}

View File

@@ -0,0 +1,114 @@
package com.orange.demo.common.core.util;
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
* Java RSA 加密工具类。
*
* @author Jerry
* @date 2020-09-25
*/
public class RsaUtil {
/**
* 密钥长度 于原文长度对应 以及越长速度越慢
*/
private static final int KEY_SIZE = 1024;
/**
* 用于封装随机产生的公钥与私钥
*/
private static Map<Integer, String> keyMap = new HashMap<>();
/**
* 随机生成密钥对。
*/
public static void genKeyPair() throws NoSuchAlgorithmException {
// KeyPairGenerator类用于生成公钥和私钥对基于RSA算法生成对象
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
// 初始化密钥对生成器
keyPairGen.initialize(KEY_SIZE, new SecureRandom());
// 生成一个密钥对保存在keyPair中
KeyPair keyPair = keyPairGen.generateKeyPair();
// 得到私钥
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// 得到公钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
String publicKeyString = Base64.getEncoder().encodeToString(publicKey.getEncoded());
// 得到私钥字符串
String privateKeyString = Base64.getEncoder().encodeToString(privateKey.getEncoded());
// 将公钥和私钥保存到Map
//0表示公钥
keyMap.put(0, publicKeyString);
//1表示私钥
keyMap.put(1, privateKeyString);
}
/**
* RSA公钥加密。
*
* @param str 加密字符串
* @param publicKey 公钥
* @return 密文
* @throws Exception 加密过程中的异常信息
*/
public static String encrypt(String str, String publicKey) throws Exception {
//base64编码的公钥
byte[] decoded = Base64.getDecoder().decode(publicKey);
RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
//RSA加密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
return Base64.getEncoder().encodeToString(cipher.doFinal(str.getBytes(StandardCharsets.UTF_8)));
}
/**
* RSA私钥解密。
*
* @param str 加密字符串
* @param privateKey 私钥
* @return 明文
* @throws Exception 解密过程中的异常信息
*/
public static String decrypt(String str, String privateKey) throws Exception {
//64位解码加密后的字符串
byte[] inputByte = Base64.getDecoder().decode(str);
//base64编码的私钥
byte[] decoded = Base64.getDecoder().decode(privateKey);
RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
//RSA解密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, priKey);
return new String(cipher.doFinal(inputByte));
}
public static void main(String[] args) throws Exception {
long temp = System.currentTimeMillis();
//生成公钥和私钥
genKeyPair();
//加密字符串
System.out.println("公钥:" + keyMap.get(0));
System.out.println("私钥:" + keyMap.get(1));
System.out.println("生成密钥消耗时间:" + (System.currentTimeMillis() - temp) / 1000.0 + "");
System.out.println("生成后的公钥前端使用!");
System.out.println("生成后的私钥后台使用!");
String message = "RSA测试ABCD~!@#$";
System.out.println("原文:" + message);
temp = System.currentTimeMillis();
String messageEn = encrypt(message, keyMap.get(0));
System.out.println("密文:" + messageEn);
System.out.println("加密消耗时间:" + (System.currentTimeMillis() - temp) / 1000.0 + "");
temp = System.currentTimeMillis();
String messageDe = decrypt(messageEn, keyMap.get(1));
System.out.println("解密:" + messageDe);
System.out.println("解密消耗时间:" + (System.currentTimeMillis() - temp) / 1000.0 + "");
}
}

View File

@@ -0,0 +1,93 @@
package com.orange.demo.common.core.util;
import lombok.Data;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
/**
* 将列表结构组建为树结构的工具类。
*
* @param <T> 对象类型。
* @param <K> 节点之间关联键的类型。
*
* @author Jerry
* @date 2020-09-25
*/
@Data
public class TreeNode<T, K> {
private K id;
private K parentId;
private T data;
private List<TreeNode<T, K>> childList = new ArrayList<>();
/**
* 将列表结构组建为树结构的工具方法。
*
* @param dataList 数据列表结构。
* @param idFunc 获取关联id的函数对象。
* @param parentIdFunc 获取关联ParentId的函数对象。
* @param root 根节点。
* @param <T> 数据对象类型。
* @param <K> 节点之间关联键的类型。
* @return 源数据对象的树结构存储。
*/
public static <T, K> List<TreeNode<T, K>> build(
List<T> dataList, Function<T, K> idFunc, Function<T, K> parentIdFunc, K root) {
List<TreeNode<T, K>> treeNodeList = new ArrayList<>();
for (T data : dataList) {
if (parentIdFunc.apply(data).equals(idFunc.apply(data))) {
continue;
}
TreeNode<T, K> dataNode = new TreeNode<>();
dataNode.setId(idFunc.apply(data));
dataNode.setParentId(parentIdFunc.apply(data));
dataNode.setData(data);
treeNodeList.add(dataNode);
}
return root == null ? toBuildTreeWithoutRoot(treeNodeList) : toBuildTree(treeNodeList, root);
}
private static <T, K> List<TreeNode<T, K>> toBuildTreeWithoutRoot(List<TreeNode<T, K>> treeNodes) {
Map<K, TreeNode<T, K>> treeNodeMap = new HashMap<>(treeNodes.size());
for (TreeNode<T, K> treeNode : treeNodes) {
treeNodeMap.put(treeNode.id, treeNode);
}
List<TreeNode<T, K>> treeNodeList = new ArrayList<>();
for (TreeNode<T, K> treeNode : treeNodes) {
TreeNode<T, K> parentNode = treeNodeMap.get(treeNode.getParentId());
if (parentNode == null) {
treeNodeList.add(treeNode);
} else {
parentNode.add(treeNode);
}
}
return treeNodeList;
}
private static <T, K> List<TreeNode<T, K>> toBuildTree(List<TreeNode<T, K>> treeNodes, K root) {
List<TreeNode<T, K>> treeNodeList = new ArrayList<>();
for (TreeNode<T, K> treeNode : treeNodes) {
if (root.equals(treeNode.getParentId())) {
treeNodeList.add(treeNode);
}
for (TreeNode<T, K> it : treeNodes) {
if (it.getParentId() == treeNode.getId()) {
if (treeNode.getChildList() == null) {
treeNode.setChildList(new ArrayList<>());
}
treeNode.add(it);
}
}
}
return treeNodeList;
}
private void add(TreeNode<T, K> node) {
childList.add(node);
}
}

View File

@@ -0,0 +1,180 @@
package com.orange.demo.common.core.util;
import com.alibaba.fastjson.JSON;
import com.orange.demo.common.core.constant.ApplicationConstant;
import com.orange.demo.common.core.constant.ErrorCodeEnum;
import com.orange.demo.common.core.object.ResponseResult;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
/**
* 上传或下载附件文件的工具类。
*
* @author Jerry
* @date 2020-09-25
*/
@Slf4j
public class UpDownloadUtil {
/**
* 执行下载操作并将读取的文件数据直接写入到HttpServletResponse应答对象。
*
* @param rootBaseDir 文件下载的根目录。
* @param modelName 所在数据表的实体对象名。
* @param fieldName 关联字段的实体对象属性名。
* @param fileName 文件名。
* @param asImage 是否为图片对象。图片是无需权限验证的,因此和附件存放在不同的子目录。
* @param response Http 应答对象。
*/
public static void doDownload(
String rootBaseDir,
String modelName,
String fieldName,
String fileName,
Boolean asImage,
HttpServletResponse response) {
StringBuilder uploadPathBuilder = new StringBuilder(128);
uploadPathBuilder.append(rootBaseDir).append("/");
if (Boolean.TRUE.equals(asImage)) {
uploadPathBuilder.append(ApplicationConstant.UPLOAD_IMAGE_PARENT_PATH);
} else {
uploadPathBuilder.append(ApplicationConstant.UPLOAD_ATTACHMENT_PARENT_PATH);
}
uploadPathBuilder.append("/").append(modelName).append("/").append(fieldName).append("/").append(fileName);
File file = new File(uploadPathBuilder.toString());
if (!file.exists()) {
log.warn("Download file [" + uploadPathBuilder.toString() + "] failed, no file found!");
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
response.setHeader("content-type", "application/octet-stream");
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
byte[] buff = new byte[2048];
try (OutputStream os = response.getOutputStream();
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
int i = bis.read(buff);
while (i != -1) {
os.write(buff, 0, buff.length);
os.flush();
i = bis.read(buff);
}
} catch (IOException e) {
log.error("Failed to call UpDownloadUtil.doDownload", e);
}
}
/**
* 执行文件上传操作并将与该文件下载对应的Url直接写入到HttpServletResponse应答对象返回给前端。
*
* @param rootBaseDir 存放上传文件的根目录。
* @param modelName 所在数据表的实体对象名。
* @param fieldName 关联字段的实体对象属性名。
* @param uploadFile Http请求中上传的文件对象。
* @param asImage 是否为图片对象。图片是无需权限验证的,因此和附件存放在不同的子目录。
* @param response Http 应答对象。
* @return 存储在本地上传文件名。
* @throws IOException 文件操作错误。
*/
public static String doUpload(
String rootBaseDir,
String modelName,
String fieldName,
Boolean asImage,
MultipartFile uploadFile,
HttpServletResponse response) throws IOException {
PrintWriter out = response.getWriter();
response.setContentType("application/json; charset=utf-8");
if (Objects.isNull(uploadFile) || uploadFile.isEmpty() || MyCommonUtil.isBlankOrNull(fieldName)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
out.print(JSON.toJSONString(ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FILE_ARGUMENT)));
return null;
}
StringBuilder uploadPathBuilder = new StringBuilder(128);
uploadPathBuilder.append(rootBaseDir).append("/");
if (Boolean.TRUE.equals(asImage)) {
uploadPathBuilder.append(ApplicationConstant.UPLOAD_IMAGE_PARENT_PATH);
} else {
uploadPathBuilder.append(ApplicationConstant.UPLOAD_ATTACHMENT_PARENT_PATH);
}
uploadPathBuilder.append("/").append(modelName).append("/").append(fieldName).append("/");
// 根据请求上传的uri构建下载uri只是将末尾的/upload改为/download即可。
HttpServletRequest request = ContextUtil.getHttpRequest();
String uri = request.getRequestURI();
uri = StringUtils.removeEnd(uri, "/");
uri = StringUtils.removeEnd(uri, "/upload");
String downloadUri = uri + "/download";
StringBuilder filenameBuilder = new StringBuilder(64);
filenameBuilder.append(MyCommonUtil.generateUuid())
.append(".").append(FilenameUtils.getExtension(uploadFile.getOriginalFilename()));
UploadFileInfo fileInfo = new UploadFileInfo();
fileInfo.downloadUri = downloadUri;
fileInfo.filename = filenameBuilder.toString();
try {
byte[] bytes = uploadFile.getBytes();
Path path = Paths.get(uploadPathBuilder.toString() + filenameBuilder.toString());
//如果没有files文件夹则创建
if (!Files.isWritable(path)) {
Files.createDirectories(Paths.get(uploadPathBuilder.toString()));
}
//文件写入指定路径
Files.write(path, bytes);
} catch (IOException e) {
log.error("Failed to write uploaded file [" + uploadFile.getOriginalFilename() + " ].", e);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
out.print(JSON.toJSONString(ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FILE_IOERROR)));
return null;
}
out.print(JSON.toJSONString(ResponseResult.success(fileInfo)));
out.flush();
out.close();
return fileInfo.filename;
}
/**
* 判断filename参数指定的文件名是否被包含在fileInfoJson参数中。
*
* @param fileInfoJson 内部类UploadFileInfo的JSONArray数组。
* @param filename 被包含的文件名。
* @return 存在返回true否则false。
*/
public static boolean containFile(String fileInfoJson, String filename) {
if (StringUtils.isAnyBlank(fileInfoJson, filename)) {
return false;
}
List<UploadFileInfo> fileInfoList = JSON.parseArray(fileInfoJson, UploadFileInfo.class);
if (CollectionUtils.isNotEmpty(fileInfoList)) {
for (UploadFileInfo fileInfo : fileInfoList) {
if (StringUtils.equals(filename, fileInfo.filename)) {
return true;
}
}
}
return false;
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private UpDownloadUtil() {
}
@Data
static class UploadFileInfo {
private String downloadUri;
private String filename;
}
}

View File

@@ -0,0 +1,10 @@
package com.orange.demo.common.core.validator;
/**
* 数据增加的验证分组。通常用于数据新增场景。
*
* @author Jerry
* @date 2020-09-25
*/
public interface AddGroup {
}

View File

@@ -0,0 +1,48 @@
package com.orange.demo.common.core.validator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 定义在Model对象中标注字段值引用自指定的常量字典和ConstDictRefValidator对象配合完成数据验证。
*
* @author Jerry
* @date 2020-09-25
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ConstDictValidator.class)
public @interface ConstDictRef {
/**
* 引用的常量字典对象该对象必须包含isValid的静态方法。
*
* @return 最大长度。
*/
Class<?> constDictClass();
/**
* 超过边界后的错误消息提示。
*
* @return 错误提示。
*/
String message() default "无效的字典引用值!";
/**
* 验证分组。
*
* @return 验证分组。
*/
Class<?>[] groups() default {};
/**
* 载荷对象类型。
*
* @return 载荷对象。
*/
Class<? extends Payload>[] payload() default {};
}

View File

@@ -0,0 +1,33 @@
package com.orange.demo.common.core.validator;
import cn.hutool.core.util.ReflectUtil;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.reflect.Method;
/**
* 数据字段自定义验证用于验证Model中字符串字段的最大长度和最小长度。
*
* @author Jerry
* @date 2020-09-25
*/
public class ConstDictValidator implements ConstraintValidator<ConstDictRef, Object> {
private ConstDictRef constDictRef;
@Override
public void initialize(ConstDictRef constDictRef) {
this.constDictRef = constDictRef;
}
@Override
public boolean isValid(Object s, ConstraintValidatorContext constraintValidatorContext) {
if (s == null) {
return true;
}
Method method =
ReflectUtil.getMethodByName(constDictRef.constDictClass(), "isValid");
return ReflectUtil.invokeStatic(method, s);
}
}

View File

@@ -0,0 +1,55 @@
package com.orange.demo.common.core.validator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 定义在Model或Dto对象中UTF-8编码的字符串字段长度的上限和下限和TextLengthValidator对象配合完成数据验证。
*
* @author Jerry
* @date 2020-09-25
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = TextLengthValidator.class)
public @interface TextLength {
/**
* 字符串字段的最小长度。
*
* @return 最小长度。
*/
int min() default 0;
/**
* 字符串字段的最大长度。
*
* @return 最大长度。
*/
int max() default Integer.MAX_VALUE;
/**
* 超过边界后的错误消息提示。
*
* @return 错误提示。
*/
String message() default "字段长度超过最大字节数!";
/**
* 验证分组。
*
* @return 验证分组。
*/
Class<?>[] groups() default { };
/**
* 载荷对象类型。
*
* @return 载荷对象。
*/
Class<? extends Payload>[] payload() default { };
}

View File

@@ -0,0 +1,39 @@
package com.orange.demo.common.core.validator;
import org.apache.commons.lang3.CharUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* 数据字段自定义验证用于验证Model中UTF-8编码的字符串字段的最大长度和最小长度。
*
* @author Jerry
* @date 2020-09-25
*/
public class TextLengthValidator implements ConstraintValidator<TextLength, String> {
private TextLength textLength;
@Override
public void initialize(TextLength textLength) {
this.textLength = textLength;
}
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (s == null) {
return true;
}
int length = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (CharUtils.isAscii(c)) {
++length;
} else {
length += 2;
}
}
return length >= textLength.min() && length <= textLength.max();
}
}

View File

@@ -0,0 +1,11 @@
package com.orange.demo.common.core.validator;
/**
* 数据修改的验证分组。通常用于数据更新的场景。
*
* @author Jerry
* @date 2020-09-25
*/
public interface UpdateGroup {
}

View File

@@ -0,0 +1,24 @@
<?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">
<parent>
<artifactId>common</artifactId>
<groupId>com.orange.demo</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-sequence</artifactId>
<version>1.0.0</version>
<name>common-sequence</name>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.orange.demo</groupId>
<artifactId>common-core</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,14 @@
package com.orange.demo.common.sequence.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
/**
* common-sequence模块的自动配置引导类。
*
* @author Jerry
* @date 2020-09-25
*/
@EnableConfigurationProperties({IdGeneratorProperties.class})
public class IdGeneratorAutoConfigure {
}

View File

@@ -0,0 +1,20 @@
package com.orange.demo.common.sequence.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* common-sequence模块的配置类。
*
* @author Jerry
* @date 2020-09-25
*/
@Data
@ConfigurationProperties(prefix = "sequence")
public class IdGeneratorProperties {
/**
* 基础版生成器所需的WorkNode参数值。仅当advanceIdGenerator为false时生效。
*/
private Integer snowflakeWorkNode = 1;
}

View File

@@ -0,0 +1,48 @@
package com.orange.demo.common.sequence.generator;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
/**
* 基础版snowflake计算工具类。
* 和SnowflakeIdGenerator相比相同点是均为基于Snowflake算法的生成器。不同点在于当前类的
* WorkNodeId是通过配置文件静态指定的。而SnowflakeIdGenerator的WorkNodeId是由zk生成的。
*
* @author Jerry
* @date 2020-09-25
*/
public class BasicIdGenerator implements MyIdGenerator {
private Snowflake snowflake;
/**
* 构造函数。
*
* @param workNode 工作节点。
*/
public BasicIdGenerator(Integer workNode) {
snowflake = IdUtil.createSnowflake(workNode, 0);
}
/**
* 获取基于Snowflake算法的数值型Id。
* 由于底层实现为synchronized方法因此计算过程串行化且线程安全。
*
* @return 计算后的全局唯一Id。
*/
@Override
public long nextLongId() {
return this.snowflake.nextId();
}
/**
* 获取基于Snowflake算法的字符串Id。
* 由于底层实现为synchronized方法因此计算过程串行化且线程安全。
*
* @return 计算后的全局唯一Id。
*/
@Override
public String nextStringId() {
return this.snowflake.nextIdStr();
}
}

View File

@@ -0,0 +1,24 @@
package com.orange.demo.common.sequence.generator;
/**
* 分布式Id生成器的统一接口。
*
* @author Jerry
* @date 2020-09-25
*/
public interface MyIdGenerator {
/**
* 获取数值型分布式Id。
*
* @return 生成后的Id。
*/
long nextLongId();
/**
* 获取字符型分布式Id。
*
* @return 生成后的Id。
*/
String nextStringId();
}

View File

@@ -0,0 +1,52 @@
package com.orange.demo.common.sequence.wrapper;
import com.orange.demo.common.sequence.config.IdGeneratorProperties;
import com.orange.demo.common.sequence.generator.BasicIdGenerator;
import com.orange.demo.common.sequence.generator.MyIdGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* 分布式Id生成器的封装类。该对象可根据配置选择不同的生成器实现类。
*
* @author Jerry
* @date 2020-09-25
*/
@Component
public class IdGeneratorWrapper {
@Autowired
private IdGeneratorProperties properties;
/**
* Id生成器接口对象。
*/
private MyIdGenerator idGenerator;
/**
* 今后如果支持更多Id生成器时可以在该函数内实现不同生成器的动态选择。
*/
@PostConstruct
public void init() {
idGenerator = new BasicIdGenerator(properties.getSnowflakeWorkNode());
}
/**
* 由于底层实现为synchronized方法因此计算过程串行化且线程安全。
*
* @return 计算后的全局唯一Id。
*/
public long nextLongId() {
return idGenerator.nextLongId();
}
/**
* 由于底层实现为synchronized方法因此计算过程串行化且线程安全。
*
* @return 计算后的全局唯一Id。
*/
public String nextStringId() {
return idGenerator.nextStringId();
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.orange.demo.common.sequence.config.IdGeneratorAutoConfigure

View File

@@ -0,0 +1,18 @@
<?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">
<parent>
<groupId>com.orange.demo</groupId>
<artifactId>OrangeSingleDemo</artifactId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common</artifactId>
<packaging>pom</packaging>
<modules>
<module>common-core</module>
<module>common-sequence</module>
</modules>
</project>