Commit 59534eb7 authored by liaozan's avatar liaozan 🏀

Make modules all in one

parent b071d6e0
...@@ -29,3 +29,5 @@ target/ ...@@ -29,3 +29,5 @@ target/
# system ignore # system ignore
.DS_Store .DS_Store
Thumbs.db Thumbs.db
work/
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>commons</artifactId>
<version>${revision}</version>
</parent>
<groupId>com.schbrain.common</groupId>
<artifactId>common-util</artifactId>
<dependencies>
<dependency>
<groupId>com.schbrain.common</groupId>
<artifactId>common</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-blackbird</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Optional -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>${mybatis-plus.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package com.schbrain.common;
import com.schbrain.common.util.support.jackson.ObjectMapperModuleConfiguration;
import com.schbrain.common.util.support.task.ThreadPoolConfiguration;
import com.schbrain.common.util.support.trace.TraceParamAspect;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Import;
/**
* @author liaozan
* @since 2022/1/11
*/
@AutoConfiguration
@Import({TraceParamAspect.class, ThreadPoolConfiguration.class, ObjectMapperModuleConfiguration.class})
public class CommonAutoConfiguration {
}
\ No newline at end of file
package com.schbrain.common.util;
import org.springframework.core.env.Environment;
/**
* @author liaozan
* @since 2021/12/31
*/
public class ApplicationName {
public static String get(Environment environment) {
return environment.getRequiredProperty("spring.application.name");
}
}
package com.schbrain.common.util;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.util.ReflectUtil;
import com.google.common.base.Joiner;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.cglib.beans.BeanCopier;
import java.util.ArrayList;
import java.util.List;
/**
* only support the same property type
*
* @author liaozan
* @since 2022/1/24
*/
public class BeanCopyUtils {
private static final Joiner CACHE_KEY_JOINER = Joiner.on("#");
/**
* copy object list
*/
public static <Source, Target> List<Target> copyList(List<Source> sourceList, Class<Target> targetType) {
if (CollectionUtils.isEmpty(sourceList)) {
return new ArrayList<>(0);
}
return StreamUtils.toList(sourceList, source -> copy(source, targetType), false, true);
}
/**
* copy object
*/
public static <Source, Target> Target copy(Source source, Class<Target> targetType) {
if (source == null || targetType == null) {
return null;
}
return copy(source, ReflectUtil.newInstanceIfPossible(targetType));
}
/**
* copy object
*/
public static <Source, Target> Target copy(Source source, Target target) {
if (source == null || target == null) {
return null;
}
BeanCopier copier = getCopier(source.getClass(), target.getClass());
copier.copy(source, target, null);
return target;
}
private static BeanCopier getCopier(Class<?> sourceClass, Class<?> targetClass) {
String cacheKey = buildCacheKey(sourceClass, targetClass);
return Singleton.get(cacheKey, () -> BeanCopier.create(sourceClass, targetClass, false));
}
private static String buildCacheKey(Class<?> source, Class<?> target) {
return CACHE_KEY_JOINER.join(source.getName(), target.getName());
}
}
\ No newline at end of file
package com.schbrain.common.util;
import cn.hutool.core.bean.BeanUtil;
import com.google.common.base.CaseFormat;
import com.google.common.base.Converter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.annotation.MergedAnnotation;
import java.util.*;
/**
* @author liaozan
* @since 2022/1/11
*/
public class ConfigurationPropertiesUtils {
private static final Converter<String, String> DEFAULT_CONVERTER = CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.LOWER_HYPHEN);
public static Map<String, Object> toMap(Object source) {
return toMap(source, true);
}
public static Map<String, Object> toMap(Object source, boolean ignoreNull) {
return toMap(source, true, DEFAULT_CONVERTER);
}
public static Map<String, Object> toMap(Object source, boolean ignoreNull, Converter<String, String> converter) {
if (source == null) {
return Collections.emptyMap();
}
Class<?> sourceClass = source.getClass();
if (sourceClass.isAnnotationPresent(ConfigurationProperties.class)) {
String prefix = getPrefix(sourceClass);
Map<String, Object> sourceMap = new LinkedHashMap<>();
return BeanUtil.beanToMap(source, sourceMap, ignoreNull, key -> prefix + "." + converter.convert(key));
}
return BeanUtil.beanToMap(source);
}
public static String getPrefix(Class<?> sourceClass) {
ConfigurationProperties configurationProperties = sourceClass.getAnnotation(ConfigurationProperties.class);
MergedAnnotation<ConfigurationProperties> mergedAnnotation = MergedAnnotation.from(configurationProperties);
return mergedAnnotation.getString(MergedAnnotation.VALUE);
}
}
\ No newline at end of file
package com.schbrain.common.util;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.extra.spring.SpringUtil;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.core.env.Environment;
/**
* 注意!! 此类获取 profile 的方法不能用在{@link BeanFactoryPostProcessor} 之前
*
* @author liaozan
* @see cn.hutool.extra.spring.SpringUtil
* @since 2021/12/13
*/
public class EnvUtils {
public static final String DEVELOPMENT = "dev";
public static final String TESTING = "test";
public static final String PRODUCTION = "prod";
public static boolean isDevelopment() {
return isDevelopment(getProfile());
}
public static boolean isDevelopment(String profile) {
return DEVELOPMENT.equals(profile);
}
public static boolean isTesting() {
return isTesting(getProfile());
}
public static boolean isTesting(String profile) {
return TESTING.equals(profile);
}
public static boolean isProduction() {
return isProduction(getProfile());
}
public static boolean isProduction(String profile) {
return PRODUCTION.equals(profile);
}
public static String getProfile() {
Environment environment = SpringUtil.getBean(Environment.class);
return getProfile(environment);
}
public static String getProfile(Environment environment) {
String[] profiles = environment.getActiveProfiles();
if (ArrayUtil.isEmpty(profiles)) {
profiles = environment.getDefaultProfiles();
}
return profiles[0];
}
public static boolean runningOnCloudPlatform(Environment environment) {
CloudPlatform cloudPlatform = CloudPlatform.getActive(environment);
return cloudPlatform != null && cloudPlatform != CloudPlatform.NONE;
}
}
\ No newline at end of file
package com.schbrain.common.util;
import com.alibaba.excel.EasyExcel;
import com.schbrain.common.util.support.excel.bean.ExcelReadResult;
import com.schbrain.common.util.support.excel.exception.ExcelException;
import com.schbrain.common.util.support.excel.listener.*;
import com.schbrain.common.util.support.excel.listener.HierarchicalDataReadListener.ImportedRecord;
import org.apache.commons.collections4.CollectionUtils;
import java.io.*;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
/**
* @author liaozan
* @since 2022/1/6
*/
public class ExcelUtils {
// file
public static ExcelReadResult<Map<Integer, Object>> read(File excelFile) {
return getMapExcelReadResult(excelFile);
}
public static <T> ExcelReadResult<T> readAs(Class<T> contentType, File excelFile) {
return getObjectExcelReadResult(contentType, excelFile);
}
public static <T> ExcelReadResult<T> readAs(Class<T> contentType, File excelFile, ExcelReadListenerBase<T> readListener) {
EasyExcel.read(excelFile, contentType, readListener).doReadAll();
return readListener.getReadResult();
}
public static List<ImportedRecord> readHierarchicalData(File excelFile) {
HierarchicalDataReadListener readListener = new HierarchicalDataReadListener();
return readHierarchicalData(excelFile, readListener);
}
public static List<ImportedRecord> readHierarchicalData(File excelFile, HierarchicalDataReadListener readListener) {
EasyExcel.read(excelFile, readListener).doReadAll();
return readListener.getImportedRecords();
}
// stream
public static ExcelReadResult<Map<Integer, Object>> read(InputStream inputStream) {
return getMapExcelReadResult(inputStream);
}
public static <T> ExcelReadResult<T> readAs(Class<T> contentType, InputStream inputStream) {
return getObjectExcelReadResult(contentType, inputStream);
}
public static <T> ExcelReadResult<T> readAs(Class<T> contentType, InputStream inputStream, ExcelReadListenerBase<T> readListener) {
EasyExcel.read(inputStream, contentType, readListener).doReadAll();
return readListener.getReadResult();
}
public static List<ImportedRecord> readHierarchicalData(InputStream inputStream) {
HierarchicalDataReadListener readListener = new HierarchicalDataReadListener();
return readHierarchicalData(inputStream, readListener);
}
public static List<ImportedRecord> readHierarchicalData(InputStream inputStream, HierarchicalDataReadListener readListener) {
EasyExcel.read(inputStream, readListener).doReadAll();
return readListener.getImportedRecords();
}
// write
public static <T> Path writeTo(Path location, List<T> dataList) {
if (CollectionUtils.isEmpty(dataList)) {
throw new ExcelException("DataList is empty");
}
return writeTo(location, dataList, dataList.get(0).getClass());
}
public static <T> Path writeTo(Path location, List<T> dataList, Class<?> head) {
if (CollectionUtils.isEmpty(dataList)) {
throw new ExcelException("DataList is empty");
}
EasyExcel.write(location.toFile())
.sheet()
.head(head)
.doWrite(dataList);
return location;
}
public static <T> OutputStream writeTo(OutputStream outputStream, List<T> dataList) {
if (CollectionUtils.isEmpty(dataList)) {
throw new ExcelException("DataList is empty");
}
return writeTo(outputStream, dataList, dataList.get(0).getClass());
}
public static <T> OutputStream writeTo(OutputStream outputStream, List<T> dataList, Class<?> head) {
if (CollectionUtils.isEmpty(dataList)) {
throw new ExcelException("DataList is empty");
}
EasyExcel.write(outputStream)
.sheet()
.head(head)
.doWrite(dataList);
return outputStream;
}
private static ExcelReadResult<Map<Integer, Object>> getMapExcelReadResult(Object excelFile) {
ExcelMapDataReadListener readListener = new ExcelMapDataReadListener();
if (excelFile instanceof File) {
EasyExcel.read((File) excelFile, readListener).doReadAll();
} else if (excelFile instanceof InputStream) {
EasyExcel.read((InputStream) excelFile, readListener).doReadAll();
} else {
throw new ExcelException("Unsupported excel file");
}
return readListener.getReadResult();
}
private static <T> ExcelReadResult<T> getObjectExcelReadResult(Class<T> contentType, Object excelFile) {
ExcelReadListenerBase<T> readListener = new ExcelBeanReadListener<>();
if (excelFile instanceof File) {
EasyExcel.read((File) excelFile, contentType, readListener).doReadAll();
} else if (excelFile instanceof InputStream) {
EasyExcel.read((InputStream) excelFile, contentType, readListener).doReadAll();
} else {
throw new ExcelException("Unsupported excel file");
}
return readListener.getReadResult();
}
}
\ No newline at end of file
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.schbrain.common.util;
import java.security.SecureRandom;
import java.util.Random;
/***
* @author adyliu (imxylz@gmail.com)
* @since 1.0
*/
public class IdWorker {
/**
* 生成的自增id的大小减少到18位
*/
private static final long ID_EPOCH = 1420041600000L;
private static final long workerIdBits = 5L;
private static final long datacenterIdBits = 5L;
private static final long maxWorkerId = ~(-1L << workerIdBits);
private static final long maxDatacenterId = ~(-1L << datacenterIdBits);
private static final long sequenceBits = 12L;
private static final long workerIdShift = sequenceBits;
private static final long datacenterIdShift = sequenceBits + workerIdBits;
private static final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private static final long sequenceMask = ~(-1L << sequenceBits);
private static final Random r = new SecureRandom();
// 需要等 r 初始化
private static final IdWorker INSTANCE = new IdWorker(ID_EPOCH);
private final long workerId;
private final long datacenterId;
private final long idEpoch;
private long lastTimestamp = -1L;
private long sequence;
public IdWorker(long idEpoch) {
this(r.nextInt((int) maxWorkerId), r.nextInt((int) maxDatacenterId), 0, idEpoch);
}
public IdWorker(long workerId, long datacenterId, long sequence) {
this(workerId, datacenterId, sequence, 1420041600000L);
}
public IdWorker(long workerId, long datacenterId, long sequence, long idEpoch) {
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
this.idEpoch = idEpoch;
if (workerId < 0 || workerId > maxWorkerId) {
throw new IllegalArgumentException("workerId is illegal: " + workerId);
}
if (datacenterId < 0 || datacenterId > maxDatacenterId) {
throw new IllegalArgumentException("datacenterId is illegal: " + workerId);
}
if (idEpoch >= System.currentTimeMillis()) {
throw new IllegalArgumentException("idEpoch is illegal: " + idEpoch);
}
}
public static long getId() {
return INSTANCE.nextId();
}
public static String getIdStr() {
return String.valueOf(INSTANCE.nextId());
}
/**
* get the timestamp (millis second) of id
*
* @param id the nextId
* @return the timestamp of id
*/
public long getIdTimestamp(long id) {
return idEpoch + (id >> timestampLeftShift);
}
private synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new IllegalStateException("Clock moved backwards.");
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = timestamp;
return ((timestamp - idEpoch) << timestampLeftShift)//
| (datacenterId << datacenterIdShift)//
| (workerId << workerIdShift)//
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
}
\ No newline at end of file
package com.schbrain.common.util;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.*;
import java.nio.ByteBuffer;
import java.util.Enumeration;
/**
* Copy from SpringCloud
*
* @author liaozan
* @since 2021/11/19
*/
@Slf4j
public class InetUtils {
public static HostInfo findFirstNonLoopBackHostInfo() {
InetAddress address = findFirstNonLoopBackAddress();
if (address != null) {
return convertAddress(address);
}
HostInfo hostInfo = new HostInfo();
hostInfo.setHostname("localhost");
hostInfo.setIpAddress("127.0.0.1");
return hostInfo;
}
private static InetAddress findFirstNonLoopBackAddress() {
InetAddress result = null;
try {
int lowest = Integer.MAX_VALUE;
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface networkInterface = interfaces.nextElement();
if (networkInterface.isUp()) {
if (networkInterface.getIndex() < lowest || result == null) {
lowest = networkInterface.getIndex();
} else {
continue;
}
Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
while (inetAddresses.hasMoreElements()) {
InetAddress address = inetAddresses.nextElement();
if (address instanceof Inet4Address && !address.isLoopbackAddress()) {
result = address;
}
}
}
}
} catch (IOException ex) {
log.error("Cannot get first non-loopBack address", ex);
}
if (result != null) {
return result;
}
try {
return InetAddress.getLocalHost();
} catch (UnknownHostException e) {
log.warn("Unable to retrieve localhost");
}
return null;
}
private static HostInfo convertAddress(final InetAddress address) {
HostInfo hostInfo = new HostInfo();
String hostname = address.getHostName();
hostInfo.setHostname(hostname);
hostInfo.setIpAddress(address.getHostAddress());
return hostInfo;
}
@Data
public static class HostInfo {
public static final String NAME = "machineHostInfo";
private String ipAddress;
private String hostname;
@JsonIgnore
public int getIpAddressAsInt() {
InetAddress inetAddress;
String host = this.ipAddress;
if (host == null) {
host = this.hostname;
}
try {
inetAddress = InetAddress.getByName(host);
} catch (final UnknownHostException e) {
throw new IllegalArgumentException(e);
}
return ByteBuffer.wrap(inetAddress.getAddress()).getInt();
}
}
}
\ No newline at end of file
package com.schbrain.common.util;
import cn.hutool.extra.spring.SpringUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.NullNode;
import com.schbrain.common.constants.ResponseActionConstants;
import com.schbrain.common.constants.ResponseCodeConstants;
import com.schbrain.common.exception.BaseException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.TypeUtils;
import java.lang.reflect.ParameterizedType;
import java.util.*;
import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
/**
* @author liaozan
* @since 2021/10/12
*/
@Slf4j
public class JacksonUtils {
private static ObjectMapper OBJECT_MAPPER;
private static ObjectMapper PRETTY_OBJECT_MAPPER;
public static ObjectMapper getObjectMapper() {
if (OBJECT_MAPPER == null) {
// Delay to get ObjectMapper from spring container to keep the same behavior with application
try {
OBJECT_MAPPER = SpringUtil.getBean(ObjectMapper.class).copy();
} catch (Exception e) {
log.warn("Could not get ObjectMapper from Spring Container, return new instance for use");
OBJECT_MAPPER = new ObjectMapper();
}
OBJECT_MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
return OBJECT_MAPPER;
}
public static ObjectMapper getPrettyObjectMapper() {
if (PRETTY_OBJECT_MAPPER == null) {
PRETTY_OBJECT_MAPPER = getObjectMapper().copy().enable(INDENT_OUTPUT);
}
return PRETTY_OBJECT_MAPPER;
}
public static String toJsonString(Object data) {
return toJsonString(data, false);
}
public static String toPrettyJsonString(Object data) {
return toJsonString(data, true);
}
public static String toJsonString(Object data, boolean pretty) {
try {
if (pretty) {
return getPrettyObjectMapper().writeValueAsString(data);
}
return getObjectMapper().writeValueAsString(data);
} catch (Exception e) {
throw new JSONException("Object 转 JSON 出错", e);
}
}
public static JsonNode getJsonNode(String json) {
try {
if (StringUtils.isBlank(json)) {
return NullNode.getInstance();
}
return getObjectMapper().readTree(json);
} catch (Exception e) {
throw new JSONException("JSON 转 JsonNode 出错", e);
}
}
public static <T> T getObjectFromBytes(byte[] bytes, Class<T> type) {
if (ArrayUtils.isEmpty(bytes)) {
return null;
}
try {
return getObjectMapper().readValue(bytes, type);
} catch (Exception e) {
throw new JSONException("Byte 转 Object 出错", e);
}
}
public static <T> T getObjectFromBytes(byte[] bytes, ParameterizedType type) {
if (ArrayUtils.isEmpty(bytes)) {
return null;
}
try {
JavaType valueType = getObjectMapper().getTypeFactory().constructType(type);
return getObjectMapper().readValue(bytes, valueType);
} catch (Exception e) {
throw new JSONException("Byte 转 Object 出错", e);
}
}
public static <T> T getObjectFromBytes(byte[] bytes, TypeReference<T> typeReference) {
if (ArrayUtils.isEmpty(bytes)) {
return null;
}
try {
return getObjectMapper().readValue(bytes, typeReference);
} catch (Exception e) {
throw new JSONException("Byte 转 Object 出错", e);
}
}
public static <T> T getObjectFromBytesWithTypeConstruct(byte[] bytes, Class<T> genericsType, Class<?>... innerTypes) {
if (ArrayUtils.isEmpty(bytes)) {
return null;
}
try {
JavaType valueType = constructType(genericsType, innerTypes);
return getObjectMapper().readValue(bytes, valueType);
} catch (Exception e) {
throw new JSONException("Byte 转 Object 出错", e);
}
}
public static <T> T getObjectFromJson(String json, Class<T> type) {
if (StringUtils.isBlank(json)) {
return null;
}
try {
return getObjectMapper().readValue(json, type);
} catch (Exception e) {
throw new JSONException("JSON 转 Object 出错", e);
}
}
public static <T> T getObjectFromJson(String json, ParameterizedType type) {
if (StringUtils.isBlank(json)) {
return null;
}
try {
JavaType valueType = getObjectMapper().getTypeFactory().constructType(type);
return getObjectMapper().readValue(json, valueType);
} catch (Exception e) {
throw new JSONException("JSON 转 Object 出错", e);
}
}
public static <T> T getObjectFromJson(String json, TypeReference<T> typeReference) {
if (StringUtils.isBlank(json)) {
return null;
}
try {
return getObjectMapper().readValue(json, typeReference);
} catch (Exception e) {
throw new JSONException("JSON 转 Object 出错", e);
}
}
public static <T> T getObjectFromJsonWithTypeConstruct(String json, Class<T> genericsType, Class<?>... innerTypes) {
if (StringUtils.isBlank(json)) {
return null;
}
try {
JavaType valueType = constructType(genericsType, innerTypes);
return getObjectMapper().readValue(json, valueType);
} catch (Exception e) {
throw new JSONException("JSON 转 Object 出错", e);
}
}
public static Map<String, Object> getMapFromJson(String json) {
return getMapFromJson(json, Object.class);
}
public static <V> Map<String, V> getMapFromJson(String json, Class<V> valueType) {
return getMapFromJson(json, String.class, valueType);
}
public static <K, V> Map<K, V> getMapFromJson(String json, Class<K> keyType, Class<V> valueType) {
if (StringUtils.isBlank(json)) {
return new LinkedHashMap<>();
}
try {
JavaType mapType = getObjectMapper().getTypeFactory().constructMapType(LinkedHashMap.class, keyType, valueType);
return getObjectMapper().readValue(json, mapType);
} catch (Exception e) {
throw new JSONException("JSON 转 Map 出错", e);
}
}
public static <T> List<T> getListFromJson(String json, Class<T> itemType) {
if (StringUtils.isBlank(json)) {
return new ArrayList<>();
}
try {
JavaType listType = getObjectMapper().getTypeFactory().constructCollectionType(ArrayList.class, itemType);
return getObjectMapper().readValue(json, listType);
} catch (Exception e) {
throw new JSONException("JSON 转 List 出错", e);
}
}
public static byte[] writeObjectAsBytes(Object data) {
try {
return getObjectMapper().writeValueAsBytes(data);
} catch (Exception e) {
throw new JSONException("Object 转 Byte 出错", e);
}
}
public static JavaType constructType(Class<?> genericsType, Class<?>... innerTypes) {
return getObjectMapper().getTypeFactory().constructType(TypeUtils.parameterize(genericsType, innerTypes));
}
public static <T> T convertValue(Object fromValue, Class<T> toValueType) {
try {
return getObjectMapper().convertValue(fromValue, toValueType);
} catch (Exception e) {
throw new JSONException("JSON 转换出错", e);
}
}
public static <T> T convertValue(Object fromValue, JavaType toValueType) {
try {
return getObjectMapper().convertValue(fromValue, toValueType);
} catch (Exception e) {
throw new JSONException("JSON 转换出错", e);
}
}
public static class JSONException extends BaseException {
private static final long serialVersionUID = 1656914307906296812L;
public JSONException(String message, Throwable cause) {
super(message, cause, ResponseCodeConstants.SERVER_ERROR, ResponseActionConstants.ALERT);
}
}
}
\ No newline at end of file
package com.schbrain.common.util;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.schbrain.common.entity.PageParam;
import com.schbrain.common.entity.PaginationInfo;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import static com.schbrain.common.constants.PageConstants.*;
/**
* @author liaozan
* @since 2022/1/7
*/
public class PageUtils {
public static <T> IPage<T> fromParam(PageParam pageParam) {
return fromParam(pageParam.getPageIndex(), pageParam.getPageSize(), pageParam.isSearchCount());
}
public static <T> IPage<T> fromParam(long pageIndex, long pageSize) {
return fromParam(pageIndex, pageSize, DEFAULT_SEARCH_COUNT);
}
public static <T> IPage<T> fromParam(long pageIndex, long pageSize, boolean searchCount) {
return Page.of(pageIndex, pageSize, searchCount);
}
public static <T> PaginationInfo<T> fromResult(IPage<T> page) {
return fromResult(page, page.getRecords());
}
public static <T, R> PaginationInfo<R> fromResult(IPage<T> page, Function<T, R> mapper) {
return fromResult(page.convert(mapper));
}
public static <T, R> PaginationInfo<R> fromResult(IPage<T> page, List<R> dataList) {
return new PaginationInfo<>(page.getCurrent(), page.getSize(), page.getTotal(), dataList);
}
public static <T, R> PaginationInfo<R> fromResult(PaginationInfo<T> info, Function<T, R> mapper) {
List<R> dataList = StreamUtils.toList(info.getDataList(), mapper);
return fromResult(info, dataList);
}
public static <T, R> PaginationInfo<R> fromResult(PaginationInfo<T> info, List<R> dataList) {
return new PaginationInfo<>(info.getPageIndex(), info.getPageSize(), info.getTotalCount(), dataList);
}
public static <T> PaginationInfo<T> emptyResult(IPage<T> page) {
return new PaginationInfo<>(page.getCurrent(), page.getSize(), DEFAULT_TOTAL_COUNT, Collections.emptyList());
}
public static <T> PaginationInfo<T> emptyResult(PageParam pageParam) {
return new PaginationInfo<>(pageParam.getPageIndex(), pageParam.getPageSize(), DEFAULT_TOTAL_COUNT, Collections.emptyList());
}
public static <T> PaginationInfo<T> emptyResult(PaginationInfo<?> page) {
return new PaginationInfo<>(page.getPageIndex(), page.getPageSize(), DEFAULT_TOTAL_COUNT, Collections.emptyList());
}
public static PageParam toParam(IPage<?> page) {
PageParam pageParam = new PageParam();
pageParam.setPageIndex(Math.toIntExact(page.getCurrent()));
pageParam.setPageSize(Math.toIntExact(page.getSize()));
pageParam.setSearchCount(page.searchCount());
return pageParam;
}
}
\ No newline at end of file
package com.schbrain.common.util;
import cn.hutool.core.util.ArrayUtil;
import com.google.common.collect.Maps;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author liaozan
* @since 2022/5/18
*/
public class ParameterDiscoverUtils {
private static final ParameterNameDiscoverer DISCOVERER = new DefaultParameterNameDiscoverer();
public static Map<String, Object> getMethodArgsMap(Method method, Object[] args) {
String[] parameterNames = DISCOVERER.getParameterNames(method);
if (ArrayUtil.isEmpty(parameterNames)) {
// Return a new instance to avoid external modification causing errors
return new LinkedHashMap<>();
}
Map<String, Object> argsMap = Maps.newLinkedHashMapWithExpectedSize(parameterNames.length);
for (int i = 0; i < parameterNames.length; i++) {
argsMap.put(parameterNames[i], args[i]);
}
return argsMap;
}
}
\ No newline at end of file
package com.schbrain.common.util;
import com.schbrain.common.exception.BaseException;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.ServerSocket;
/**
* net util
*
* @author xuxueli 2017-11-29 17:00:25
*/
@Slf4j
public class PortUtils {
public static int findAvailablePort(int defaultPort) {
int portTmp = defaultPort;
while (portTmp < 65535) {
if (!isPortUsed(portTmp)) {
return portTmp;
} else {
portTmp++;
}
}
throw new BaseException("no available port.");
}
public static boolean isPortUsed(int port) {
boolean used = false;
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
} catch (IOException e) {
log.warn("current port[{}] is in use", port);
used = true;
} finally {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
log.info(e.getMessage(), e);
}
}
}
return used;
}
}
package com.schbrain.common.util;
import org.apache.commons.lang3.StringUtils;
import org.springframework.expression.*;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SpelUtils {
private static final ExpressionParser spELParser = new SpelExpressionParser();
private static final ConcurrentHashMap<String, Expression> expressionCache = new ConcurrentHashMap<>();
public static <T> T parse(String express, Map<String, Object> variables, Class<T> valueType) {
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.setVariables(variables);
return parse(express, ctx, valueType);
}
public static <T> T parse(String express, EvaluationContext context, Class<T> valueType) {
if (StringUtils.isBlank(express)) {
return null;
}
return getExpression(express).getValue(context, valueType);
}
private static Expression getExpression(String exp) {
Expression expression = expressionCache.get(exp);
if (null == expression) {
expression = spELParser.parseExpression(exp);
expressionCache.put(exp, expression);
}
return expression;
}
}
\ No newline at end of file
package com.schbrain.common.util;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.*;
/**
* @author liaozan
* @since 2022/8/21
*/
@Slf4j
public class StreamUtils {
public static <T> List<T> filterToList(Collection<T> data, Predicate<T> predicate) {
return filter(data, predicate, Collectors.toList());
}
public static <T> Set<T> filterToSet(Collection<T> data, Predicate<T> predicate) {
return filter(data, predicate, Collectors.toSet());
}
public static <T, C extends Collection<T>> C filter(Collection<T> data, Predicate<T> predicate, Collector<T, ?, C> collector) {
return Optional.ofNullable(data).orElse(emptyList()).stream().filter(predicate).collect(collector);
}
public static <T, E> List<E> toList(Collection<T> data, Function<T, E> mapper) {
return toList(data, mapper, false);
}
public static <T, E> List<E> toList(Collection<T> data, Function<T, E> mapper, boolean distinct) {
return toList(data, mapper, distinct, false);
}
public static <T, E> List<E> toList(Collection<T> data, Function<T, E> mapper, boolean distinct, boolean ignoreNull) {
return extract(data, mapper, distinct, ignoreNull, Collectors.toList());
}
public static <T, E> Set<E> toSet(Collection<T> data, Function<T, E> mapper) {
return toSet(data, mapper, false);
}
public static <T, E> Set<E> toSet(Collection<T> data, Function<T, E> mapper, boolean ignoreNull) {
return extract(data, mapper, ignoreNull, false, Collectors.toSet());
}
public static <K, T> Map<K, T> toMap(Collection<T> data, Function<T, K> keyMapper) {
return toMap(data, keyMapper, false);
}
public static <K, T> Map<K, T> toMap(Collection<T> data, Function<T, K> keyMapper, boolean ordered) {
return toMap(data, keyMapper, Function.identity(), ordered);
}
public static <K, T, V> Map<K, V> toMap(Collection<T> data, Function<T, K> keyMapper, Function<T, V> valueMapper) {
return toMap(data, keyMapper, valueMapper, false);
}
public static <K, T, V> Map<K, V> toMap(Collection<T> data, Function<T, K> keyMapper, Function<T, V> valueMapper, boolean ordered) {
Supplier<Map<K, V>> mapFactory = HashMap::new;
if (ordered) {
mapFactory = LinkedHashMap::new;
}
return toMap(data, keyMapper, valueMapper, mapFactory);
}
public static <K, T, V, M extends Map<K, V>> Map<K, V> toMap(Collection<T> data, Function<T, K> keyMapper, Function<T, V> valueMapper, Supplier<M> mapFactory) {
return Optional.ofNullable(data)
.orElse(emptyList())
.stream()
.collect(Collectors.toMap(keyMapper, valueMapper, (oldValue, newValue) -> {
// Could not get the key when mergeFunction invoke
log.warn("There are multiple values with the same key when toMap, return the old one");
return oldValue;
}, mapFactory));
}
public static <K, T> Map<K, List<T>> groupBy(Collection<T> data, Function<T, K> mapper) {
return groupBy(data, mapper, false);
}
public static <K, T> Map<K, List<T>> groupBy(Collection<T> data, Function<T, K> keyMapper, boolean ignoreNullKey) {
return groupBy(data, keyMapper, Collectors.toList(), ignoreNullKey);
}
public static <K, T, V> Map<K, V> groupBy(Collection<T> data, Function<T, K> mapper, Collector<T, ?, V> collectors) {
return groupBy(data, mapper, collectors, false);
}
public static <K, T, V> Map<K, V> groupBy(Collection<T> data, Function<T, K> mapper, Collector<T, ?, V> collectors, boolean ignoreNullKey) {
return groupBy(data, mapper, Function.identity(), collectors, ignoreNullKey);
}
public static <K, T, V> Map<K, List<V>> groupBy(Collection<T> data, Function<T, K> keyMapper, Function<T, V> valueMapper) {
return groupBy(data, keyMapper, valueMapper, Collectors.toList(), false);
}
public static <K, T, V> Map<K, List<V>> groupBy(Collection<T> data, Function<T, K> keyMapper, Function<T, V> valueMapper, boolean ignoreNullKey) {
return groupBy(data, keyMapper, valueMapper, Collectors.toList(), ignoreNullKey);
}
public static <K, T, V, C> Map<K, C> groupBy(Collection<T> data, Function<T, K> keyMapper, Function<T, V> valueMapper, Collector<V, ?, C> collector) {
return groupBy(data, keyMapper, valueMapper, collector, false);
}
public static <K, T, V, C> Map<K, C> groupBy(Collection<T> data, Function<T, K> keyMapper, Function<T, V> valueMapper, Collector<V, ?, C> collector, boolean ignoreNullKey) {
return groupBy(data, keyMapper, valueMapper, collector, ignoreNullKey, HashMap::new);
}
public static <K, T, V, C> Map<K, C> groupBy(Collection<T> data, Function<T, K> keyMapper, Function<T, V> valueMapper, Collector<V, ?, C> collector, boolean ignoreNullKey, Supplier<Map<K, C>> mapSupplier) {
Stream<T> stream = Optional.ofNullable(data)
.orElse(emptyList())
.stream();
if (ignoreNullKey) {
stream = stream.filter(item -> null != keyMapper.apply(item));
}
return stream.collect(groupingBy(keyMapper, mapSupplier, mapping(valueMapper, collector)));
}
public static <T> String join(Collection<T> data, CharSequence delimiter) {
return join(data, delimiter, Objects::toString);
}
public static <T> String join(Collection<T> data, CharSequence delimiter, Function<T, ? extends CharSequence> toStringFunction) {
return join(data, delimiter, "", "", toStringFunction);
}
public static <T> String join(Collection<T> data, CharSequence delimiter, String prefix, String suffix, Function<T, ? extends CharSequence> toStringFunction) {
return Optional.ofNullable(data)
.orElse(emptyList())
.stream().map(toStringFunction).collect(joining(delimiter, prefix, suffix));
}
public static <T, E, R> R extract(Collection<T> data, Function<T, E> mapper, boolean distinct, boolean ignoreNull, Collector<E, ?, R> collector) {
Predicate<E> predicate = null;
if (ignoreNull) {
predicate = Objects::nonNull;
}
return extract(data, mapper, predicate, distinct, collector);
}
public static <T, E, R> R extract(Collection<T> data, Function<T, E> mapper, Predicate<E> predicate, boolean distinct, Collector<E, ?, R> collector) {
Stream<E> stream = Optional.ofNullable(data)
.orElse(emptyList())
.stream()
.map(mapper);
if (distinct) {
stream = stream.distinct();
}
if (predicate != null) {
stream = stream.filter(predicate);
}
return stream.collect(collector);
}
}
\ No newline at end of file
package com.schbrain.common.util;
import cn.hutool.core.util.ClassLoaderUtil;
import cn.hutool.core.util.IdUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
import org.slf4j.MDC;
/**
* @author liaozan
* @since 2021/10/10
*/
public class TraceIdUtils {
public static final String TRACE_ID = "traceId";
private static final boolean skywalkingTracePresent;
private static final ThreadLocal<String> TRACE_ID_CONTAINER = InheritableThreadLocal.withInitial(TraceIdUtils::create);
static {
skywalkingTracePresent = ClassLoaderUtil.isPresent("org.apache.skywalking.apm.toolkit.trace.TraceContext", TraceIdUtils.class.getClassLoader());
}
public static String get() {
return TRACE_ID_CONTAINER.get();
}
public static void set(String traceId) {
if (traceId == null) {
return;
}
MDC.put(TRACE_ID, traceId);
TRACE_ID_CONTAINER.set(traceId);
}
public static void clear() {
MDC.remove(TRACE_ID);
TRACE_ID_CONTAINER.remove();
}
private static String create() {
String traceId = MDC.get(TRACE_ID);
if (traceId == null) {
if (skywalkingTracePresent) {
traceId = TraceContext.traceId();
}
if (StringUtils.isBlank(traceId) || "N/A".equals(traceId)) {
traceId = IdUtil.objectId().toUpperCase();
}
MDC.put(TRACE_ID, traceId);
}
return traceId;
}
}
\ No newline at end of file
package com.schbrain.common.util.cipher;
import cn.hutool.crypto.symmetric.AES;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.Security;
import java.util.Base64;
import static java.nio.charset.StandardCharsets.UTF_8;
@Slf4j
public class AES128ECBWithPKCS7 {
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* encrypt input text
*/
public static String encrypt(String input, String key) {
byte[] encrypted = encryptByte(input, key);
if (null == encrypted) {
return "";
}
return Base64.getEncoder().encodeToString(encrypted);
}
public static byte[] encryptByte(String input, String key) {
byte[] encrypted;
try {
encrypted = new AES("ECB", "PKCS7Padding", key.getBytes(UTF_8)).encrypt(input);
} catch (Exception e) {
log.error("AES128ECBWithPKCS7 encrypt ({}) error! ", input, e);
return null;
}
return encrypted;
}
/**
* decrypt input text
*/
public static String decrypt(String input, String key) {
return decryptByte(Base64.getDecoder().decode(input), key);
}
public static String decryptByte(byte[] input, String key) {
try {
byte[] output = new AES("ECB", "PKCS7Padding", key.getBytes(UTF_8)).decrypt(input);
return new String(output, UTF_8);
} catch (Exception e) {
log.error("AES128ECBWithPKCS7 decryptByte ({}) error!", new String(input, UTF_8), e);
return "";
}
}
}
\ No newline at end of file
package com.schbrain.common.util.log;
import lombok.Data;
@Data
public class LogEvent<T extends LogEventAction> {
/**
* @see com.schbrain.common.constants.LogConstants.ProductTypeEnum
*/
private String product;
private T eventAction;
public LogEvent(String product) {
this.product = product;
}
}
\ No newline at end of file
package com.schbrain.common.util.log;
import java.util.HashMap;
public class LogEventAction extends HashMap<Object, Object> {
private static final long serialVersionUID = -8663962491116732785L;
private static final String NAME = "name";
public LogEventAction(String actionName) {
put(NAME, actionName);
}
public String getActionName() {
return (String) get(NAME);
}
public void setActionName(String actionName) {
put(NAME, actionName);
}
public void setActionParam(Object key, Object value) {
put(key, value);
}
public Object getActionParam(Object key) {
return get(key);
}
}
\ No newline at end of file
package com.schbrain.common.util.log;
import com.schbrain.common.util.JacksonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class StatisticLogUtil {
public static final Logger log = LoggerFactory.getLogger(StatisticLogUtil.class);
public static <T extends LogEventAction> void logEvent(LogEvent<T> logEvent) {
if (null != logEvent) {
log.info(JacksonUtils.toJsonString(logEvent));
}
}
}
\ No newline at end of file
package com.schbrain.common.util.properties;
import com.schbrain.common.util.ConfigurationPropertiesUtils;
import org.springframework.core.env.MapPropertySource;
import java.util.Map;
/**
* mark class to ensure property order
*
* @author liaozan
* @since 2021/12/6
*/
public class SchbrainMapPropertySource extends MapPropertySource {
public SchbrainMapPropertySource(String name, Object source) {
this(name, ConfigurationPropertiesUtils.toMap(source));
}
public SchbrainMapPropertySource(String name, Map<String, Object> source) {
super(name, source);
}
}
\ No newline at end of file
package com.schbrain.common.util.support;
import com.schbrain.common.util.ConfigurationPropertiesUtils;
import lombok.Data;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.core.env.ConfigurableEnvironment;
import java.beans.Introspector;
/**
* <b> WARNING!!! </b>
* <p>
* If you want to use a subclass of this class before {@link BeanPostProcessor},
* please load it through {@link com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils}
*
* @author liaozan
* @since 2022/1/10
*/
@Data
public abstract class ConfigurableProperties {
/**
* the namespace of remote config
*/
protected String namespace = getDefaultNamespace();
/**
* the prefix of properties
*/
protected String prefix = getPossiblePrefix();
/**
* the name of propertySource
*/
protected String name = Introspector.decapitalize(getClass().getSimpleName());
public String getDefaultNamespace() {
return "application";
}
@SuppressWarnings({"unchecked", "unused"})
public <T extends ConfigurableProperties> T bindOrCreate(ConfigurableEnvironment environment, boolean afterMerge) {
return (T) Binder.get(environment, bindHandler()).bindOrCreate(getPrefix(), getClass());
}
protected BindHandler bindHandler() {
return BindHandler.DEFAULT;
}
private String getPossiblePrefix() {
ConfigurationProperties annotation = getClass().getAnnotation(ConfigurationProperties.class);
if (annotation == null) {
String className = ConfigurationProperties.class.getName();
String errorDetail = getClass().getSimpleName() + " must annotated @" + className + " or overwrite getPrefix method";
throw new IllegalStateException(errorDetail);
}
return ConfigurationPropertiesUtils.getPrefix(getClass());
}
}
\ No newline at end of file
package com.schbrain.common.util.support;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
/**
* @author liaozan
* @since 2021/3/27
*/
public interface ValidateSupport {
default boolean hasText(String text) {
return StringUtils.isNotBlank(text);
}
default boolean isBlank(String text) {
return !hasText(text);
}
default boolean isNull(Object object) {
return object == null;
}
default boolean isNotNull(Object object) {
return !isNull(object);
}
default boolean isEmpty(Collection<?> collection) {
return CollectionUtils.isEmpty(collection);
}
default boolean isNotEmpty(Collection<?> collection) {
return !isEmpty(collection);
}
default boolean isEmpty(Map<?, ?> map) {
return MapUtils.isEmpty(map);
}
default boolean isNotEmpty(Map<?, ?> map) {
return !isEmpty(map);
}
default String fixNull(String value) {
return fixNull(value, "");
}
default <T> List<T> fixNull(List<T> value) {
return fixNull(value, new ArrayList<>());
}
default <T> Set<T> fixNull(Set<T> value) {
return fixNull(value, new HashSet<>());
}
default <T> T fixNull(T value, T defaultValue) {
return value != null ? value : defaultValue;
}
}
\ No newline at end of file
package com.schbrain.common.util.support.delay;
import cn.hutool.core.thread.ThreadUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author lik
* @since 2023/4/17
*/
@Slf4j
public abstract class AbstractDelayMessageListener<Message> implements Runnable, InitializingBean, DisposableBean {
private final String threadNamePrefix;
private final String queueName;
private final String listenerName;
private final AtomicBoolean started = new AtomicBoolean(false);
private ExecutorService executor;
public AbstractDelayMessageListener(String queueName) {
this(null, queueName);
}
public AbstractDelayMessageListener(String threadNamePrefix, String queueName) {
this.listenerName = getClass().getSimpleName();
this.threadNamePrefix = threadNamePrefix == null ? listenerName + "#delay-message-" : threadNamePrefix;
this.queueName = queueName;
}
@Override
public void run() {
try {
while (started.get()) {
Message message = DelayedQueueUtils.takeMsg(queueName);
if (log.isDebugEnabled()) {
log.debug("{} receive message :{}", listenerName, message);
}
onMessage(message);
}
} catch (Exception e) {
log.error("{} occur error: {}", listenerName, e.getMessage(), e);
}
}
@Override
public void afterPropertiesSet() {
executor = ThreadUtil.newFixedExecutor(1, threadNamePrefix, true);
started.set(true);
executor.execute(this);
log.info("{} start listen...", listenerName);
}
@Override
public void destroy() {
started.set(false);
executor.shutdown();
log.info("{} stop listen... ", listenerName);
}
/**
* for subClass
*/
protected abstract void onMessage(Message message);
}
\ No newline at end of file
package com.schbrain.common.util.support.delay;
import cn.hutool.core.text.StrPool;
import cn.hutool.extra.spring.SpringUtil;
import com.schbrain.common.exception.BaseException;
import com.schbrain.common.util.ApplicationName;
import org.redisson.api.*;
import org.springframework.beans.BeansException;
import org.springframework.core.env.Environment;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* @author lik
* @since 2023/4/17
*/
public class DelayedQueueUtils {
private static final String APPLICATION_NAME = ApplicationName.get(SpringUtil.getBean(Environment.class));
private static final ConcurrentHashMap<String, RBlockingQueue<?>> blockingQueueCache = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String, RDelayedQueue<?>> delayedQueueCache = new ConcurrentHashMap<>();
private static RedissonClient CLIENT;
public static <T> void offerMsg(T message, String queueName, Long delay, TimeUnit timeUnit) {
batchOfferMsg(List.of(message), queueName, delay, timeUnit);
}
public static <T> void batchOfferMsg(List<T> messages, String queueName, Long delay, TimeUnit timeUnit) {
if (CollectionUtils.isEmpty(messages)) {
return;
}
RDelayedQueue<T> delayedQueue = getDelayedQueue(queueName);
messages.forEach(msg -> delayedQueue.offer(msg, delay, timeUnit));
}
public static <T> T takeMsg(String queueName) {
RBlockingQueue<T> blockingQueue = getBlockingQueue(queueName);
try {
return blockingQueue.take();
} catch (InterruptedException e) {
throw new BaseException("redis blocking queue thread has been interrupted", e);
}
}
public static <T> Boolean removeMsg(String queueName, T msg) {
if (msg == null) {
return false;
}
RDelayedQueue<Object> delayedQueue = getDelayedQueue(queueName);
return delayedQueue.remove(msg);
}
public static void removeQueue(String queueName) {
String wrappedQueueName = withPrefix(queueName);
RBlockingQueue<?> blockingQueue = blockingQueueCache.get(wrappedQueueName);
RDelayedQueue<?> delayedQueue = delayedQueueCache.get(wrappedQueueName);
if (delayedQueue != null) {
delayedQueue.destroy();
delayedQueueCache.remove(wrappedQueueName);
}
if (blockingQueue != null) {
blockingQueue.delete();
blockingQueueCache.remove(wrappedQueueName);
}
}
@SuppressWarnings("unchecked")
private static <T> RDelayedQueue<T> getDelayedQueue(String queueName) {
String cacheKey = withPrefix(queueName);
return (RDelayedQueue<T>) delayedQueueCache.computeIfAbsent(cacheKey, key -> {
RBlockingQueue<?> blockingQueue = getBlockingQueue(queueName);
return getClient().getDelayedQueue(blockingQueue);
});
}
@SuppressWarnings("unchecked")
private static <T> RBlockingQueue<T> getBlockingQueue(String queueName) {
String cacheKey = withPrefix(queueName);
return (RBlockingQueue<T>) blockingQueueCache.computeIfAbsent(cacheKey, key -> getClient().getBlockingQueue(cacheKey));
}
private static RedissonClient getClient() {
if (CLIENT == null) {
try {
CLIENT = SpringUtil.getBean(RedissonClient.class);
} catch (BeansException e) {
throw new BaseException("Could not get RedissonClient, please put it into Spring container", e);
}
}
return CLIENT;
}
private static String withPrefix(String queueName) {
return String.join(StrPool.COLON, APPLICATION_NAME, "delay-queue", queueName);
}
}
\ No newline at end of file
package com.schbrain.common.util.support.excel.bean;
import com.google.common.collect.Table;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* @author liaozan
* @since 2022/1/6
*/
@Data
public class ExcelReadResult<T> {
private List<T> dataList;
private Map<Integer, String> headMap;
private Table<String, Integer, String> errors;
private String errorsAsString;
public boolean hasError() {
return !errors.isEmpty();
}
}
\ No newline at end of file
package com.schbrain.common.util.support.excel.exception;
import com.schbrain.common.exception.BaseException;
/**
* @author liaozan
* @since 2022/1/6
*/
public class ExcelException extends BaseException {
private static final long serialVersionUID = -2338463360273587530L;
public ExcelException(String message) {
super(message);
}
public ExcelException(String message, Throwable cause) {
super(message, cause);
}
}
package com.schbrain.common.util.support.excel.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.metadata.holder.ReadSheetHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import javax.validation.ConstraintViolation;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author liaozan
* @since 2022/1/7
*/
@Slf4j
public class ExcelBeanReadListener<T> extends ExcelReadListenerBase<T> {
@Override
protected boolean validate(T data, AnalysisContext context) {
Set<ConstraintViolation<T>> violations = getValidator().validate(data);
if (CollectionUtils.isEmpty(violations)) {
return true;
}
collectErrorMsg(context, violations);
return false;
}
protected void collectErrorMsg(AnalysisContext context, Set<ConstraintViolation<T>> violations) {
String errorMsg = violations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(","));
ReadSheetHolder currentSheet = context.readSheetHolder();
String sheetName = currentSheet.getSheetName();
Integer rowIndex = currentSheet.getRowIndex();
getErrors().put(sheetName, rowIndex + 1, errorMsg);
}
}
\ No newline at end of file
package com.schbrain.common.util.support.excel.listener;
import java.util.Map;
/**
* @author liaozan
* @since 2022/1/6
*/
public class ExcelMapDataReadListener extends ExcelReadListenerBase<Map<Integer, Object>> {
}
\ No newline at end of file
package com.schbrain.common.util.support.excel.listener;
import cn.hutool.extra.spring.SpringUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.schbrain.common.util.support.excel.bean.ExcelReadResult;
import com.schbrain.common.util.support.excel.exception.ExcelException;
import lombok.*;
import javax.validation.Validator;
import java.util.*;
/**
* @author liaozan
* @since 2022/1/6
*/
@Getter(AccessLevel.PROTECTED)
@Setter(AccessLevel.PROTECTED)
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class ExcelReadListenerBase<T> extends AnalysisEventListener<T> {
protected final Validator validator = SpringUtil.getBean(Validator.class);
protected List<T> dataList = new LinkedList<>();
protected Map<Integer, String> headers = new HashMap<>();
protected Table<String, Integer, String> errors = HashBasedTable.create();
protected boolean terminateOnValidateFail = false;
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
this.headers = headMap;
}
@Override
public void invoke(T data, AnalysisContext context) {
boolean validated = validate(data, context);
if (!validated) {
if (isTerminateOnValidateFail()) {
throw new ExcelException(getErrorMsg());
}
}
this.dataList.add(data);
}
@Override
public void onException(Exception exception, AnalysisContext context) {
throw new ExcelException(exception.getMessage(), exception);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
public ExcelReadResult<T> getReadResult() {
ExcelReadResult<T> readResult = new ExcelReadResult<>();
readResult.setDataList(dataList);
readResult.setHeadMap(headers);
readResult.setErrors(errors);
readResult.setErrorsAsString(getErrorMsg());
return readResult;
}
protected String getErrorMsg() {
StringBuilder msgBuilder = new StringBuilder();
errors.rowMap().forEach((sheetName, rows) -> {
msgBuilder.append("sheet: [ ").append(sheetName).append(" ] ");
rows.forEach((rowIndex, error) -> {
String formattedErrorMsg = String.format("第%d行: [ %s ] ", rowIndex, error);
msgBuilder.append(formattedErrorMsg);
});
msgBuilder.append("\n");
});
return msgBuilder.toString();
}
protected boolean validate(T data, AnalysisContext context) {
return true;
}
}
\ No newline at end of file
package com.schbrain.common.util.support.excel.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.schbrain.common.util.support.excel.exception.ExcelException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
/**
* 层级数据 excel 读取类
*
* @author liaozan
* @since 2022/3/29
*/
@Slf4j
public class HierarchicalDataReadListener extends ExcelReadListenerBase<Map<Integer, String>> {
private final List<ImportedRecord> importedRecords = new LinkedList<>();
private final Table<Integer, Integer, ImportedRecord> coordinateTable = HashBasedTable.create();
@Override
public void invoke(Map<Integer, String> data, AnalysisContext context) {
super.invoke(data, context);
Integer rowIndex = context.readRowHolder().getRowIndex();
data.forEach((columnIndex, value) -> {
if (StringUtils.isNotBlank(value)) {
buildImportedRow(rowIndex, columnIndex, value);
}
});
}
public List<ImportedRecord> getImportedRecords() {
return importedRecords;
}
@Override
protected boolean validate(Map<Integer, String> data, AnalysisContext context) {
Integer rowIndex = context.readRowHolder().getRowIndex();
if (MapUtils.isEmpty(data)) {
throw new ExcelException(String.format("第 %d 行未读到数据", rowIndex + 1));
}
if (rowIndex == 0) {
if (StringUtils.isBlank(data.get(0))) {
throw new ExcelException("第一行第一列未读到数据");
}
}
return true;
}
protected void buildImportedRow(Integer rowIndex, Integer columnIndex, String text) {
ImportedRecord importedRecord = new ImportedRecord();
importedRecord.setText(text);
coordinateTable.put(rowIndex, columnIndex, importedRecord);
if (columnIndex == 0) {
importedRecords.add(importedRecord);
} else {
int currentRowIndex = rowIndex;
// 定位到前一列的单元格
Map<Integer, ImportedRecord> prevColumn = coordinateTable.column(columnIndex - 1);
// 从当前行往上找,找到非空的记录,即视为 parent
ImportedRecord parent = prevColumn.get(currentRowIndex);
while (parent == null && currentRowIndex > 0) {
parent = prevColumn.get(--currentRowIndex);
}
if (parent == null) {
throw new ExcelException("数据格式错误,请对比模板调整");
}
parent.getChildren().add(importedRecord);
}
}
@Data
public static class ImportedRecord {
private String text;
private List<ImportedRecord> children = new LinkedList<>();
public boolean hasChildren() {
return CollectionUtils.isNotEmpty(children);
}
}
}
\ No newline at end of file
package com.schbrain.common.util.support.jackson;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.*;
import com.fasterxml.jackson.datatype.jsr310.ser.*;
import com.schbrain.common.constants.DateTimeFormatters;
import java.time.*;
/**
* @author liaozan
* @since 2022/8/12
*/
public class JavaTimeModule extends SimpleModule {
private static final long serialVersionUID = -1848725752934764693L;
public JavaTimeModule() {
this.setup();
}
protected void setup() {
this.addSerializer(YearMonth.class, new YearMonthSerializer(DateTimeFormatters.YEAR_MONTH));
this.addSerializer(MonthDay.class, new MonthDaySerializer(DateTimeFormatters.MONTH_DATE));
this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatters.DATE));
this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatters.TIME));
this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatters.DATE_TIME));
this.addDeserializer(YearMonth.class, new YearMonthDeserializer(DateTimeFormatters.YEAR_MONTH));
this.addDeserializer(MonthDay.class, new MonthDayDeserializer(DateTimeFormatters.MONTH_DATE));
this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatters.DATE));
this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatters.TIME));
this.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatters.DATE_TIME));
}
}
\ No newline at end of file
package com.schbrain.common.util.support.jackson;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.blackbird.BlackbirdModule;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author liaozan
* @since 2022/1/11
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class)
public class ObjectMapperModuleConfiguration {
@Bean
public BlackbirdModule blackbirdModule() {
return new BlackbirdModule();
}
@Bean
@ConditionalOnMissingBean
public JavaTimeModule javaTimeModule() {
return new JavaTimeModule();
}
}
\ No newline at end of file
package com.schbrain.common.util.support.lock;
import cn.hutool.core.text.StrPool;
import cn.hutool.extra.spring.SpringUtil;
import com.schbrain.common.exception.BaseException;
import com.schbrain.common.util.ApplicationName;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeansException;
import org.springframework.core.env.Environment;
import java.time.Duration;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
/**
* @author liaozan
* @since 2023-03-15
*/
@Slf4j
public class RedisLockUtils {
private static final String APPLICATION_NAME = ApplicationName.get(SpringUtil.getBean(Environment.class));
private static RedissonClient CLIENT;
public static void executeWithLock(String lockName, Runnable action) {
executeWithLock(lockName, Duration.ofSeconds(3), action);
}
public static void executeWithLock(String lockName, Duration timeout, Runnable action) {
executeWithLock(lockName, timeout, () -> {
action.run();
return null;
});
}
public static <T> T executeWithLock(String lockName, Callable<T> action) {
return executeWithLock(lockName, Duration.ofSeconds(3), action);
}
public static <T> T executeWithLock(String lockName, Duration timeout, Callable<T> action) {
RLock lock = getClient().getLock(withPrefix(lockName));
boolean locked;
try {
locked = lock.tryLock(timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
throw new BaseException("Lock thread has been interrupted", e);
}
if (!locked) {
throw new BaseException(String.format("Lock cannot be acquired within %d seconds", timeout.toSeconds()));
}
try {
return action.call();
} catch (Exception e) {
throw new BaseException(e.getMessage(), e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
private static RedissonClient getClient() {
if (CLIENT == null) {
try {
CLIENT = SpringUtil.getBean(RedissonClient.class);
} catch (BeansException e) {
throw new BaseException("Could not get RedissonClient, please put it into Spring container", e);
}
}
return CLIENT;
}
private static String withPrefix(String lockName) {
return String.join(StrPool.COLON, APPLICATION_NAME, "lock", lockName);
}
}
\ No newline at end of file
package com.schbrain.common.util.support.task;
import org.slf4j.MDC;
import org.springframework.core.task.TaskDecorator;
import java.util.Map;
/**
* @author liaozan
* @since 2022/1/11
*/
public class MdcContextPropagationTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
runnable.run();
};
}
}
\ No newline at end of file
package com.schbrain.common.util.support.task;
import org.springframework.boot.task.TaskExecutorCustomizer;
import org.springframework.boot.task.TaskSchedulerCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author liaozan
* @since 2021/11/22
*/
@Configuration(proxyBeanMethods = false)
public class ThreadPoolConfiguration {
@Bean
public TaskExecutorCustomizer mdcSupportTaskExecutorCustomizer() {
return taskExecutor -> {
taskExecutor.setTaskDecorator(new MdcContextPropagationTaskDecorator());
taskExecutor.setThreadFactory(new UnCaughtExceptionHandleThreadFactory(taskExecutor));
};
}
@Bean
public TaskSchedulerCustomizer mdcSupportTaskSchedulerCustomizer() {
return taskScheduler -> taskScheduler.setThreadFactory(new UnCaughtExceptionHandleThreadFactory(taskScheduler));
}
}
\ No newline at end of file
package com.schbrain.common.util.support.task;
import lombok.extern.slf4j.Slf4j;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.concurrent.ThreadFactory;
/**
* @author liaozan
* @since 2022/1/11
*/
public class UnCaughtExceptionHandleThreadFactory implements ThreadFactory {
private final ThreadFactory delegate;
public UnCaughtExceptionHandleThreadFactory(ThreadFactory delegate) {
this.delegate = delegate;
}
@Override
public Thread newThread(Runnable runnable) {
Thread thread = delegate.newThread(runnable);
thread.setUncaughtExceptionHandler(new LoggingUnCaughtExceptionHandler());
return thread;
}
@Slf4j
public static class LoggingUnCaughtExceptionHandler implements UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
log.warn("uncaughtException on {}", thread.getName(), throwable);
thread.interrupt();
}
}
}
\ No newline at end of file
package com.schbrain.common.util.support.trace;
import com.schbrain.common.util.JacksonUtils;
import java.lang.annotation.*;
import java.lang.reflect.Method;
import java.util.Map;
import static com.schbrain.common.util.ParameterDiscoverUtils.getMethodArgsMap;
/**
* Please use it only for method
* <p>
* Trace the parameter of method which annotated with this annotation
*
* @author liaozan
* @since 2022/3/31
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TraceParam {
FormatShape shape() default FormatShape.PRETTY_JSON;
enum FormatShape {
RAW {
@Override
protected String format0(Map<String, Object> argsMap) {
return argsMap.toString();
}
},
PRETTY_JSON {
@Override
public String format0(Map<String, Object> argsMap) {
return JacksonUtils.toPrettyJsonString(argsMap);
}
};
public String format(Method method, Object[] args) {
Map<String, Object> methodArgsMap = getMethodArgsMap(method, args);
return format0(methodArgsMap);
}
protected abstract String format0(Map<String, Object> argsMap);
}
}
\ No newline at end of file
package com.schbrain.common.util.support.trace;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.LoggerFactory;
import org.springframework.aop.Advisor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import java.lang.reflect.Method;
/**
* @author liaozan
* @since 2022/3/31
*/
@Slf4j
@Aspect
@ConditionalOnClass({Advisor.class, Aspect.class})
public class TraceParamAspect {
@Before("@annotation(traceParam)")
public void tracedMethod(JoinPoint joinPoint, TraceParam traceParam) {
try {
tracingParam(joinPoint, traceParam);
} catch (Exception e) {
log.warn("Could not extract args for method annotated with @{}", TraceParam.class.getSimpleName(), e);
}
}
protected void tracingParam(JoinPoint joinPoint, TraceParam annotation) {
if (!(joinPoint instanceof MethodSignature)) {
return;
}
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Class<?> declaringType = methodSignature.getDeclaringType();
Method method = methodSignature.getMethod();
Object[] args = joinPoint.getArgs();
String formattedArgs = annotation.shape().format(method, args);
String content = format(method, formattedArgs);
LoggerFactory.getLogger(declaringType).info(content);
}
protected String format(Method method, Object formattedArgs) {
return String.format("%s\n%s", method.toGenericString(), formattedArgs);
}
}
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>commons</artifactId>
<version>${revision}</version>
</parent>
<groupId>com.schbrain.common</groupId>
<artifactId>common</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package com.schbrain.common.annotation;
import java.lang.annotation.*;
/**
* 免登注解
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface IgnoreLogin {
/**
* 是否忽略登录
* @return
*/
boolean ignore() default true;
}
package com.schbrain.common.constants;
import java.time.format.DateTimeFormatter;
import static java.time.ZoneId.systemDefault;
import static java.time.format.DateTimeFormatter.ofPattern;
/**
* @author liaozan
* @since 2021/10/15
*/
@SuppressWarnings("unused")
public class DateTimeFormatters {
public static final String YEAR_MONTH_PATTERN = "yyyy-MM";
public static final String MONTH_DATE_PATTERN = "MM-dd";
public static final String DATE_PATTERN = "yyyy-MM-dd";
public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
public static final String TIME_PATTERN = "HH:mm:ss";
public static final DateTimeFormatter YEAR_MONTH = ofPattern(YEAR_MONTH_PATTERN).withZone(systemDefault());
public static final DateTimeFormatter MONTH_DATE = ofPattern(MONTH_DATE_PATTERN).withZone(systemDefault());
public static final DateTimeFormatter DATE = ofPattern(DATE_PATTERN).withZone(systemDefault());
public static final DateTimeFormatter DATE_TIME = ofPattern(DATE_TIME_PATTERN).withZone(systemDefault());
public static final DateTimeFormatter TIME = ofPattern(TIME_PATTERN).withZone(systemDefault());
public static final String YEAR_MONTH_WITH_SLASH_PATTERN = "yyyy/MM";
public static final String DATE_WITH_SLASH_PATTERN = "yyyy/MM/dd";
public static final DateTimeFormatter YEAR_MONTH_WITH_SLASH = ofPattern(YEAR_MONTH_WITH_SLASH_PATTERN).withZone(systemDefault());
public static final DateTimeFormatter DATE_WITH_SLASH = ofPattern(DATE_WITH_SLASH_PATTERN).withZone(systemDefault());
}
\ No newline at end of file
package com.schbrain.common.constants;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.NoSuchElementException;
public class LogConstants {
@Getter
@AllArgsConstructor
public enum ProductTypeEnum {
JSC("jsc", "驾驶舱"),
ZS("zs", "中枢"),
ZYB("zyb", "作业宝"),
SZ("sz", "数治");
private final String type;
private final String desc;
public static ProductTypeEnum valueOfType(String type) {
for (ProductTypeEnum productTypeEnum : values()) {
if (productTypeEnum.getType().equals(type)) {
return productTypeEnum;
}
}
throw new NoSuchElementException(type);
}
}
}
\ No newline at end of file
package com.schbrain.common.constants;
/**
* @author liaozan
* @since 2022/8/12
*/
public class PageConstants {
/**
* 分页时是否 count
*/
public static final boolean DEFAULT_SEARCH_COUNT = true;
/**
* 默认页码数
*/
public static final int DEFAULT_PAGE_INDEX = 1;
/**
* 默认分页大小
*/
public static final int DEFAULT_PAGE_SIZE = 20;
/**
* 默认总页数
*/
public static final long DEFAULT_TOTAL_PAGE_COUNT = 1;
/**
* 默认总记录数
*/
public static final long DEFAULT_TOTAL_COUNT = 0;
}
\ No newline at end of file
package com.schbrain.common.constants;
/**
* @author liwu
* @since 2019/3/29
*/
public class ResponseActionConstants {
/**
* 业务无异常时统一返回0
*/
public static final int NO_ACTION = 0;
/**
* 忽略异常
*/
public static final int IGNORE = -1;
/**
* 弹框
*/
public static final int ALERT = -2;
/**
* toast
*/
public static final int TOAST = -3;
/**
* 弹框,点击确定后刷新页面
*/
public static final int ALERT_REFRESH = -4;
}
\ No newline at end of file
package com.schbrain.common.constants;
/**
* @author liwu
* @since 2019/3/29
*/
public class ResponseCodeConstants {
/**
* 成功
*/
public static final int SUCCESS = 0;
/**
* 服务器错误,空指针、数组越界等非业务代码抛出异常
*/
public static final int SERVER_ERROR = -1;
/**
* 非法请求,参数异常、参数格式错误等接口的请求非法性抛出的通用错误
*/
public static final int PARAM_INVALID = -2;
/**
* 无权限
*/
public static final int ACCESS_DENIED = -3;
/**
* 用户未登录,且该接口需要登录
*/
public static final int LOGIN_REQUIRED = -4;
/**
* 系统维护
*/
public static final int SYSTEM_MAINTENANCE = -5;
}
\ No newline at end of file
package com.schbrain.common.entity;
import com.schbrain.common.constants.PageConstants;
import lombok.Data;
import java.io.Serializable;
/**
* @author liaozan
* @since 2022/1/7
*/
@Data
public class PageParam implements Serializable {
private static final long serialVersionUID = 4760680296146863368L;
private int pageIndex = PageConstants.DEFAULT_PAGE_INDEX;
private int pageSize = PageConstants.DEFAULT_PAGE_SIZE;
private boolean searchCount = PageConstants.DEFAULT_SEARCH_COUNT;
}
\ No newline at end of file
package com.schbrain.common.entity;
import lombok.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import static com.schbrain.common.constants.PageConstants.*;
/**
* @author liaozan
* @since 2021/10/15
*/
@Data
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class PaginationInfo<T> implements Serializable {
public static final long serialVersionUID = 1320105164315113239L;
/**
* 页索引
*/
private long pageIndex = DEFAULT_PAGE_INDEX;
/**
* 每个页面大小
*/
private long pageSize = DEFAULT_PAGE_SIZE;
/**
* 当前结果集记录数量
*/
private long currentPageSize = DEFAULT_PAGE_INDEX;
/**
* 总页面数量
*/
private long totalPageCount = DEFAULT_TOTAL_COUNT;
/**
* 满足条件的记录数量
*/
private long totalCount = DEFAULT_TOTAL_COUNT;
/**
* 是否有前一页
*/
private boolean hasPrevPage = false;
/**
* 是否有下一页
*/
private boolean hasNextPage = false;
/**
* 结果集, Use new ArrayList() instead of collections.emptyList() to prevent errors when users edit it later
*/
private List<T> dataList = new ArrayList<>(0);
public PaginationInfo(long pageIndex, long pageSize, long totalCount) {
this(pageIndex, pageSize, totalCount, new ArrayList<>(0));
}
public PaginationInfo(long pageIndex, long pageSize, long totalCount, List<T> dataList) {
this.setPageIndex(pageIndex);
this.setPageSize(pageSize);
this.setTotalCount(totalCount);
this.setDataList(dataList);
}
public void setPageIndex(long pageIndex) {
if (pageIndex <= 0) {
pageIndex = DEFAULT_PAGE_INDEX;
}
this.pageIndex = pageIndex;
}
public void setPageSize(long pageSize) {
if (pageSize <= 0) {
pageSize = DEFAULT_PAGE_SIZE;
}
this.pageSize = pageSize;
}
public void setTotalCount(long totalCount) {
if (totalCount < 0L) {
totalCount = DEFAULT_TOTAL_COUNT;
}
if (totalCount == 0L) {
this.totalPageCount = DEFAULT_TOTAL_PAGE_COUNT;
} else {
this.totalPageCount = (totalCount - 1L) / this.pageSize + 1L;
}
this.hasPrevPage = this.pageIndex > DEFAULT_PAGE_INDEX;
this.hasNextPage = this.pageIndex < totalPageCount;
this.totalCount = totalCount;
}
public void setDataList(List<T> dataList) {
if (dataList == null) {
dataList = new ArrayList<>(0);
}
this.dataList = dataList;
this.currentPageSize = dataList.size();
}
}
\ No newline at end of file
package com.schbrain.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author huangxi
* @since 2022/08/23
*/
@Getter
@AllArgsConstructor
public enum BooleanEnum {
/**
* true
*/
TRUE(1),
/**
* false
*/
FALSE(0);
private final Integer value;
public static boolean validate(Integer value) {
for (BooleanEnum booleanEnum : values()) {
if (booleanEnum.getValue().equals(value)) {
return true;
}
}
return false;
}
}
\ No newline at end of file
package com.schbrain.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author huangxi
* @since 2022/08/23
*/
@Getter
@AllArgsConstructor
public enum ValidateEnum {
/**
* 有效
*/
VALID(0),
/**
* 无效
*/
INVALID(-1);
private final Integer value;
}
\ No newline at end of file
package com.schbrain.common.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
import static com.schbrain.common.constants.ResponseActionConstants.ALERT;
import static com.schbrain.common.constants.ResponseCodeConstants.SERVER_ERROR;
/**
* @author liaozan
* @since 2021/10/15
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class BaseException extends RuntimeException {
private static final long serialVersionUID = 6235672740816644251L;
protected final int code;
protected final int action;
public BaseException(String message) {
this(message, null);
}
public BaseException(String message, Throwable throwable) {
this(message, throwable, SERVER_ERROR, ALERT);
}
public BaseException(String message, int code, int action) {
this(message, null, code, action);
}
public BaseException(String message, Throwable cause, int code, int action) {
super(message, cause);
this.code = code;
this.action = action;
}
}
\ No newline at end of file
package com.schbrain.common.exception;
import static com.schbrain.common.constants.ResponseActionConstants.ALERT;
import static com.schbrain.common.constants.ResponseCodeConstants.PARAM_INVALID;
/**
* @author liwu
* @since 2019/3/29
*/
public class ParamInvalidException extends BaseException {
private static final long serialVersionUID = -4015658097738003486L;
public ParamInvalidException(String message) {
super(message, PARAM_INVALID, ALERT);
}
}
\ No newline at end of file
package com.schbrain.common.util;
import com.schbrain.common.exception.ParamInvalidException;
import java.util.*;
public class ValidateUtils {
/**
* Constructor. This class should not normally be instantiated.
*/
private ValidateUtils() {
}
public static void isTrue(boolean expression) {
isTrue(expression, "操作有误");
}
public static void isTrue(boolean expression, String message) {
if (!expression) {
throw new ParamInvalidException(message);
}
}
public static void isFalse(boolean expression) {
isFalse(expression, "操作有误");
}
public static void isFalse(boolean expression, String message) {
if (expression) {
throw new ParamInvalidException(message);
}
}
public static void notNull(Object object) {
notNull(object, "The validated object is null");
}
public static void notNull(Object object, String message) {
if (object == null) {
throw new ParamInvalidException(message);
}
}
public static void isNull(Object object) {
isNull(object, "The validated object is not null");
}
public static void isNull(Object object, String message) {
if (object != null) {
throw new ParamInvalidException(message);
}
}
public static void notEmpty(String value) {
notEmpty(value, "The validated string is empty");
}
public static void notEmpty(String value, String message) {
if (null == value || value.isBlank()) {
throw new ParamInvalidException(message);
}
}
public static void isEmpty(String value) {
isEmpty(value, "The validated string is not empty");
}
public static void isEmpty(String value, String message) {
if (value != null && !value.isEmpty()) {
throw new ParamInvalidException(message);
}
}
public static void notEmpty(Object[] array) {
notEmpty(array, "The validated array is empty");
}
public static void notEmpty(Object[] array, String message) {
if (array == null || array.length == 0) {
throw new ParamInvalidException(message);
}
}
public static void isEmpty(Object[] array) {
isEmpty(array, "The validated array is not empty");
}
public static void isEmpty(Object[] array, String message) {
if (array != null && array.length != 0) {
throw new ParamInvalidException(message);
}
}
public static void notEmpty(Collection<?> collection) {
notEmpty(collection, "The validated collection is empty");
}
public static void notEmpty(Collection<?> collection, String message) {
if (collection == null || collection.isEmpty()) {
throw new ParamInvalidException(message);
}
}
public static void isEmpty(Collection<?> collection) {
isEmpty(collection, "The validated collection is not empty");
}
public static void isEmpty(Collection<?> collection, String message) {
if (collection != null && !collection.isEmpty()) {
throw new ParamInvalidException(message);
}
}
public static void notEmpty(Map<?, ?> map) {
notEmpty(map, "The validated map is empty");
}
public static void notEmpty(Map<?, ?> map, String message) {
if (map == null || map.isEmpty()) {
throw new ParamInvalidException(message);
}
}
public static void isEmpty(Map<?, ?> map) {
isEmpty(map, "The validated map is not empty");
}
public static void isEmpty(Map<?, ?> map, String message) {
if (map != null && !map.isEmpty()) {
throw new ParamInvalidException(message);
}
}
public static void noNullElements(Object[] array) {
notNull(array);
for (int i = 0; i < array.length; i++) {
if (array[i] == null) {
throw new ParamInvalidException("The validated array contains null element at index: " + i);
}
}
}
public static void noNullElements(Object[] array, String message) {
notNull(array);
for (Object item : array) {
if (item == null) {
throw new ParamInvalidException(message);
}
}
}
public static void noNullElements(Collection<?> collection, String message) {
notNull(collection);
for (Object item : collection) {
if (item == null) {
throw new ParamInvalidException(message);
}
}
}
public static void noNullElements(Collection<?> collection) {
notNull(collection);
int i = 0;
for (Iterator<?> it = collection.iterator(); it.hasNext(); i++) {
if (it.next() == null) {
throw new ParamInvalidException("The validated collection contains null element at index: " + i);
}
}
}
public static void allElementsOfType(Collection<?> collection, Class<?> clazz, String message) {
notNull(collection);
notNull(clazz);
for (Object item : collection) {
if (!clazz.isInstance(item)) {
throw new ParamInvalidException(message);
}
}
}
public static void allElementsOfType(Collection<?> collection, Class<?> clazz) {
notNull(collection);
notNull(clazz);
int i = 0;
for (Iterator<?> it = collection.iterator(); it.hasNext(); i++) {
if (!clazz.isInstance(it.next())) {
throw new ParamInvalidException("The validated collection contains an element not of type " + clazz.getName() + " at index: " + i);
}
}
}
}
\ No newline at end of file
<?xml version='1.0' encoding='utf-8'?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>commons</artifactId>
<version>${revision}</version>
</parent>
<groupId>com.schbrain.common</groupId>
<artifactId>module-tree</artifactId>
<dependencies>
<dependency>
<groupId>com.schbrain.framework</groupId>
<artifactId>schbrain-base-dao</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package com.schbrain.common.module.tree;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 结构化的树节点
*
* @author hzchengyi
* @since 2019/1/21
*/
@Data
public class StructureTreeNode<NODE> implements Serializable {
private static final long serialVersionUID = -7732621737666937981L;
private NODE node;
private List<StructureTreeNode<NODE>> children;
}
\ No newline at end of file
package com.schbrain.common.module.tree;
/**
* @author hzchengyi
* @since 2019/1/21
*/
public interface TreeNode {
Long getId();
void setId(Long id);
Long getRelateId();
void setRelateId(Long relateId);
Long getParentId();
void setParentId(Long parentId);
Integer getDepth();
void setDepth(Integer depth);
Integer getLft();
void setLft(Integer lft);
Integer getRgt();
void setRgt(Integer rgt);
Integer getValidate();
void setValidate(Integer validate);
Long getDeleteVersion();
void setDeleteVersion(Long deleteVersion);
default boolean isLeaf() {
return getRgt() == getLft() + 1;
}
}
\ No newline at end of file
package com.schbrain.common.module.tree;
import com.github.pagehelper.Page;
import com.schbrain.common.module.tree.constant.TreeConstant;
import com.schbrain.common.module.tree.dao.TreeNodeDao;
import com.schbrain.common.module.tree.event.TreeOperationAware;
import com.schbrain.common.module.tree.event.TreeOperationEvent;
import com.schbrain.framework.dao.BaseDao;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
/**
* 节点树处理器
*
* @author hzchengyi
* @since 2019/1/21
*/
public class TreeNodeProcessor<NODE extends TreeNode> {
private final TreeNodeDao<NODE> treeNodeDao;
private final TreeOperationAware<NODE> operationHandler;
public TreeNodeProcessor(BaseDao<NODE> baseDao, TreeOperationAware<NODE> operationHandler) {
this.treeNodeDao = new TreeNodeDao<>(baseDao);
this.operationHandler = operationHandler;
}
public List<NODE> listByNode(NODE node) {
return treeNodeDao.listByNode(node);
}
public NODE getById(Long nodeId) {
return treeNodeDao.getById(nodeId);
}
public List<NODE> listByIds(List<Long> nodeIds) {
return treeNodeDao.listByIds(nodeIds);
}
public Integer getDepth(Long nodeId) {
NODE node = treeNodeDao.getById(nodeId);
if (node == null) {
return null;
}
return node.getDepth();
}
public Integer countChildren(Long relateId, Long nodeId, TreeQueryOption option) {
NODE node = treeNodeDao.getById(nodeId);
if (node == null) {
return null;
}
return treeNodeDao.countChildren(relateId, node, option);
}
public Map<Long, Integer> countChildren(Long relateId, List<Long> nodeIds, TreeQueryOption option) {
Map<Long, Integer> result = new HashMap<>();
for (Long nodeId : nodeIds) {
result.put(nodeId, countChildren(relateId, nodeId, option));
}
return result;
}
@Transactional
public NODE addNode(Long parentId, Long preBroNodeId, NODE newNode) {
operationHandler.before(TreeOperationEvent.ADD, Collections.singletonList(newNode));
treeNodeDao.addNode(parentId, preBroNodeId, newNode);
operationHandler.after(TreeOperationEvent.ADD, Collections.singletonList(newNode));
return newNode;
}
@Transactional
public NODE createTree(NODE newNode) {
if (treeNodeDao.countByRelateId(newNode.getRelateId()) > 0) {
// relateId已经存在,不能创建新树
return null;
}
return addNode(null, null, newNode);
}
public Boolean isSubNode(NODE parent, NODE child) {
return parent.getRelateId().equals(child.getRelateId())
&& parent.getLft() < child.getLft()
&& parent.getRgt() > child.getRgt();
}
public Boolean isRelateTo(Long relateId, List<NODE> nodes) {
for (NODE node : nodes) {
if (!node.getRelateId().equals(relateId)) {
return false;
}
}
return true;
}
public Boolean areBros(List<NODE> nodes) {
if (nodes == null || nodes.isEmpty()) {
return false;
}
Long parentId = nodes.get(0).getParentId();
for (NODE node : nodes) {
if (!parentId.equals(node.getParentId())) {
return false;
}
}
return true;
}
@Transactional
public Boolean moveBros(List<NODE> nodes, NODE parent) {
operationHandler.before(TreeOperationEvent.MOVE, nodes);
if (null == nodes || nodes.isEmpty() || null == parent) {
return false;
}
if (nodes.get(0).getParentId() <= 0) {
return false;
}
if (!areBros(nodes)) {
return false;
}
Long relateId = parent.getRelateId();
if (null == relateId) {
return false;
}
if (!isRelateTo(relateId, nodes)) {
return false;
}
NODE srcParent = treeNodeDao.getById(nodes.get(0).getParentId());
if (null == srcParent) {
return false;
}
NODE targetParent = parent;
// 同级目录下移动,不做任何操作
if (srcParent.getId().equals(targetParent.getId())) {
return true;
}
List<Long> nodeIdList = new ArrayList<>(nodes.size());
for (NODE node : nodes) {
nodeIdList.add(node.getId());
// 判断新的parent不是node的子节点以及parent不是节点本身
if (isSubNode(node, targetParent) || node.getId().equals(targetParent.getId())) {
return false;
}
}
int totalNodes = 0;
Map<Long, Integer> nodeCountMap = new HashMap<>(nodes.size());
for (Long nodeId : nodeIdList) {
Integer nodeCount = countChildren(relateId, nodeId, TreeQueryOption.instance().queryIncludeSelf());
if (null == nodeCount) {
nodeCountMap.put(nodeId, 0);
} else {
int intValue = nodeCount;
nodeCountMap.put(nodeId, intValue);
totalNodes += intValue;
}
}
if (nodes.size() > 1) {
// 将节点进行排序
nodes.sort(Comparator.comparingInt(TreeNode::getLft));
}
NODE farLeftNode = nodes.get(0);
TreeQueryOption queryOption = TreeQueryOption.instance().queryExcludeSelf().queryDirectChildren();
Integer srcParentChildrenCount = treeNodeDao.countChildren(relateId, srcParent, queryOption);
// 如果不是移动srcParent下面的所有节点
if (srcParentChildrenCount > nodes.size()) {
// 将所有节点按顺序移动到最右边
for (NODE node : nodes) {
treeNodeDao.moveNodeToFarRight(node.getId(), nodeCountMap.get(node.getId()), srcParent);
}
// 这个时候farLeftNode发生了变化,重新获取
farLeftNode = treeNodeDao.getById(nodes.get(0).getId());
// targetParent也可能发生了变化,重新获取
targetParent = treeNodeDao.getById(targetParent.getId());
}
Long tempRelateId = farLeftNode.getId() * -1;
// 将相关节点独立出来
for (NODE node : nodes) {
// 将节点的relateId更新成tempRelateId
TreeQueryOption option = TreeQueryOption.instance().queryIncludeSelf();
treeNodeDao.updateSubTreeBySql("SET relate_id = " + tempRelateId, relateId, node.getId(), option);
}
// 把parentId改成targetParent的id
treeNodeDao.updateParentId(nodeIdList, targetParent.getId());
// 执行移动
move(relateId, tempRelateId, farLeftNode, totalNodes, srcParent, targetParent);
operationHandler.after(TreeOperationEvent.MOVE, nodes);
return true;
}
public Boolean isRoot(NODE node) {
return node.getParentId().equals(TreeConstant.ROOT_PARENT_ID);
}
@Transactional
public Boolean delete(Long relateId, List<Long> nodeIdList) {
List<NODE> nodeList = listByIds(nodeIdList);
if (CollectionUtils.isEmpty(nodeList)) {
return false;
}
if (nodeList.size() != nodeIdList.size()) {
return false;
}
for (NODE node : nodeList) {
if (!node.getRelateId().equals(relateId)) {
return false;
}
}
operationHandler.before(TreeOperationEvent.DELETE, nodeList);
for (NODE node : nodeList) {
delete(relateId, node);
}
operationHandler.after(TreeOperationEvent.DELETE, nodeList);
return true;
}
public List<NODE> listParent(Long relateId, NODE node, TreeQueryOption option) {
return treeNodeDao.listParent(relateId, node, option);
}
public StructureTreeNode<NODE> convert2StructureTree(Long relateId) {
List<NODE> nodes = batchListNode(relateId);
Map<Long, List<NODE>> nodeSubNodeMap = getNodeSubNodeMap(nodes);
NODE root = nodeSubNodeMap.get(TreeConstant.ROOT_PARENT_ID).get(0);
return constructTree(root, nodeSubNodeMap);
}
public Boolean update(Long relateId, NODE node) {
node.setValidate(null);
node.setDeleteVersion(null);
node.setLft(null);
node.setRgt(null);
node.setRelateId(null);
node.setParentId(null);
return treeNodeDao.updateNodeById(node);
}
public Integer getTreeHeight(Long relateId, Long nodeId) {
NODE node = treeNodeDao.getById(nodeId);
if (node == null) {
return 0;
}
NODE maxDepthSubLeaf = treeNodeDao.getMaxDepthSubLeaf(relateId, node);
if (maxDepthSubLeaf == null) {
return 0;
}
return maxDepthSubLeaf.getDepth() - node.getDepth() + 1;
}
public Page<NODE> pageByParent(Long parentId, Integer pageIndex, Integer pageSize) {
return treeNodeDao.pageByParent(parentId, pageIndex, pageSize);
}
public List<NODE> listByParent(Long parentId) {
return treeNodeDao.listByParent(parentId);
}
public List<NODE> listByParent(Long parentId, Integer levelCount) {
return treeNodeDao.listByParent(parentId, levelCount);
}
@Transactional
public void repairLeftAndRight(Long rootId) {
doRepairLeftAndRight(getById(rootId), null, null, null, null);
}
public int updateByIds(NODE updateNode, List<Long> nodeIds) {
// 此接口不允许更新树结构相关的字段
allTreeFiledSetNull(updateNode);
return treeNodeDao.updateNodeByIds(updateNode, nodeIds);
}
public List<NODE> listNode(List<Long> nodeIds) {
return treeNodeDao.listByIds(nodeIds);
}
private void move(Long relateId, Long tempRelateId, NODE farLeftNode, int nodeCount, NODE srcParent, NODE targetParent) {
int minLeft, maxLeft, minRight, maxRight, lrDiff;
NODE farRightOfTargetParent = treeNodeDao.getFarRightNode(targetParent.getRelateId(), targetParent.getId());
if ((farRightOfTargetParent == null && farLeftNode.getLft() < targetParent.getLft()) ||
(farRightOfTargetParent != null && farLeftNode.getLft() < farRightOfTargetParent.getRgt())) {
// 往右边移
minLeft = srcParent.getRgt();
maxLeft = targetParent.getRgt();
if (null != farRightOfTargetParent) {
maxLeft = farRightOfTargetParent.getRgt();
}
minRight = srcParent.getRgt();
maxRight = targetParent.getRgt();
lrDiff = -1 * nodeCount * 2;
} else {
// 往左边移
minLeft = targetParent.getRgt();
if (null != farRightOfTargetParent) {
minLeft = farRightOfTargetParent.getRgt();
}
maxLeft = srcParent.getRgt();
minRight = targetParent.getRgt();
maxRight = srcParent.getRgt();
lrDiff = nodeCount * 2;
}
treeNodeDao.updateLeftWithRang(relateId, lrDiff, minLeft, false, maxLeft, false);
treeNodeDao.updateRightWithRang(relateId, lrDiff, minRight, true, maxRight, false);
// 执行上面两句操作以后,targetParent和farRightOfTargetParent的left和right都发生了变化,所以需要重新获取
if (null == farRightOfTargetParent) {
targetParent = treeNodeDao.getById(targetParent.getId());
lrDiff = targetParent.getLft() - farLeftNode.getLft() + 1;
} else {
farRightOfTargetParent = treeNodeDao.getFarRightNode(targetParent.getRelateId(), targetParent.getId());
lrDiff = farRightOfTargetParent.getRgt() - farLeftNode.getLft() + 1;
}
// 更新要移动节点的left、right、depth,并恢复relateId
int depthDiff = targetParent.getDepth() - srcParent.getDepth();
treeNodeDao.updateLRAndDepthAndRelateIdWithRelateId(tempRelateId, relateId, lrDiff, depthDiff);
}
private void delete(Long relateId, NODE node) {
treeNodeDao.deleteSubTree(relateId, node);
}
private List<NODE> batchListNode(Long relateId) {
int pageIndex = 1;
Integer pageSize = 500;
Page<NODE> page = treeNodeDao.page(relateId, pageIndex, pageSize, "id desc");
List<NODE> result = new LinkedList<>(page);
while (pageIndex < page.getPages()) {
page = treeNodeDao.page(relateId, ++pageIndex, pageSize, "id desc");
result.addAll(page);
}
return result;
}
private Map<Long, List<NODE>> getNodeSubNodeMap(List<NODE> nodes) {
Map<Long, List<NODE>> map = new HashMap<>();
for (NODE node : nodes) {
if (map.containsKey(node.getParentId())) {
map.get(node.getParentId()).add(node);
} else {
List<NODE> nodeLevel = new LinkedList<>();
nodeLevel.add(node);
map.put(node.getParentId(), nodeLevel);
}
}
return map;
}
private StructureTreeNode<NODE> constructTree(NODE node, Map<Long, List<NODE>> nodeSubNodeMap) {
StructureTreeNode<NODE> structureNode = new StructureTreeNode<>();
structureNode.setNode(node);
if (nodeSubNodeMap.containsKey(node.getId())) {
List<NODE> childrenNode = nodeSubNodeMap.get(node.getId());
List<StructureTreeNode<NODE>> children = new ArrayList<>();
for (NODE childNode : childrenNode) {
children.add(constructTree(childNode, nodeSubNodeMap));
}
children.sort(Comparator.comparingInt(e -> e.getNode().getLft()));
structureNode.setChildren(children);
} else {
structureNode.setChildren(new LinkedList<>());
}
return structureNode;
}
private void doRepairLeftAndRight(NODE node, Long parentId, Integer parentLeft, Integer parentDepth, NODE preBro) {
int left, right, depth;
if (null == parentId) {
left = 1;
depth = 1;
} else {
left = parentLeft + 1;
depth = parentDepth + 1;
}
if (null != preBro) {
left = preBro.getRgt() + 1;
}
List<NODE> children = treeNodeDao.listByParent(node.getId());
if (null == children || children.isEmpty()) {
right = left + 1;
} else {
NODE preNode = null;
for (NODE child : children) {
doRepairLeftAndRight(child, node.getId(), left, depth, preNode);
preNode = child;
}
right = preNode.getRgt() + 1;
}
if (left != node.getLft() || right != node.getRgt() || depth != node.getDepth()) {
treeNodeDao.updateLRAndDepth(left, right, depth, node.getId());
node.setLft(left);
node.setRgt(right);
node.setDepth(depth);
}
}
private void allTreeFiledSetNull(NODE node) {
node.setValidate(null);
node.setRelateId(null);
node.setParentId(null);
node.setDepth(null);
node.setDeleteVersion(null);
node.setLft(null);
node.setRgt(null);
}
}
\ No newline at end of file
package com.schbrain.common.module.tree;
/**
* @author hzchengyi
* @since 2019/1/21
*/
public class TreeQueryOption {
/**
* 包括节点自身
*/
public static final int TREE_QUERY_SELF_INCLUDE = 0;
/**
* 不包括节点自身
*/
public static final int TREE_QUERY_SELF_EXCLUDE = 1;
/**
* 只包含直接子节点
*/
public static final int TREE_QUERY_CHILDREN_DIRECT = 0;
/**
* 包含所有子节点
*/
public static final int TREE_QUERY_CHILDREN_ALL = 1;
/**
* 深度排序-从根到叶子节点
*/
public static final int TREE_QUERY_DEPTH_ORDER_ROOT_2_LEAF = 0;
/**
* 深度排序-从叶子节点到根
*/
public static final int TREE_QUERY_DEPTH_ORDER_LEAF_2_ROOT = 1;
private int selfIncludeMode;
private int childrenMode;
private int depthOrder;
private TreeQueryOption() {
}
public static TreeQueryOption instance() {
TreeQueryOption option = new TreeQueryOption();
option.selfIncludeMode = TREE_QUERY_SELF_EXCLUDE;
option.childrenMode = TREE_QUERY_CHILDREN_ALL;
option.depthOrder = TREE_QUERY_DEPTH_ORDER_ROOT_2_LEAF;
return option;
}
public Integer getSelfIncludeMode() {
return this.selfIncludeMode;
}
public Integer getChildrenMode() {
return this.childrenMode;
}
public Integer getDepthOrder() {
return this.depthOrder;
}
public TreeQueryOption queryIncludeSelf() {
this.selfIncludeMode = TREE_QUERY_SELF_INCLUDE;
return this;
}
public TreeQueryOption queryExcludeSelf() {
this.selfIncludeMode = TREE_QUERY_SELF_EXCLUDE;
return this;
}
public TreeQueryOption queryDirectChildren() {
this.childrenMode = TREE_QUERY_CHILDREN_DIRECT;
return this;
}
public TreeQueryOption queryAllChildren() {
this.childrenMode = TREE_QUERY_CHILDREN_ALL;
return this;
}
public TreeQueryOption depthOrderRoot2Leaf() {
this.depthOrder = TREE_QUERY_DEPTH_ORDER_ROOT_2_LEAF;
return this;
}
public TreeQueryOption depthOrderLeaf2Root() {
this.depthOrder = TREE_QUERY_DEPTH_ORDER_LEAF_2_ROOT;
return this;
}
}
\ No newline at end of file
package com.schbrain.common.module.tree.constant;
/**
* @author hzchengyi
* @since 2019/1/21
*/
public class TreeConstant {
public static final Long ROOT_PARENT_ID = -1L;
public static final Long NODE_DELETE_VERSION_DEFAULT = 0L;
}
\ No newline at end of file
package com.schbrain.common.module.tree.dao;
import com.github.pagehelper.Page;
import com.schbrain.common.enums.ValidateEnum;
import com.schbrain.common.module.tree.TreeNode;
import com.schbrain.common.module.tree.TreeQueryOption;
import com.schbrain.common.module.tree.constant.TreeConstant;
import com.schbrain.framework.dao.BaseDao;
import com.schbrain.framework.dao.util.SQLUtil;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
/**
* @author hzchengyi
* @since 2019/1/21
*/
public class TreeNodeDao<NODE extends TreeNode> {
private final BaseDao<NODE> baseDao;
public TreeNodeDao(BaseDao<NODE> baseDao) {
this.baseDao = baseDao;
}
public List<NODE> listByNode(NODE node) {
return baseDao.listByObject(node);
}
public NODE getById(Long id) {
return baseDao.getOneByCondition("id = #{id} AND validate = #{validate}", id, ValidateEnum.VALID.getValue());
}
public List<NODE> listByIds(List<Long> ids) {
return baseDao.listByCondition("validate = #{validate} AND " + SQLUtil.buidInClause("id", Long.class, ids), ValidateEnum.VALID.getValue());
}
public Integer countParent(Long relateId, NODE node, TreeQueryOption option) {
StringBuilder condition = new StringBuilder();
List<Object> params = new LinkedList<>();
params.add(relateId);
condition
.append("relate_id = #{relateId} ")
.append(" AND ").append(getParentLeftRange(node, option, params))
.append(" AND ").append(getParentRightRange(node, option, params))
.append(" AND validate = #{validate}");
params.add(ValidateEnum.VALID.getValue());
return baseDao.getCountByCondition(condition.toString(), params.toArray());
}
public Integer countChildren(Long relateId, NODE node, TreeQueryOption option) {
StringBuilder condition = new StringBuilder();
List<Object> params = new LinkedList<>();
params.add(relateId);
condition
.append("relate_id = #{relateId}")
.append(" AND ").append(getChildrenLeftRange(node, option, params))
.append(" AND ").append(getChildrenRightRange(node, option, params))
.append(" AND validate = #{validate}");
params.add(ValidateEnum.VALID.getValue());
if (option.getChildrenMode().equals(TreeQueryOption.TREE_QUERY_CHILDREN_DIRECT)) {
// 只查询直接子节点
condition.append(" AND parent_id = #{parentId} ");
params.add(node.getId());
}
return baseDao.getCountByCondition(condition.toString(), params.toArray());
}
public Integer countByRelateId(Long relateId) {
return baseDao.getCountByCondition("relate_id = #{relateId} AND validate = #{validate}", relateId, ValidateEnum.VALID.getValue());
}
public boolean addNode(Long parentId, Long preBroNodeId, NODE newNode) {
newNode.setValidate(ValidateEnum.VALID.getValue());
newNode.setDeleteVersion(TreeConstant.NODE_DELETE_VERSION_DEFAULT);
NODE parent = null;
if (null != parentId) {
parent = getById(parentId);
}
NODE preBroNode = null;
if (null != preBroNodeId) {
preBroNode = getById(preBroNodeId);
}
// 如果parent和preBroNode都为null,说明是根节点
if (null == parent && null == preBroNode) {
newNode.setLft(1);
newNode.setRgt(2);
newNode.setDepth(1);
newNode.setParentId(TreeConstant.ROOT_PARENT_ID);
return baseDao.add(newNode);
}
// 如果parent不为null并且preBroNode为null,则代表该节点直接为该父节点下的第一个子节点
if (null == preBroNode) {
// 说明是父节点的第一个子节点
newNode.setLft(parent.getLft() + 1);
newNode.setRgt(newNode.getLft() + 1);
newNode.setDepth(parent.getDepth() + 1);
newNode.setParentId(parent.getId());
// 增加父节点右边所有节点的left和right,留出空位
increaseNodesLeftAndRight(newNode.getRelateId(), parent.getLft(), parent.getLft(), false, 2);
// 添加节点
return baseDao.add(newNode);
}
// 处理非唯一叶子节点的情况
newNode.setLft(preBroNode.getRgt() + 1);
newNode.setRgt(newNode.getLft() + 1);
newNode.setDepth(preBroNode.getDepth());
newNode.setParentId(preBroNode.getParentId());
// 增加兄弟节点右边所有节点的left和right,留出空位
increaseNodesLeftAndRight(preBroNode.getRelateId(), preBroNode.getRgt(), preBroNode.getRgt(), false, 2);
// 添加节点
return baseDao.add(newNode);
}
public int deleteSubTree(Long relateId, NODE node) {
if (null == node) {
return 0;
}
if (!relateId.equals(node.getRelateId())) {
return 0;
}
Long deleteVersion = System.currentTimeMillis();
String updateSql = "UPDATE " + baseDao.getTableName()
+ " SET validate = #{validate} , delete_version = #{deleteVersion} WHERE"
+ " relate_id = #{relateId} AND lft >= #{lft} AND rgt <= #{rgt} AND validate = #{validate}";
int nodeCount = baseDao.updateByCompleteSql(
updateSql, ValidateEnum.INVALID.getValue(), deleteVersion,
node.getRelateId(), node.getLft(), node.getRgt(), ValidateEnum.VALID.getValue());
// 更新右边节点的left和right
return decreaseNodesLeftAndRight(node.getRelateId(), node.getRgt(), node.getRgt(), nodeCount * 2);
}
public List<NODE> listParent(Long relateId, NODE node, TreeQueryOption option) {
StringBuilder condition = new StringBuilder();
List<Object> params = new LinkedList<>();
params.add(relateId);
condition
.append("relate_id = #{relateId}")
.append(" AND ").append(getParentLeftRange(node, option, params))
.append(" AND ").append(getParentRightRange(node, option, params))
.append(" AND validate = #{validate} ");
params.add(ValidateEnum.VALID.getValue());
condition.append(getOrderBy(option));
return baseDao.listByCondition(condition.toString(), params.toArray());
}
public Page<NODE> page(Long relateId, Integer pageIndex, Integer pageSize, String orderCondition) {
String condition = "relate_id = #{relateId} AND validate = #{validate}";
if (StringUtils.isNotBlank(orderCondition)) {
condition += " order by " + orderCondition;
}
return baseDao.pageByCondition(pageIndex, pageSize, condition, relateId, ValidateEnum.VALID.getValue());
}
public boolean updateNodeById(NODE node) {
return baseDao.updateById(node);
}
public NODE getMaxDepthSubLeaf(Long relateId, NODE node) {
String condition = " relate_id = #{relateId} AND lft >= #{lft} AND rgt <= #{rgt} AND validate = #{validate} ORDER BY depth DESC";
return baseDao.getOneByCondition(condition, relateId, node.getLft(), node.getRgt(), ValidateEnum.VALID.getValue());
}
public int updateSubTreeBySql(String updateSql, Long relateId, Long nodeId, TreeQueryOption option) {
NODE node = getById(nodeId);
if (node == null) {
return 0;
}
List<Object> params = new LinkedList<>();
String sql = "UPDATE " + baseDao.getTableName() + " " + updateSql
+ " WHERE " + getChildrenLeftRange(node, option, params)
+ " AND " + getChildrenRightRange(node, option, params)
+ " AND relate_id = #{relateId} AND validate = #{validate}";
params.add(relateId);
params.add(ValidateEnum.VALID.getValue());
return baseDao.updateByCompleteSql(sql, params.toArray());
}
public NODE getFarRightNode(Long relateId, Long parentId) {
String condition = "relate_id = #{relateId} AND parent_id = #{parentId} AND validate = #{validate} ORDER BY lft DESC limit 1";
return baseDao.getOneByCondition(condition, relateId, parentId, ValidateEnum.VALID.getValue());
}
public void moveNodeToFarRight(Long nodeId, Integer nodeCount, NODE parent) {
NODE node = getById(nodeId);
// 1.更新节点及所有子节点的relateId
Long tempRelateId = nodeId * -1;
StringBuilder set = new StringBuilder();
set.append("SET relate_id = ").append(tempRelateId);
updateSubTreeBySql(set.toString(), parent.getRelateId(), nodeId, TreeQueryOption.instance().queryIncludeSelf());
// 2.右边的节点更新left和right
set.delete(0, set.length());
set.append("UPDATE ")
.append(baseDao.getTableName()).append(" ")
.append("SET lft = lft - ").append(nodeCount * 2)
.append(", rgt = rgt - ").append(nodeCount * 2)
.append(" WHERE lft > #{lft} AND rgt < #{rgt} AND relate_id = #{relateId} AND validate = #{validate}");
baseDao.updateByCompleteSql(set.toString(), node.getRgt(), parent.getRgt(), parent.getRelateId(), ValidateEnum.VALID.getValue());
// 3.更新节点及所有子节点的left和right,恢复relateId
set.delete(0, set.length());
NODE farRightNode = getFarRightNode(parent.getRelateId(), parent.getId());
int increment = farRightNode.getRgt() - node.getLft() + 1;
updateLRAndDepthAndRelateIdWithRelateId(tempRelateId, parent.getRelateId(), increment, 0);
}
public int updateLeftWithRang(Long relateId, int diff, Integer minLeft, boolean includeMinLeft, Integer maxLeft, boolean includeMaxLeft) {
String sql = "UPDATE " + baseDao.getTableName() + " " +
"SET lft = lft + " + diff + " WHERE lft " +
(includeMinLeft ? ">=" : ">") + " " + minLeft + " AND lft " +
(includeMaxLeft ? "<=" : "<") + " " + maxLeft +
" AND relate_id = #{relateId} AND validate = #{validate}";
return baseDao.updateByCompleteSql(sql, relateId, ValidateEnum.VALID.getValue());
}
public int updateRightWithRang(Long relateId, int diff, Integer minRight, boolean includeMinRight,
Integer maxRight, boolean includeMaxRight) {
String sql = "UPDATE " + baseDao.getTableName() + " " +
"SET rgt = rgt + " + diff + " WHERE rgt " +
(includeMinRight ? ">=" : ">") + " " + minRight + " AND rgt " +
(includeMaxRight ? "<=" : "<") + " " + maxRight +
" AND relate_id = #{relateId} AND validate = #{validate}";
return baseDao.updateByCompleteSql(sql, relateId, ValidateEnum.VALID.getValue());
}
public int updateLRAndDepthAndRelateIdWithRelateId(Long oldRelateId, Long newRelateId, int lrDiff, int depthDiff) {
String sql = "UPDATE " + baseDao.getTableName() + " "
+ "SET relate_id = #{relateId}" + ", lft = lft + " + lrDiff
+ ", rgt = rgt + " + lrDiff
+ ", depth = depth + " + depthDiff
+ " WHERE relate_id = #{relateId} AND validate = #{validate}";
return baseDao.updateByCompleteSql(sql, newRelateId, oldRelateId, ValidateEnum.VALID.getValue());
}
public Page<NODE> pageByParent(Long parentId, Integer pageIndex, Integer pageSize) {
return baseDao.pageByCondition(pageIndex, pageSize, "parent_id = #{parentId} order by lft", parentId);
}
public int updateParentId(List<Long> nodeIds, Long parentId) {
String sql = "UPDATE " + baseDao.getTableName() + " SET parent_id = #{parentId} WHERE " + SQLUtil.buidInClause("id", Long.class, nodeIds);
return baseDao.updateByCompleteSql(sql, parentId);
}
public List<NODE> listByParent(Long parentId) {
String condition = "parent_id = #{parentId} AND validate = #{validate} order by lft ASC";
return baseDao.listByCondition(condition, parentId, ValidateEnum.VALID.getValue());
}
public List<NODE> listByParent(Long parentId, Integer levelCount) {
NODE parent = getById(parentId);
if (null == parent) {
return Collections.emptyList();
}
int depth = parent.getDepth() + levelCount;
String condition = "relate_id = #{relateId} AND lft > #{lft} AND rgt < #{rgt} AND depth <= #{depth} AND validate = #{validate} order by lft ASC";
return baseDao.listByCondition(condition, parent.getRelateId(), parent.getLft(), parent.getRgt(), depth, ValidateEnum.VALID.getValue());
}
public int updateLRAndDepth(Integer left, Integer right, Integer depth, Long nodeId) {
String sql = "UPDATE " + baseDao.getTableName() + " SET lft = #{lft}, rgt = #{rgt}, depth = #{depth} WHERE id = #{id}";
return baseDao.updateByCompleteSql(sql, left, right, depth, nodeId);
}
public int updateNodeByIds(NODE updateNode, List<Long> nodeIds) {
return baseDao.updateByCondition(updateNode, SQLUtil.buidInClause("id", Long.class, nodeIds));
}
private String getParentLeftRange(NODE node, TreeQueryOption option, List<Object> params) {
params.add(node.getLft());
switch (option.getSelfIncludeMode()) {
case TreeQueryOption.TREE_QUERY_SELF_INCLUDE:
return " lft <= #{lft} ";
case TreeQueryOption.TREE_QUERY_SELF_EXCLUDE:
return " lft < #{lft} ";
default:
// never goes here
throw new IllegalArgumentException("param option invalid.");
}
}
private String getParentRightRange(NODE node, TreeQueryOption option, List<Object> params) {
params.add(node.getRgt());
switch (option.getSelfIncludeMode()) {
case TreeQueryOption.TREE_QUERY_SELF_INCLUDE:
return " rgt >= #{rgt} ";
case TreeQueryOption.TREE_QUERY_SELF_EXCLUDE:
return " rgt > #{rgt} ";
default:
// never goes here
throw new IllegalArgumentException("param option invalid.");
}
}
private String getChildrenLeftRange(NODE node, TreeQueryOption option, List<Object> params) {
StringBuilder condition = new StringBuilder();
params.add(node.getLft());
switch (option.getSelfIncludeMode()) {
case TreeQueryOption.TREE_QUERY_SELF_INCLUDE:
condition.append(" lft >= #{lft} ");
break;
case TreeQueryOption.TREE_QUERY_SELF_EXCLUDE:
condition.append(" lft > #{lft} ");
break;
}
return condition.toString();
}
private String getChildrenRightRange(NODE node, TreeQueryOption option, List<Object> params) {
StringBuilder condition = new StringBuilder();
params.add(node.getRgt());
switch (option.getSelfIncludeMode()) {
case TreeQueryOption.TREE_QUERY_SELF_INCLUDE:
condition.append(" rgt <= #{rgt} ");
break;
case TreeQueryOption.TREE_QUERY_SELF_EXCLUDE:
condition.append(" rgt < #{rgt} ");
break;
}
return condition.toString();
}
/**
* 如果是父节点的left和right,则includeRight为true,因为父节点的right值也需要更新;
* 如果是兄弟节点的left和right,则include为false,因为系统节点的right值不需要更新。
*/
private int increaseNodesLeftAndRight(Long relateId, Integer left, Integer right, boolean includeRight, Integer increment) {
StringBuilder sql = new StringBuilder();
sql.append("UPDATE ")
.append(baseDao.getTableName())
.append(" SET lft = lft + ").append(increment)
.append(" WHERE relate_id = #{relateId} AND lft > #{lft} AND validate = #{validate}");
baseDao.updateByCompleteSql(sql.toString(), relateId, left, ValidateEnum.VALID.getValue());
sql.delete(0, sql.length());
sql.append("UPDATE ").append(baseDao.getTableName())
.append(" SET rgt = rgt + ").append(increment)
.append(" WHERE relate_id = #{relateId} AND rgt ")
.append(includeRight ? ">=" : ">")
.append(" #{rgt} AND validate = #{validate}");
return baseDao.updateByCompleteSql(sql.toString(), relateId, right, ValidateEnum.VALID.getValue());
}
private int decreaseNodesLeftAndRight(Long relateId, Integer left, Integer right, Integer decrement) {
StringBuilder sql = new StringBuilder();
sql.append("UPDATE ")
.append(baseDao.getTableName())
.append(" set lft = lft - ").append(decrement)
.append(" WHERE relate_id = #{relateId} AND lft > #{lft} AND validate = #{validate}");
baseDao.updateByCompleteSql(sql.toString(), relateId, left, ValidateEnum.VALID.getValue());
sql.delete(0, sql.length());
sql.append("UPDATE ").append(baseDao.getTableName())
.append(" set rgt = rgt - ").append(decrement)
.append(" WHERE relate_id = #{relateId} AND rgt > #{rgt} AND validate = #{validate}");
return baseDao.updateByCompleteSql(sql.toString(), relateId, right, ValidateEnum.VALID.getValue());
}
private String getOrderBy(TreeQueryOption option) {
switch (option.getDepthOrder()) {
case TreeQueryOption.TREE_QUERY_DEPTH_ORDER_ROOT_2_LEAF:
return " ORDER BY lft ASC";
case TreeQueryOption.TREE_QUERY_DEPTH_ORDER_LEAF_2_ROOT:
return " ORDER BY lft DESC";
default:
// never goes here
throw new RuntimeException("查询错误");
}
}
}
\ No newline at end of file
package com.schbrain.common.module.tree.event;
import com.schbrain.common.module.tree.TreeNode;
import java.util.List;
/**
* Created by hzchengyi on 2019/1/21.
*/
public class EmptyTreeOperationAware<NODE extends TreeNode> implements TreeOperationAware<NODE> {
@Override
public void before(TreeOperationEvent event, List<NODE> nodes) {
}
@Override
public void after(TreeOperationEvent event, List<NODE> nodes) {
}
}
\ No newline at end of file
package com.schbrain.common.module.tree.event;
import com.schbrain.common.module.tree.TreeNode;
import java.util.List;
/**
* Created by hzchengyi on 2019/1/21.
*/
public interface TreeOperationAware<NODE extends TreeNode> {
/**
* 执行操作之前调用
*/
void before(TreeOperationEvent event, List<NODE> nodes);
/**
* 执行操作之后调用
*/
void after(TreeOperationEvent event, List<NODE> nodes);
}
\ No newline at end of file
package com.schbrain.common.module.tree.event;
/**
* Created by hzchengyi on 2019/1/21.
*/
public enum TreeOperationEvent {
ADD,
DELETE,
MOVE;
}
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>schbrain-parent</artifactId>
<version>${revision}</version>
</parent>
<artifactId>commons</artifactId>
<packaging>pom</packaging>
<modules>
<module>common</module>
<module>web-common</module>
<module>common-util</module>
<module>module-tree</module>
</modules>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>commons</artifactId>
<version>${revision}</version>
</parent>
<groupId>com.schbrain.common</groupId>
<artifactId>web-common</artifactId>
<dependencies>
<dependency>
<groupId>com.schbrain.common</groupId>
<artifactId>common-util</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!-- Used for com.schbrain.common.web.exception.DefaultGlobalExceptionHandler#handleDataAccessException -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<!-- Optional -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package com.schbrain.common.web;
import com.schbrain.common.web.argument.BodyParamArgumentResolverWebMvcConfigurer;
import com.schbrain.common.web.exception.*;
import com.schbrain.common.web.log.RequestLoggingFilter;
import com.schbrain.common.web.properties.WebProperties;
import com.schbrain.common.web.result.ResponseBodyHandler;
import com.schbrain.common.web.servlet.*;
import com.schbrain.common.web.support.authentication.AuthenticationInterceptor;
import com.schbrain.common.web.support.authentication.Authenticator;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.boot.autoconfigure.condition.*;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.client.RestTemplate;
import java.util.List;
/**
* @author liaozan
* @since 2021/11/19
*/
@AutoConfiguration
@ConditionalOnWebApplication
@EnableConfigurationProperties(WebProperties.class)
public class WebCommonAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(Authenticator.class)
public AuthenticationInterceptor defaultAuthenticationInterceptor(Authenticator authenticator) {
return new AuthenticationInterceptor(authenticator);
}
@Bean
@ConditionalOnMissingBean
public GlobalExceptionHandler defaultGlobalExceptionHandler() {
return new DefaultGlobalExceptionHandler();
}
@Bean
@ConditionalOnMissingBean
public ExceptionHandlerWebMcvConfigurer defaultExceptionHandlerWebMcvConfigurer(WebProperties webProperties, GlobalExceptionHandler exceptionHandler) {
return new ExceptionHandlerWebMcvConfigurer(webProperties, exceptionHandler);
}
@Bean
@ConditionalOnMissingBean
public BodyParamArgumentResolverWebMvcConfigurer defaultBodyParamArgumentResolverWebMvcConfigurer() {
return new BodyParamArgumentResolverWebMvcConfigurer();
}
@Bean
@Lazy
@ConditionalOnMissingBean
public RestTemplate restTemplate(ObjectProvider<RestTemplateBuilder> restTemplateBuilder) {
RestTemplateBuilder builder = restTemplateBuilder.getIfAvailable();
if (builder == null) {
return new RestTemplate();
}
return builder.build();
}
@Bean
@ConditionalOnMissingBean
public ResponseBodyHandler defaultResponseBodyHandler(WebProperties properties, BeanFactory beanFactory) {
List<String> basePackages = AutoConfigurationPackages.get(beanFactory);
return new ResponseBodyHandler(properties, basePackages);
}
@Bean
@ConditionalOnMissingBean
public TraceIdInitializeServletListener traceIdInitializeServletListener() {
return new TraceIdInitializeServletListener();
}
@Bean
@ConditionalOnMissingBean
public CharacterEncodingServletContextInitializer characterEncodingServletContextInitializer(WebProperties webProperties) {
return new CharacterEncodingServletContextInitializer(webProperties.getEncoding());
}
@Bean
@ConditionalOnMissingBean
public RequestLoggingFilter requestLoggingFilter(WebProperties properties) {
return new RequestLoggingFilter(properties);
}
@Bean
@ConditionalOnMissingBean
public AllowAllCorsConfigurer allowAllCorsConfigurer() {
return new AllowAllCorsConfigurer();
}
}
\ No newline at end of file
package com.schbrain.common.web.annotation;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ValueConstants;
import java.lang.annotation.*;
/**
* @author liaozan
* @see RequestParam
* @since 2022-12-02
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BodyParam {
/**
* @see RequestParam#value()
*/
@AliasFor("name")
String value() default "";
/**
* @see RequestParam#name()
*/
@AliasFor("value")
String name() default "";
/**
* @see RequestParam#required()
*/
boolean required() default true;
/**
* @see RequestParam#defaultValue()
*/
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
\ No newline at end of file
package com.schbrain.common.web.annotation;
import java.lang.annotation.*;
/**
* @author liaozan
* @see com.schbrain.common.web.result.ResponseDTO
* @since 2022/8/29
*/
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseWrapOption {
/**
* 是否忽略返回值处理
*/
boolean ignore() default true;
/**
* 是否忽略异常处理
*/
boolean ignoreException() default true;
}
\ No newline at end of file
package com.schbrain.common.web.argument;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* @author liaozan
* @since 2022-12-02
*/
public class BodyParamArgumentResolverWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new BodyParamMethodArgumentResolver());
}
}
\ No newline at end of file
package com.schbrain.common.web.argument;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.schbrain.common.util.JacksonUtils;
import com.schbrain.common.web.annotation.BodyParam;
import lombok.Setter;
import org.springframework.core.MethodParameter;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
/**
* @author liaozan
* @since 2022-12-02
*/
@Setter
public class BodyParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
private static final String METHOD_BODY_CACHE_KEY = BodyParamMethodArgumentResolver.class.getName() + ".bodyParamCache";
private ObjectMapper objectMapper;
public BodyParamMethodArgumentResolver() {
this(JacksonUtils.getObjectMapper());
}
public BodyParamMethodArgumentResolver(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(BodyParam.class);
}
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
BodyParam annotation = parameter.getParameterAnnotation(BodyParam.class);
Assert.notNull(annotation, "annotation should never be null");
return new NamedValueInfo(annotation.name(), annotation.required(), annotation.defaultValue());
}
@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
JsonNode paramNode = getParamNode(request);
JsonNode parameterValue = paramNode.get(name);
if (parameterValue == null || parameterValue.isNull()) {
return null;
}
Class<?> parameterType = parameter.getParameterType();
return objectMapper.convertValue(parameterValue, parameterType);
}
private JsonNode getParamNode(NativeWebRequest nativeWebRequest) throws IOException {
HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
Assert.state(request != null, "request must not be null");
JsonNode paramNode = (JsonNode) request.getAttribute(METHOD_BODY_CACHE_KEY);
if (paramNode == null) {
InputStream inputStream = StreamUtils.nonClosing(request.getInputStream());
paramNode = objectMapper.readTree(inputStream);
request.setAttribute(METHOD_BODY_CACHE_KEY, paramNode);
}
return paramNode;
}
}
\ No newline at end of file
package com.schbrain.common.web.exception;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import com.schbrain.common.exception.BaseException;
import com.schbrain.common.util.EnvUtils;
import com.schbrain.common.web.result.ResponseDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.validator.internal.engine.path.PathImpl;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.util.ClassUtils;
import org.springframework.validation.*;
import org.springframework.web.*;
import org.springframework.web.bind.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.sql.SQLException;
import java.util.*;
import static com.schbrain.common.constants.ResponseCodeConstants.*;
/**
* @author liaozan
* @since 2019/10/14
*/
@Slf4j
@ResponseBody
@ResponseStatus(HttpStatus.OK)
public class DefaultGlobalExceptionHandler implements GlobalExceptionHandler {
/************************************* Base Exception Handing *************************************/
@ExceptionHandler(BaseException.class)
public ResponseDTO<String> handleBaseException(BaseException ex) {
return loggingThenBuildResponse(ex, ex.getCode());
}
/************************************* Common Exception Handing *************************************/
@ExceptionHandler(Throwable.class)
public ResponseDTO<String> handleAll(Throwable ex) {
return loggingThenBuildResponse(ex, SERVER_ERROR);
}
@ExceptionHandler(NullPointerException.class)
public ResponseDTO<String> handleNullPointerException(NullPointerException ex) {
return loggingThenBuildResponse(ex, SERVER_ERROR);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseDTO<String> handleIllegalArgumentException(IllegalArgumentException ex) {
return loggingThenBuildResponse(ex, SERVER_ERROR);
}
@ExceptionHandler(IllegalStateException.class)
public ResponseDTO<String> handleIllegalStateException(IllegalStateException ex) {
return loggingThenBuildResponse(ex, SERVER_ERROR);
}
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseDTO<String> handleNoHandlerFoundException(NoHandlerFoundException ex) {
return loggingThenBuildResponse(ex, PARAM_INVALID);
}
@ExceptionHandler(AsyncRequestTimeoutException.class)
public ResponseDTO<String> handleAsyncRequestTimeoutException(AsyncRequestTimeoutException ex) {
return loggingThenBuildResponse(ex, SERVER_ERROR);
}
/************************************* SQL Exception Handing *************************************/
@ExceptionHandler(SQLException.class)
public ResponseDTO<String> handleSQLException(SQLException ex) {
return loggingThenBuildResponse(ex, SERVER_ERROR);
}
@ExceptionHandler(DataAccessException.class)
public ResponseDTO<String> handleDataAccessException(DataAccessException ex) {
return loggingThenBuildResponse(ex, SERVER_ERROR);
}
/************************************* Http Request Exception Handing *************************************/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseDTO<String> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException ex) {
String errorMsg = StrUtil.format("不支持该HTTP方法: {}, 请使用 {}", ex.getMethod(), Arrays.toString(ex.getSupportedMethods()));
log.error(errorMsg);
return buildResponse(ex, PARAM_INVALID, errorMsg);
}
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ResponseDTO<String> handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException ex) {
String errorMsg = StrUtil.format("不支持该媒体类型: {}, 请使用 {}", ex.getContentType(), ex.getSupportedMediaTypes());
log.error(errorMsg);
return buildResponse(ex, PARAM_INVALID, errorMsg);
}
@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
public ResponseDTO<String> handleHttpMediaTypeNotAcceptableException(HttpMediaTypeNotAcceptableException ex) {
String errorMsg = StrUtil.format("不支持的媒体类型, 请使用 {}", ex.getSupportedMediaTypes());
log.error(errorMsg);
return buildResponse(ex, PARAM_INVALID, errorMsg);
}
/************************************* Method Parameter Exception Handing *************************************/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseDTO<String> handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) {
String errorMsg = StrUtil.format("参数解析失败, {}", ex.getMessage());
log.error(errorMsg);
return buildResponse(ex, PARAM_INVALID, errorMsg);
}
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseDTO<String> handlerMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex) {
Object value = ex.getValue();
String variableName = ex.getName();
Class<?> requiredTypeClass = ex.getRequiredType();
String requiredType = ClassUtils.getQualifiedName(requiredTypeClass == null ? Object.class : requiredTypeClass);
String providedType = ClassUtils.getDescriptiveType(value);
String errorMsg = StrUtil.format("参数类型不匹配, 参数名: {}, 需要: {}, 传入: {} 的 {}", variableName, requiredType, providedType, value);
log.error(errorMsg);
return buildResponse(ex, PARAM_INVALID, errorMsg);
}
@ExceptionHandler(MissingPathVariableException.class)
public ResponseDTO<String> handleMissingPathVariableException(MissingPathVariableException ex) {
String errorMsg = StrUtil.format("丢失路径参数, 参数名: {}, 参数类型: {}", ex.getVariableName(), ex.getParameter().getParameterType());
log.error(errorMsg);
return buildResponse(ex, PARAM_INVALID, errorMsg);
}
@ExceptionHandler(MissingRequestCookieException.class)
public ResponseDTO<String> handleMissingRequestCookieException(MissingRequestCookieException ex) {
String errorMsg = StrUtil.format("丢失Cookie参数, 参数名: {}, 参数类型: {}", ex.getCookieName(), ex.getParameter().getParameterType());
log.error(errorMsg);
return buildResponse(ex, PARAM_INVALID, errorMsg);
}
@ExceptionHandler(MissingRequestHeaderException.class)
public ResponseDTO<String> handleMissingRequestHeaderException(MissingRequestHeaderException ex) {
String errorMsg = StrUtil.format("丢失Header参数, 参数名: {}, 参数类型: {}", ex.getHeaderName(), ex.getParameter().getParameterType());
log.error(errorMsg);
return buildResponse(ex, PARAM_INVALID, errorMsg);
}
@ExceptionHandler(MissingServletRequestPartException.class)
public ResponseDTO<String> handleMissingServletRequestPartException(MissingServletRequestPartException ex) {
String errorMsg = StrUtil.format("丢失参数: {}", ex.getRequestPartName());
log.error(errorMsg);
return buildResponse(ex, PARAM_INVALID, errorMsg);
}
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseDTO<String> handleServletRequestParameterException(MissingServletRequestParameterException ex) {
String errorMsg = StrUtil.format("丢失Query参数, 参数名: {}, 参数类型: {}", ex.getParameterName(), ex.getParameterType());
log.error(errorMsg);
return buildResponse(ex, PARAM_INVALID, errorMsg);
}
@ExceptionHandler(ServletRequestBindingException.class)
public ResponseDTO<String> handleServletRequestBindingException(ServletRequestBindingException ex) {
String errorMsg = StrUtil.format("参数绑定失败: {}", ex.getMessage());
log.error(errorMsg);
return buildResponse(ex, PARAM_INVALID, errorMsg);
}
/************************************* Parameter Binding Exception Handing *************************************/
@ExceptionHandler(BindException.class)
public ResponseDTO<String> handleBindException(BindException ex) {
String errorMsg = buildBindingErrorMsg(ex.getBindingResult());
log.error(errorMsg);
return buildResponse(ex, PARAM_INVALID, errorMsg);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseDTO<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
String errorMsg = buildBindingErrorMsg(ex.getBindingResult());
log.error(errorMsg);
return buildResponse(ex, PARAM_INVALID, errorMsg);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseDTO<String> handleConstraintViolationException(ConstraintViolationException ex) {
String errorMsg = buildBindingErrorMsg(ex.getConstraintViolations());
log.error(errorMsg);
return buildResponse(ex, PARAM_INVALID, errorMsg);
}
protected ResponseDTO<String> loggingThenBuildResponse(Throwable throwable, int errorCode) {
Throwable rootCause = ExceptionUtil.getRootCause(throwable);
logError(rootCause);
return buildResponse(rootCause, errorCode, rootCause.getMessage());
}
protected ResponseDTO<String> buildResponse(Throwable throwable, int errorCode, String message) {
boolean production = EnvUtils.isProduction();
ResponseDTO<String> responseDTO = getExceptionResponseMapping(throwable, production);
if (responseDTO != null) {
return responseDTO;
}
if (production) {
return ResponseDTO.error("系统错误", errorCode);
}
if (StringUtils.isBlank(message)) {
return ResponseDTO.error("系统错误", errorCode);
}
return ResponseDTO.error(message, errorCode);
}
protected ResponseDTO<String> getExceptionResponseMapping(Throwable throwable, boolean production) {
return null;
}
protected String buildBindingErrorMsg(BindingResult bindingResult) {
String prefix = "参数验证失败: ";
StringJoiner joiner = new StringJoiner(", ");
for (ObjectError error : bindingResult.getAllErrors()) {
String errorMessage = Optional.ofNullable(error.getDefaultMessage()).orElse("验证失败");
String source;
if (error instanceof FieldError) {
source = ((FieldError) error).getField();
} else {
source = error.getObjectName();
}
joiner.add(source + " " + errorMessage);
}
return prefix + joiner;
}
protected String buildBindingErrorMsg(Set<ConstraintViolation<?>> constraintViolations) {
String prefix = "参数验证失败: ";
StringJoiner joiner = new StringJoiner(", ");
for (ConstraintViolation<?> violation : constraintViolations) {
PathImpl propertyPath = (PathImpl) violation.getPropertyPath();
joiner.add(propertyPath.asString() + " " + violation.getMessage());
}
return prefix + joiner;
}
protected void logError(Throwable throwable) {
String exMsg = ExceptionUtil.getMessage(throwable);
log.error(exMsg, throwable);
}
}
\ No newline at end of file
package com.schbrain.common.web.exception;
import com.schbrain.common.web.properties.WebProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import java.util.List;
/**
* @author liaozan
* @since 2022/8/29
*/
@Slf4j
public class ExceptionHandlerWebMcvConfigurer implements WebMvcConfigurer {
private final WebProperties webProperties;
private final GlobalExceptionHandler globalExceptionHandler;
public ExceptionHandlerWebMcvConfigurer(WebProperties webProperties, GlobalExceptionHandler globalExceptionHandler) {
this.webProperties = webProperties;
this.globalExceptionHandler = globalExceptionHandler;
}
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
if (!webProperties.isEnableGlobalExceptionHandler()) {
log.warn("Global exception handing is disabled");
return;
}
ExceptionHandlerExceptionResolver adviceExceptionResolver = null;
for (HandlerExceptionResolver resolver : resolvers) {
if (resolver instanceof ExceptionHandlerExceptionResolver) {
adviceExceptionResolver = (ExceptionHandlerExceptionResolver) resolver;
break;
}
}
if (adviceExceptionResolver == null) {
log.warn("ExceptionHandlerExceptionResolver is not exist, ignore global exception handing");
return;
}
addGlobalExceptionResolver(resolvers, adviceExceptionResolver);
}
protected void addGlobalExceptionResolver(List<HandlerExceptionResolver> resolvers, ExceptionHandlerExceptionResolver adviceExceptionResolver) {
int index = resolvers.indexOf(adviceExceptionResolver) + 1;
resolvers.add(index, createGlobalExceptionResolver(adviceExceptionResolver));
}
protected GlobalExceptionResolver createGlobalExceptionResolver(ExceptionHandlerExceptionResolver adviceExceptionResolver) {
return new GlobalExceptionResolver(adviceExceptionResolver, webProperties, globalExceptionHandler);
}
}
\ No newline at end of file
package com.schbrain.common.web.exception;
/**
* 标记接口
*
* @author liaozan
* @since 2022/10/26
*/
public interface GlobalExceptionHandler {
}
\ No newline at end of file
package com.schbrain.common.web.exception;
import com.schbrain.common.web.annotation.ResponseWrapOption;
import com.schbrain.common.web.properties.WebProperties;
import com.schbrain.common.web.utils.HandlerMethodAnnotationUtils;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
import org.springframework.web.method.support.*;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author liaozan
* @since 2022/8/30
*/
@Slf4j
@Data
@EqualsAndHashCode(callSuper = true)
public class GlobalExceptionResolver extends AbstractHandlerMethodExceptionResolver {
private final WebProperties webProperties;
private final HandlerMethodArgumentResolverComposite argumentResolverComposite;
private final HandlerMethodReturnValueHandlerComposite returnValueHandlerComposite;
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap<>(64);
private GlobalExceptionHandler exceptionHandler;
private ExceptionHandlerMethodResolver handlerMethodResolver;
public GlobalExceptionResolver(ExceptionHandlerExceptionResolver handlerMethodResolver, WebProperties webProperties,
GlobalExceptionHandler exceptionHandler) {
this.exceptionHandler = exceptionHandler;
this.webProperties = webProperties;
this.handlerMethodResolver = new ExceptionHandlerMethodResolver(exceptionHandler.getClass());
this.argumentResolverComposite = handlerMethodResolver.getArgumentResolvers();
this.returnValueHandlerComposite = handlerMethodResolver.getReturnValueHandlers();
}
@Override
protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
if (!webProperties.isWrapResponse()) {
return false;
}
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
ResponseWrapOption responseWrapOption = HandlerMethodAnnotationUtils.getAnnotation(handlerMethod, ResponseWrapOption.class);
if (responseWrapOption == null) {
return true;
}
return Boolean.FALSE.equals(responseWrapOption.ignoreException());
}
return true;
}
@Override
protected final ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerMethod handlerMethod, Exception exception) {
ServletInvocableHandlerMethod exceptionHandlerMethod = createExceptionHandlerMethod(exception, handlerMethod, exceptionHandler);
if (exceptionHandlerMethod == null) {
return null;
}
ServletWebRequest webRequest = new ServletWebRequest(request, response);
return doResolveException(webRequest, exceptionHandlerMethod, getArguments(exception, handlerMethod));
}
protected final ModelAndView doResolveException(ServletWebRequest webRequest, ServletInvocableHandlerMethod targetMethod, Object[] arguments) {
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
try {
targetMethod.invokeAndHandle(webRequest, mavContainer, arguments);
} catch (Exception e) {
log.warn("Failure in @ExceptionHandler " + targetMethod, e);
return null;
}
if (mavContainer.isRequestHandled()) {
return new ModelAndView();
}
return null;
}
protected ServletInvocableHandlerMethod createExceptionHandlerMethod(Exception exception, @Nullable HandlerMethod handlerMethod, Object handler) {
Method targetMethod = resolveTargetMethod(exception, handlerMethod);
if (targetMethod == null) {
return null;
}
ServletInvocableHandlerMethod exceptionHandlerMethod = new ServletInvocableHandlerMethod(handler, targetMethod);
exceptionHandlerMethod.setHandlerMethodArgumentResolvers(argumentResolverComposite);
exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(returnValueHandlerComposite);
return exceptionHandlerMethod;
}
protected Method resolveTargetMethod(Exception exception, @Nullable HandlerMethod handlerMethod) {
Method resolvedMethod = null;
if (handlerMethod != null) {
Class<?> handlerType = handlerMethod.getBeanType();
resolvedMethod = getHandlerMethodResolver(handlerType).resolveMethod(exception);
}
if (resolvedMethod == null) {
resolvedMethod = handlerMethodResolver.resolveMethod(exception);
}
return resolvedMethod;
}
private ExceptionHandlerMethodResolver getHandlerMethodResolver(Class<?> handlerType) {
return exceptionHandlerCache.computeIfAbsent(handlerType, key -> new ExceptionHandlerMethodResolver(handlerType));
}
/**
* copy from spring
*/
private Object[] getArguments(Exception exception, HandlerMethod handlerMethod) {
List<Throwable> exceptions = new ArrayList<>();
Throwable exToExpose = exception;
while (exToExpose != null) {
exceptions.add(exToExpose);
Throwable cause = exToExpose.getCause();
exToExpose = (cause != exToExpose ? cause : null);
}
Object[] arguments = new Object[exceptions.size() + 1];
exceptions.toArray(arguments);
arguments[arguments.length - 1] = handlerMethod;
return arguments;
}
}
\ No newline at end of file
package com.schbrain.common.web.log;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.ArrayUtil;
import com.schbrain.common.web.properties.WebProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.servlet.filter.OrderedFilter;
import org.springframework.core.Ordered;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.WebUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
/**
* 请求日志拦截器
*/
@Slf4j
public class RequestLoggingFilter extends OncePerRequestFilter implements OrderedFilter {
private final WebProperties webProperties;
public RequestLoggingFilter(WebProperties webProperties) {
this.webProperties = webProperties;
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
if (shouldSkip(request)) {
chain.doFilter(request, response);
return;
}
request = wrapRequestIfRequired(request);
long startTime = System.currentTimeMillis();
try {
chain.doFilter(request, response);
} finally {
long endTime = System.currentTimeMillis();
log.info(buildLogContent(request, startTime, endTime));
}
}
protected boolean shouldSkip(HttpServletRequest request) {
if (!webProperties.isEnableRequestLogging()) {
return true;
}
return CorsUtils.isPreFlightRequest(request);
}
protected String buildLogContent(HttpServletRequest request, long startTime, long endTime) {
long cost = endTime - startTime;
String method = request.getMethod();
String requestUri = request.getRequestURI();
String queryString = request.getQueryString();
String body = getRequestBody(request);
StringBuilder builder = new StringBuilder();
builder.append("requestUri: ").append(method).append(StrPool.C_SPACE).append(requestUri);
if (StringUtils.hasText(queryString)) {
builder.append(", queryString: ").append(queryString);
}
if (StringUtils.hasText(body)) {
builder.append(", body: ").append(body);
}
builder.append(", startTime: ").append(startTime);
builder.append(", endTime: ").append(endTime);
builder.append(", cost: ").append(cost).append("ms");
return builder.toString();
}
protected String getRequestBody(HttpServletRequest request) {
ContentCachingRequestWrapper nativeRequest = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (nativeRequest == null) {
return null;
}
byte[] content = nativeRequest.getContentAsByteArray();
if (ArrayUtil.isEmpty(content)) {
return null;
}
String charset = nativeRequest.getCharacterEncoding();
try {
return new String(content, charset);
} catch (UnsupportedEncodingException e) {
log.warn("unsupported charset {} detect during convert body to String", charset, e);
return null;
}
}
protected HttpServletRequest wrapRequestIfRequired(HttpServletRequest request) {
if (request instanceof ContentCachingRequestWrapper) {
return request;
} else {
return new ContentCachingRequestWrapper(request);
}
}
}
\ No newline at end of file
package com.schbrain.common.web.properties;
import com.schbrain.common.util.support.ConfigurableProperties;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.nio.charset.StandardCharsets;
/**
* @author liaozan
* @since 2022/8/29
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties(prefix = "schbrain.web")
public class WebProperties extends ConfigurableProperties {
/**
* whether to enable the request logging
*/
private boolean enableRequestLogging = true;
/**
* whether to enable the response wrap
*/
private boolean wrapResponse = true;
/**
* whether to enable the global exception handing
*/
private boolean enableGlobalExceptionHandler = true;
/**
* encoding for request/response
*/
private String encoding = StandardCharsets.UTF_8.name();
/**
* authenticationVariableName for login auth
*/
private String authenticationVariableName = "token";
@Override
public String getDefaultNamespace() {
return "web-common";
}
}
\ No newline at end of file
package com.schbrain.common.web.result;
import com.schbrain.common.web.annotation.ResponseWrapOption;
import com.schbrain.common.web.properties.WebProperties;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author liaozan
* @since 2021/10/15
*/
@RestControllerAdvice
public class ResponseBodyHandler implements ResponseBodyAdvice<Object> {
private final WebProperties webProperties;
private final List<String> basePackages;
private final Map<Method, Boolean> methodCache;
public ResponseBodyHandler(WebProperties webProperties, List<String> basePackages) {
this.webProperties = webProperties;
this.basePackages = basePackages;
this.methodCache = new ConcurrentHashMap<>();
}
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
if (!webProperties.isWrapResponse()) {
return false;
}
return methodCache.computeIfAbsent(returnType.getMethod(), this::shouldApply);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
return beforeBodyWrite(body);
}
protected Object beforeBodyWrite(Object body) {
if (body instanceof ResponseDTO) {
return body;
}
if (body == null) {
return ResponseDTO.success();
} else {
return ResponseDTO.success(body);
}
}
protected boolean shouldApply(Method targetMethod) {
if (targetMethod == null) {
return false;
}
Class<?> declaringClass = targetMethod.getDeclaringClass();
ResponseWrapOption responseWrapOption = targetMethod.getAnnotation(ResponseWrapOption.class);
if (responseWrapOption == null) {
responseWrapOption = declaringClass.getAnnotation(ResponseWrapOption.class);
}
if (responseWrapOption != null) {
return !responseWrapOption.ignore();
}
String packageName = declaringClass.getPackage().getName();
return basePackages.stream().anyMatch(packageName::startsWith);
}
}
\ No newline at end of file
package com.schbrain.common.web.result;
import com.schbrain.common.constants.ResponseActionConstants;
import com.schbrain.common.constants.ResponseCodeConstants;
import com.schbrain.common.exception.BaseException;
import com.schbrain.common.util.TraceIdUtils;
import lombok.Data;
import java.io.Serializable;
/**
* @author liaozan
* @since 2021/10/15
*/
@Data
public class ResponseDTO<T> implements Serializable {
private static final long serialVersionUID = 8559474982311419998L;
private int code;
private int action;
private String message;
private T data;
private String uuid = TraceIdUtils.get();
public static <T> ResponseDTO<T> success() {
return success(null);
}
public static <T> ResponseDTO<T> success(T data) {
ResponseDTO<T> result = new ResponseDTO<>();
result.setMessage(null);
result.setCode(ResponseCodeConstants.SUCCESS);
result.setAction(ResponseActionConstants.NO_ACTION);
result.setData(data);
return result;
}
public static <T> ResponseDTO<T> error(String message) {
return error(message, ResponseCodeConstants.SERVER_ERROR);
}
public static <T> ResponseDTO<T> error(String message, int code) {
return error(message, code, ResponseActionConstants.ALERT);
}
public static <T> ResponseDTO<T> error(BaseException exception) {
return error(exception.getMessage(), exception.getCode(), exception.getAction());
}
public static <T> ResponseDTO<T> error(String message, int code, int action) {
ResponseDTO<T> result = new ResponseDTO<>();
result.setMessage(message);
result.setCode(code);
result.setAction(action);
return result;
}
}
\ No newline at end of file
package com.schbrain.common.web.servlet;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.time.Duration;
/**
* @author liaozan
* @since 2022/11/19
*/
public class AllowAllCorsConfigurer implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowCredentials(false)
.allowedOrigins(CorsConfiguration.ALL)
.allowedHeaders(CorsConfiguration.ALL)
.allowedMethods(CorsConfiguration.ALL)
.exposedHeaders(CorsConfiguration.ALL)
.maxAge(Duration.ofHours(1).toSeconds());
}
}
\ No newline at end of file
package com.schbrain.common.web.servlet;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import javax.servlet.ServletContext;
/**
* @author liaozan
* @since 2022/11/11
*/
public class CharacterEncodingServletContextInitializer implements ServletContextInitializer {
private final String encoding;
public CharacterEncodingServletContextInitializer(String encoding) {
this.encoding = encoding;
}
@Override
public void onStartup(ServletContext servletContext) {
servletContext.setRequestCharacterEncoding(encoding);
servletContext.setResponseCharacterEncoding(encoding);
}
}
\ No newline at end of file
package com.schbrain.common.web.servlet;
import com.schbrain.common.util.TraceIdUtils;
import org.springframework.web.context.request.RequestContextListener;
import javax.servlet.ServletRequestEvent;
/**
* @author liaozan
* @since 2021/12/8
*/
public class TraceIdInitializeServletListener extends RequestContextListener {
@Override
public void requestInitialized(ServletRequestEvent event) {
super.requestInitialized(event);
// Make sure the traceId is initialized
TraceIdUtils.get();
}
@Override
public void requestDestroyed(ServletRequestEvent event) {
super.requestDestroyed(event);
// Make sure the traceId can be cleared
TraceIdUtils.clear();
}
}
\ No newline at end of file
package com.schbrain.common.web.support;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author liaozan
* @since 2022/11/11
*/
public class BaseHandlerInterceptor implements AsyncHandlerInterceptor {
@Override
public final boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (CorsUtils.isPreFlightRequest(request)) {
return true;
}
if (handler instanceof HandlerMethod) {
return preHandle(request, response, (HandlerMethod) handler);
}
return true;
}
@Override
public final void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if (handler instanceof HandlerMethod) {
postHandle(request, response, (HandlerMethod) handler, modelAndView);
}
}
@Override
public final void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
if (handler instanceof HandlerMethod) {
afterCompletion(request, response, (HandlerMethod) handler, ex);
}
}
@Override
public final void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
afterConcurrentHandlingStarted(request, response, (HandlerMethod) handler);
}
}
protected boolean preHandle(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler) throws Exception {
return true;
}
protected void postHandle(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler, ModelAndView modelAndView) throws Exception {
}
protected void afterCompletion(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler, Exception ex) throws Exception {
}
protected void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) {
}
}
\ No newline at end of file
package com.schbrain.common.web.support.authentication;
import cn.hutool.extra.spring.SpringUtil;
import com.schbrain.common.annotation.IgnoreLogin;
import com.schbrain.common.web.properties.WebProperties;
import com.schbrain.common.web.utils.HandlerMethodAnnotationUtils;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;
import org.springframework.web.method.HandlerMethod;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author liaozan
* @since 2022/11/12
*/
@Data
@Slf4j
public abstract class AbstractAuthenticator implements Authenticator {
private final String authenticationVariableName;
public AbstractAuthenticator() {
this(SpringUtil.getBean(WebProperties.class).getAuthenticationVariableName());
}
public AbstractAuthenticator(String authenticationVariableName) {
Assert.hasText(authenticationVariableName, "authenticationVariableName must not be empty");
this.authenticationVariableName = authenticationVariableName;
}
@Override
public boolean validate(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler) {
boolean ignore = HandlerMethodAnnotationUtils.hasAnnotation(handler, IgnoreLogin.class);
if (ignore) {
return true;
}
String authentication = getAuthentication(request);
if (StringUtils.isBlank(authentication)) {
return false;
}
return doValidate(authentication, request, response, handler);
}
protected boolean doValidate(String authentication, HttpServletRequest request, HttpServletResponse response, HandlerMethod handler) {
return true;
}
@Nullable
protected String getAuthentication(HttpServletRequest request) {
String authentication = request.getHeader(authenticationVariableName);
if (StringUtils.isBlank(authentication)) {
authentication = request.getParameter(authenticationVariableName);
}
if (StringUtils.isBlank(authentication)) {
log.warn("Can not get authentication from request, authenticationVariableName: {}", authenticationVariableName);
}
return authentication;
}
}
\ No newline at end of file
package com.schbrain.common.web.support.authentication;
import cn.hutool.extra.servlet.ServletUtil;
import com.schbrain.common.util.JacksonUtils;
import com.schbrain.common.web.result.ResponseDTO;
import com.schbrain.common.web.support.BaseHandlerInterceptor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.method.HandlerMethod;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static com.schbrain.common.constants.ResponseActionConstants.ALERT;
import static com.schbrain.common.constants.ResponseCodeConstants.LOGIN_REQUIRED;
/**
* @author liaozan
* @since 2022/11/11
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class AuthenticationInterceptor extends BaseHandlerInterceptor {
private Authenticator authenticator;
public AuthenticationInterceptor(Authenticator authenticator) {
Assert.notNull(authenticator, "authenticator must not be null");
this.authenticator = authenticator;
}
@Override
protected boolean preHandle(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler) {
boolean validated = authenticator.validate(request, response, handler);
if (validated) {
return true;
}
writeResult(response, buildAccessDeniedResponse());
return false;
}
@Override
protected void afterCompletion(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler, Exception ex) {
authenticator.afterCompletion(request, response, handler, ex);
}
protected void writeResult(HttpServletResponse response, ResponseDTO<?> result) {
String resultString = JacksonUtils.toJsonString(result);
ServletUtil.write(response, resultString, MediaType.APPLICATION_JSON_VALUE);
}
protected <T> ResponseDTO<T> buildAccessDeniedResponse() {
return ResponseDTO.error("未获取到认证信息, 请尝试重新登录", LOGIN_REQUIRED, ALERT);
}
}
\ No newline at end of file
package com.schbrain.common.web.support.authentication;
import org.springframework.web.method.HandlerMethod;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author liaozan
* @since 2022/11/11
*/
public interface Authenticator {
/**
* 校验当前请求是否合法
*/
boolean validate(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler);
/**
* 请求完成后的回调
*/
void afterCompletion(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler, Exception exception);
}
\ No newline at end of file
package com.schbrain.common.web.support.concurrent;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import java.util.Collections;
import java.util.Map;
/**
* @author liaozan
* @since 2022/5/5
*/
public class NoOpRateLimitCacheKeyVariablesContributor implements RateLimitCacheKeyVariablesContributor {
@Override
public Map<String, Object> contribute(RateLimiter rateLimiter, JoinPoint joinPoint, MethodSignature signature) {
return Collections.emptyMap();
}
}
\ No newline at end of file
package com.schbrain.common.web.support.concurrent;
import cn.hutool.extra.spring.SpringUtil;
import com.schbrain.common.exception.BaseException;
import com.schbrain.common.util.*;
import com.schbrain.common.web.utils.ServletUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.Advice;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.CollectionUtils;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author liaozan
* @see com.schbrain.common.web.support.concurrent.RateLimiter
* @since 2022/5/5
*/
@Slf4j
@Aspect
@ConditionalOnBean(StringRedisTemplate.class)
@ConditionalOnClass({Advice.class, RedisConnectionFactory.class})
public class RateLimitAspect {
private final Map<Class<?>, RateLimitCacheKeyVariablesContributor> contributorMap = new ConcurrentHashMap<>();
private final String keyPrefix;
private final StringRedisTemplate stringRedisTemplate;
public RateLimitAspect(Environment environment, StringRedisTemplate stringRedisTemplate) {
this.keyPrefix = ApplicationName.get(environment);
this.stringRedisTemplate = stringRedisTemplate;
}
@Before("@annotation(rateLimiter)")
public void beforeExecute(JoinPoint joinPoint, RateLimiter rateLimiter) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
doRateLimit(rateLimiter, joinPoint, methodSignature);
}
protected void doRateLimit(RateLimiter rateLimiter, JoinPoint joinPoint, MethodSignature signature) {
Map<String, Object> variables = prepareVariables(rateLimiter, joinPoint, signature);
String cacheKey = SpelUtils.parse(rateLimiter.cacheKey(), variables, String.class);
if (cacheKey == null) {
throw new BaseException("cacheKey should not be null");
}
String formattedCacheKey = formatCacheKey(cacheKey);
BoundValueOperations<String, String> rateLimitOps = stringRedisTemplate.boundValueOps(formattedCacheKey);
try {
long accessCount = Optional.ofNullable(rateLimitOps.increment()).orElse(1L);
if (accessCount > rateLimiter.permits()) {
throw new BaseException("访问频次太快,请稍后再试。");
}
if (accessCount <= 1) {
rateLimitOps.expire(rateLimiter.expireTime(), rateLimiter.unit());
}
} catch (Exception ex) {
log.error("RateLimit encountered an unknown error, remove cacheKey: {}", formattedCacheKey, ex);
// Remove cacheKey to prevent the cache from never expiring
stringRedisTemplate.delete(formattedCacheKey);
throw ex;
}
}
protected Map<String, Object> createEvaluationVariables(MethodSignature methodSignature, Object[] args) {
Map<String, Object> variables = new HashMap<>();
Map<String, Object> methodArgsMap = ParameterDiscoverUtils.getMethodArgsMap(methodSignature.getMethod(), args);
if (!CollectionUtils.isEmpty(methodArgsMap)) {
variables.putAll(methodArgsMap);
}
variables.put("request", ServletUtils.getRequest());
variables.put("response", ServletUtils.getResponse());
variables.put("applicationContext", SpringUtil.getApplicationContext());
variables.put("beanFactory", SpringUtil.getBeanFactory());
variables.put("args", args);
return variables;
}
protected String formatCacheKey(String cacheKey) {
return "rateLimit:" + keyPrefix + ":" + cacheKey;
}
private Map<String, Object> prepareVariables(RateLimiter rateLimiter, JoinPoint joinPoint, MethodSignature signature) {
Map<String, Object> variables = createEvaluationVariables(signature, joinPoint.getArgs());
Class<? extends RateLimitCacheKeyVariablesContributor> contributorClass = rateLimiter.contributor();
if (contributorClass == null || contributorClass == NoOpRateLimitCacheKeyVariablesContributor.class) {
return variables;
}
RateLimitCacheKeyVariablesContributor contributor = contributorMap.get(contributorClass);
if (contributor == null) {
contributor = BeanUtils.instantiateClass(contributorClass);
contributorMap.put(contributorClass, contributor);
}
Map<String, Object> contributeVariables = contributor.contribute(rateLimiter, joinPoint, signature);
if (!CollectionUtils.isEmpty(contributeVariables)) {
variables.putAll(contributeVariables);
}
return variables;
}
}
\ No newline at end of file
package com.schbrain.common.web.support.concurrent;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import java.util.Map;
/**
* @author liaozan
* @since 2022/5/5
*/
public interface RateLimitCacheKeyVariablesContributor {
Map<String, Object> contribute(RateLimiter rateLimiter, JoinPoint joinPoint, MethodSignature signature);
}
\ No newline at end of file
package com.schbrain.common.web.support.concurrent;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* @author liaozan
* @since 2022/5/5
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
/**
* 过期时间
*/
long expireTime() default 10;
/**
* 过期时间单位
*/
TimeUnit unit() default TimeUnit.SECONDS;
/**
* 过期时间内允许的许可数
*/
int permits() default 3;
/**
* 缓存的 key,使用 spel 进行解析
* <p>
* 可用的变量 {@link RateLimitAspect#createEvaluationVariables(MethodSignature, Object[])}
*/
String cacheKey();
/**
* evaluation variables contributor
*/
Class<? extends RateLimitCacheKeyVariablesContributor> contributor() default NoOpRateLimitCacheKeyVariablesContributor.class;
}
\ No newline at end of file
package com.schbrain.common.web.utils;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.schbrain.common.util.support.excel.exception.ExcelException;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.util.CollectionUtils;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* @author liaozan
* @since 2022/8/24
*/
public class ExcelUtils extends com.schbrain.common.util.ExcelUtils {
public static <T> void writeToResponse(List<T> dataList, String fileName) {
if (CollectionUtils.isEmpty(dataList)) {
throw new ExcelException("DataList is empty");
}
writeToResponse(dataList, dataList.get(0).getClass(), fileName);
}
public static <T> void writeToResponse(List<T> dataList, Class<?> head, String fileName) {
if (CollectionUtils.isEmpty(dataList)) {
throw new ExcelException("DataList is empty");
}
try {
HttpServletResponse response = ServletUtils.getResponse();
ContentDisposition contentDisposition = ContentDisposition
.attachment()
.filename(fileName, StandardCharsets.UTF_8)
.build();
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString());
ServletOutputStream outputStream = response.getOutputStream();
EasyExcel.write(outputStream)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.sheet()
.head(head)
.doWrite(dataList);
} catch (IOException e) {
throw new ExcelException("Excel download fail", e);
}
}
}
\ No newline at end of file
package com.schbrain.common.web.utils;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.web.method.HandlerMethod;
import javax.annotation.Nullable;
import java.lang.annotation.Annotation;
/**
* @author liaozan
* @since 2022-12-15
*/
public class HandlerMethodAnnotationUtils {
@Nullable
public static <T extends Annotation> T getAnnotation(HandlerMethod handlerMethod, Class<T> annotationType) {
T annotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getMethod(), annotationType);
if (annotation == null) {
annotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), annotationType);
}
return annotation;
}
public static <T extends Annotation> boolean hasAnnotation(HandlerMethod handlerMethod, Class<T> annotationType) {
boolean hasAnnotation = AnnotatedElementUtils.hasAnnotation(handlerMethod.getMethod(), annotationType);
if (!hasAnnotation) {
hasAnnotation = AnnotatedElementUtils.hasAnnotation(handlerMethod.getBeanType(), annotationType);
}
return hasAnnotation;
}
}
\ No newline at end of file
package com.schbrain.common.web.utils;
import com.schbrain.common.exception.BaseException;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author liaozan
* @since 2022/1/6
*/
public class ServletUtils {
public static HttpServletRequest getRequest() {
ServletRequestAttributes requestAttributes = getRequestAttributes();
if (requestAttributes == null) {
throw new BaseException("No HttpServletRequest available");
}
return requestAttributes.getRequest();
}
public static HttpServletResponse getResponse() {
ServletRequestAttributes requestAttributes = getRequestAttributes();
if (requestAttributes == null) {
throw new BaseException("No HttpServletResponse available");
}
return requestAttributes.getResponse();
}
@Nullable
public static ServletRequestAttributes getRequestAttributes() {
return (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
}
}
\ No newline at end of file
{
"groups": [
{
"name": "schbrain.web",
"type": "com.schbrain.common.web.properties.WebProperties",
"sourceType": "com.schbrain.common.web.properties.WebProperties"
}
],
"properties": [
{
"name": "schbrain.web.authentication-variable-name",
"type": "java.lang.String",
"description": "authenticationVariableName for login auth",
"sourceType": "com.schbrain.common.web.properties.WebProperties",
"defaultValue": "token"
},
{
"name": "schbrain.web.enable-global-exception-handler",
"type": "java.lang.Boolean",
"description": "whether to enable the global exception handing",
"sourceType": "com.schbrain.common.web.properties.WebProperties",
"defaultValue": true
},
{
"name": "schbrain.web.enable-request-logging",
"type": "java.lang.Boolean",
"description": "whether to enable the request logging",
"sourceType": "com.schbrain.common.web.properties.WebProperties",
"defaultValue": true
},
{
"name": "schbrain.web.encoding",
"type": "java.lang.String",
"description": "encoding for request\/response",
"sourceType": "com.schbrain.common.web.properties.WebProperties"
},
{
"name": "schbrain.web.name",
"type": "java.lang.String",
"sourceType": "com.schbrain.common.web.properties.WebProperties"
},
{
"name": "schbrain.web.namespace",
"type": "java.lang.String",
"sourceType": "com.schbrain.common.web.properties.WebProperties"
},
{
"name": "schbrain.web.prefix",
"type": "java.lang.String",
"sourceType": "com.schbrain.common.web.properties.WebProperties"
},
{
"name": "schbrain.web.wrap-response",
"type": "java.lang.Boolean",
"description": "whether to enable the response wrap",
"sourceType": "com.schbrain.common.web.properties.WebProperties",
"defaultValue": true
}
],
"hints": []
}
\ No newline at end of file
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
### How to develop
* add the following content to your maven settings.xml
* active jenkins profile on your ide
```xml
<profile>
<id>jenkins</id>
<repositories>
<repository>
<id>repo.jenkins-ci.org</id>
<url>https://repo.jenkins-ci.org/public/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>repo.jenkins-ci.org</id>
<url>https://repo.jenkins-ci.org/public/</url>
</pluginRepository>
</pluginRepositories>
</profile>
```
### How to build
run `mvn clean package -Dmaven.test.skip` in terminal, plugin will be stored in the target directory named `integration-jenkins-plugin.hpi`, upload it to your jenkins server
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<!-- Baseline Jenkins version you use to build and test the plugin. Users must have this version or newer to run. -->
<version>4.61</version>
<relativePath/>
</parent>
<groupId>com.schbrain.ci.jenkins.plugins</groupId>
<artifactId>integration-jenkins-plugin</artifactId>
<version>1.0</version>
<packaging>hpi</packaging>
<scm>
<tag>integration-1.0</tag>
</scm>
<properties>
<jenkins.version>2.397</jenkins.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>git</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.tools</groupId>
<artifactId>git-parameter</artifactId>
<version>0.9.18</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>localization-zh-cn</artifactId>
<version>1.0.24</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>display-info</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>repo.jenkins-ci.org</id>
<url>https://repo.jenkins-ci.org/public/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>repo.jenkins-ci.org</id>
<url>https://repo.jenkins-ci.org/public/</url>
</pluginRepository>
</pluginRepositories>
</project>
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.action;
import hudson.FilePath;
import hudson.model.*;
import jenkins.model.RunAction2;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import java.io.File;
/**
* @author liaozan
* @since 2022/2/16
*/
public class ViewBuildScriptAction implements RunAction2 {
private final String buildScriptDir;
private transient Run<?, ?> run;
public ViewBuildScriptAction(File buildScriptDir) {
this.buildScriptDir = buildScriptDir.getAbsolutePath();
}
@Override
public String getIconFileName() {
return "document.png";
}
@Override
public String getDisplayName() {
return "构建脚本";
}
@Override
public String getUrlName() {
return "build-scripts";
}
@Override
public void onAttached(Run<?, ?> run) {
this.run = run;
}
@Override
public void onLoad(Run<?, ?> run) {
this.run = run;
}
public Run<?, ?> getRun() {
return run;
}
@SuppressWarnings({"unused", "rawtypes"})
public DirectoryBrowserSupport doList(StaplerRequest request, StaplerResponse response) {
AbstractBuild build = (AbstractBuild) request.findAncestor(AbstractBuild.class).getObject();
FilePath filePath = new FilePath(new File(buildScriptDir));
return new DirectoryBrowserSupport(build, filePath, "构建脚本", "folder.png", true);
}
}
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.builder;
import com.schbrain.ci.jenkins.plugins.integration.builder.env.BuildEnvContributor;
import com.schbrain.ci.jenkins.plugins.integration.builder.util.Logger;
import com.schbrain.ci.jenkins.plugins.integration.builder.util.TemplateUtils;
import hudson.*;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.tasks.Shell;
import java.io.IOException;
import java.util.Objects;
/**
* @author zhangdd on 2022/1/21
*/
public class BuilderContext {
private final AbstractBuild<?, ?> build;
private final Launcher launcher;
private final FilePath workspace;
private final BuildListener listener;
private final Logger logger;
private final EnvVars envVars;
private boolean imageHasBeenBuilt;
private BuilderContext(Builder builder) {
this.build = builder.build;
this.launcher = builder.launcher;
this.workspace = builder.workspace;
this.listener = builder.listener;
this.logger = builder.logger;
this.envVars = builder.envVars;
this.imageHasBeenBuilt = false;
}
public void execute(String command) throws InterruptedException, IOException {
String resolvedCommand = TemplateUtils.evaluate(command, envVars);
if (!Objects.equals(resolvedCommand, command)) {
log("before resolve: %s", command);
command = resolvedCommand;
}
log("%s", command);
BuildEnvContributor.saveEnvVarsToDisk(this);
Shell shell = new Shell(command);
shell.perform(getBuild(), getLauncher(), getListener());
}
public AbstractBuild<?, ?> getBuild() {
return build;
}
public Launcher getLauncher() {
return launcher;
}
public FilePath getWorkspace() {
return workspace;
}
public BuildListener getListener() {
return listener;
}
public Logger getLogger() {
return logger;
}
public EnvVars getEnvVars() {
return envVars;
}
public boolean isImageHasBeenBuilt() {
return imageHasBeenBuilt;
}
public void setImageHasBeenBuilt() {
this.imageHasBeenBuilt = true;
}
public void log(String template) {
log(template, (Object) null);
}
public void log(String template, Object... arguments) {
logger.println(template, true, arguments);
}
public static class Builder {
private AbstractBuild<?, ?> build;
private Launcher launcher;
private FilePath workspace;
private BuildListener listener;
private Logger logger;
private EnvVars envVars;
public Builder build(AbstractBuild<?, ?> build) {
this.build = build;
return this;
}
public Builder launcher(Launcher launcher) {
this.launcher = launcher;
return this;
}
public Builder workspace(FilePath workspace) {
this.workspace = workspace;
return this;
}
public Builder listener(BuildListener listener) {
this.listener = listener;
return this;
}
public Builder logger(Logger logger) {
this.logger = logger;
return this;
}
public Builder envVars(EnvVars envVars) {
this.envVars = envVars;
return this;
}
public BuilderContext build() {
return new BuilderContext(this);
}
}
}
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.builder;
import hudson.model.AbstractBuild;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import static com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.BuildConstants.*;
/**
* @author liaozan
* @since 2022/2/8
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
public class FileManager {
public static File getCurrentBuildDir(AbstractBuild<?, ?> build) {
return build.getRootDir();
}
public static File getWorkspaceBuildScriptDir(AbstractBuild<?, ?> build) {
return new File(Objects.requireNonNull(build.getWorkspace()).getRemote(), BUILD_SCRIPT_NAME);
}
public static File getEnvVarsFile(AbstractBuild<?, ?> build) throws IOException {
File buildScriptDir = getBuildScriptDir(build);
File envVarsFile = new File(buildScriptDir, ENV_VARS);
if (!envVarsFile.exists()) {
envVarsFile.createNewFile();
}
return envVarsFile;
}
public static File getBuildScriptDir(AbstractBuild<?, ?> build) {
File buildDir = getCurrentBuildDir(build);
File buildScriptDir = new File(buildDir, BUILD_SCRIPT_NAME);
if (!buildScriptDir.exists()) {
buildScriptDir.mkdirs();
}
return buildScriptDir;
}
}
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.builder;
import com.schbrain.ci.jenkins.plugins.integration.action.ViewBuildScriptAction;
import com.schbrain.ci.jenkins.plugins.integration.builder.config.*;
import com.schbrain.ci.jenkins.plugins.integration.builder.config.DockerConfig.PushConfig;
import com.schbrain.ci.jenkins.plugins.integration.builder.util.FileUtils;
import com.schbrain.ci.jenkins.plugins.integration.builder.util.Logger;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import hudson.*;
import hudson.model.*;
import hudson.tasks.Builder;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
import org.springframework.lang.Nullable;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import static com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.BuildConstants.*;
import static com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.*;
import static com.schbrain.ci.jenkins.plugins.integration.builder.util.FileUtils.lookupFile;
/**
* @author liaozan
* @since 2022/1/14
*/
@SuppressWarnings("unused")
public class IntegrationBuilder extends Builder {
private final MavenConfig mavenConfig;
private final DockerConfig dockerConfig;
private final DeployToK8sConfig deployToK8sConfig;
@DataBoundConstructor
public IntegrationBuilder(@Nullable MavenConfig mavenConfig,
@Nullable DockerConfig dockerConfig,
@Nullable DeployToK8sConfig deployToK8sConfig) {
this.mavenConfig = mavenConfig;
this.dockerConfig = dockerConfig;
this.deployToK8sConfig = deployToK8sConfig;
}
@Nullable
public MavenConfig getMavenConfig() {
return mavenConfig;
}
@Nullable
public DockerConfig getDockerConfig() {
return dockerConfig;
}
@Nullable
public DeployToK8sConfig getDeployToK8sConfig() {
return deployToK8sConfig;
}
/**
* Builder start
*/
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
build.addAction(new ViewBuildScriptAction(FileManager.getBuildScriptDir(build)));
BuilderContext builderContext = new BuilderContext.Builder()
.build(build)
.launcher(launcher)
.listener(listener)
.logger(Logger.of(listener.getLogger()))
.workspace(checkWorkspaceValid(build.getWorkspace()))
.envVars(createEnvVars(build))
.build();
return doPerformBuild(builderContext);
}
@Override
public IntegrationDescriptor getDescriptor() {
return (IntegrationDescriptor) super.getDescriptor();
}
protected boolean doPerformBuild(BuilderContext context) throws IOException, InterruptedException {
try {
// download build scripts
downloadBuildScript(context);
// maven build
performMavenBuild(context);
// read maven build-info
readMavenBuildInfo(context);
// docker build
performDockerBuild(context);
// docker push
performDockerPush(context);
// deploy
deployToRemote(context);
} finally {
if (context.isImageHasBeenBuilt()) {
// delete the built image if possible
deleteImageAfterBuild(context);
}
// setup description
setBuildDescription(context);
// copy to buildDir
saveTheBuildScripts(context);
}
return true;
}
private EnvVars createEnvVars(AbstractBuild<?, ?> build) {
EnvVars envVars = new EnvVars();
ParametersAction parametersAction = build.getAction(ParametersAction.class);
if (parametersAction == null) {
return envVars;
}
List<ParameterValue> allParameters = parametersAction.getAllParameters();
for (ParameterValue parameter : allParameters) {
if (parameter.getValue() == null) {
continue;
}
envVars.put(parameter.getName(), parameter.getValue().toString());
}
return envVars;
}
private void downloadBuildScript(BuilderContext context) throws IOException, InterruptedException {
String buildScriptUrl = DEFAULT_SCRIPT_GIT_REPO;
String buildScriptBranch = DEFAULT_SCRIPT_GIT_BRANCH;
// if buildScriptUrl and buildScriptBranch is set, will use specified value to download build-script, otherwise will use default value
if (getDockerConfig() != null) {
if (StringUtils.isNotBlank(getDockerConfig().getBuildScriptUrl())) {
buildScriptUrl = getDockerConfig().getBuildScriptUrl();
}
if (StringUtils.isNotBlank(getDockerConfig().getBuildScriptBranch())) {
buildScriptBranch = getDockerConfig().getBuildScriptBranch();
}
}
// git archive the build scripts
String archiveCommand = String.format("git archive -o %s --format=zip --remote=%s %s", SCRIPT_ZIP_NAME, buildScriptUrl, buildScriptBranch);
context.execute(archiveCommand);
// unzip to directory in workspace
String unzipCommand = String.format("unzip -o %s -d %s", SCRIPT_ZIP_NAME, BUILD_SCRIPT_NAME);
context.execute(unzipCommand);
}
private void setBuildDescription(BuilderContext context) throws IOException, InterruptedException {
FilePath gitPropertiesFile = lookupFile(context, GitConstants.GIT_PROPERTIES_FILE);
if (gitPropertiesFile == null) {
return;
}
Map<String, String> gitProperties = FileUtils.filePathToMap(gitPropertiesFile);
String author = gitProperties.get(GitConstants.GIT_COMMITTER);
String branch = gitProperties.get(GitConstants.GIT_BRANCH);
AbstractBuild<?, ?> build = context.getBuild();
String description = String.format("author: %s, branch: %s", author, branch);
build.setDescription(description);
}
/**
* Check workspace
*/
private FilePath checkWorkspaceValid(@CheckForNull FilePath workspace) throws IOException, InterruptedException {
if (workspace == null) {
throw new IllegalStateException("workspace is null");
}
if (!workspace.exists()) {
throw new IllegalStateException("workspace is not exist");
}
return workspace;
}
private void performMavenBuild(BuilderContext context) throws IOException, InterruptedException {
MavenConfig mavenConfig = getMavenConfig();
if (mavenConfig == null) {
context.log("maven build is not checked");
return;
}
mavenConfig.build(context);
}
private void readMavenBuildInfo(BuilderContext context) throws IOException, InterruptedException {
EnvVars envVars = context.getEnvVars();
FilePath dockerBuildInfo = lookupFile(context, DockerConstants.BUILD_INFO_FILE_NAME);
if (dockerBuildInfo == null) {
context.log("%s file not exist, skip docker build", DockerConstants.BUILD_INFO_FILE_NAME);
return;
}
// overwriting existing environment variables is not allowed
FileUtils.filePathToMap(dockerBuildInfo).forEach(envVars::putIfAbsent);
}
private void performDockerBuild(BuilderContext context) throws IOException, InterruptedException {
DockerConfig dockerConfig = getDockerConfig();
if (dockerConfig == null) {
context.log("docker build is not checked");
return;
}
dockerConfig.build(context);
}
private void performDockerPush(BuilderContext context) throws IOException, InterruptedException {
DockerConfig dockerConfig = getDockerConfig();
if (dockerConfig == null) {
context.log("docker build is not checked");
return;
}
PushConfig pushConfig = dockerConfig.getPushConfig();
if (pushConfig == null) {
context.log("docker push is not checked");
return;
}
pushConfig.build(context);
}
/**
* Delete the image produced in the build
*/
private void deleteImageAfterBuild(BuilderContext context) throws InterruptedException, IOException {
DockerConfig dockerConfig = getDockerConfig();
if (dockerConfig == null) {
context.log("docker build is not checked");
return;
}
if (!dockerConfig.getDeleteImageAfterBuild()) {
context.log("delete built image is skip");
return;
}
String imageName = context.getEnvVars().get(DockerConstants.IMAGE);
if (imageName == null) {
return;
}
String command = String.format("docker rmi -f %s", imageName);
context.execute(command);
}
/**
* 部署镜像到远端
*/
private void deployToRemote(BuilderContext context) throws IOException, InterruptedException {
DeployToK8sConfig k8sConfig = getDeployToK8sConfig();
if (k8sConfig == null) {
context.log("k8s deploy is not checked");
return;
}
k8sConfig.build(context);
}
private void saveTheBuildScripts(BuilderContext context) throws InterruptedException, IOException {
String copy = String.format("cp -r %s %s", BUILD_SCRIPT_NAME, FileManager.getCurrentBuildDir(context.getBuild()));
context.execute(copy);
}
// can not move outside builder class
@Extension
public static class IntegrationDescriptor extends Descriptor<Builder> {
public IntegrationDescriptor() {
load();
}
@Override
public String getDisplayName() {
return "发布集成";
}
@Override
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
save();
return super.configure(req, formData);
}
}
}
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.builder.config;
import com.schbrain.ci.jenkins.plugins.integration.builder.BuilderContext;
import com.schbrain.ci.jenkins.plugins.integration.builder.util.Logger;
import hudson.*;
import hudson.model.*;
import java.io.IOException;
/**
* @author liaozan
* @since 2022/1/17
*/
public abstract class BuildConfig<T extends AbstractDescribableImpl<T>> extends AbstractDescribableImpl<T> {
protected AbstractBuild<?, ?> build;
protected Launcher launcher;
protected FilePath workspace;
protected BuildListener listener;
protected Logger logger;
protected EnvVars envVars;
protected BuilderContext context;
public void build(BuilderContext context) throws IOException, InterruptedException {
this.context = context;
this.build = context.getBuild();
this.launcher = context.getLauncher();
this.workspace = context.getWorkspace();
this.listener = context.getListener();
this.logger = context.getLogger();
this.envVars = context.getEnvVars();
doBuild();
}
protected abstract void doBuild() throws InterruptedException, IOException;
}
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.builder.config;
import com.schbrain.ci.jenkins.plugins.integration.builder.config.deploy.DeployStyleRadio;
import com.schbrain.ci.jenkins.plugins.integration.builder.config.deploy.service.ServiceDeployConfig;
import com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.DockerConstants;
import com.schbrain.ci.jenkins.plugins.integration.builder.util.FileUtils;
import hudson.*;
import hudson.model.Descriptor;
import jenkins.model.Jenkins;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* @author liaozan
* @since 2022/1/16
*/
@SuppressWarnings("unused")
public class DeployToK8sConfig extends BuildConfig<DeployToK8sConfig> {
private final String configLocation;
private final DeployStyleRadio deployStyle;
private final ServiceDeployConfig serviceDeployConfig;
@DataBoundConstructor
public DeployToK8sConfig(String configLocation, DeployStyleRadio deployStyle, ServiceDeployConfig serviceDeployConfig) {
this.configLocation = Util.fixNull(configLocation);
this.deployStyle = deployStyle;
this.serviceDeployConfig = serviceDeployConfig;
}
public String getConfigLocation() {
return configLocation;
}
public DeployStyleRadio getDeployStyle() {
return deployStyle;
}
public ServiceDeployConfig getServiceDeployConfig() {
return serviceDeployConfig;
}
public void doBuild() throws InterruptedException, IOException {
String imageName = envVars.get(DockerConstants.IMAGE);
if (StringUtils.isBlank(imageName)) {
context.log("image name is empty ,skip deploy");
return;
}
// make sure to build Deployment first
buildDeployment();
buildService();
}
private void buildDeployment() throws IOException, InterruptedException {
DeployStyleRadio deployStyle = getDeployStyle();
if (null == deployStyle) {
return;
}
String deployFileLocation = deployStyle.getDeployFileLocation(context);
executeK8sCommand(deployFileLocation);
}
private void buildService() throws IOException, InterruptedException {
if (serviceDeployConfig == null) {
return;
}
String deployFileLocation = serviceDeployConfig.getServiceDeployFileLocation(context);
executeK8sCommand(deployFileLocation);
}
private void executeK8sCommand(String deployFileLocation) throws InterruptedException, IOException {
String configLocation = getConfigLocation();
if (null == configLocation) {
context.log("not specified configLocation of k8s config ,will use default config .");
}
String deployFileRelativePath = FileUtils.toRelativePath(workspace, new FilePath(new File(deployFileLocation)));
String command = String.format("kubectl apply -f %s", deployFileRelativePath);
if (StringUtils.isNotBlank(configLocation)) {
command = command + " --kubeconfig " + configLocation;
}
context.execute(command);
}
@Extension
public static class DescriptorImpl extends Descriptor<DeployToK8sConfig> {
public List<Descriptor<DeployStyleRadio>> getDeployStyles() {
return Jenkins.get().getDescriptorList(DeployStyleRadio.class);
}
}
}
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.builder.config;
import com.schbrain.ci.jenkins.plugins.integration.builder.util.FileUtils;
import com.schbrain.ci.jenkins.plugins.integration.builder.util.TemplateUtils;
import hudson.*;
import hudson.model.Descriptor;
import org.apache.commons.lang3.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.springframework.lang.Nullable;
import java.io.IOException;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import static com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.BuildConstants.BUILD_SCRIPT_NAME;
import static com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.DockerConstants.*;
import static com.schbrain.ci.jenkins.plugins.integration.builder.util.FileUtils.lookupFile;
/**
* @author liaozan
* @since 2022/1/16
*/
@SuppressWarnings("unused")
public class DockerConfig extends BuildConfig<DockerConfig> {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneId.systemDefault());
private final Boolean buildImage;
private final PushConfig pushConfig;
private final Boolean deleteImageAfterBuild;
private final String javaOpts;
private final String buildScriptUrl;
private final String buildScriptBranch;
private final Boolean disableSkywalking;
@DataBoundConstructor
public DockerConfig(Boolean buildImage, PushConfig pushConfig, Boolean deleteImageAfterBuild, String javaOpts,
String buildScriptUrl, String buildScriptBranch, Boolean disableSkywalking) {
this.buildImage = Util.fixNull(buildImage, false);
this.pushConfig = pushConfig;
this.deleteImageAfterBuild = Util.fixNull(deleteImageAfterBuild, false);
this.javaOpts = Util.fixNull(javaOpts);
this.buildScriptUrl = buildScriptUrl;
this.buildScriptBranch = buildScriptBranch;
this.disableSkywalking = disableSkywalking;
}
@Nullable
public PushConfig getPushConfig() {
return pushConfig;
}
public Boolean getBuildImage() {
return buildImage;
}
public Boolean getDeleteImageAfterBuild() {
return deleteImageAfterBuild;
}
public String getJavaOpts() {
return javaOpts;
}
public String getBuildScriptUrl() {
return buildScriptUrl;
}
public String getBuildScriptBranch() {
return buildScriptBranch;
}
public Boolean getDisableSkywalking() {
return disableSkywalking;
}
@Override
public void doBuild() throws IOException, InterruptedException {
if (!getBuildImage()) {
context.log("docker build image is skipped");
return;
}
String javaOpts = this.javaOpts;
if (Boolean.TRUE.equals(getDisableSkywalking())) {
if (StringUtils.isBlank(javaOpts)) {
javaOpts = DISABLE_SKYWALKING_OPTIONS;
} else {
javaOpts = javaOpts + " " + DISABLE_SKYWALKING_OPTIONS;
}
}
envVars.put(JAVA_OPTS, javaOpts);
String imageName = getFullImageName();
if (imageName == null) {
return;
}
envVars.put(IMAGE, imageName);
FilePath dockerfile = lookupFile(workspace.child(BUILD_SCRIPT_NAME), DOCKERFILE_NAME, context.getLogger());
if (dockerfile == null) {
context.log("Dockerfile not exist, skip docker build");
return;
}
TemplateUtils.evaluate(Paths.get(dockerfile.getRemote()), context);
String relativePath = FileUtils.toRelativePath(workspace, dockerfile);
String command = String.format("docker build --pull -t %s -f %s .", imageName, relativePath);
context.execute(command);
context.setImageHasBeenBuilt();
}
private String getFullImageName() {
String registry = null;
PushConfig pushConfig = getPushConfig();
if (pushConfig != null) {
registry = pushConfig.getRegistry();
}
if (StringUtils.isBlank(registry)) {
registry = envVars.get(REGISTRY);
}
if (StringUtils.isBlank(registry)) {
throw new IllegalArgumentException("REGISTRY is null or empty");
}
String appName = envVars.get(APP_NAME);
String version = envVars.get(VERSION);
String buildTime = DATE_TIME_FORMATTER.format(Instant.ofEpochMilli(build.getStartTimeInMillis()));
return String.format("%s/%s:%s-%s", registry, appName, version, buildTime);
}
@Extension
public static class DescriptorImpl extends Descriptor<DockerConfig> {
}
public static class PushConfig extends BuildConfig<PushConfig> {
private final Boolean pushImage;
private final String registry;
@DataBoundConstructor
public PushConfig(Boolean pushImage, String registry) {
this.pushImage = pushImage;
this.registry = registry;
}
public Boolean getPushImage() {
return pushImage;
}
public String getRegistry() {
return registry;
}
@Override
public void doBuild() throws IOException, InterruptedException {
if (!getPushImage()) {
logger.println("docker push image is skipped");
return;
}
String imageName = envVars.get(IMAGE);
if (imageName == null) {
return;
}
String command = String.format("docker push %s", imageName);
context.execute(command);
}
@Extension
@SuppressWarnings("unused")
public static class DescriptorImpl extends Descriptor<PushConfig> {
}
}
}
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.builder.config;
import hudson.Extension;
import hudson.Util;
import hudson.model.Descriptor;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import java.io.IOException;
/**
* @author liaozan
* @since 2022/1/16
*/
@SuppressWarnings("unused")
public class MavenConfig extends BuildConfig<MavenConfig> {
private final String mvnCommand;
private final String javaHome;
@DataBoundConstructor
public MavenConfig(String mvnCommand, String javaHome) {
this.mvnCommand = Util.fixNull(mvnCommand);
this.javaHome = Util.fixNull(javaHome);
}
public String getMvnCommand() {
return mvnCommand;
}
public String getJavaHome() {
return javaHome;
}
@Override
public void doBuild() throws IOException, InterruptedException {
String mavenCommand = getMvnCommand();
if (StringUtils.isBlank(mavenCommand)) {
logger.println("maven command is empty, skip maven build");
return;
}
String javaHome = getJavaHome();
if (StringUtils.isNotBlank(javaHome)) {
envVars.put("JAVA_HOME", javaHome);
}
context.execute(mavenCommand);
}
@Extension
public static class DescriptorImpl extends Descriptor<MavenConfig> {
}
}
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.builder.config.deploy;
import com.schbrain.ci.jenkins.plugins.integration.builder.BuilderContext;
import hudson.model.Describable;
import hudson.model.Descriptor;
import jenkins.model.Jenkins;
import java.io.IOException;
/**
* @author zhangdd on 2022/1/20
*/
public abstract class DeployStyleRadio implements Describable<DeployStyleRadio> {
//---------------------------------------------------------------------
// Abstract methods to be implemented by subclasses
//---------------------------------------------------------------------
public abstract String getDeployFileLocation(BuilderContext builderContext) throws IOException, InterruptedException;
@Override
@SuppressWarnings("unchecked")
public Descriptor<DeployStyleRadio> getDescriptor() {
return Jenkins.get().getDescriptor(getClass());
}
public abstract static class InventoryDescriptor extends Descriptor<DeployStyleRadio> {
}
}
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.builder.config.deploy;
import com.schbrain.ci.jenkins.plugins.integration.builder.BuilderContext;
import com.schbrain.ci.jenkins.plugins.integration.builder.FileManager;
import com.schbrain.ci.jenkins.plugins.integration.builder.util.TemplateUtils;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.EnvVars;
import hudson.Extension;
import org.apache.commons.lang3.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import static com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.DeployConstants.*;
/**
* @author zhangdd on 2022/1/20
*/
@SuppressWarnings("unused")
public class DeployTemplateComponent extends DeployStyleRadio {
private final String namespace;
private final String replicas;
private final String memoryRequest;
private final String memoryLimit;
private final String nodeTag;
private final String port;
@DataBoundConstructor
public DeployTemplateComponent(String namespace, String replicas, String memoryRequest, String memoryLimit, String nodeTag, String port) {
this.namespace = namespace;
this.replicas = replicas;
this.memoryRequest = memoryRequest;
this.memoryLimit = memoryLimit;
this.nodeTag = nodeTag;
this.port = port;
}
public String getNamespace() {
return namespace;
}
public String getReplicas() {
return replicas;
}
public String getMemoryRequest() {
return memoryRequest;
}
public String getMemoryLimit() {
return memoryLimit;
}
public String getNodeTag() {
return nodeTag;
}
public String getPort() {
return port;
}
@Override
public String getDeployFileLocation(BuilderContext context) throws IOException, InterruptedException {
Path templateFile = getDeployTemplate(context);
contributeEnv(context.getEnvVars());
TemplateUtils.evaluate(templateFile, context);
return templateFile.toString();
}
private void contributeEnv(EnvVars envVars) {
envVars.put(K8S_POD_NAMESPACE, getNamespace());
envVars.put(K8S_POD_PORT, getPort());
envVars.put(K8S_POD_REPLICAS, getReplicas());
envVars.put(K8S_POD_MEMORY_REQUEST, getMemoryRequest());
envVars.put(K8S_POD_MEMORY_LIMIT, getMemoryLimit());
envVars.put(K8S_POD_NODE_TAG, StringUtils.isBlank(getNodeTag()) ? "app" : getNodeTag());
}
private Path getDeployTemplate(BuilderContext context) {
File buildScriptDir = FileManager.getWorkspaceBuildScriptDir(context.getBuild());
return Paths.get(buildScriptDir.getPath(), DEPLOYMENT_TEMPLATE_FILE_NAME);
}
@Extension
public static class DescriptorImpl extends InventoryDescriptor {
@NonNull
@Override
public String getDisplayName() {
return "使用默认模版";
}
}
}
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.builder.config.deploy;
import com.schbrain.ci.jenkins.plugins.integration.builder.BuilderContext;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import org.kohsuke.stapler.DataBoundConstructor;
/**
* @author zhangdd on 2022/1/20
*/
@SuppressWarnings("unused")
public class PointDeployFileComponent extends DeployStyleRadio {
private final String deployFileLocation;
@DataBoundConstructor
public PointDeployFileComponent(String deployFileLocation) {
this.deployFileLocation = deployFileLocation;
}
public String getDeployFileLocation() {
return deployFileLocation;
}
@Override
public String getDeployFileLocation(BuilderContext builderContext) {
return getDeployFileLocation();
}
@Extension
public static class DescriptorImpl extends InventoryDescriptor {
@NonNull
@Override
public String getDisplayName() {
return "指定部署文件位置";
}
}
}
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.builder.config.deploy.service;
import com.schbrain.ci.jenkins.plugins.integration.builder.BuilderContext;
import com.schbrain.ci.jenkins.plugins.integration.builder.FileManager;
import com.schbrain.ci.jenkins.plugins.integration.builder.util.TemplateUtils;
import hudson.EnvVars;
import org.kohsuke.stapler.DataBoundConstructor;
import org.springframework.util.StringUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import static com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.DeployConstants.*;
import static com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.DockerConstants.APP_NAME;
/**
* @author liaozan
* @since 2022/3/10
*/
public class ServiceDeployConfig {
private final String serviceMode;
private final String serviceNamespace;
private final String serviceName;
private final String servicePort;
@DataBoundConstructor
public ServiceDeployConfig(String serviceMode, String serviceNamespace, String serviceName, String servicePort) {
this.serviceMode = serviceMode;
this.serviceNamespace = serviceNamespace;
this.serviceName = serviceName;
this.servicePort = servicePort;
}
public String getServiceMode() {
return serviceMode;
}
public String getServiceNamespace() {
return serviceNamespace;
}
public String getServiceName() {
return serviceName;
}
public String getServicePort() {
return servicePort;
}
public String getServiceDeployFileLocation(BuilderContext context) throws IOException, InterruptedException {
Path templateFile = getServiceDeployTemplate(context);
contributeEnv(context.getEnvVars());
TemplateUtils.evaluate(templateFile, context);
return templateFile.toString();
}
private void contributeEnv(EnvVars envVars) {
envVars.put(K8S_SERVICE_MODE, getServiceMode());
if (StringUtils.hasText(getServiceNamespace())) {
envVars.put(K8S_SERVICE_NAMESPACE, getServiceNamespace());
} else {
envVars.put(K8S_SERVICE_NAMESPACE, envVars.get(K8S_POD_NAMESPACE));
}
if (StringUtils.hasText(getServiceName())) {
envVars.put(K8S_SERVICE_NAME, getServiceName());
} else {
envVars.put(K8S_SERVICE_NAME, envVars.get(APP_NAME));
}
if (StringUtils.hasText(getServicePort())) {
envVars.put(K8S_SERVICE_PORT, getServicePort());
} else {
envVars.put(K8S_SERVICE_PORT, envVars.get(K8S_POD_PORT));
}
}
private Path getServiceDeployTemplate(BuilderContext context) {
File buildScriptDir = FileManager.getWorkspaceBuildScriptDir(context.getBuild());
return Paths.get(buildScriptDir.getPath(), SERVICE_TEMPLATE_FILE_NAME);
}
}
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.builder.constants;
/**
* @author zhangdd on 2022/1/20
*/
public class Constants {
public static class BuildConstants {
public static final String DEFAULT_SCRIPT_GIT_REPO = "git@gitlab.schbrain.com:tools/build-script.git";
public static final String DEFAULT_SCRIPT_GIT_BRANCH = "main";
public static final String SCRIPT_ZIP_NAME = "build-script.zip";
public static final String BUILD_SCRIPT_NAME = "build-script";
public static final String ENV_VARS = "envVars";
}
public static class DeployConstants {
public static final String DEPLOYMENT_TEMPLATE_FILE_NAME = "k8s-deploy-template.yaml";
public static final String SERVICE_TEMPLATE_FILE_NAME = "k8s-service-template.yaml";
public static final String K8S_POD_NAMESPACE = "NAMESPACE";
public static final String K8S_POD_PORT = "PORT";
public static final String K8S_POD_REPLICAS = "REPLICAS";
public static final String K8S_POD_MEMORY_LIMIT = "MEMORY_LIMIT";
public static final String K8S_POD_MEMORY_REQUEST = "MEMORY_REQUEST";
public static final String K8S_POD_NODE_TAG = "NODE_TAG";
public static final String K8S_SERVICE_MODE = "SERVICE_MODE";
public static final String K8S_SERVICE_NAMESPACE = "SERVICE_NAMESPACE";
public static final String K8S_SERVICE_NAME = "SERVICE_NAME";
public static final String K8S_SERVICE_PORT = "SERVICE_PORT";
}
public static class DockerConstants {
public static final String BUILD_INFO_FILE_NAME = "dockerBuildInfo";
public static final String DOCKERFILE_NAME = "Dockerfile";
public static final String IMAGE = "IMAGE";
public static final String REGISTRY = "REGISTRY";
public static final String APP_NAME = "APP_NAME";
public static final String VERSION = "VERSION";
public static final String JAVA_OPTS = "JAVA_OPTS";
public static final String DISABLE_SKYWALKING_OPTIONS = "-Dskywalking.agent.enable=false";
}
public static class GitConstants {
public static final String GIT_PROPERTIES_FILE = "git.properties";
public static final String GIT_BRANCH = "git.branch";
public static final String GIT_COMMITTER = "git.commit.user.name";
}
}
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.builder.env;
import com.schbrain.ci.jenkins.plugins.integration.builder.BuilderContext;
import com.schbrain.ci.jenkins.plugins.integration.builder.FileManager;
import com.schbrain.ci.jenkins.plugins.integration.builder.util.FileUtils;
import hudson.EnvVars;
import hudson.Extension;
import hudson.model.AbstractBuild;
import hudson.model.BuildVariableContributor;
import java.io.File;
import java.io.IOException;
import java.util.Map;
/**
* @author liaozan
* @since 2022/1/17
*/
@Extension
public class BuildEnvContributor extends BuildVariableContributor {
private static final String DELIMITER = "=";
public static void saveEnvVarsToDisk(BuilderContext context) throws IOException {
File envVarsFile = FileManager.getEnvVarsFile(context.getBuild());
FileUtils.writeUtf8String("", envVarsFile);
FileUtils.writeUtf8Map(context.getEnvVars(), envVarsFile, DELIMITER);
}
@Override
public void buildVariablesFor(AbstractBuild build, Map<String, String> variables) {
File envFilePath;
try {
envFilePath = FileManager.getEnvVarsFile(build);
} catch (IOException e) {
return;
}
EnvVars envVars = new EnvVars();
for (String line : FileUtils.readUtf8Lines(envFilePath)) {
String[] variablePair = line.split(DELIMITER);
// variables may not be split by =
if (variablePair.length != 1) {
envVars.putIfNotNull(variablePair[0], variablePair[1]);
}
}
variables.putAll(envVars);
}
}
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.builder.util;
import com.schbrain.ci.jenkins.plugins.integration.builder.BuilderContext;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import hudson.FilePath;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.Map.Entry;
/**
* @author liaozan
* @since 2022/1/17
*/
public class FileUtils {
/**
* lookup the special file
*/
@CheckForNull
public static FilePath lookupFile(BuilderContext context, String fileName) throws IOException, InterruptedException {
return lookupFile(context.getWorkspace(), fileName, context.getLogger());
}
/**
* lookup the special file
*/
@CheckForNull
public static FilePath lookupFile(FilePath searchLocation, String fileName, Logger logger) throws IOException, InterruptedException {
if (searchLocation == null || !searchLocation.exists()) {
logger.println("searchLocation not exist", true);
return null;
}
FilePath[] fileList = searchLocation.list("**/" + fileName);
if (fileList.length == 0) {
logger.println("could not found matched file: %s", fileName);
return null;
}
return getTheClosestFile(fileList);
}
public static String toRelativePath(FilePath root, FilePath filePath) {
Path rootPath = Paths.get(root.getRemote());
Path targetFilePath = Paths.get(filePath.getRemote());
return rootPath.relativize(targetFilePath).toString();
}
public static FilePath getTheClosestFile(FilePath[] fileList) {
FilePath matched = fileList[0];
if (fileList.length == 1) {
return matched;
}
for (FilePath filePath : fileList) {
String filePathName = filePath.getRemote();
if (filePathName.length() < matched.getRemote().length()) {
matched = filePath;
}
}
return matched;
}
public static Map<String, String> filePathToMap(FilePath lookupFile) throws IOException, InterruptedException {
Map<String, String> result = new HashMap<>();
Properties properties = new Properties();
properties.load(new StringReader(lookupFile.readToString()));
for (String propertyName : properties.stringPropertyNames()) {
result.put(propertyName, properties.getProperty(propertyName));
}
return result;
}
public static void writeUtf8String(String content, File file) {
writeUtf8String(content, file.getPath());
}
public static void writeUtf8String(String content, String path) {
writeString(content, path, StandardCharsets.UTF_8);
}
public static void writeString(String content, String path, Charset charset) {
try {
Path filePath = getFilePath(path);
Files.write(filePath, content.getBytes(charset));
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
public static void writeUtf8Map(Map<String, String> variables, File file, String delimiter) {
try {
Path filePath = getFilePath(file.getPath());
BufferedWriter writer = Files.newBufferedWriter(filePath);
for (Entry<String, String> entry : variables.entrySet()) {
String content = String.format("%s%s%s", entry.getKey(), delimiter, entry.getValue());
writer.write(content);
writer.newLine();
writer.flush();
}
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
public static List<String> readUtf8Lines(File file) {
try {
Path filePath = getFilePath(file.getPath());
return Files.readAllLines(filePath);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private static Path getFilePath(String path) throws IOException {
Path filePath = Paths.get(path);
if (Files.notExists(filePath)) {
Files.createFile(filePath);
}
return filePath;
}
}
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.builder.util;
import java.io.PrintStream;
/**
* @author liaozan
* @since 2022/1/20
*/
public class Logger extends PrintStream {
private final PrintStream delegate;
private Logger(PrintStream delegate) {
super(delegate);
this.delegate = delegate;
}
public static Logger of(PrintStream delegate) {
return new Logger(delegate);
}
public void println(String content, Object... args) {
println(content, true, args);
}
public void println(String content, boolean format, Object... args) {
content = String.format(content, args);
println(content, format);
}
public void println(String content, boolean format) {
if (format) {
String wrappedContent = "|| " + content + " ||";
StringBuilder wrapperLine = new StringBuilder();
wrapperLine.append("=".repeat(wrappedContent.length()));
delegate.println();
delegate.println(wrapperLine);
delegate.println(wrappedContent);
delegate.println(wrapperLine);
delegate.println();
} else {
delegate.println(content);
}
}
}
\ No newline at end of file
package com.schbrain.ci.jenkins.plugins.integration.builder.util;
import com.schbrain.ci.jenkins.plugins.integration.builder.BuilderContext;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author liaozan
* @since 2022/1/23
*/
public class TemplateUtils {
public static void evaluate(Path templateFile, BuilderContext context) throws IOException, InterruptedException {
if (templateFile == null) {
return;
}
String resolved = evaluate(Files.readString(templateFile), context.getEnvVars());
Files.write(templateFile, resolved.getBytes(StandardCharsets.UTF_8));
}
public static String evaluate(String template, Map<String, String> variables) {
if (null == template) {
return null;
}
if (null == variables || variables.isEmpty()) {
return template;
}
Map<String, Object> params = new LinkedHashMap<>(variables);
VelocityContext velocityContext = new VelocityContext(params);
StringWriter writer = new StringWriter();
Velocity.evaluate(velocityContext, writer, "Template Evaluate", template);
return writer.getBuffer().toString();
}
}
\ No newline at end of file
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:st="jelly:stapler">
<l:layout title="build-scripts">
<l:main-panel>
<h2>构建脚本</h2>
<j:set var="url" value="${h.getNearestAncestorUrl(request,it)}"/>
<st:redirect url="${url}/list"/>
</l:main-panel>
</l:layout>
</j:jelly>
\ No newline at end of file
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:entry/>
<f:optionalProperty field="mavenConfig" title="Maven编译"/>
<f:optionalProperty field="dockerConfig" title="Docker构建"/>
<f:optionalProperty field="deployToK8sConfig" title="Kubernetes部署"/>
<f:entry/>
</j:jelly>
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:entry/>
<f:entry title="kubectl --config" description="为空时使用默认配置文件">
<f:textbox field="configLocation" value="${instance.configLocation}"/>
</f:entry>
<f:entry title="选择 Deployment 部署方式">
<f:hetero-radio field="deployStyle" descriptors="${descriptor.getDeployStyles()}"/>
</f:entry>
<f:optionalBlock title="部署 Service" field="serviceDeployConfig" checked="${instance.serviceDeployConfig != null}">
<j:set var="currentConfig" value="${instance.serviceDeployConfig}"/>
<j:set var="defaultValue" value="ClusterIP"/>
<f:entry title="Service 类型">
<f:radio name="serviceMode" title="ClusterIP" checked="${currentConfig.serviceMode == null || currentConfig.serviceMode == 'ClusterIP'}" value="ClusterIP"/>
<f:radio name="serviceMode" title="LoadBalancer" checked="${currentConfig.serviceMode == 'LoadBalancer'}" value="LoadBalancer"/>
</f:entry>
<f:entry title="Service 命名空间(为空时取 PodNamespace)">
<f:textbox field="serviceNamespace" value="${instance.serviceDeployConfig.serviceNamespace}"/>
</f:entry>
<f:entry title="Service 名称(为空时取 PodName)">
<f:textbox field="serviceName" value="${instance.serviceDeployConfig.serviceName}"/>
</f:entry>
<f:entry title="Service 端口号(为空时取 PodPort)">
<f:textbox field="servicePort" value="${instance.serviceDeployConfig.servicePort}"/>
</f:entry>
</f:optionalBlock>
<f:entry/>
</j:jelly>
\ No newline at end of file
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:entry/>
<f:entry title="构建脚本仓库地址" description="如未配置默认为内置的build-script">
<f:textbox field="buildScriptUrl"/>
</f:entry>
<f:entry title="构建脚本仓库分支" description="如未配置默认master分支">
<f:textbox field="buildScriptBranch"/>
</f:entry>
<f:entry>
<f:optionalBlock>
<f:checkbox title="构建镜像" field="buildImage" checked="${instance.buildImage}"/>
</f:optionalBlock>
</f:entry>
<f:entry title="JAVA_OPTS" description="配置Dockerfile中jvm参数">
<f:textbox field="javaOpts"/>
</f:entry>
<f:entry>
<f:optionalBlock>
<f:checkbox title="禁用Skywalking" field="disableSkywalking" checked="${instance.disableSkywalking}"/>
</f:optionalBlock>
</f:entry>
<f:entry>
<f:optionalBlock title="推送镜像" field="pushConfig" checked="${instance.pushConfig.pushImage}">
<f:invisibleEntry>
<f:checkbox field="pushImage" checked="true" value="${instance.pushConfig.pushImage}"/>
</f:invisibleEntry>
<f:entry/>
<f:entry title="镜像仓库" description="为空时默认获取 pom.xml 插件中配置的仓库地址,如项目未指定,则采用 parent pom 插件中的默认值(根据 profile 区分)">
<f:textbox field="registry" value="${instance.pushConfig.registry}"/>
</f:entry>
</f:optionalBlock>
</f:entry>
<f:entry>
<f:optionalBlock>
<f:checkbox title="构建后删除" field="deleteImageAfterBuild" checked="${instance.deleteImageAfterBuild}"/>
</f:optionalBlock>
</f:entry>
<f:entry/>
</j:jelly>
\ No newline at end of file
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:entry/>
<f:entry title="Maven命令" description="如未配置 M2_HOME 环境,请在命令前加入指向 mvn 命令的全路径">
<f:textbox field="mvnCommand" default="mvn clean package -U -B -Dmaven.test.skip=true"/>
</f:entry>
<f:entry title="JAVA_HOME" description="如未配置 JAVA_HOME 环境,则会使用本地默认 JAVA_HOME">
<f:textbox field="javaHome"/>
</f:entry>
<f:entry/>
</j:jelly>
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:entry/>
<f:entry title="Pod 命名空间">
<f:textbox field="namespace" value="${instance.namespace}"/>
</f:entry>
<f:entry title="Pod 副本数">
<f:textbox field="replicas" value="${instance.replicate}" default="1"/>
</f:entry>
<f:entry title="Pod 内存限制(request)">
<f:textbox field="memoryRequest" value="${instance.memoryRequest}" default="2048Mi"/>
</f:entry>
<f:entry title="Pod 内存限制(limit)">
<f:textbox field="memoryLimit" value="${instance.memoryLimit}" default="2048Mi"/>
</f:entry>
<f:entry title="Pod 部署节点标签 (用于指定亲和性)">
<f:textbox field="nodeTag" value="${instance.nodeTag}" default="app"/>
</f:entry>
<f:entry title="Pod 端口号">
<f:textbox field="port" value="${instance.port}"/>
</f:entry>
<f:entry/>
</j:jelly>
\ No newline at end of file
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:entry/>
<f:entry title="部署文件路径" description="指定使用的部署文件位置">
<f:textbox field="deployFileLocation" value="${instance.deployFileLocation}"/>
</f:entry>
<f:entry/>
</j:jelly>
\ No newline at end of file
<?jelly escape-by-default='true'?>
<!--
This view is used to render the installed plugins page.
-->
<div>
This plugin provider integration with Kubernetes.
</div>
\ No newline at end of file
### Summary
> this is a maven plugin that simplify container deployment. it will generate `kubernetes-deploy.yaml`、`Dockerfile` according to your configuration
### Requirement
- your project should be a Spring Boot project
- your project will running in container
### How to use
**First** add `integration-maven-plugin` in the `pom.xml` , it must be added under the `spring-boot-maven-plugin` . like this
```.xml
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.schbrain.maven.plugin</groupId>
<artifactId>integration-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
```
**Second** specify build-script repository.
```.xml
<configuration>
<gitRepository>git@gitlab.xxx.com:tools/build-script.git</gitRepository>
<branch>main</branch>
</configuration>
```
---
The completion config like this:
```.xml
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.schbrain.maven.plugin</groupId>
<artifactId>integration-maven-plugin</artifactId>
<configuration>
<gitRepository>git@gitlab.xxx.com:tools/build-script.git</gitRepository>
<branch>main</branch>
</configuration>
</plugin>
</plugins>
</build>
```
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>integration</artifactId>
<version>${revision}</version>
</parent>
<groupId>com.schbrain.maven.plugin</groupId>
<artifactId>integration-maven-plugin</artifactId>
<packaging>maven-plugin</packaging>
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>${maven.core.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>${maven.core.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>${maven.plugin.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
<version>${maven.plugin.version}</version>
<configuration>
<mojoDependencies>
<param>com.schbrain.maven.plugin</param>
</mojoDependencies>
</configuration>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
package com.schbrain.maven.plugin.mojo;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.*;
import org.apache.maven.project.MavenProject;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.util.HashMap;
import java.util.Map;
/**
* @author liaozan
* @since 2022/1/4
*/
@SuppressWarnings({"unused", "MismatchedQueryAndUpdateOfCollection"})
@Mojo(name = "prepare", threadSafe = true, defaultPhase = LifecyclePhase.PACKAGE,
requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class PrepareMojo extends AbstractMojo {
private static final String DOCKER_BUILD_INFO = "dockerBuildInfo";
/**
* maven project
*/
@Parameter(defaultValue = "${project}", readonly = true, required = true)
private MavenProject project;
/**
* maven session
*/
@Parameter(defaultValue = "${session}", readonly = true, required = true)
private MavenSession session;
/**
* 构造最终的名字
*/
@Parameter(defaultValue = "${project.build.finalName}", readonly = true)
private String finalName;
/**
* 构建输出目录
*/
@Parameter(defaultValue = "${project.build.directory}", readonly = true)
private File buildDirectory;
/**
* docker 镜像推送地址
*/
@Parameter(required = true, defaultValue = "${docker.registry}")
private String dockerRegistry;
/**
* spring 启动的 profile
*/
@Parameter(required = true, defaultValue = "${spring.profile}")
private String springProfile;
/**
* 是否包含系统属性
*/
@Parameter(defaultValue = "false")
private boolean includeSystemProperties;
/**
* 是否包含系统环境变量
*/
@Parameter(defaultValue = "false")
private boolean includeSystemEnv;
/**
* 打包的名字, 为空取 root project artifactId
*/
@Parameter
private String appName;
/**
* 打包的版本, 为空取 root project version
*/
@Parameter
private String version;
/**
* 额外的属性
*/
@Parameter
private Map<String, String> additionalProperties;
public void execute() throws MojoExecutionException {
if (shouldSkip()) {
return;
}
validateParam();
Path targetFile = getTargetFile(finalName, buildDirectory);
if (!Files.exists(targetFile)) {
throw new MojoExecutionException("target jar is not present, it's required for build");
}
try {
Map<Object, Object> variables = collectProjectVariables(targetFile);
storeVariablesToFile(variables);
getLog().info("generate build properties: \n" + JSONUtil.toJsonPrettyStr(variables));
} catch (Exception e) {
throw new MojoExecutionException(e);
}
}
private void validateParam() throws MojoExecutionException {
MavenProject topLevelProject = session.getTopLevelProject();
if (StrUtil.isBlank(dockerRegistry)) {
throw new MojoExecutionException("docker.registry is required for build, but found empty value, please check the pom configuration");
}
if (StrUtil.isBlank(springProfile)) {
throw new MojoExecutionException("spring.profile is required for build, but found empty value, please check the pom configuration");
}
if (StrUtil.isBlank(appName)) {
appName = topLevelProject.getArtifactId();
}
if (StrUtil.isBlank(version)) {
version = topLevelProject.getVersion();
}
}
private Map<Object, Object> collectProjectVariables(Path targetFile) {
MavenProject topProject = session.getTopLevelProject();
Map<Object, Object> variables = new HashMap<>();
// required for docker build
variables.put("APP_NAME", appName);
variables.put("JAR_FILE", getJarFileRelativePath(topProject.getBasedir(), targetFile));
variables.put("VERSION", version);
variables.put("REGISTRY", dockerRegistry);
variables.put("PROFILE", springProfile);
// add additionalProperties
if (additionalProperties != null) {
variables.putAll(additionalProperties);
}
// add system properties
if (includeSystemProperties) {
variables.putAll(System.getProperties());
}
// add system env
if (includeSystemEnv) {
variables.putAll(System.getenv());
}
return variables;
}
private void storeVariablesToFile(Map<Object, Object> variables) throws IOException {
Path dockerBuildInfo = Paths.get(buildDirectory.getAbsolutePath(), DOCKER_BUILD_INFO);
FileUtil.del(dockerBuildInfo);
Files.createFile(dockerBuildInfo);
FileUtil.writeUtf8Map(variables, dockerBuildInfo.toFile(), "=", false);
}
private String getJarFileRelativePath(File basedir, Path jarFile) {
return Paths.get(basedir.getPath()).relativize(jarFile).toString();
}
private boolean shouldSkip() {
if ("pom".equals(this.project.getPackaging())) {
getLog().warn("docker-build goal could not be applied to pom project");
return true;
}
return false;
}
private Path getTargetFile(String finalName, File targetDirectory) {
return Paths.get(targetDirectory.getPath(), withExtension(finalName));
}
private String withExtension(String fileName) {
return String.format("%s.%s", fileName, project.getArtifact().getArtifactHandler().getExtension());
}
}
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>schbrain-parent</artifactId>
<version>${revision}</version>
</parent>
<artifactId>integration</artifactId>
<packaging>pom</packaging>
<modules>
<module>integration-maven-plugin</module>
<module>integration-jenkins-plugin</module>
</modules>
</project>
\ No newline at end of file
...@@ -112,11 +112,20 @@ ...@@ -112,11 +112,20 @@
<alibaba-dingtalk-sdk.version>2.0.0</alibaba-dingtalk-sdk.version> <alibaba-dingtalk-sdk.version>2.0.0</alibaba-dingtalk-sdk.version>
<!-- maven plugins --> <!-- maven plugins -->
<maven.core.version>3.8.6</maven.core.version>
<maven.plugin.version>3.6.4</maven.plugin.version>
<flatten-maven-plugin.version>1.4.1</flatten-maven-plugin.version> <flatten-maven-plugin.version>1.4.1</flatten-maven-plugin.version>
<integration-maven-plugin.version>${revision}</integration-maven-plugin.version> <integration-maven-plugin.version>${revision}</integration-maven-plugin.version>
<maven.repository.url>https://maven.schbrain.com/repository</maven.repository.url> <maven.repository.url>https://maven.schbrain.com/repository</maven.repository.url>
</properties> </properties>
<modules>
<module>commons</module>
<module>starters</module>
<module>support</module>
<module>integration</module>
</modules>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<dependency> <dependency>
......
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>starters</artifactId>
<version>${revision}</version>
</parent>
<artifactId>apollo-spring-boot-starter</artifactId>
<dependencies>
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
</dependency>
<dependency>
<groupId>com.schbrain.framework</groupId>
<artifactId>schbrain-spring-support</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package com.schbrain.framework.autoconfigure.apollo;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import com.ctrip.framework.apollo.spring.boot.ApolloAutoConfiguration;
import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants;
import com.schbrain.framework.autoconfigure.apollo.properties.ApolloProperties;
import com.schbrain.framework.autoconfigure.apollo.properties.ConfigurationPropertiesRegistry;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Import;
/**
* @author liaozan
* @since 2021/11/15
*/
@EnableApolloConfig
@Import({SchbrainPropertySourcesProcessor.class, ConfigurationPropertiesRegistry.class})
@EnableConfigurationProperties(ApolloProperties.class)
@AutoConfiguration(before = ApolloAutoConfiguration.class)
@ConditionalOnProperty(value = PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, matchIfMissing = true)
public class SchbrainApolloAutoConfiguration {
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.apollo;
import com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer;
import com.schbrain.framework.autoconfigure.apollo.util.PropertySourceOrderUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
/**
* @author liaozan
* @since 2022/4/19
*/
@Order(ApolloApplicationContextInitializer.DEFAULT_ORDER + 1)
public class SchbrainApolloPropertiesEnvironmentPostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
PropertySourceOrderUtils.adjustPropertySourceOrder(environment);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.apollo;
import com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor;
import com.schbrain.framework.autoconfigure.apollo.util.PropertySourceOrderUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.core.env.ConfigurableEnvironment;
/**
* @author liaozan
* @since 2021/12/6
*/
public class SchbrainPropertySourcesProcessor extends PropertySourcesProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
super.postProcessBeanFactory(beanFactory);
PropertySourceOrderUtils.adjustPropertySourceOrder(beanFactory.getBean(ConfigurableEnvironment.class));
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.apollo.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.core.env.Environment;
import static com.schbrain.framework.autoconfigure.apollo.properties.ApolloProperties.PREFIX;
/**
* this class MUST NOT load from remote
*
* @author liaozan
* @since 2021/12/6
*/
@Data
@ConfigurationProperties(prefix = PREFIX)
public class ApolloProperties {
public static final String PREFIX = "schbrain.apollo";
private boolean remoteFirst = false;
public static ApolloProperties get(Environment environment) {
return Binder.get(environment).bindOrCreate(PREFIX, ApolloProperties.class);
}
}
package com.schbrain.framework.autoconfigure.apollo.properties;
import com.ctrip.framework.foundation.Foundation;
import com.schbrain.common.util.ApplicationName;
import com.schbrain.common.util.EnvUtils;
import com.schbrain.framework.support.spring.EnvironmentPostProcessorAdapter;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.util.StringUtils;
import java.util.LinkedHashMap;
import java.util.Map;
import static com.ctrip.framework.apollo.core.ApolloClientSystemConsts.*;
import static com.ctrip.framework.apollo.core.ConfigConsts.APOLLO_META_KEY;
import static com.ctrip.framework.apollo.spring.config.PropertySourcesConstants.*;
/**
* @author liaozan
* @since 2021/11/6
*/
public class ApolloPropertiesPreparer extends EnvironmentPostProcessorAdapter implements Ordered {
// get properties after configData loaded
public static final Integer ORDER = ConfigDataEnvironmentPostProcessor.ORDER + 1;
public static final String ENV_KEY = "env";
private static final Map<String, Object> INIT_PROPERTIES = new LinkedHashMap<>();
public ApolloPropertiesPreparer(DeferredLogFactory deferredLogFactory, ConfigurableBootstrapContext bootstrapContext) {
super(deferredLogFactory, bootstrapContext);
}
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
if (disabled(environment)) {
print("apollo is disabled");
return;
}
setRequiredProperty(environment);
}
@Override
public int getOrder() {
return ORDER;
}
@Override
protected void onBootstrapContextClosed(ConfigurableApplicationContext context) {
context.getBeanFactory().addBeanPostProcessor(new ConfigurablePropertiesBeanPostProcessor(context));
}
private boolean disabled(ConfigurableEnvironment environment) {
Boolean enabled = environment.getProperty(APOLLO_BOOTSTRAP_ENABLED, Boolean.class, true);
return Boolean.FALSE.equals(enabled);
}
private void setRequiredProperty(ConfigurableEnvironment environment) {
String appId = getAppId(environment);
saveProperty(APP_ID, appId);
String env = getEnv(environment);
saveProperty(ENV_KEY, env);
String apolloUrl = getApolloUrl(environment, env);
saveProperty(APOLLO_META, apolloUrl);
saveProperty(APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED, true);
saveProperty(APOLLO_BOOTSTRAP_ENABLED, true);
saveProperty(APOLLO_PROPERTY_ORDER_ENABLE, true);
saveProperty(APOLLO_PROPERTY_NAMES_CACHE_ENABLE, true);
printProperties();
}
private void saveProperty(String key, Object value) {
INIT_PROPERTIES.put(key, value);
System.setProperty(key, value.toString());
}
private void printProperties() {
INIT_PROPERTIES.forEach((k, v) -> print(k + " : " + v));
}
private void print(String message) {
getLog().debug(message);
System.out.println(message);
}
private String getApolloUrl(ConfigurableEnvironment environment, String env) {
String fallbackKey = env + ".meta";
// {env}.meta
String searchKey = fallbackKey;
String apolloUrl = environment.getProperty(searchKey);
if (StringUtils.hasText(apolloUrl)) {
return apolloUrl;
}
// apollo.meta.{env}
searchKey = APOLLO_META_KEY + "." + env;
apolloUrl = environment.getProperty(searchKey);
if (StringUtils.hasText(apolloUrl)) {
return apolloUrl;
}
// apollo.meta
searchKey = APOLLO_META_KEY;
apolloUrl = environment.getProperty(searchKey);
if (StringUtils.hasText(apolloUrl)) {
return apolloUrl;
}
// fallback
return Foundation.getProperty(fallbackKey, null);
}
private String getEnv(ConfigurableEnvironment environment) {
return EnvUtils.getProfile(environment);
}
private String getAppId(ConfigurableEnvironment environment) {
String appId;
if (environment.containsProperty(APP_ID)) {
appId = environment.getRequiredProperty(APP_ID);
} else {
appId = ApplicationName.get(environment);
}
return appId;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.apollo.properties;
import com.schbrain.common.util.support.ConfigurableProperties;
import com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils;
import com.schbrain.framework.support.spring.BeanPostProcessorAdapter;
import org.springframework.context.ConfigurableApplicationContext;
/**
* @author liaozan
* @since 2022/9/16
*/
public class ConfigurablePropertiesBeanPostProcessor extends BeanPostProcessorAdapter<ConfigurableProperties> {
public ConfigurablePropertiesBeanPostProcessor(ConfigurableApplicationContext applicationContext) {
this.setApplicationContext(applicationContext);
}
@Override
protected ConfigurableProperties doPostProcessBeforeInstantiation(Class<ConfigurableProperties> beanClass) {
return ConfigUtils.loadConfig(environment, beanClass);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.apollo.properties;
import cn.hutool.core.text.StrPool;
import com.ctrip.framework.apollo.spring.property.SpringValue;
import com.ctrip.framework.apollo.spring.property.SpringValueRegistry;
import com.ctrip.framework.apollo.spring.util.SpringInjector;
import com.google.common.base.CaseFormat;
import org.apache.commons.collections4.MapUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.boot.context.properties.ConfigurationPropertiesBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Map;
import static org.springframework.beans.factory.config.PlaceholderConfigurerSupport.*;
/**
* @author liaozan
* @since 2022/9/19
*/
public class ConfigurationPropertiesRegistry implements SmartInitializingSingleton, ApplicationContextAware {
private ApplicationContext applicationContext;
private AutowireCapableBeanFactory beanFactory;
private SpringValueRegistry springValueRegistry;
@Override
public void afterSingletonsInstantiated() {
Map<String, ConfigurationPropertiesBean> beanMap = ConfigurationPropertiesBean.getAll(applicationContext);
if (MapUtils.isEmpty(beanMap)) {
return;
}
beanMap.forEach((beanName, propertiesBean) -> {
String prefix = propertiesBean.getAnnotation().prefix();
Object instance = propertiesBean.getInstance();
ReflectionUtils.doWithFields(instance.getClass(),
field -> register(beanName, prefix, instance, field),
field -> !Modifier.isFinal(field.getModifiers()));
});
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
this.beanFactory = applicationContext.getAutowireCapableBeanFactory();
this.springValueRegistry = SpringInjector.getInstance(SpringValueRegistry.class);
}
private void register(String beanName, String prefix, Object instance, Field field) {
String propertyName = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, field.getName());
String key = prefix + StrPool.DOT + propertyName;
String placeholder = toPlaceHolder(key);
SpringValue springValue = new SpringValue(key, placeholder, instance, beanName, field, false);
springValueRegistry.register(beanFactory, key, springValue);
}
private String toPlaceHolder(String key) {
return DEFAULT_PLACEHOLDER_PREFIX + key + DEFAULT_VALUE_SEPARATOR + DEFAULT_PLACEHOLDER_SUFFIX;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.apollo.util;
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.enums.ConfigSourceType;
import com.schbrain.common.util.ConfigurationPropertiesUtils;
import com.schbrain.common.util.properties.SchbrainMapPropertySource;
import com.schbrain.common.util.support.ConfigurableProperties;
import com.schbrain.framework.autoconfigure.apollo.properties.ApolloProperties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.DefaultPropertiesPropertySource;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.core.env.*;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import static com.ctrip.framework.apollo.core.ApolloClientSystemConsts.APP_ID;
/**
* @author liaozan
* @since 2021/12/6
*/
public class ConfigUtils {
private static final Log LOGGER = LogFactory.getLog(ConfigUtils.class);
private static final boolean APOLLO_DISABLED = System.getProperty(APP_ID) == null;
public static boolean isApolloDisabled() {
return APOLLO_DISABLED;
}
public static <T extends ConfigurableProperties> T loadConfig(ConfigurableEnvironment environment, Class<T> propertyClass) {
// load default and local properties
T target = BeanUtils.instantiateClass(propertyClass).bindOrCreate(environment, false);
Map<String, Object> defaultProperties = ConfigurationPropertiesUtils.toMap(target);
// load remote config
Map<String, Object> loadedProperties = loadConfig(target.getNamespace(), target.getPrefix());
// merge
ApolloProperties apolloProperties = ApolloProperties.get(environment);
Map<String, Object> mergedProperties = mergeProperties(loadedProperties, defaultProperties, apolloProperties.isRemoteFirst());
// add to environment
addToEnvironment(environment, target.getName(), mergedProperties);
// rebind after addToEnvironment
return target.bindOrCreate(environment, true);
}
public static Map<String, Object> loadConfig(String namespace) {
return loadConfig(namespace, null);
}
public static Map<String, Object> loadConfig(String namespace, String prefix) {
if (isApolloDisabled()) {
return new LinkedHashMap<>();
}
Config config = ConfigService.getConfig(namespace);
if (config.getSourceType() == ConfigSourceType.LOCAL) {
LOGGER.warn(String.format("Failed to get config from Apollo namespace: %s, will use the local cache value", namespace));
}
Map<String, Object> configs = new LinkedHashMap<>();
for (String propertyName : config.getPropertyNames()) {
if (prefix != null) {
if (!propertyName.startsWith(prefix)) {
continue;
}
}
String propertyValue = config.getProperty(propertyName, null);
configs.put(propertyName, propertyValue);
}
return configs;
}
public static <T extends ConfigurableProperties> void addToEnvironment(ConfigurableEnvironment environment, T properties) {
addToEnvironment(environment, new SchbrainMapPropertySource(properties.getName(), properties));
}
public static void addToEnvironment(ConfigurableEnvironment environment, String name, Map<String, Object> properties) {
addToEnvironment(environment, new SchbrainMapPropertySource(name, properties));
}
public static void addToEnvironment(ConfigurableEnvironment environment, MapPropertySource propertySource) {
MutablePropertySources propertySources = environment.getPropertySources();
String propertySourceName = propertySource.getName();
boolean alreadyExist = propertySources.contains(propertySourceName);
if (alreadyExist) {
PropertySource<?> existing = propertySources.get(propertySourceName);
if (existing instanceof MapPropertySource) {
Map<String, Object> existingSource = ((MapPropertySource) existing).getSource();
existingSource.putAll(propertySource.getSource());
} else {
LOGGER.warn("Existing propertySource is not an instance of MapPropertySource, overwrite existing...");
propertySources.replace(propertySourceName, propertySource);
}
} else {
propertySources.addLast(propertySource);
}
resolvePlaceHolders(environment, propertySource);
ConfigurationPropertySources.attach(environment);
DefaultPropertiesPropertySource.moveToEnd(environment);
}
public static void resolvePlaceHolders(ConfigurableEnvironment environment, MapPropertySource propertySource) {
Map<String, Object> source = propertySource.getSource();
for (Entry<String, Object> entry : source.entrySet()) {
Object value = entry.getValue();
if (value instanceof String) {
String resolvedValue = environment.resolvePlaceholders((String) value);
source.put(entry.getKey(), resolvedValue);
}
}
}
private static Map<String, Object> mergeProperties(Map<String, Object> loadedProperties,
Map<String, Object> defaultProperties,
boolean remoteFirst) {
Map<String, Object> mergedProperties;
if (remoteFirst) {
defaultProperties.putAll(loadedProperties);
mergedProperties = defaultProperties;
} else {
loadedProperties.putAll(defaultProperties);
mergedProperties = loadedProperties;
}
return mergedProperties;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.apollo.util;
import com.schbrain.framework.autoconfigure.apollo.properties.ApolloProperties;
import org.springframework.boot.DefaultPropertiesPropertySource;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.core.env.*;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static com.ctrip.framework.apollo.spring.config.PropertySourcesConstants.*;
/**
* @author liaozan
* @since 2022/4/19
*/
public class PropertySourceOrderUtils {
private static final Set<String> APOLLO_PROPERTIES = Set.of(APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME, APOLLO_PROPERTY_SOURCE_NAME);
/**
* Adjust {@link MutablePropertySources} according to the
* {@link ApolloProperties#isRemoteFirst()} to ensure the configuration properties work as expected
*/
public static void adjustPropertySourceOrder(ConfigurableEnvironment environment) {
MutablePropertySources mutablePropertySources = environment.getPropertySources();
List<PropertySource<?>> remotePropertySources = mutablePropertySources.stream()
.filter(propertySource -> APOLLO_PROPERTIES.contains(propertySource.getName()))
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(remotePropertySources)) {
return;
}
ApolloProperties apolloProperties = ApolloProperties.get(environment);
for (PropertySource<?> remotePropertySource : remotePropertySources) {
mutablePropertySources.remove(remotePropertySource.getName());
if (apolloProperties.isRemoteFirst()) {
mutablePropertySources.addFirst(remotePropertySource);
} else {
mutablePropertySources.addLast(remotePropertySource);
}
}
// Make sure the default configurations always in the end
DefaultPropertiesPropertySource.moveToEnd(environment);
ConfigurationPropertySources.attach(environment);
}
}
\ No newline at end of file
default.meta=https://kp-config-dev.schbrain.com
dev.meta=https://kp-config-dev.schbrain.com
test.meta=https://kp-config-test.schbrain.com
pre.meta=http://service-apollo-meta-server-pre.apollo:8080
prod.meta=http://service-apollo-meta-server-prod.apollo:8080
\ No newline at end of file
{
"groups": [
{
"name": "schbrain.apollo",
"type": "com.schbrain.framework.autoconfigure.apollo.properties.ApolloProperties",
"sourceType": "com.schbrain.framework.autoconfigure.apollo.properties.ApolloProperties"
}
],
"properties": [
{
"name": "schbrain.apollo.remote-first",
"type": "java.lang.Boolean",
"sourceType": "com.schbrain.framework.autoconfigure.apollo.properties.ApolloProperties",
"defaultValue": false
}
],
"hints": []
}
\ No newline at end of file
org.springframework.boot.env.EnvironmentPostProcessor=\
com.schbrain.framework.autoconfigure.apollo.properties.ApolloPropertiesPreparer,\
com.schbrain.framework.autoconfigure.apollo.SchbrainApolloPropertiesEnvironmentPostProcessor
\ No newline at end of file
com.schbrain.framework.autoconfigure.apollo.SchbrainApolloAutoConfiguration
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>starters</artifactId>
<version>${revision}</version>
</parent>
<artifactId>cache-spring-boot-starter</artifactId>
<dependencies>
<dependency>
<groupId>com.schbrain.framework</groupId>
<artifactId>apollo-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package com.schbrain.framework.autoconfigure.cache;
import com.schbrain.framework.autoconfigure.cache.exception.CacheException;
import com.schbrain.framework.autoconfigure.cache.provider.CacheProvider;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import java.time.Duration;
import java.util.*;
import java.util.function.Supplier;
@Slf4j
public class CacheUtils {
private static CacheProvider cacheProvider;
public static CacheProvider getCacheProvider() {
if (cacheProvider == null) {
throw new CacheException("CacheProvider is null, Please ensure the cache config is correct");
}
return cacheProvider;
}
public static void setCacheProvider(CacheProvider cacheProvider) {
CacheUtils.cacheProvider = cacheProvider;
}
/**
* 缓存是否过期
*/
public static boolean isExpired(String cacheKey) {
return getCacheProvider().isExpired(cacheKey);
}
/**
* 设置过期时间
*/
public static void expire(String cacheKey, Duration expiration) {
getCacheProvider().expire(cacheKey, expiration);
}
/**
* 获取过期时间
*/
public static Duration getExpire(String cacheKey) {
return getCacheProvider().getExpire(cacheKey);
}
/**
* 获取缓存数据
*/
public static <T> T getValue(String cacheKey, Class<T> valueType) {
return getCacheProvider().get(cacheKey, valueType);
}
/**
* 设置缓存数据
*/
public static <T> void putValue(String cacheKey, T value, Duration expiration) {
getCacheProvider().set(cacheKey, value, expiration);
}
/**
* 获取缓存数据列表
*/
public static <T> List<T> getList(String cacheKey, Class<T> valueType) {
return getCacheProvider().getList(cacheKey, valueType);
}
/**
* 设置缓存数据列表
*/
public static <T> void putList(String cacheKey, List<T> value, Duration expiration) {
getCacheProvider().set(cacheKey, value, expiration);
}
/**
* 设置新的缓存,返回旧的缓存
*/
public static <T> T getAndSet(String cacheKey, T value, Class<T> valueType, Duration expiration) {
T cachedData = getValue(cacheKey, valueType);
putValue(cacheKey, value, expiration);
return cachedData;
}
/**
* 设置新的缓存,返回旧的缓存
*/
public static <T> List<T> getAndSet(String cacheKey, List<T> value, Class<T> valueType, Duration expiration) {
List<T> cachedData = getList(cacheKey, valueType);
putList(cacheKey, value, expiration);
return cachedData;
}
/**
* 缓存List
*/
public static <T> List<T> getListIfPresent(String cacheKey, Duration expiration, Class<T> valueType, Supplier<List<T>> dataLoader) {
List<T> cachedData = getList(cacheKey, valueType);
if (CollectionUtils.isNotEmpty(cachedData)) {
return cachedData;
}
List<T> dataList = dataLoader.get();
if (CollectionUtils.isEmpty(dataList)) {
log.warn("The cacheKey {} did not get value from the cache, and the dataLoader returned empty list", cacheKey);
} else {
putList(cacheKey, dataList, expiration);
}
return dataList;
}
/**
* 缓存数据
*/
public static <T> T getValueIfPresent(String cacheKey, Duration expiration, Class<T> valueType, Supplier<T> dataLoader) {
T cachedData = getValue(cacheKey, valueType);
if (cachedData != null) {
return cachedData;
}
cachedData = dataLoader.get();
if (cachedData == null) {
log.warn("The cacheKey {} did not get value from the cache, and the dataLoader returned empty value", cacheKey);
} else {
putValue(cacheKey, cachedData, expiration);
}
return cachedData;
}
/**
* 批量获取
*/
public static <T> Map<String, T> multiGet(Collection<String> cacheKeys, Class<T> valueType) {
return multiGet(cacheKeys, valueType, true);
}
/**
* 批量获取
*/
public static <T> Map<String, T> multiGet(Collection<String> cacheKeys, Class<T> valueType, boolean discardIfValueIsNull) {
return getCacheProvider().multiGet(cacheKeys, valueType, discardIfValueIsNull);
}
/**
* 批量设置
*/
public static <T> void multiSet(Map<String, T> data, Duration expiration) {
getCacheProvider().multiSet(data, expiration);
}
/**
* 删除缓存
*/
public static void del(String... cacheKeys) {
del(Arrays.asList(cacheKeys));
}
/**
* 删除缓存
*/
public static void del(List<String> cacheKeys) {
getCacheProvider().del(cacheKeys);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.cache;
import com.schbrain.framework.autoconfigure.cache.properties.CacheProperties;
import com.schbrain.framework.autoconfigure.cache.provider.CacheProvider;
import com.schbrain.framework.autoconfigure.cache.provider.CacheProviderDelegate;
import com.schbrain.framework.autoconfigure.cache.provider.redis.SchbrainRedisCacheConfiguration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
/**
* AutoConfiguration for schbrain cache
*
* @author zhuyf
* @since 2022/7/25
*/
@AutoConfiguration(after = RedisAutoConfiguration.class)
@EnableConfigurationProperties(CacheProperties.class)
@Import(SchbrainRedisCacheConfiguration.class)
public class SchbrainCacheAutoConfiguration {
@Bean
@ConditionalOnBean(CacheProvider.class)
public CacheProviderDelegate cacheServiceDelegate(CacheProvider cacheProvider, CacheProperties cacheProperties,
Environment environment) {
CacheProviderDelegate delegate = new CacheProviderDelegate(cacheProperties, cacheProvider, environment);
CacheUtils.setCacheProvider(delegate);
return delegate;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.cache.exception;
/**
* @author zhuyf
* @since 2022/7/25
*/
public class CacheException extends RuntimeException {
private static final long serialVersionUID = -1948413195425101948L;
public CacheException(String message) {
super(message);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.cache.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author zhuyf
* @since 2022/7/26
*/
@Data
@ConfigurationProperties(prefix = "schbrain.cache")
public class CacheProperties {
/**
* cache prefix
*/
private String prefix;
/**
* cache prefix delimiter
*/
private String delimiter = ":";
/**
* whatever to enable prefix append
*/
private boolean appendPrefix = true;
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.cache.provider;
import java.time.Duration;
import java.util.*;
/**
* @author zhuyf
* @since 2020/9/24
**/
public interface CacheProvider {
/**
* 指定缓存失效时间
*/
void expire(String cacheKey, Duration expiration);
/**
* 根据cacheKey 获取过期时间
*/
Duration getExpire(String cacheKey);
/**
* 判断cacheKey是否存在
*/
boolean hasKey(String cacheKey);
/**
* 删除缓存
*/
void del(List<String> cacheKeys);
/**
* 缓存获取
*/
<T> T get(String cacheKey, Class<T> valueType);
/**
* 缓存获取
*/
<T> Map<String, T> multiGet(Collection<String> cacheKeys, Class<T> valueType, boolean discardIfValueIsNull);
/**
* list 缓存获取
*/
<T> List<T> getList(String cacheKey, Class<T> valueType);
/**
* 缓存放入并设置时间
*/
<T> void set(String cacheKey, T value, Duration expiration);
/**
* 缓存放入并设置时间
*/
<T> void multiSet(Map<String, T> data, Duration expiration);
/**
* 查询key是否过期
*/
boolean isExpired(String cacheKey);
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.cache.provider;
import com.google.common.collect.Maps;
import com.schbrain.common.util.ApplicationName;
import com.schbrain.common.util.StreamUtils;
import com.schbrain.framework.autoconfigure.cache.exception.CacheException;
import com.schbrain.framework.autoconfigure.cache.properties.CacheProperties;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.env.Environment;
import java.time.Duration;
import java.util.*;
import java.util.Map.Entry;
/**
* @author liaozan
* @since 2022/8/1
*/
public class CacheProviderDelegate implements CacheProvider {
private final String prefixWithDelimiter;
private final CacheProvider cacheProvider;
public CacheProviderDelegate(CacheProperties properties, CacheProvider cacheProvider, Environment environment) {
this.cacheProvider = cacheProvider;
if (properties.isAppendPrefix()) {
String prefix = properties.getPrefix();
if (StringUtils.isBlank(prefix)) {
prefix = ApplicationName.get(environment);
}
this.prefixWithDelimiter = prefix + properties.getDelimiter();
} else {
this.prefixWithDelimiter = null;
}
}
@Override
public void expire(String cacheKey, Duration expiration) {
checkDuration(expiration);
getCacheProvider().expire(withKeyPrefix(cacheKey), expiration);
}
@Override
public Duration getExpire(String cacheKey) {
return getCacheProvider().getExpire(withKeyPrefix(cacheKey));
}
@Override
public boolean hasKey(String cacheKey) {
return getCacheProvider().hasKey(withKeyPrefix(cacheKey));
}
@Override
public void del(List<String> cacheKeys) {
if (CollectionUtils.isEmpty(cacheKeys)) {
return;
}
List<String> keysWithPrefix = StreamUtils.toList(cacheKeys, this::withKeyPrefix);
getCacheProvider().del(keysWithPrefix);
}
@Override
public <T> T get(String cacheKey, Class<T> valueType) {
return getCacheProvider().get(withKeyPrefix(cacheKey), valueType);
}
@Override
public <T> Map<String, T> multiGet(Collection<String> cacheKeys, Class<T> valueType, boolean discardIfValueIsNull) {
if (CollectionUtils.isEmpty(cacheKeys)) {
return Collections.emptyMap();
}
List<String> keysWithPrefix = StreamUtils.toList(cacheKeys, this::withKeyPrefix);
Map<String, T> cachedDate = getCacheProvider().multiGet(keysWithPrefix, valueType, discardIfValueIsNull);
Map<String, T> result = Maps.newHashMapWithExpectedSize(keysWithPrefix.size());
// 这里不能用 Stream toMap, toMap 不允许 value 是 null
if (MapUtils.isEmpty(cachedDate)) {
if (discardIfValueIsNull) {
result = Collections.emptyMap();
} else {
for (String cacheKey : keysWithPrefix) {
result.put(removeKeyPrefix(cacheKey), null);
}
}
return result;
} else {
if (discardIfValueIsNull) {
// 值为 null 的key 在实现类获取时已经被丢弃,直接遍历 put 即可
for (Entry<String, T> cacheEntry : cachedDate.entrySet()) {
result.put(removeKeyPrefix(cacheEntry.getKey()), cacheEntry.getValue());
}
} else {
for (String cacheKey : keysWithPrefix) {
result.put(removeKeyPrefix(cacheKey), cachedDate.get(cacheKey));
}
}
}
return result;
}
@Override
public <T> List<T> getList(String cacheKey, Class<T> valueType) {
return getCacheProvider().getList(withKeyPrefix(cacheKey), valueType);
}
@Override
public <T> void set(String cacheKey, T value, Duration expiration) {
checkDuration(expiration);
getCacheProvider().set(withKeyPrefix(cacheKey), value, expiration);
}
@Override
public <T> void multiSet(Map<String, T> data, Duration expiration) {
if (MapUtils.isEmpty(data)) {
return;
}
checkDuration(expiration);
Map<String, T> dataWithPrefixedKey = Maps.newHashMapWithExpectedSize(data.size());
for (Entry<String, T> entry : data.entrySet()) {
dataWithPrefixedKey.put(withKeyPrefix(entry.getKey()), entry.getValue());
}
getCacheProvider().multiSet(dataWithPrefixedKey, expiration);
}
@Override
public boolean isExpired(String cacheKey) {
return getCacheProvider().isExpired(withKeyPrefix(cacheKey));
}
public CacheProvider getCacheProvider() {
return cacheProvider;
}
protected String withKeyPrefix(String cacheKey) {
if (StringUtils.isBlank(prefixWithDelimiter)) {
return cacheKey;
}
return prefixWithDelimiter + cacheKey;
}
protected String removeKeyPrefix(String cacheKey) {
if (StringUtils.isBlank(cacheKey)) {
return cacheKey;
}
return StringUtils.removeStart(cacheKey, prefixWithDelimiter);
}
protected void checkDuration(Duration expiration) {
if (expiration.isZero() || expiration.isNegative()) {
throw new CacheException("expiration must be a positive number");
}
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.cache.provider.redis;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.schbrain.common.util.JacksonUtils;
import com.schbrain.framework.autoconfigure.cache.exception.CacheException;
import com.schbrain.framework.autoconfigure.cache.provider.CacheProvider;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.CollectionUtils;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @author zhuyf
* @since 2022/7/25
**/
@Slf4j
public class RedisCacheProvider implements CacheProvider {
private static final int DEFAULT_BATCH_SIZE = 100;
private final StringRedisTemplate redisTemplate;
public RedisCacheProvider(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 指定缓存失效时间
*/
@Override
public void expire(String key, Duration expiration) {
redisTemplate.expire(key, expiration.toMillis(), TimeUnit.MILLISECONDS);
}
/**
* 根据key 获取过期时间
*/
@Override
public Duration getExpire(String key) {
Long expiration = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS);
if (expiration == null) {
throw new CacheException("should not be null");
}
return Duration.ofMillis(expiration);
}
/**
* 判断key是否存在
*/
@Override
public boolean hasKey(String cacheKey) {
return Boolean.TRUE.equals(redisTemplate.hasKey(cacheKey));
}
/**
* 删除缓存
*/
@Override
public void del(List<String> cacheKeys) {
if (CollectionUtils.isEmpty(cacheKeys)) {
return;
}
redisTemplate.delete(cacheKeys);
}
/**
* 缓存获取
*/
@Override
public <T> T get(String cacheKey, Class<T> type) {
return JacksonUtils.getObjectFromJson(getValueFromRedis(cacheKey), type);
}
@Override
public <T> Map<String, T> multiGet(Collection<String> cacheKeys, Class<T> type, boolean discardIfValueIsNull) {
Map<String, T> result = Maps.newHashMapWithExpectedSize(cacheKeys.size());
Iterables.partition(cacheKeys, DEFAULT_BATCH_SIZE).forEach(subKeys -> {
List<String> valueList = Objects.requireNonNull(redisTemplate.opsForValue().multiGet(subKeys));
for (int i = 0; i < subKeys.size(); i++) {
T rawValue = JacksonUtils.getObjectFromJson(valueList.get(i), type);
if (discardIfValueIsNull && rawValue == null) {
continue;
}
result.put(subKeys.get(i), rawValue);
}
});
return result;
}
/**
* list 缓存获取
*/
@Override
public <T> List<T> getList(String cacheKey, Class<T> type) {
return JacksonUtils.getListFromJson(getValueFromRedis(cacheKey), type);
}
/**
* 普通缓存放入并设置时间
*/
@Override
public <T> void set(String cacheKey, T value, Duration expiration) {
setValueToRedis(cacheKey, value, expiration);
}
@Override
public <T> void multiSet(Map<String, T> data, Duration expiration) {
Iterables.partition(data.keySet(), DEFAULT_BATCH_SIZE).forEach(keys ->
redisTemplate.executePipelined((RedisCallback<Void>) connection -> {
Map<byte[], byte[]> byteMap = Maps.newHashMapWithExpectedSize(keys.size());
for (String key : keys) {
byteMap.put(key.getBytes(StandardCharsets.UTF_8), JacksonUtils.writeObjectAsBytes(data.get(key)));
}
connection.mSet(byteMap);
for (byte[] rawKey : byteMap.keySet()) {
connection.pExpire(rawKey, expiration.toMillis());
}
return null;
}));
}
/**
* 查询key是否过期
*/
@Override
public boolean isExpired(String cacheKey) {
return !hasKey(cacheKey);
}
private String getValueFromRedis(String cacheKey) {
if (StringUtils.isBlank(cacheKey)) {
return null;
}
return redisTemplate.opsForValue().get(cacheKey);
}
private <T> void setValueToRedis(String cacheKey, T value, Duration expiration) {
String cacheDate = JacksonUtils.toJsonString(value);
redisTemplate.opsForValue().set(cacheKey, cacheDate, expiration.toMillis(), TimeUnit.MILLISECONDS);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.cache.provider.redis;
import com.schbrain.framework.autoconfigure.cache.provider.CacheProvider;
import org.springframework.boot.autoconfigure.condition.*;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* Redis cache configuration
*
* @author liaozan
* @since 2022/8/7
*/
@ConditionalOnClass(RedisConnectionFactory.class)
public class SchbrainRedisCacheConfiguration {
@Bean
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(RedisCacheProvider.class)
public CacheProvider schbrainRedisCacheProvider(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(redisConnectionFactory);
return new RedisCacheProvider(stringRedisTemplate);
}
}
\ No newline at end of file
{
"groups": [
{
"name": "schbrain.cache",
"type": "com.schbrain.framework.autoconfigure.cache.properties.CacheProperties",
"sourceType": "com.schbrain.framework.autoconfigure.cache.properties.CacheProperties"
}
],
"properties": [
{
"name": "schbrain.cache.append-prefix",
"type": "java.lang.Boolean",
"description": "whatever to enable prefix append",
"sourceType": "com.schbrain.framework.autoconfigure.cache.properties.CacheProperties",
"defaultValue": true
},
{
"name": "schbrain.cache.delimiter",
"type": "java.lang.String",
"description": "cache prefix delimiter",
"sourceType": "com.schbrain.framework.autoconfigure.cache.properties.CacheProperties",
"defaultValue": ":"
},
{
"name": "schbrain.cache.prefix",
"type": "java.lang.String",
"description": "cache prefix",
"sourceType": "com.schbrain.framework.autoconfigure.cache.properties.CacheProperties"
}
],
"hints": []
}
\ No newline at end of file
com.schbrain.framework.autoconfigure.cache.SchbrainCacheAutoConfiguration
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>starters</artifactId>
<version>${revision}</version>
</parent>
<artifactId>dubbo-spring-boot-starter</artifactId>
<dependencies>
<dependency>
<groupId>com.schbrain.framework</groupId>
<artifactId>apollo-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-x-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-common</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-config-spring</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-remoting-netty4</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-remoting-zookeeper-curator5</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-rpc-dubbo</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-rpc-triple</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-serialization-hessian2</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-serialization-fastjson2</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-metadata-report-zookeeper</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package com.schbrain.framework.autoconfigure.dubbo;
import com.schbrain.framework.autoconfigure.dubbo.properties.DubboProperties;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import static org.apache.dubbo.config.ConfigKeys.DUBBO_SCAN_BASE_PACKAGES;
/**
* @author liaozan
* @since 2021/11/5
*/
@AutoConfiguration
@EnableConfigurationProperties(DubboProperties.class)
@EnableDubbo(scanBasePackages = "${" + DUBBO_SCAN_BASE_PACKAGES + "}")
public class DubboAutoConfiguration {
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.dubbo.filter;
import cn.hutool.core.exceptions.ExceptionUtil;
import com.schbrain.common.exception.BaseException;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
import org.apache.dubbo.rpc.filter.ExceptionFilter;
import org.apache.dubbo.rpc.service.GenericService;
import java.util.Arrays;
/**
* @author liaozan
* @since 2022/1/19
*/
@Slf4j
@Activate(group = CommonConstants.PROVIDER, order = 1)
public class DubboExceptionFilter extends ExceptionFilter {
@Override
public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
if (!appResponse.hasException() || GenericService.class == invoker.getInterface()) {
return;
}
Throwable cause = ExceptionUtil.getRootCause(appResponse.getException());
appResponse.setException(cause);
logErrorDetail(invoker, cause);
if (cause instanceof BaseException) {
return;
}
super.onResponse(appResponse, invoker, invocation);
}
private void logErrorDetail(Invoker<?> invoker, Throwable exception) {
RpcServiceContext context = RpcContext.getServiceContext();
String arguments = Arrays.toString(context.getArguments());
String serviceName = invoker.getInterface().getSimpleName();
String methodName = context.getMethodName();
String remoteHost = context.getRemoteHost();
String remoteApplication = context.getRemoteApplicationName();
log.error("Catch rpc exception, client: {}@{}, target: {}#{}, args: {}", remoteApplication, remoteHost, serviceName, methodName, arguments, exception);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.dubbo.filter;
import com.schbrain.common.util.TraceIdUtils;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
/**
* @author liaozan
* @since 2021/10/10
*/
@Activate(group = CommonConstants.CONSUMER)
public class TraceConsumerRpcFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
String traceId = TraceIdUtils.get();
RpcContext.getClientAttachment().setAttachment(TraceIdUtils.TRACE_ID, traceId);
return invoker.invoke(invocation);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.dubbo.filter;
import com.schbrain.common.util.TraceIdUtils;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
/**
* @author liaozan
* @since 2021/10/10
*/
@Activate(group = CommonConstants.PROVIDER)
public class TraceProviderRpcFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
String traceId = RpcContext.getServerAttachment().getAttachment(TraceIdUtils.TRACE_ID);
TraceIdUtils.set(traceId);
return invoker.invoke(invocation);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.dubbo.properties;
import com.schbrain.common.util.support.ConfigurableProperties;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.dubbo.config.spring.util.EnvironmentUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.env.ConfigurableEnvironment;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* used to fetch dubbo.* config from remote
*
* @author liaozan
* @since 2021/12/6
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties(prefix = "dubbo")
public class DubboProperties extends ConfigurableProperties {
private Map<String, String> externalConfigurations;
@Override
public String getDefaultNamespace() {
return "dubbo-common";
}
@Override
@SuppressWarnings("unchecked")
public DubboProperties bindOrCreate(ConfigurableEnvironment environment, boolean afterMerge) {
DubboProperties dubboProperties = super.bindOrCreate(environment, afterMerge);
if (afterMerge) {
Map<String, String> externalConfigurations = new LinkedHashMap<>(EnvironmentUtils.filterDubboProperties(environment));
Map<String, String> configuredProperties = dubboProperties.getExternalConfigurations();
if (configuredProperties == null) {
configuredProperties = new LinkedHashMap<>();
}
externalConfigurations.putAll(configuredProperties);
dubboProperties.setExternalConfigurations(externalConfigurations);
}
return dubboProperties;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.dubbo.properties;
import com.alibaba.fastjson2.JSONFactory;
import com.schbrain.common.exception.BaseException;
import com.schbrain.common.util.ApplicationName;
import com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils;
import org.apache.dubbo.config.bootstrap.DubboBootstrap;
import org.apache.dubbo.config.spring.ConfigCenterBean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.apache.dubbo.config.ConfigKeys.DUBBO_SCAN_BASE_PACKAGES;
/**
* @author liaozan
* @since 2021/10/10
*/
public class DubboPropertiesPreparer implements EnvironmentPostProcessor, Ordered {
public static final String DUBBO_APPLICATION_NAME = "dubbo.application.name";
public static final Integer DEFAULT_ORDER = Ordered.LOWEST_PRECEDENCE;
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
DubboProperties dubboProperties = ConfigUtils.loadConfig(environment, DubboProperties.class);
addAdditionalProperties(environment, application, dubboProperties);
setUpConfigCenter(dubboProperties);
JSONFactory.setUseJacksonAnnotation(false);
}
@Override
public int getOrder() {
return DEFAULT_ORDER;
}
private void setUpConfigCenter(DubboProperties dubboProperties) {
ConfigCenterBean configCenterConfig = buildConfigCenter(dubboProperties);
DubboBootstrap.getInstance().configCenter(configCenterConfig);
}
private ConfigCenterBean buildConfigCenter(DubboProperties dubboProperties) {
ConfigCenterBean configCenter = new ConfigCenterBean();
configCenter.setExternalConfig(dubboProperties.getExternalConfigurations());
return configCenter;
}
private void addAdditionalProperties(ConfigurableEnvironment environment, SpringApplication application, DubboProperties dubboProperties) {
Map<String, String> configurations = dubboProperties.getExternalConfigurations();
configurations.put(DUBBO_SCAN_BASE_PACKAGES, getBasePackage(application));
if (!configurations.containsKey(DUBBO_APPLICATION_NAME)) {
configurations.put(DUBBO_APPLICATION_NAME, ApplicationName.get(environment));
}
Map<String, Object> properties = new LinkedHashMap<>(dubboProperties.getExternalConfigurations());
ConfigUtils.addToEnvironment(environment, dubboProperties.getName(), properties);
}
private String getBasePackage(SpringApplication application) {
return application.getAllSources()
.stream()
.filter(Class.class::isInstance)
.map(source -> (Class<?>) source)
.map(Class::getPackage)
.map(Package::getName)
.findFirst()
.orElseThrow(() -> new BaseException("should never go here"));
}
}
\ No newline at end of file
trace-consumer=com.schbrain.framework.autoconfigure.dubbo.filter.TraceConsumerRpcFilter
trace-provider=com.schbrain.framework.autoconfigure.dubbo.filter.TraceProviderRpcFilter
dubboException=com.schbrain.framework.autoconfigure.dubbo.filter.DubboExceptionFilter
{
"groups": [
{
"name": "dubbo",
"type": "com.schbrain.framework.autoconfigure.dubbo.properties.DubboProperties",
"sourceType": "com.schbrain.framework.autoconfigure.dubbo.properties.DubboProperties"
}
],
"properties": [
{
"name": "dubbo.external-configurations",
"type": "java.util.Map<java.lang.String,java.lang.String>",
"sourceType": "com.schbrain.framework.autoconfigure.dubbo.properties.DubboProperties"
},
{
"name": "dubbo.name",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.dubbo.properties.DubboProperties"
},
{
"name": "dubbo.namespace",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.dubbo.properties.DubboProperties"
},
{
"name": "dubbo.prefix",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.dubbo.properties.DubboProperties"
}
],
"hints": []
}
\ No newline at end of file
org.springframework.boot.env.EnvironmentPostProcessor=com.schbrain.framework.autoconfigure.dubbo.properties.DubboPropertiesPreparer
\ No newline at end of file
com.schbrain.framework.autoconfigure.dubbo.DubboAutoConfiguration
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>starters</artifactId>
<version>${revision}</version>
</parent>
<artifactId>logger-spring-boot-starter</artifactId>
<dependencies>
<dependency>
<groupId>com.schbrain.framework</groupId>
<artifactId>apollo-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
</dependency>
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-logback-1.x</artifactId>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package com.schbrain.framework.autoconfigure.logger;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.*;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
import cn.hutool.json.JSONObject;
import com.schbrain.common.util.*;
import com.schbrain.common.util.InetUtils.HostInfo;
import com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils;
import com.schbrain.framework.autoconfigure.logger.logstash.SchbrainLogstashEncoder;
import com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import net.logstash.logback.appender.LogstashTcpSocketAppender;
import net.logstash.logback.encoder.LogstashEncoder;
import net.logstash.logback.fieldnames.ShortenedFieldNames;
import org.apache.commons.collections4.IteratorUtils;
import org.slf4j.LoggerFactory;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.nio.file.Paths;
import java.util.List;
/**
* Enable the json logging, will be auto active when the application is running in cloudPlatform
*
* @author liaozan
* @see CloudPlatform
* @since 2021/12/11
*/
@Slf4j
@Setter
public class JsonLoggerInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
/**
* 暴露 set 方法以便单独启动日志服务时进行配置
*/
private ConfigurableEnvironment environment;
private LoggingFileProperties loggingFileProperties;
private String applicationName;
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
this.environment = applicationContext.getEnvironment();
this.loggingFileProperties = ConfigUtils.loadConfig(environment, LoggingFileProperties.class);
this.applicationName = ApplicationName.get(environment);
this.init();
}
public void init() {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
for (Logger logger : context.getLoggerList()) {
registerAppender(logger, context);
}
}
private void registerAppender(Logger logger, LoggerContext context) {
List<Appender<ILoggingEvent>> appenderList = getAppenderList(logger);
if (CollectionUtils.isEmpty(appenderList)) {
return;
}
if (loggingFileProperties.isEnableJsonFileOutput()) {
Appender<ILoggingEvent> appender = buildFileAppender(context);
logger.addAppender(appender);
}
if (loggingFileProperties.isEnableJsonConsoleOutput()) {
Appender<ILoggingEvent> appender = buildConsoleAppender(context);
logger.addAppender(appender);
}
if (loggingFileProperties.isEnableJsonLogWriteToLogstash() || EnvUtils.runningOnCloudPlatform(environment)) {
if (!StringUtils.hasText(loggingFileProperties.getLogstashAddress())) {
log.warn("logstash address is unset, will NOT write log to logstash");
return;
}
Appender<ILoggingEvent> logstashAppender = buildLogstashAppender(context);
logger.addAppender(logstashAppender);
}
}
private Appender<ILoggingEvent> buildLogstashAppender(LoggerContext context) {
LogstashTcpSocketAppender appender = new LogstashTcpSocketAppender();
appender.setContext(context);
appender.addDestination(loggingFileProperties.getLogstashAddress());
appender.setEncoder(createJsonEncoder(context));
appender.start();
return appender;
}
private List<Appender<ILoggingEvent>> getAppenderList(Logger logger) {
return IteratorUtils.toList(logger.iteratorForAppenders());
}
private Appender<ILoggingEvent> buildConsoleAppender(LoggerContext loggerContext) {
ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
appender.setContext(loggerContext);
appender.setEncoder(createJsonEncoder(loggerContext));
appender.start();
return appender;
}
private LogstashEncoder createJsonEncoder(LoggerContext loggerContext) {
LogstashEncoder logstashEncoder = new SchbrainLogstashEncoder();
logstashEncoder.setContext(loggerContext);
logstashEncoder.setFieldNames(new ShortenedFieldNames());
logstashEncoder.setShortenedLoggerNameLength(40);
logstashEncoder.setCustomFields(getCustomFields());
logstashEncoder.start();
return logstashEncoder;
}
private String getCustomFields() {
HostInfo hostInfo = InetUtils.findFirstNonLoopBackHostInfo();
JSONObject customFields = new JSONObject();
customFields.set("appName", applicationName);
customFields.set("hostName", hostInfo.getHostname());
customFields.set("podIp", hostInfo.getIpAddress());
return customFields.toString();
}
private Appender<ILoggingEvent> buildFileAppender(LoggerContext loggerContext) {
RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
appender.setContext(loggerContext);
appender.setFile(getPathLocation("json/json.log"));
appender.setRollingPolicy(createRollingPolicy(loggerContext, appender));
appender.setEncoder(createJsonEncoder(loggerContext));
appender.start();
return appender;
}
private String getPathLocation(String path) {
return Paths.get(loggingFileProperties.getLogPath(), path).toString();
}
private TimeBasedRollingPolicy<ILoggingEvent> createRollingPolicy(Context context, FileAppender<ILoggingEvent> appender) {
TimeBasedRollingPolicy<ILoggingEvent> rollingPolicy = new TimeBasedRollingPolicy<>();
rollingPolicy.setMaxHistory(loggingFileProperties.getMaxHistory());
rollingPolicy.setFileNamePattern(getPathLocation("json/json-%d{yyyy-MM-dd}.log"));
rollingPolicy.setContext(context);
rollingPolicy.setParent(appender);
rollingPolicy.start();
return rollingPolicy;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.logger;
import com.schbrain.framework.autoconfigure.logger.apollo.DynamicLoggerConfiguration;
import com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
/**
* @author liaozan
* @since 2021/11/19
*/
@AutoConfiguration
@Import(DynamicLoggerConfiguration.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@EnableConfigurationProperties(LoggingFileProperties.class)
public class LoggerAutoConfiguration {
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.logger.apollo;
import com.ctrip.framework.apollo.*;
import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
import com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils;
import com.schbrain.framework.autoconfigure.logger.apollo.listener.LoggingConfigFileChangeListener;
import com.schbrain.framework.autoconfigure.logger.apollo.listener.LoggingLevelChangeListener;
import com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.util.StringUtils;
/**
* 动态日志配置
*
* @author liaozan
* @since 2021/11/19
**/
@Slf4j
@EnableConfigurationProperties(LoggingNamespaceProperties.class)
public class DynamicLoggerConfiguration {
private final ConfigurableEnvironment environment;
private final LoggingSystem loggingSystem;
private final LoggingNamespaceProperties loggerProperties;
public DynamicLoggerConfiguration(ConfigurableEnvironment environment, LoggingSystem loggingSystem,
LoggingNamespaceProperties loggerProperties) {
this.environment = environment;
this.loggingSystem = loggingSystem;
this.loggerProperties = loggerProperties;
this.init();
}
private void init() {
if (ConfigUtils.isApolloDisabled()) {
return;
}
listenToLoggingLevelChange();
listenToLoggingConfigFileChange();
}
private void listenToLoggingConfigFileChange() {
String loggerConfigFileNamespace = loggerProperties.getLoggerConfigFile();
if (!StringUtils.hasText(loggerConfigFileNamespace)) {
log.debug("logger config file reload is disabled");
return;
}
log.debug("init logger config file listener, config file namespace: {}", loggerConfigFileNamespace);
ConfigFile loggingConfiguration = ConfigService.getConfigFile(loggerConfigFileNamespace, ConfigFileFormat.XML);
if (!loggingConfiguration.hasContent()) {
return;
}
loggingConfiguration.addChangeListener(new LoggingConfigFileChangeListener(loggingSystem, environment, loggerConfigFileNamespace));
}
private void listenToLoggingLevelChange() {
String loggerNamespace = loggerProperties.getLogger();
if (!StringUtils.hasText(loggerNamespace)) {
log.debug("logger level reload is disabled");
return;
}
log.debug("init logger level listener, logger namespace: {}", loggerNamespace);
Config config = ConfigService.getConfig(loggerNamespace);
if (config == null) {
return;
}
config.addChangeListener(new LoggingLevelChangeListener(loggingSystem));
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.logger.apollo.listener;
import cn.hutool.extra.spring.SpringUtil;
import com.ctrip.framework.apollo.ConfigFileChangeListener;
import com.ctrip.framework.apollo.model.ConfigFileChangeEvent;
import com.schbrain.framework.autoconfigure.logger.JsonLoggerInitializer;
import com.schbrain.framework.autoconfigure.logger.util.LoggerUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.logging.*;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* @author liaozan
* @since 2021/11/8
*/
@Slf4j
public class LoggingConfigFileChangeListener implements ConfigFileChangeListener {
private final LoggingSystem loggingSystem;
private final ConfigurableEnvironment environment;
private final String loggerFileName;
public LoggingConfigFileChangeListener(LoggingSystem loggingSystem, ConfigurableEnvironment environment, String loggerFileName) {
this.loggingSystem = loggingSystem;
this.environment = environment;
this.loggerFileName = loggerFileName;
}
@Override
public void onChange(ConfigFileChangeEvent changeEvent) {
String content = changeEvent.getNewValue();
if (!StringUtils.hasText(content)) {
log.warn("Empty logging configuration, reInitialize loggingSystem is disabled");
return;
}
String configurationLocation = LoggerUtils.storeConfiguration(loggerFileName, content);
if (configurationLocation == null) {
return;
}
reinitialize(configurationLocation);
log.debug("ReInitialize loggingSystem, configFile location: {}", configurationLocation);
}
private void reinitialize(String configLocation) {
List<LoggerConfiguration> configurations = loggingSystem.getLoggerConfigurations();
loggingSystem.cleanUp();
loggingSystem.initialize(new LoggingInitializationContext(environment), configLocation, null);
configurations.forEach(configuration -> loggingSystem.setLogLevel(configuration.getName(), configuration.getConfiguredLevel()));
// reInitialize json logger
new JsonLoggerInitializer().initialize(SpringUtil.getBean(ConfigurableApplicationContext.class));
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.logger.apollo.listener;
import com.ctrip.framework.apollo.ConfigChangeListener;
import com.ctrip.framework.apollo.enums.PropertyChangeType;
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.logging.*;
import java.util.Set;
import static com.ctrip.framework.apollo.enums.PropertyChangeType.*;
import static org.springframework.boot.logging.LoggingSystem.ROOT_LOGGER_NAME;
/**
* @author liaozan
* @since 2021/11/8
*/
@Slf4j
public class LoggingLevelChangeListener implements ConfigChangeListener {
private static final String LOGGER_TAG = "logging.level.";
private final LoggingSystem loggingSystem;
public LoggingLevelChangeListener(LoggingSystem loggingSystem) {
this.loggingSystem = loggingSystem;
}
@Override
public void onChange(ConfigChangeEvent changeEvent) {
Set<String> changedKeys = changeEvent.changedKeys();
for (String key : changedKeys) {
if (key.startsWith(LOGGER_TAG)) {
String loggerName = key.substring(LOGGER_TAG.length());
LogLevel newLogLevel = getNewLogLevel(changeEvent, key);
configureLoggerLevel(loggerName, newLogLevel);
}
}
}
private LogLevel getNewLogLevel(ConfigChangeEvent changeEvent, String key) {
// default is INFO
LogLevel newLogLevel = LogLevel.INFO;
ConfigChange configChange = changeEvent.getChange(key);
PropertyChangeType changeType = configChange.getChangeType();
if (changeType == ADDED || changeType == MODIFIED) {
String newValue = configChange.getNewValue();
newLogLevel = LogLevel.valueOf(newValue.toUpperCase());
}
return newLogLevel;
}
private void configureLoggerLevel(String loggerName, LogLevel logLevel) {
LoggerConfiguration configuration = getLoggerConfiguration(loggerName);
LogLevel configuredLevel = configuration.getConfiguredLevel();
if (configuredLevel == logLevel) {
return;
}
loggingSystem.setLogLevel(loggerName, logLevel);
log.info("change [{}] logger level from {} to {}", loggerName, configuredLevel, logLevel);
}
private LoggerConfiguration getLoggerConfiguration(String loggerName) {
LoggerConfiguration configuration = loggingSystem.getLoggerConfiguration(loggerName);
if (configuration == null) {
configuration = loggingSystem.getLoggerConfiguration(loggerName.toUpperCase());
}
if (configuration == null) {
configuration = loggingSystem.getLoggerConfiguration(ROOT_LOGGER_NAME);
}
return configuration;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.logger.logback;
import ch.qos.logback.classic.LoggerContext;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
/**
* @author liaozan
* @since 2023-04-08
*/
public class LogbackTraceIdConfiguration implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
loggerContext.addTurboFilter(new TraceIdInitializeTurboFilter());
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.logger.logback;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.turbo.TurboFilter;
import ch.qos.logback.core.spi.FilterReply;
import com.schbrain.common.util.TraceIdUtils;
import org.slf4j.Marker;
/**
* @author liaozan
* @since 2023-04-08
*/
public class TraceIdInitializeTurboFilter extends TurboFilter {
@Override
public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable throwable) {
// Make sure the traceId is initialized
TraceIdUtils.get();
return FilterReply.NEUTRAL;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.logger.logstash;
import ch.qos.logback.classic.spi.ILoggingEvent;
import cn.hutool.core.date.LocalDateTimeUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import net.logstash.logback.composite.AbstractFieldJsonProvider;
import net.logstash.logback.composite.JsonWritingUtils;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @author liaozan
* @since 2022/1/11
*/
public class EventDateStringValueJsonProvider extends AbstractFieldJsonProvider<ILoggingEvent> {
public static final String FIELD_EVENT_DATE = "eventDate";
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd");
public EventDateStringValueJsonProvider() {
setFieldName(FIELD_EVENT_DATE);
}
@Override
public void writeTo(JsonGenerator generator, ILoggingEvent event) throws IOException {
JsonWritingUtils.writeStringField(generator, FIELD_EVENT_DATE, getEventDate(event));
}
private String getEventDate(ILoggingEvent event) {
LocalDateTime eventTime = LocalDateTimeUtil.of(event.getTimeStamp());
return DATE_TIME_FORMATTER.format(eventTime);
}
}
package com.schbrain.framework.autoconfigure.logger.logstash;
import ch.qos.logback.classic.spi.ILoggingEvent;
import net.logstash.logback.composite.AbstractCompositeJsonFormatter;
import net.logstash.logback.encoder.LogstashEncoder;
/**
* @author liaozan
* @since 2022/1/4
*/
public class SchbrainLogstashEncoder extends LogstashEncoder {
@Override
protected AbstractCompositeJsonFormatter<ILoggingEvent> createFormatter() {
return new SchbrainLogstashFormatter(this);
}
}
package com.schbrain.framework.autoconfigure.logger.logstash;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.spi.ContextAware;
import net.logstash.logback.LogstashFormatter;
import net.logstash.logback.composite.JsonProvider;
import net.logstash.logback.composite.LogstashVersionJsonProvider;
import net.logstash.logback.composite.loggingevent.LogLevelValueJsonProvider;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
/**
* @author liaozan
* @since 2022/1/4
*/
public class SchbrainLogstashFormatter extends LogstashFormatter {
public SchbrainLogstashFormatter(ContextAware declaredOrigin) {
super(declaredOrigin);
configureProviders();
}
private void configureProviders() {
removeUnnecessaryProviders();
addAdditionalProviders();
}
private void addAdditionalProviders() {
getProviders().addProvider(new EventDateStringValueJsonProvider());
}
/**
* be careful!!! getProviders().getProviders() is a unmodifiable List
*/
private void removeUnnecessaryProviders() {
List<JsonProvider<ILoggingEvent>> providers = getProviders().getProviders();
if (CollectionUtils.isEmpty(providers)) {
return;
}
List<JsonProvider<ILoggingEvent>> excludeProviders = getExcludeProviders(providers);
if (CollectionUtils.isEmpty(excludeProviders)) {
return;
}
for (JsonProvider<ILoggingEvent> excludeProvider : excludeProviders) {
getProviders().removeProvider(excludeProvider);
}
}
private List<JsonProvider<ILoggingEvent>> getExcludeProviders(List<JsonProvider<ILoggingEvent>> providers) {
List<JsonProvider<ILoggingEvent>> excludeProviders = new ArrayList<>();
for (JsonProvider<ILoggingEvent> provider : providers) {
if (provider instanceof LogLevelValueJsonProvider) {
excludeProviders.add(provider);
}
if (provider instanceof LogstashVersionJsonProvider) {
excludeProviders.add(provider);
}
}
return excludeProviders;
}
}
package com.schbrain.framework.autoconfigure.logger.properties;
import com.google.common.collect.Maps;
import com.schbrain.common.util.InetUtils;
import com.schbrain.common.util.InetUtils.HostInfo;
import com.schbrain.framework.autoconfigure.apollo.properties.ApolloPropertiesPreparer;
import com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils;
import com.schbrain.framework.autoconfigure.logger.util.LoggerUtils;
import com.schbrain.framework.support.spring.EnvironmentPostProcessorAdapter;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.logging.LoggingApplicationListener;
import org.springframework.boot.logging.*;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import java.util.Map;
import static com.schbrain.framework.autoconfigure.logger.util.LoggerUtils.getLoggerConfigurationLocation;
import static org.springframework.boot.context.logging.LoggingApplicationListener.CONFIG_PROPERTY;
/**
* @author liaozan
* @since 2021/11/19
*/
public class LoggerPropertiesPreparer extends EnvironmentPostProcessorAdapter implements Ordered {
public LoggerPropertiesPreparer(DeferredLogFactory factory, ConfigurableBootstrapContext bootstrapContext) {
super(factory, bootstrapContext);
}
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
LoggerUtils.setLogFactory(getDeferredLogFactory());
bindLoggingProperties(environment);
bindHostInfoProperty(environment);
earlyLoadLoggingConfig(environment);
}
@Override
public int getOrder() {
// Configure after the apollo property is initialize
return ApolloPropertiesPreparer.ORDER + 1;
}
@Override
public void onBootstrapContextClosed(ConfigurableApplicationContext applicationContext) {
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
if (!beanFactory.containsSingleton(HostInfo.NAME)) {
HostInfo hostInfo = InetUtils.findFirstNonLoopBackHostInfo();
beanFactory.registerSingleton(HostInfo.NAME, hostInfo);
}
}
private void bindHostInfoProperty(ConfigurableEnvironment environment) {
HostInfo hostInfo = InetUtils.findFirstNonLoopBackHostInfo();
Map<String, Object> source = Maps.newHashMapWithExpectedSize(2);
source.put("application.hostname", hostInfo.getHostname());
source.put("application.ipAddress", hostInfo.getIpAddress());
ConfigUtils.addToEnvironment(environment, HostInfo.NAME, source);
}
/**
* @see LoggingApplicationListener#setLogLevels(LoggingSystem, ConfigurableEnvironment)
*/
@SuppressWarnings("JavadocReference")
private void earlyLoadLoggingConfig(ConfigurableEnvironment environment) {
LoggingNamespaceProperties properties = ConfigUtils.loadConfig(environment, LoggingNamespaceProperties.class);
// load logging level properties
Map<String, Object> loggingLevelProperties = ConfigUtils.loadConfig(properties.getLogger());
ConfigUtils.addToEnvironment(environment, "loggingLevelProperties", loggingLevelProperties);
// load logging file properties
ConfigUtils.loadConfig(environment, LoggingFileProperties.class);
}
/**
* Add {@link LoggingApplicationListener#CONFIG_PROPERTY} property to SystemProperty
*
* @see LoggingApplicationListener#initializeSystem(ConfigurableEnvironment, LoggingSystem, LogFile)
*/
@SuppressWarnings("JavadocReference")
private void bindLoggingProperties(ConfigurableEnvironment environment) {
if (environment.containsProperty(CONFIG_PROPERTY)) {
return;
}
String loggerConfigurationLocation = getLoggerConfigurationLocation(environment);
if (loggerConfigurationLocation == null) {
return;
}
System.setProperty(CONFIG_PROPERTY, loggerConfigurationLocation);
getLog().debug(String.format("%s is set to %s", CONFIG_PROPERTY, loggerConfigurationLocation));
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.logger.properties;
import com.schbrain.common.util.support.ConfigurableProperties;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
/**
* @author liaozan
* @since 2021/12/11
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties(prefix = "schbrain.logging.file")
public class LoggingFileProperties extends ConfigurableProperties {
public static final String DEFAULT_LOG_PATH = "/data/logs";
private boolean enableJsonConsoleOutput = false;
private boolean enableJsonFileOutput = false;
private boolean enableJsonLogWriteToLogstash = false;
private String logstashAddress;
private String logPath = DEFAULT_LOG_PATH;
private int maxHistory = (int) Duration.ofDays(30).toDays();
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.logger.properties;
import com.schbrain.common.util.support.ConfigurableProperties;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author liaozan
* @since 2021/11/24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties(prefix = "schbrain.logging.namespace")
public class LoggingNamespaceProperties extends ConfigurableProperties {
private String logger = "logger-common";
private String loggerConfigFile = "logback-spring";
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.logger.util;
import cn.hutool.system.SystemUtil;
import com.ctrip.framework.apollo.ConfigFile;
import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
import com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils;
import com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties;
import org.apache.commons.logging.Log;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.util.StringUtils;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
/**
* @author liaozan
* @since 2021/11/15
*/
public class LoggerUtils {
private static Log LOGGER;
public static void setLogFactory(DeferredLogFactory deferredLog) {
LOGGER = deferredLog.getLog(LoggerUtils.class);
}
@Nullable
public static String getLoggerConfigurationLocation(ConfigurableEnvironment environment) {
if (ConfigUtils.isApolloDisabled()) {
return null;
}
LoggingNamespaceProperties properties = ConfigUtils.loadConfig(environment, LoggingNamespaceProperties.class);
String namespace = properties.getLoggerConfigFile();
ConfigFile loggingConfiguration = ConfigService.getConfigFile(namespace, ConfigFileFormat.XML);
String content = loggingConfiguration.getContent();
if (!StringUtils.hasText(content)) {
LOGGER.warn("empty logging configuration, reinitialize loggingSystem is disabled");
return null;
}
return storeConfiguration(namespace, content);
}
@Nullable
public static String storeConfiguration(String fileName, String content) {
String tempDir = SystemUtil.getUserInfo().getTempDir();
Path storeLocation = Paths.get(tempDir, fileName + ".xml");
try {
return Files.write(storeLocation, content.getBytes(StandardCharsets.UTF_8)).toString();
} catch (IOException e) {
LOGGER.warn("failed to write logging file, will not behave as expected", e);
return null;
}
}
}
\ No newline at end of file
{
"groups": [
{
"name": "schbrain.logging.file",
"type": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties",
"sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties"
},
{
"name": "schbrain.logging.namespace",
"type": "com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties",
"sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties"
}
],
"properties": [
{
"name": "schbrain.logging.file.enable-json-console-output",
"type": "java.lang.Boolean",
"sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties",
"defaultValue": false
},
{
"name": "schbrain.logging.file.enable-json-file-output",
"type": "java.lang.Boolean",
"sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties",
"defaultValue": false
},
{
"name": "schbrain.logging.file.enable-json-log-write-to-logstash",
"type": "java.lang.Boolean",
"sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties",
"defaultValue": false
},
{
"name": "schbrain.logging.file.log-path",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties",
"defaultValue": "\/data\/logs"
},
{
"name": "schbrain.logging.file.logstash-address",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties"
},
{
"name": "schbrain.logging.file.max-history",
"type": "java.lang.Integer",
"sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties",
"defaultValue": 0
},
{
"name": "schbrain.logging.file.name",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties"
},
{
"name": "schbrain.logging.file.namespace",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties"
},
{
"name": "schbrain.logging.file.prefix",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties"
},
{
"name": "schbrain.logging.namespace.logger",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties",
"defaultValue": "logger-common"
},
{
"name": "schbrain.logging.namespace.logger-config-file",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties",
"defaultValue": "logback-spring"
},
{
"name": "schbrain.logging.namespace.name",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties"
},
{
"name": "schbrain.logging.namespace.namespace",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties"
},
{
"name": "schbrain.logging.namespace.prefix",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties"
}
],
"hints": []
}
\ No newline at end of file
org.springframework.context.ApplicationContextInitializer=\
com.schbrain.framework.autoconfigure.logger.JsonLoggerInitializer,\
com.schbrain.framework.autoconfigure.logger.logback.LogbackTraceIdConfiguration
org.springframework.boot.env.EnvironmentPostProcessor=com.schbrain.framework.autoconfigure.logger.properties.LoggerPropertiesPreparer
\ No newline at end of file
com.schbrain.framework.autoconfigure.logger.LoggerAutoConfiguration
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>starters</artifactId>
<version>${revision}</version>
</parent>
<artifactId>mybatis-spring-boot-starter</artifactId>
<dependencies>
<dependency>
<groupId>com.schbrain.framework</groupId>
<artifactId>apollo-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<!-- used for code generation -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusPropertiesCustomizer;
import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.schbrain.framework.autoconfigure.mybatis.biz.BizIdInjectInterceptor;
import com.schbrain.framework.autoconfigure.mybatis.configuration.MybatisConfigurationCustomizer;
import com.schbrain.framework.autoconfigure.mybatis.configuration.MybatisPlusGlobalConfigCustomizer;
import com.schbrain.framework.autoconfigure.mybatis.datasource.DataSourceConnectionPostProcessor;
import com.schbrain.framework.autoconfigure.mybatis.datasource.customizer.DataSourceCustomizer;
import com.schbrain.framework.autoconfigure.mybatis.datasource.customizer.DefaultDataSourceCustomizer;
import com.schbrain.framework.autoconfigure.mybatis.datasource.extractor.*;
import com.schbrain.framework.autoconfigure.mybatis.listener.TableConstraintCheckerBean;
import com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties;
import com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties;
import com.schbrain.framework.autoconfigure.mybatis.sql.injector.DefaultMethodSqlInjector;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.*;
import javax.sql.DataSource;
import java.util.List;
/**
* @author liaozan
* @since 2021/10/14
*/
@AutoConfiguration(before = MybatisPlusAutoConfiguration.class)
@EnableConfigurationProperties({MybatisProperties.class, DataSourceConnectionProperties.class})
@Import({HikariDataSourcePropertiesExtractor.class, DruidDataSourcePropertiesExtractor.class})
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class MybatisAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MybatisPlusInterceptor mybatisPlusInterceptor(MybatisProperties mybatisProperties,
ObjectProvider<BlockAttackInnerInterceptor> attackInterceptor,
ObjectProvider<PaginationInnerInterceptor> paginationInterceptor,
ObjectProvider<BizIdInjectInterceptor> bizIdInjectInterceptor) {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
if (mybatisProperties.isAddBlockAttackInterceptor()) {
interceptor.addInnerInterceptor(attackInterceptor.getIfUnique(BlockAttackInnerInterceptor::new));
}
if (mybatisProperties.isAddPageInterceptor()) {
interceptor.addInnerInterceptor(paginationInterceptor.getIfUnique(PaginationInnerInterceptor::new));
}
bizIdInjectInterceptor.ifUnique(interceptor::addInnerInterceptor);
return interceptor;
}
@Bean
@ConditionalOnMissingBean
public ISqlInjector defaultSqlInjector() {
return new DefaultMethodSqlInjector();
}
@Bean
@ConditionalOnMissingBean
public BizIdInjectInterceptor bizIdInjectInterceptor() {
return new BizIdInjectInterceptor();
}
@Bean
@ConditionalOnMissingBean
public DataSourceCustomizer dataSourceCustomizer(List<DataSourcePropertiesExtractor> extractors) {
return new DefaultDataSourceCustomizer(extractors);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(DataSource.class)
public TableConstraintCheckerBean constraintCheckInitializer(DataSource dataSource, MybatisProperties mybatisProperties) {
return new TableConstraintCheckerBean(dataSource, mybatisProperties);
}
@Bean
public MybatisPlusPropertiesCustomizer globalConfigCustomizer() {
return new MybatisPlusGlobalConfigCustomizer();
}
@Bean
public MybatisConfigurationCustomizer configurationCustomizer(MybatisProperties mybatisProperties) {
return new MybatisConfigurationCustomizer(mybatisProperties);
}
@Bean
public DataSourceConnectionPostProcessor dataSourceConnectionPostProcessor(ObjectProvider<DataSourceCustomizer> dataSourceCustomizers,
ObjectProvider<DataSourceConnectionProperties> connectionProperties) {
return new DataSourceConnectionPostProcessor(dataSourceCustomizers, connectionProperties);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.annotation;
import com.schbrain.framework.autoconfigure.mybatis.biz.BizIdType;
import java.lang.annotation.*;
/**
* @author liaozan
* @since 2023-03-22
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BizId {
/**
* 逻辑主键列名,为空时取字段名
*
* @see com.schbrain.framework.autoconfigure.mybatis.core.BizIdColumnField
*/
String value() default "";
/**
* 逻辑主键类型
*/
BizIdType type() default BizIdType.ID_WORKER;
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.base;
import com.baomidou.mybatisplus.annotation.*;
import com.schbrain.framework.autoconfigure.mybatis.constant.MybatisConstants;
import lombok.Data;
import java.time.LocalDateTime;
/**
* @author liaozan
* @since 2021/10/14
*/
@Data
public class BaseEntity {
/**
* 主键 id
*/
@TableId(value = MybatisConstants.ID, type = IdType.AUTO)
protected Long id;
/**
* 创建时间
*/
@TableField(value = MybatisConstants.CREATE_TIME)
protected LocalDateTime createTime;
/**
* 修改时间
*/
@TableField(value = MybatisConstants.MODIFY_TIME)
protected LocalDateTime modifyTime;
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.base;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.schbrain.framework.autoconfigure.mybatis.constant.MybatisConstants;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author liaozan
* @since 2021/11/25
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class BaseEntityWithLogicDelete extends BaseEntity {
/**
* 逻辑删除
* 注意:只有写 sql 明确指定查询此字段的时候才有值, update 时,无法修改此字段
*/
@TableLogic
@TableField(value = MybatisConstants.DELETED, select = false)
protected boolean deleted;
/**
* 逻辑删除版本
* 注意:只有写 sql 明确指定查询此字段的时候才有值
*/
@TableField(value = MybatisConstants.DELETE_VERSION, select = false)
protected Long deleteVersion;
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.base;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers;
import org.apache.ibatis.annotations.Param;
/**
* @author liaozan
* @since 2021/10/14
*/
public interface BaseMapper<T> extends com.baomidou.mybatisplus.core.mapper.BaseMapper<T> {
/**
* 根据 id 更新,null 会被更新为 null
*/
int alwaysUpdateSomeColumnById(@Param(Constants.ENTITY) T entity);
/**
* 便捷方法查询数据
*/
default LambdaQueryChainWrapper<T> lambdaQuery() {
return ChainWrappers.lambdaQueryChain(this);
}
/**
* 便捷方法更新数据
*/
default LambdaUpdateChainWrapper<T> lambdaUpdate() {
return ChainWrappers.lambdaUpdateChain(this);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.base;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.*;
import java.util.function.Supplier;
public interface BaseService<T extends BaseEntity> extends IService<T> {
/**
* 根据 id 获取记录
*
* @param throwIfNotFound 未获取到记录时是否抛异常
*/
T getById(Long id, boolean throwIfNotFound);
/**
* 根据 id 获取记录
*
* @param notFoundSupplier 未获取到记录时的异常处理
*/
T getById(Long id, Supplier<? extends RuntimeException> notFoundSupplier);
/**
* 根据 id 获取
*/
Map<Long, T> getMapByIds(Collection<Long> ids);
/**
* 根据业务主键获取记录
*/
T getByBizId(String bizId);
/**
* 根据业务主键获取记录
*
* @param throwIfNotFound 未获取到记录时是否抛异常
*/
T getByBizId(String bizId, boolean throwIfNotFound);
/**
* 根据业务主键获取记录
*
* @param notFoundSupplier 未获取到记录时是否抛异常
*/
T getByBizId(String bizId, Supplier<? extends RuntimeException> notFoundSupplier);
/**
* 根据业务主键获取
*/
List<T> listByBizIds(Collection<String> bizIds);
/**
* 根据业务主键获取
*/
Map<String, T> getMapByBizIds(Collection<String> bizIds);
/**
* 根据 id 更新,null 会被更新为 null
*/
boolean updateByIdWithNull(T entity);
/**
* 根据 id 批量更新,null 会被更新为 null, 默认批量大小 1000
*/
boolean updateBatchByIdsWithNull(Collection<T> entityList);
/**
* 根据 id 批量更新,null 会被更新为 null
*/
boolean updateBatchByIdsWithNull(Collection<T> entityList, int batchSize);
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.base;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
import com.schbrain.common.exception.BaseException;
import com.schbrain.common.util.StreamUtils;
import com.schbrain.common.util.support.ValidateSupport;
import com.schbrain.framework.autoconfigure.mybatis.annotation.BizId;
import com.schbrain.framework.autoconfigure.mybatis.biz.BizIdHelper;
import com.schbrain.framework.autoconfigure.mybatis.core.BizIdColumnField;
import com.schbrain.framework.autoconfigure.mybatis.exception.NoSuchRecordException;
import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ReflectionUtils;
import java.io.Serializable;
import java.util.*;
import java.util.function.Supplier;
/**
* @author liaozan
* @since 2021/10/14
*/
public class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseEntity> extends ServiceImpl<M, T> implements BaseService<T>, ValidateSupport, InitializingBean {
private BizIdColumnField bizIdColumnField;
@Override
public T getById(Serializable id) {
return getById((Long) id, false);
}
@Override
public T getById(Long id, boolean throwIfNotFound) {
Supplier<? extends RuntimeException> notFoundSupplier = null;
if (throwIfNotFound) {
notFoundSupplier = () -> new NoSuchRecordException(entityClass, id);
}
return getById(id, notFoundSupplier);
}
@Override
public T getById(Long id, Supplier<? extends RuntimeException> notFoundSupplier) {
T entity = super.getById(id);
if (entity == null && notFoundSupplier != null) {
throw notFoundSupplier.get();
}
return entity;
}
@Override
public Map<Long, T> getMapByIds(Collection<Long> ids) {
if (isEmpty(ids)) {
return Collections.emptyMap();
}
return StreamUtils.toMap(super.listByIds(ids), T::getId);
}
@Override
public T getByBizId(String bizId) {
return getByBizId(bizId, false);
}
@Override
public T getByBizId(String bizId, boolean throwIfNotFound) {
Supplier<? extends RuntimeException> notFoundSupplier = null;
if (throwIfNotFound) {
notFoundSupplier = () -> new NoSuchRecordException(entityClass, bizId);
}
return getByBizId(bizId, notFoundSupplier);
}
@Override
public T getByBizId(String bizId, Supplier<? extends RuntimeException> notFoundSupplier) {
assertBidColumnFieldExist();
T entity = query().eq(bizIdColumnField.getColumnName(), bizId).one();
if (entity == null && notFoundSupplier != null) {
throw notFoundSupplier.get();
}
return entity;
}
@Override
public List<T> listByBizIds(Collection<String> bizIds) {
assertBidColumnFieldExist();
if (isEmpty(bizIds)) {
return Collections.emptyList();
}
return query().in(bizIdColumnField.getColumnName(), bizIds).list();
}
@Override
public Map<String, T> getMapByBizIds(Collection<String> bizIds) {
assertBidColumnFieldExist();
if (isEmpty(bizIds)) {
return Collections.emptyMap();
}
return StreamUtils.toMap(listByBizIds(bizIds), entity -> bizIdColumnField.getValue(entity));
}
@Override
public boolean updateByIdWithNull(T entity) {
return SqlHelper.retBool(getBaseMapper().alwaysUpdateSomeColumnById(entity));
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateBatchByIdsWithNull(Collection<T> entityList) {
return updateBatchByIdsWithNull(entityList, DEFAULT_BATCH_SIZE);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateBatchByIdsWithNull(Collection<T> entityList, int batchSize) {
String sqlStatement = getUpdateByIdWithNullStatementId(mapperClass);
return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
ParamMap<T> param = new ParamMap<>();
param.put(Constants.ENTITY, entity);
sqlSession.update(sqlStatement, param);
});
}
@Override
public void afterPropertiesSet() {
ReflectionUtils.doWithFields(entityClass, bizId -> {
if (this.bizIdColumnField != null) {
throw new BaseException(String.format("@BizId can't more than one in Class: \"%s\"", entityClass.getName()));
}
this.bizIdColumnField = new BizIdColumnField(entityClass, bizId);
BizIdHelper.putBizColumnField(entityClass, bizIdColumnField);
}, field -> field.isAnnotationPresent(BizId.class));
}
private void assertBidColumnFieldExist() {
if (bizIdColumnField == null) {
throw new BaseException(String.format("@BizId not exist in Class: \"%s\"", entityClass.getName()));
}
}
private String getUpdateByIdWithNullStatementId(Class<M> mapperClass) {
return mapperClass.getName() + StringPool.DOT + "alwaysUpdateSomeColumnById";
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.biz;
/**
* @author liaozan
* @since 2023-04-17
*/
@FunctionalInterface
public interface BizIdGenerator {
/**
* 生成 BizId 的值
*/
String generate(Object entity);
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.biz;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.schbrain.framework.autoconfigure.mybatis.annotation.BizId;
import com.schbrain.framework.autoconfigure.mybatis.core.BizIdColumnField;
import org.apache.ibatis.session.Configuration;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author liaozan
* @since 2023-04-17
*/
public class BizIdHelper {
private static final Map<Class<?>, BizIdColumnField> BIZ_ID_COLUMN_CACHE = new ConcurrentHashMap<>();
public static BizIdColumnField getBizColumnField(Class<?> entityClass) {
if (entityClass == null) {
return null;
}
return BIZ_ID_COLUMN_CACHE.get(entityClass);
}
public static void putBizColumnField(Class<?> entityClass, BizIdColumnField bizIdColumnField) {
if (entityClass == null) {
return;
}
BIZ_ID_COLUMN_CACHE.put(entityClass, bizIdColumnField);
}
public static String getColumnName(Class<?> entityClass, Field bizIdField, BizId annotation) {
if (StringUtils.isNotBlank(annotation.value())) {
return annotation.value();
}
TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
Configuration configuration = tableInfo.getConfiguration();
if (configuration.isMapUnderscoreToCamelCase()) {
return StringUtils.camelToUnderline(bizIdField.getName());
}
return bizIdField.getName();
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.biz;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.schbrain.framework.autoconfigure.mybatis.core.BizIdColumnField;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import java.sql.SQLException;
/**
* @author liaozan
* @since 2023-04-17
*/
public class BizIdInjectInterceptor implements InnerInterceptor {
@Override
public void beforeUpdate(Executor executor, MappedStatement ms, Object entity) throws SQLException {
SqlCommandType sqlCommandType = ms.getSqlCommandType();
if (sqlCommandType != SqlCommandType.INSERT) {
return;
}
Class<?> entityClass = entity.getClass();
BizIdColumnField bizColumnField = BizIdHelper.getBizColumnField(entityClass);
if (bizColumnField == null) {
return;
}
doBizIdFill(entity, bizColumnField);
}
protected void doBizIdFill(Object entity, BizIdColumnField bizColumnField) {
BizIdType bizIdType = bizColumnField.getAnnotation().type();
if (bizIdType == BizIdType.INPUT) {
return;
}
if (bizIdType == BizIdType.ID_WORKER) {
String bizIdValue = bizColumnField.getValue(entity);
if (StringUtils.isBlank(bizIdValue)) {
bizColumnField.setValue(entity, bizIdType.generateBizId(entity));
}
}
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.biz;
import com.schbrain.common.util.IdWorker;
import lombok.AllArgsConstructor;
/**
* @author liaozan
* @since 2023-04-17
*/
@AllArgsConstructor
public enum BizIdType {
/**
* 用户输入
*/
INPUT(entity -> null),
/**
* idWorker
*/
ID_WORKER(entity -> IdWorker.getIdStr());
/**
* bizId 生成
*/
private final BizIdGenerator generator;
/**
* 生成 bizId 的值
*/
public String generateBizId(Object entity) {
return generator.generate(entity);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.configuration;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.schbrain.framework.autoconfigure.mybatis.core.MybatisXmlLanguageDriver;
import com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties;
import com.schbrain.framework.autoconfigure.mybatis.type.InstantToLongTypeHandler;
import org.apache.ibatis.type.EnumOrdinalTypeHandler;
import java.time.Instant;
/**
* @author liaozan
* @since 2021/11/8
*/
public class MybatisConfigurationCustomizer implements ConfigurationCustomizer {
private final MybatisProperties mybatisProperties;
public MybatisConfigurationCustomizer(MybatisProperties mybatisProperties) {
this.mybatisProperties = mybatisProperties;
}
@Override
public void customize(MybatisConfiguration configuration) {
configuration.setCacheEnabled(false);
configuration.setDefaultEnumTypeHandler(EnumOrdinalTypeHandler.class);
configuration.setUseActualParamName(true);
configuration.setMapUnderscoreToCamelCase(true);
// set MybatisXmlLanguageDriver default to support deleteVersion field fill
configuration.setDefaultScriptingLanguage(MybatisXmlLanguageDriver.class);
if (mybatisProperties.isConvertInstantToLong()) {
configuration.getTypeHandlerRegistry().register(Instant.class, new InstantToLongTypeHandler());
}
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.configuration;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusPropertiesCustomizer;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.config.GlobalConfig.DbConfig;
/**
* 当前类在 mybatis-plus 自动配置之前初始化,所以这里只能设置 {@link GlobalConfig} 一些基本属性,类里的其他对象还没进行初始化
* <p>
* 如果设置了其他属性,也会在 {@link com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration} 中被覆盖
* <p>
* 例如
* <ul>
* <li>{@link com.baomidou.mybatisplus.core.handlers.MetaObjectHandler}</li>
* <li>{@link com.baomidou.mybatisplus.core.incrementer.IKeyGenerator}</li>
* <li>{@link com.baomidou.mybatisplus.core.injector.ISqlInjector}</li>
* <li>{@link com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator}</li>
* </ul>
* <p>
* 如果需要设置其他属性,可以往容器里注入{@link com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer}
*
* @author liaozan
* @see MybatisConfigurationCustomizer
* @since 2021/11/3
*/
public class MybatisPlusGlobalConfigCustomizer implements MybatisPlusPropertiesCustomizer {
@Override
public void customize(MybatisPlusProperties properties) {
GlobalConfig globalConfig = properties.getGlobalConfig();
globalConfig.setBanner(false);
DbConfig dbConfig = globalConfig.getDbConfig();
dbConfig.setIdType(IdType.AUTO);
dbConfig.setInsertStrategy(FieldStrategy.NOT_NULL);
dbConfig.setUpdateStrategy(FieldStrategy.NOT_NULL);
dbConfig.setWhereStrategy(FieldStrategy.NOT_NULL);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.constant;
/**
* @author liaozan
* @since 2022/8/30
*/
public class MybatisConstants {
/**
* 主键
*/
public static final String ID = "id";
/**
* 创建时间
*/
public static final String CREATE_TIME = "create_time";
/**
* 修改时间
*/
public static final String MODIFY_TIME = "modify_time";
/**
* 是否删除
*/
public static final String DELETED = "deleted";
/**
* 删除版本
*/
public static final String DELETE_VERSION = "delete_version";
/**
* 当前时间戳
*/
public static final String CURRENT_TIMESTAMP = "current_timestamp";
/**
* 自增
*/
public static final String AUTO_INCREMENT = "auto_increment";
/**
* 更新为当前时间戳
*/
public static final String UPDATE_WITH_CURRENT_TIMESTAMP = "on update current_timestamp";
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.constraint;
import lombok.Data;
/**
* @author liaozan
* @since 2022/8/30
*/
@Data
public class ColumnMeta {
/**
* 表名
*/
private String tableName;
/**
* 列名
*/
private String columnName;
/**
* 数据类型
*/
private String dataType;
/**
* 是否允许为空
*/
private boolean nullable;
/**
* 列默认值
*/
private String columnDefault;
/**
* 扩展信息
*/
private String extra;
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.constraint;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @author liaozan
* @since 2022/8/30
*/
public class ColumnMetaRowMapper implements RowMapper<ColumnMeta> {
@Override
public ColumnMeta mapRow(ResultSet resultSet, int rowNum) throws SQLException {
ColumnMeta columnMeta = new ColumnMeta();
columnMeta.setTableName(resultSet.getString("TABLE_NAME"));
columnMeta.setColumnName(resultSet.getString("COLUMN_NAME"));
columnMeta.setDataType(resultSet.getString("DATA_TYPE"));
columnMeta.setNullable(isNullable(resultSet.getString("IS_NULLABLE")));
columnMeta.setColumnDefault(resultSet.getString("COLUMN_DEFAULT"));
columnMeta.setExtra(resultSet.getString("EXTRA"));
return columnMeta;
}
private boolean isNullable(String value) {
return "YES".equalsIgnoreCase(value);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.constraint;
import com.mysql.cj.MysqlType;
import com.schbrain.framework.autoconfigure.mybatis.exception.TableConstraintException;
import static com.schbrain.framework.autoconfigure.mybatis.constant.MybatisConstants.*;
/**
* @author liaozan
* @since 2022/8/30
*/
@SuppressWarnings("DuplicatedCode")
public class DefaultTableConstraintChecker implements TableConstraintChecker {
@Override
public void checkBasicField(Table table) {
checkIdField(table);
checkCreateTimeField(table);
checkModifyTimeField(table);
}
@Override
public void checkLogicDeleteField(Table table) {
checkDeletedField(table);
checkDeleteVersionField(table);
}
protected void checkIdField(Table table) {
ColumnMeta idColumnMeta = table.getColumnMeta(ID);
if (idColumnMeta == null) {
addMissingFieldError(table, ID);
return;
}
checkNullable(table, idColumnMeta);
if (!MysqlType.BIGINT.getName().equalsIgnoreCase(idColumnMeta.getDataType())) {
addError(table, idColumnMeta, "should be type of 'bigint'");
}
if (!AUTO_INCREMENT.equalsIgnoreCase(idColumnMeta.getExtra())) {
addError(table, idColumnMeta, "should be 'auto_increment'");
}
}
protected void checkCreateTimeField(Table table) {
ColumnMeta createTimeColumnMeta = table.getColumnMeta(CREATE_TIME);
if (createTimeColumnMeta == null) {
addMissingFieldError(table, CREATE_TIME);
return;
}
checkNullable(table, createTimeColumnMeta);
if (!MysqlType.DATETIME.getName().equalsIgnoreCase(createTimeColumnMeta.getDataType())) {
addError(table, createTimeColumnMeta, "should be type of 'datetime'");
}
if (!CURRENT_TIMESTAMP.equalsIgnoreCase(createTimeColumnMeta.getColumnDefault())) {
addError(table, createTimeColumnMeta, "default value should be 'current_timestamp'");
}
}
protected void checkModifyTimeField(Table table) {
ColumnMeta modifyTimeColumnMeta = table.getColumnMeta(MODIFY_TIME);
if (modifyTimeColumnMeta == null) {
addMissingFieldError(table, MODIFY_TIME);
return;
}
checkNullable(table, modifyTimeColumnMeta);
if (!MysqlType.DATETIME.getName().equalsIgnoreCase(modifyTimeColumnMeta.getDataType())) {
addError(table, modifyTimeColumnMeta, "should be type of 'datetime'");
}
if (!CURRENT_TIMESTAMP.equalsIgnoreCase(modifyTimeColumnMeta.getColumnDefault())) {
addError(table, modifyTimeColumnMeta, "default value should be 'current_timestamp'");
}
if (!modifyTimeColumnMeta.getExtra().toLowerCase().contains(UPDATE_WITH_CURRENT_TIMESTAMP)) {
addError(table, modifyTimeColumnMeta, "need set to 'on update current_timestamp'");
}
}
protected void checkDeletedField(Table table) {
ColumnMeta deletedColumnMeta = table.getColumnMeta(DELETED);
if (deletedColumnMeta == null) {
addMissingFieldError(table, DELETED);
return;
}
checkNullable(table, deletedColumnMeta);
if (!MysqlType.TINYINT.getName().equalsIgnoreCase(deletedColumnMeta.getDataType())) {
addError(table, deletedColumnMeta, "should be type of 'tinyint'");
}
}
protected void checkDeleteVersionField(Table table) {
ColumnMeta deleteVersionColumnMeta = table.getColumnMeta(DELETE_VERSION);
if (deleteVersionColumnMeta == null) {
addMissingFieldError(table, DELETE_VERSION);
return;
}
checkNullable(table, deleteVersionColumnMeta);
if (!MysqlType.BIGINT.getName().equalsIgnoreCase(deleteVersionColumnMeta.getDataType())) {
addError(table, deleteVersionColumnMeta, "should be type of 'bigint'");
}
if (!"0".equalsIgnoreCase(deleteVersionColumnMeta.getColumnDefault())) {
addError(table, deleteVersionColumnMeta, "default value should be '0'");
}
}
protected void checkNullable(Table table, ColumnMeta columnMeta) {
if (columnMeta.isNullable()) {
addError(table, columnMeta, "should be 'not null'");
}
}
private void addError(Table table, ColumnMeta columnMeta, String errorMsg) {
addError(table, columnMeta.getColumnName(), errorMsg);
}
private void addMissingFieldError(Table table, String columnName) {
addError(table, columnName, "not exist");
}
private void addError(Table table, String columnName, String errorMsg) {
table.addError(new TableConstraintException(table.getTableName(), columnName, errorMsg));
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.constraint;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import java.util.*;
/**
* @author liaozan
* @since 2022/8/30
*/
public class DefaultTableMetaDataLoader implements TableMetaDataLoader {
public static final String METADATA_QUERY = "SELECT `TABLE_NAME`,`COLUMN_NAME`,`COLUMN_DEFAULT`,`DATA_TYPE`,`EXTRA`,`IS_NULLABLE` FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ?";
private final RowMapper<ColumnMeta> rowMapper = new ColumnMetaRowMapper();
private final JdbcTemplate jdbcTemplate;
public DefaultTableMetaDataLoader(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public Map<String, List<ColumnMeta>> loadTableMeta(String database) {
List<ColumnMeta> columnMetas = jdbcTemplate.query(METADATA_QUERY, rowMapper, database);
Map<String, List<ColumnMeta>> metaMap = new HashMap<>();
for (ColumnMeta columnMeta : columnMetas) {
String tableName = columnMeta.getTableName();
List<ColumnMeta> metaList = metaMap.computeIfAbsent(tableName, name -> new ArrayList<>());
metaList.add(columnMeta);
}
return metaMap;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.constraint;
import java.lang.annotation.*;
/**
* @author liaozan
* @since 2023-03-18
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreConstraintCheck {
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.constraint;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.schbrain.common.util.StreamUtils;
import com.schbrain.framework.autoconfigure.mybatis.exception.TableConstraintException;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import javax.annotation.Nullable;
import java.util.*;
/**
* @author liaozan
* @since 2022/9/2
*/
public class Table {
private final TableInfo tableInfo;
private final List<FieldInfo> fieldInfoList;
private final Map<String, ColumnMeta> columnMetaMap;
private final List<TableConstraintException> errors = new ArrayList<>();
public Table(TableInfo tableInfo, Map<String, ColumnMeta> columnMetaMap) {
this.tableInfo = tableInfo;
this.fieldInfoList = StreamUtils.toList(tableInfo.getFieldList(), FieldInfo::new);
this.columnMetaMap = columnMetaMap;
}
public String getTableName() {
return tableInfo.getTableName();
}
@Nullable
public ColumnMeta getColumnMeta(String column) {
return columnMetaMap.get(column);
}
public boolean containsColumn(String column) {
return columnMetaMap.containsKey(column);
}
public List<FieldInfo> getFieldInfoList() {
return fieldInfoList;
}
public void addError(TableConstraintException error) {
errors.add(error);
}
public List<TableConstraintException> getErrors() {
return errors;
}
@Data
public static class FieldInfo {
private String field;
private String column;
public FieldInfo(TableFieldInfo tableFieldInfo) {
this.field = tableFieldInfo.getField().getName();
this.column = StringUtils.unwrap(tableFieldInfo.getColumn(), "`");
}
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.constraint;
import com.schbrain.framework.autoconfigure.mybatis.exception.TableConstraintException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
/**
* @author liaozan
* @since 2022/8/31
*/
@Slf4j
public class TableConstraintCheckFailureAnalyzer extends AbstractFailureAnalyzer<TableConstraintException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, TableConstraintException cause) {
return new FailureAnalysis(cause.getMessage(), null, cause);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.constraint;
/**
* @author liaozan
* @since 2022/8/30
*/
public interface TableConstraintChecker {
/**
* 检查基础字段
*/
void checkBasicField(Table table);
/**
* 检查逻辑删除字段
*/
void checkLogicDeleteField(Table table);
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.constraint;
import java.util.List;
import java.util.Map;
/**
* @author liaozan
* @since 2022/8/30
*/
public interface TableMetaDataLoader {
/**
* 加载指定数据库的所有表元信息
*/
Map<String, List<ColumnMeta>> loadTableMeta(String database);
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.core;
import com.schbrain.common.exception.BaseException;
import com.schbrain.framework.autoconfigure.mybatis.annotation.BizId;
import com.schbrain.framework.autoconfigure.mybatis.biz.BizIdHelper;
import lombok.Data;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Field;
import static java.lang.invoke.MethodHandles.*;
/**
* @author liaozan
* @since 2023-03-23
*/
@Data
public class BizIdColumnField {
private final BizId annotation;
private final String columnName;
private final MethodHandle bizIdFieldGetterMethodHandle;
private final MethodHandle bizIdFieldSetterMethodHandle;
public BizIdColumnField(Class<?> entityClass, Field bizIdField) {
this.annotation = bizIdField.getAnnotation(BizId.class);
this.columnName = BizIdHelper.getColumnName(entityClass, bizIdField, this.annotation);
try {
this.bizIdFieldGetterMethodHandle = privateLookupIn(entityClass, lookup()).findGetter(entityClass, bizIdField.getName(), String.class);
this.bizIdFieldSetterMethodHandle = privateLookupIn(entityClass, lookup()).findSetter(entityClass, bizIdField.getName(), String.class);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new BaseException(e.getMessage(), e);
}
}
public <T> String getValue(T entity) {
try {
return (String) bizIdFieldGetterMethodHandle.invoke(entity);
} catch (Throwable e) {
throw new BaseException(e.getMessage(), e);
}
}
public <T> void setValue(T entity, String value) {
try {
bizIdFieldSetterMethodHandle.invoke(entity, value);
} catch (Throwable e) {
throw new BaseException(e.getMessage(), e);
}
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.core;
import org.apache.ibatis.mapping.*;
import java.util.List;
/**
* @author liaozan
* @since 2021/11/27
*/
public class LogicDeleteSupportSqlSource implements SqlSource {
public static final String DELETE_VERSION = "deleteVersion";
private final SqlSource sqlSource;
public LogicDeleteSupportSqlSource(SqlSource sqlSource) {
this.sqlSource = sqlSource;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
if (hasDeleteVersionProperty(boundSql.getParameterMappings())) {
boundSql.setAdditionalParameter(DELETE_VERSION, System.currentTimeMillis());
}
return boundSql;
}
private boolean hasDeleteVersionProperty(List<ParameterMapping> mappings) {
return mappings.stream()
.map(ParameterMapping::getProperty)
.anyMatch(property -> property.equals(DELETE_VERSION));
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.core;
import com.baomidou.mybatisplus.core.MybatisXMLLanguageDriver;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.session.Configuration;
/**
* @author liaozan
* @since 2021/11/27
*/
public class MybatisXmlLanguageDriver extends MybatisXMLLanguageDriver {
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
SqlSource sqlSource = super.createSqlSource(configuration, script, parameterType);
return new LogicDeleteSupportSqlSource(sqlSource);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.datasource;
import com.schbrain.framework.autoconfigure.mybatis.datasource.customizer.DataSourceCustomizer;
import com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties;
import com.schbrain.framework.support.spring.BeanPostProcessorAdapter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import javax.sql.DataSource;
import java.sql.SQLException;
/**
* @author liaozan
* @since 2021/11/23
*/
@Slf4j
public class DataSourceConnectionPostProcessor extends BeanPostProcessorAdapter<DataSource> {
// use ObjectProvider to avoid early initialization beans
private final ObjectProvider<DataSourceCustomizer> customizers;
private final ObjectProvider<DataSourceConnectionProperties> connectionProperties;
public DataSourceConnectionPostProcessor(ObjectProvider<DataSourceCustomizer> customizers,
ObjectProvider<DataSourceConnectionProperties> connectionProperties) {
this.customizers = customizers;
this.connectionProperties = connectionProperties;
}
@Override
protected void processBeforeInitialization(DataSource dataSource, String beanName) throws BeansException {
DataSourceConnectionProperties connectionProperties = this.connectionProperties.getObject();
customizers.orderedStream().forEach(customizer -> {
try {
customizer.customize(dataSource, connectionProperties);
} catch (SQLException e) {
log.warn("failed to customize dataSource connectionProperties", e);
}
});
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.datasource.customizer;
import com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties;
import javax.sql.DataSource;
import java.sql.SQLException;
/**
* @author liaozan
* @since 2021/11/28
*/
public interface DataSourceCustomizer {
void customize(DataSource dataSource, DataSourceConnectionProperties properties) throws SQLException;
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.datasource.customizer;
import com.schbrain.framework.autoconfigure.mybatis.datasource.extractor.DataSourcePropertiesExtractor;
import com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.*;
/**
* @author liaozan
* @since 2021/11/28
*/
public class DefaultDataSourceCustomizer implements DataSourceCustomizer {
private final List<DataSourcePropertiesExtractor> extractors;
public DefaultDataSourceCustomizer(List<DataSourcePropertiesExtractor> extractors) {
AnnotationAwareOrderComparator.sort(extractors);
this.extractors = extractors;
}
@Override
public void customize(DataSource dataSource, DataSourceConnectionProperties properties) throws SQLException {
Map<String, String> connectionProps = properties.toConfigurationMap();
for (DataSourcePropertiesExtractor extractor : extractors) {
if (extractor.support(dataSource)) {
Properties originProps = extractor.extract(dataSource, connectionProps);
if (originProps != null) {
originProps.putAll(connectionProps);
}
}
}
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.datasource.extractor;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
/**
* @author liaozan
* @since 2021/11/23
*/
public interface DataSourcePropertiesExtractor {
boolean support(DataSource dataSource) throws SQLException;
Properties extract(DataSource dataSource, Map<String, String> properties) throws SQLException;
}
package com.schbrain.framework.autoconfigure.mybatis.datasource.extractor;
import org.springframework.util.TypeUtils;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
/**
* @author liaozan
* @since 2021/11/23
*/
public abstract class DataSourcePropertiesExtractorSupport implements DataSourcePropertiesExtractor {
@Override
public boolean support(DataSource dataSource) {
Class<? extends DataSource> supportedType = getSupportedType();
return TypeUtils.isAssignable(supportedType, dataSource.getClass());
}
public abstract Class<? extends DataSource> getSupportedType();
@Override
public Properties extract(DataSource dataSource, Map<String, String> properties) throws SQLException {
return extract(dataSource);
}
protected abstract Properties extract(DataSource dataSource) throws SQLException;
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.datasource.extractor;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.Properties;
/**
* @author liaozan
* @since 2021/11/23
*/
@Slf4j
@ConditionalOnClass(DruidDataSource.class)
public class DruidDataSourcePropertiesExtractor extends DataSourcePropertiesExtractorSupport {
@Override
public Class<DruidDataSource> getSupportedType() {
return DruidDataSource.class;
}
@Override
protected Properties extract(DataSource dataSource) throws SQLException {
return dataSource.unwrap(DruidDataSource.class).getConnectProperties();
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.datasource.extractor;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.Properties;
/**
* @author liaozan
* @since 2021/11/23
*/
@ConditionalOnClass(HikariDataSource.class)
public class HikariDataSourcePropertiesExtractor extends DataSourcePropertiesExtractorSupport {
@Override
public Class<? extends DataSource> getSupportedType() {
return HikariDataSource.class;
}
@Override
protected Properties extract(DataSource dataSource) throws SQLException {
return dataSource.unwrap(HikariDataSource.class).getDataSourceProperties();
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.exception;
import com.schbrain.common.exception.BaseException;
import java.io.Serializable;
/**
* @author liaozan
* @since 2022/04/15
*/
public class NoSuchRecordException extends BaseException {
private static final long serialVersionUID = -2197824144318250175L;
public NoSuchRecordException(Class<?> entityClass, Serializable id) {
super(String.format("No %s entity with id %s exists!", entityClass.getSimpleName(), id));
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.exception;
import cn.hutool.core.text.StrFormatter;
import com.schbrain.common.exception.BaseException;
import com.schbrain.common.util.StreamUtils;
import lombok.Getter;
import java.util.List;
/**
* @author liaozan
* @since 2022/8/30
*/
@Getter
public class TableConstraintException extends BaseException {
private static final long serialVersionUID = -3139175416089223586L;
public TableConstraintException(String message, Object... args) {
super(StrFormatter.format(message, args));
}
public TableConstraintException(String tableName, String column, String message) {
super("Table: '" + tableName + "' , Column: '" + column + "' " + message);
}
public TableConstraintException(List<TableConstraintException> errors) {
super(StreamUtils.join(StreamUtils.toList(errors, Throwable::getMessage), System.lineSeparator()));
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.listener;
import com.baomidou.mybatisplus.core.mapper.Mapper;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.schbrain.common.exception.BaseException;
import com.schbrain.common.util.StreamUtils;
import com.schbrain.framework.autoconfigure.mybatis.base.*;
import com.schbrain.framework.autoconfigure.mybatis.constraint.*;
import com.schbrain.framework.autoconfigure.mybatis.constraint.Table.FieldInfo;
import com.schbrain.framework.autoconfigure.mybatis.exception.TableConstraintException;
import com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.*;
import static java.util.stream.Collectors.toList;
/**
* @author liaozan
* @since 2022/8/28
*/
@Slf4j
public class TableConstraintCheckerBean implements SmartInitializingSingleton, BeanFactoryAware {
private final DataSource dataSource;
private final MybatisProperties mybatisProperties;
private ConfigurableListableBeanFactory beanFactory;
public TableConstraintCheckerBean(DataSource dataSource, MybatisProperties mybatisProperties) {
this.dataSource = dataSource;
this.mybatisProperties = mybatisProperties;
}
@Override
public void afterSingletonsInstantiated() {
if (!mybatisProperties.isEnableTableConstraintCheck()) {
log.warn("Table constraint check is disabled");
return;
}
log.info("Table constraint check started");
List<TableMetaDataLoader> metaDataLoaders = beanFactory.getBeanProvider(TableMetaDataLoader.class).orderedStream().collect(toList());
if (CollectionUtils.isEmpty(metaDataLoaders)) {
JdbcTemplate jdbcTemplate = beanFactory.getBean(JdbcTemplate.class);
// Avoid add to a immutable collection
metaDataLoaders = List.of(new DefaultTableMetaDataLoader(jdbcTemplate));
}
Map<String, List<ColumnMeta>> tableMetadata = loadTableMetadata(metaDataLoaders);
if (MapUtils.isEmpty(tableMetadata)) {
log.warn("Table metadata is empty, ignore table constraint check");
return;
}
List<TableConstraintChecker> checkers = beanFactory.getBeanProvider(TableConstraintChecker.class).orderedStream().collect(toList());
if (CollectionUtils.isEmpty(checkers)) {
// Avoid add to a immutable collection
checkers = List.of(new DefaultTableConstraintChecker());
}
List<TableConstraintException> errors = new ArrayList<>();
for (Class<?> mapper : getAllMappers()) {
doConstraintCheck(mapper, tableMetadata, checkers, errors);
}
if (CollectionUtils.isNotEmpty(errors)) {
throw new TableConstraintException(errors);
}
log.info("Table constraint check completed");
beanFactory.destroyBean(this);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory);
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
}
private void doConstraintCheck(Class<?> mapperClass, Map<String, List<ColumnMeta>> tableMetadata,
List<TableConstraintChecker> checkers, List<TableConstraintException> errors) {
if (!ClassUtils.isAssignable(BaseMapper.class, mapperClass)) {
return;
}
Class<?> entityClass = ReflectionKit.getSuperClassGenericType(mapperClass, Mapper.class, 0);
if (!ClassUtils.isAssignable(BaseEntity.class, entityClass)) {
return;
}
if (entityClass.isAssignableFrom(IgnoreConstraintCheck.class)) {
return;
}
TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
if (tableInfo == null) {
errors.add(new TableConstraintException("Could not get tableInfo for {}", entityClass.getName()));
return;
}
Map<String, ColumnMeta> columnMetaMap = StreamUtils.toMap(tableMetadata.get(tableInfo.getTableName()), ColumnMeta::getColumnName);
if (MapUtils.isEmpty(columnMetaMap)) {
errors.add(new TableConstraintException("Table: '{}' not exist ", tableInfo.getTableName()));
return;
}
Table table = new Table(tableInfo, columnMetaMap);
for (TableConstraintChecker checker : checkers) {
checkAllFieldExist(table);
checker.checkBasicField(table);
if (ClassUtils.isAssignable(BaseEntityWithLogicDelete.class, entityClass)) {
checker.checkLogicDeleteField(table);
}
}
errors.addAll(table.getErrors());
}
private String getDatabaseName() {
try {
return dataSource.getConnection().getCatalog();
} catch (SQLException e) {
throw new BaseException("Can not get connection from DataSource", e);
}
}
private Map<String, List<ColumnMeta>> loadTableMetadata(List<TableMetaDataLoader> loaders) {
String database = getDatabaseName();
Map<String, List<ColumnMeta>> tableMetadata = null;
for (TableMetaDataLoader metaDataLoader : loaders) {
tableMetadata = metaDataLoader.loadTableMeta(database);
if (tableMetadata != null) {
break;
}
}
return tableMetadata;
}
private List<Class<?>> getAllMappers() {
Map<String, SqlSessionFactory> sqlSessionFactoryMap = beanFactory.getBeansOfType(SqlSessionFactory.class);
return sqlSessionFactoryMap.values().stream()
.flatMap(sqlSessionFactory -> sqlSessionFactory.getConfiguration().getMapperRegistry().getMappers().stream())
.collect(toList());
}
/**
* @see TableInfoHelper#getAllFields(Class)
*/
private void checkAllFieldExist(Table table) {
for (FieldInfo fieldInfo : table.getFieldInfoList()) {
if (!table.containsColumn(fieldInfo.getColumn())) {
table.addError(new TableConstraintException(table.getTableName(), fieldInfo.getColumn(), "not exist"));
}
}
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.properties;
import com.mysql.cj.conf.PropertyDefinitions.ZeroDatetimeBehavior;
import com.mysql.cj.conf.PropertyKey;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author liaozan
* @since 2021/11/23
*/
@Data
@ConfigurationProperties(prefix = "schbrain.datasource.connection")
public class DataSourceConnectionProperties {
/**
* 使用 ssl 连接
*/
private Boolean useSsl = false;
/**
* tinyint(1) 视为 boolean
*/
private Boolean tinyInt1isBit = true;
/**
* 重写批处理sql
*/
private Boolean rewriteBatchedStatements = true;
/**
* 是否允许一个 statement 用分号分割执行多个查询语句
*/
private Boolean allowMultiQueries = true;
/**
* 允许从服务端获取公钥进行连接
*/
private Boolean allowPublicKeyRetrieval = true;
/**
* 连接数据库使用的时区
*/
private ZoneId serverTimeZone = ZoneId.systemDefault();
/**
* 时间格式字段值为 0 的时候的处理方式
*/
private ZeroDatetimeBehavior zeroDatetimeBehavior = ZeroDatetimeBehavior.CONVERT_TO_NULL;
/**
* 数据库连接字符编码
*/
private String characterEncoding = StandardCharsets.UTF_8.name();
public Map<String, String> toConfigurationMap() {
Map<String, String> properties = new LinkedHashMap<>();
properties.put(PropertyKey.useSSL.getKeyName(), this.useSsl.toString());
properties.put(PropertyKey.tinyInt1isBit.getKeyName(), this.tinyInt1isBit.toString());
properties.put(PropertyKey.rewriteBatchedStatements.getKeyName(), this.rewriteBatchedStatements.toString());
properties.put(PropertyKey.allowMultiQueries.getKeyName(), this.allowMultiQueries.toString());
properties.put(PropertyKey.connectionTimeZone.getKeyName(), this.serverTimeZone.getId());
properties.put(PropertyKey.allowPublicKeyRetrieval.getKeyName(), this.allowPublicKeyRetrieval.toString());
properties.put(PropertyKey.zeroDateTimeBehavior.getKeyName(), this.zeroDatetimeBehavior.name());
properties.put(PropertyKey.characterEncoding.getKeyName(), this.characterEncoding);
return properties;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.properties;
import com.schbrain.common.util.support.ConfigurableProperties;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author liaozan
* @since 2021/11/23
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties(prefix = "schbrain.mybatis")
public class MybatisProperties extends ConfigurableProperties {
/**
* 分页拦截器
*/
private boolean addPageInterceptor = true;
/**
* 阻断全表更新操作,禁止不带 where 更新,删除
*/
private boolean addBlockAttackInterceptor = true;
/**
* 是否开启表约束检查
*/
private boolean enableTableConstraintCheck = true;
/**
* Instant 转为 long
*/
private boolean convertInstantToLong = true;
@Override
public String getDefaultNamespace() {
return "mybatis-common";
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.sql.injector;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.AbstractSqlInjector;
import com.baomidou.mybatisplus.core.injector.methods.*;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.extension.injector.methods.AlwaysUpdateSomeColumnById;
import com.google.common.collect.Lists;
import com.schbrain.framework.autoconfigure.mybatis.constant.MybatisConstants;
import com.schbrain.framework.autoconfigure.mybatis.sql.method.Delete;
import com.schbrain.framework.autoconfigure.mybatis.sql.method.DeleteBatchByIds;
import com.schbrain.framework.autoconfigure.mybatis.sql.method.DeleteByMap;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* @author liaozan
* @since 2021/11/26
*/
@Slf4j
public class DefaultMethodSqlInjector extends AbstractSqlInjector {
/**
* <ul>
* <li>replace {@link com.baomidou.mybatisplus.core.injector.methods.Delete} to {@link Delete}</li>
* <li>replace {@link com.baomidou.mybatisplus.core.injector.methods.DeleteById} to {@link com.schbrain.framework.autoconfigure.mybatis.sql.method.DeleteById}</li>
* <li>replace {@link com.baomidou.mybatisplus.core.injector.methods.DeleteByMap} to {@link DeleteByMap}</li>
* <li>replace {@link com.baomidou.mybatisplus.core.injector.methods.DeleteBatchByIds} to {@link DeleteBatchByIds}</li>
* </ul>
*/
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
List<AbstractMethod> methodList = Lists.newArrayListWithExpectedSize(20);
methodList.add(new Insert());
methodList.add(new Delete());
methodList.add(new DeleteByMap());
methodList.add(new Update());
methodList.add(new SelectByMap());
methodList.add(new SelectCount());
methodList.add(new SelectMaps());
methodList.add(new SelectMapsPage());
methodList.add(new SelectObjs());
methodList.add(new SelectList());
methodList.add(new SelectPage());
if (tableInfo.havePK()) {
methodList.add(new com.schbrain.framework.autoconfigure.mybatis.sql.method.DeleteById());
methodList.add(new DeleteBatchByIds());
methodList.add(new UpdateById());
methodList.add(new AlwaysUpdateSomeColumnById(field -> !field.getColumn().equals(MybatisConstants.DELETE_VERSION)));
methodList.add(new SelectById());
methodList.add(new SelectBatchByIds());
} else {
log.warn("{} ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.", tableInfo.getEntityType());
}
return methodList;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.sql.method;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.schbrain.framework.autoconfigure.mybatis.util.SqlUtils;
/**
* @author liaozan
* @since 2021/11/26
*/
public class Delete extends com.baomidou.mybatisplus.core.injector.methods.Delete {
private static final long serialVersionUID = -4047186946220703735L;
@Override
protected String sqlLogicSet(TableInfo table) {
String logicSet = super.sqlLogicSet(table);
return SqlUtils.withLogicDeleteVersionIfNecessary(table, logicSet);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.sql.method;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.schbrain.framework.autoconfigure.mybatis.util.SqlUtils;
/**
* @author liaozan
* @since 2021/11/26
*/
public class DeleteBatchByIds extends com.baomidou.mybatisplus.core.injector.methods.DeleteBatchByIds {
private static final long serialVersionUID = -6821464569587694540L;
@Override
protected String sqlLogicSet(TableInfo table) {
String logicSet = super.sqlLogicSet(table);
return SqlUtils.withLogicDeleteVersionIfNecessary(table, logicSet);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.sql.method;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.schbrain.framework.autoconfigure.mybatis.util.SqlUtils;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
/**
* Mybatis-Plus 对于 DeleteById 的逻辑删除和其他 Delete 行为不一致, 所以这里去掉了不一致的行为
* <a href="https://github.com/baomidou/mybatis-plus/issues/4781">详情见 Github Issue</a>
*
* @author liaozan
* @since 2021/11/26
*/
public class DeleteById extends com.baomidou.mybatisplus.core.injector.methods.DeleteById {
private static final long serialVersionUID = 998500455669716402L;
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
if (tableInfo.isWithLogicDelete()) {
return addLogicDeleteMappedStatement(mapperClass, modelClass, tableInfo);
} else {
return addDeleteMappedStatement(mapperClass, tableInfo);
}
}
@Override
protected String sqlLogicSet(TableInfo table) {
String logicSet = super.sqlLogicSet(table);
return SqlUtils.withLogicDeleteVersionIfNecessary(table, logicSet);
}
private MappedStatement addDeleteMappedStatement(Class<?> mapperClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.DELETE_BY_ID;
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Object.class);
return this.addDeleteMappedStatement(mapperClass, sqlMethod.getMethod(), sqlSource);
}
private MappedStatement addLogicDeleteMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.LOGIC_DELETE_BY_ID;
String sql = getLogicDeleteSql(tableInfo, sqlMethod);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Object.class);
return addUpdateMappedStatement(mapperClass, modelClass, sqlMethod.getMethod(), sqlSource);
}
private String getLogicDeleteSql(TableInfo tableInfo, SqlMethod sqlMethod) {
String tableName = tableInfo.getTableName();
String keyColumn = tableInfo.getKeyColumn();
String keyProperty = tableInfo.getKeyProperty();
String logicDeleteSql = tableInfo.getLogicDeleteSql(true, true);
return String.format(sqlMethod.getSql(), tableName, sqlLogicSet(tableInfo), keyColumn, keyProperty, logicDeleteSql);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.sql.method;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.schbrain.framework.autoconfigure.mybatis.util.SqlUtils;
/**
* @author liaozan
* @since 2021/11/26
*/
public class DeleteByMap extends com.baomidou.mybatisplus.core.injector.methods.DeleteByMap {
private static final long serialVersionUID = 1937536236892111475L;
@Override
protected String sqlLogicSet(TableInfo table) {
String logicSet = super.sqlLogicSet(table);
return SqlUtils.withLogicDeleteVersionIfNecessary(table, logicSet);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.type;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.*;
import java.time.Instant;
/**
* @author liaozan
* @since 2022/8/31
*/
public class InstantToLongTypeHandler extends BaseTypeHandler<Instant> {
@Override
public void setNonNullParameter(PreparedStatement ps, int index, Instant parameter, JdbcType jdbcType) throws SQLException {
ps.setLong(index, parameter.toEpochMilli());
}
@Override
public Instant getNullableResult(ResultSet rs, String columnName) throws SQLException {
long result = rs.getLong(columnName);
return result == 0 && rs.wasNull() ? null : Instant.ofEpochMilli(result);
}
@Override
public Instant getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
long result = rs.getLong(columnIndex);
return result == 0 && rs.wasNull() ? null : Instant.ofEpochMilli(result);
}
@Override
public Instant getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
long result = cs.getLong(columnIndex);
return result == 0 && cs.wasNull() ? null : Instant.ofEpochMilli(result);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.mybatis.util;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.schbrain.framework.autoconfigure.mybatis.base.BaseEntityWithLogicDelete;
import org.springframework.util.ClassUtils;
/**
* @author liaozan
* @since 2022/8/27
*/
public class SqlUtils {
public static String withLogicDeleteVersionIfNecessary(TableInfo tableInfo, String logicDeleteSql) {
Class<?> entityType = tableInfo.getEntityType();
if (ClassUtils.isAssignable(BaseEntityWithLogicDelete.class, entityType)) {
logicDeleteSql = logicDeleteSql + ", delete_version=#{deleteVersion}";
}
return logicDeleteSql;
}
}
\ No newline at end of file
{
"groups": [
{
"name": "schbrain.datasource.connection",
"type": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties"
},
{
"name": "schbrain.mybatis",
"type": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties"
}
],
"properties": [
{
"name": "schbrain.datasource.connection.allow-multi-queries",
"type": "java.lang.Boolean",
"description": "是否允许一个 statement 用分号分割执行多个查询语句",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties",
"defaultValue": true
},
{
"name": "schbrain.datasource.connection.allow-public-key-retrieval",
"type": "java.lang.Boolean",
"description": "允许从服务端获取公钥进行连接",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties",
"defaultValue": true
},
{
"name": "schbrain.datasource.connection.character-encoding",
"type": "java.lang.String",
"description": "数据库连接字符编码",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties"
},
{
"name": "schbrain.datasource.connection.rewrite-batched-statements",
"type": "java.lang.Boolean",
"description": "重写批处理sql",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties",
"defaultValue": true
},
{
"name": "schbrain.datasource.connection.server-time-zone",
"type": "java.time.ZoneId",
"description": "连接数据库使用的时区",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties"
},
{
"name": "schbrain.datasource.connection.tiny-int1is-bit",
"type": "java.lang.Boolean",
"description": "tinyint(1) 视为 boolean",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties",
"defaultValue": true
},
{
"name": "schbrain.datasource.connection.use-ssl",
"type": "java.lang.Boolean",
"description": "使用 ssl 连接",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties",
"defaultValue": false
},
{
"name": "schbrain.datasource.connection.zero-datetime-behavior",
"type": "com.mysql.cj.conf.PropertyDefinitions$ZeroDatetimeBehavior",
"description": "时间格式字段值为 0 的时候的处理方式",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties"
},
{
"name": "schbrain.mybatis.add-block-attack-interceptor",
"type": "java.lang.Boolean",
"description": "阻断全表更新操作,禁止不带 where 更新,删除",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties",
"defaultValue": true
},
{
"name": "schbrain.mybatis.add-page-interceptor",
"type": "java.lang.Boolean",
"description": "分页拦截器",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties",
"defaultValue": true
},
{
"name": "schbrain.mybatis.convert-instant-to-long",
"type": "java.lang.Boolean",
"description": "Instant 转为 long",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties",
"defaultValue": true
},
{
"name": "schbrain.mybatis.enable-table-constraint-check",
"type": "java.lang.Boolean",
"description": "是否开启表约束检查",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties",
"defaultValue": true
},
{
"name": "schbrain.mybatis.name",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties"
},
{
"name": "schbrain.mybatis.namespace",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties"
},
{
"name": "schbrain.mybatis.prefix",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties"
}
],
"hints": []
}
\ No newline at end of file
org.springframework.boot.diagnostics.FailureAnalyzer=com.schbrain.framework.autoconfigure.mybatis.constraint.TableConstraintCheckFailureAnalyzer
\ No newline at end of file
com.schbrain.framework.autoconfigure.mybatis.MybatisAutoConfiguration
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>starters</artifactId>
<version>${revision}</version>
</parent>
<artifactId>oss-spring-boot-starter</artifactId>
<dependencies>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<dependency>
<groupId>com.schbrain.framework</groupId>
<artifactId>apollo-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package com.schbrain.framework.autoconfigure.oss;
import com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils;
import com.schbrain.framework.autoconfigure.oss.properties.OssProperties;
import com.schbrain.framework.autoconfigure.oss.util.OssUtils;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
/**
* @author liaozan
* @since 2021/12/3
*/
@AutoConfiguration
@EnableConfigurationProperties(OssProperties.class)
public class OssAutoConfiguration {
public OssAutoConfiguration(ConfigurableApplicationContext applicationContext) {
initialize(applicationContext);
}
private void initialize(ConfigurableApplicationContext applicationContext) {
ConfigurableEnvironment environment = applicationContext.getEnvironment();
OssProperties ossProperties = ConfigUtils.loadConfig(environment, OssProperties.class);
OssUtils.initialize(environment, ossProperties);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.oss.bean;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author lik
* @since 2022/9/6
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class CopyResult extends OssOperationResult {
private static final long serialVersionUID = 7322484488471640978L;
private String destinationKey;
private String destinationBucket;
public static CopyResult success(String sourceBucket, String sourceKey, String destinationBucket, String destinationKey) {
CopyResult result = new CopyResult();
result.setSuccess(true);
result.setBucket(sourceBucket);
result.setObjectKey(sourceKey);
result.setDestinationBucket(destinationBucket);
result.setDestinationKey(destinationKey);
return result;
}
public static CopyResult fail(String sourceBucket, String sourceKey, String destinationBucket, String destinationKey, String errorMsg) {
CopyResult result = new CopyResult();
result.setSuccess(false);
result.setBucket(sourceBucket);
result.setObjectKey(sourceKey);
result.setDestinationBucket(destinationBucket);
result.setDestinationKey(destinationKey);
result.setErrorMsg(errorMsg);
return result;
}
}
package com.schbrain.framework.autoconfigure.oss.bean;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* @author lik
* @since 2022/9/6
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DeleteResult extends OssOperationResult {
private static final long serialVersionUID = -1964404722922995469L;
private List<String> deleteObjectKeys;
public static DeleteResult success(String bucket, List<String> objectKeys) {
DeleteResult result = new DeleteResult();
result.setSuccess(true);
result.setBucket(bucket);
result.setDeleteObjectKeys(objectKeys);
return result;
}
public static DeleteResult fail(String bucket, List<String> objectKeys, String errorMsg) {
DeleteResult result = new DeleteResult();
result.setSuccess(false);
result.setBucket(bucket);
result.setErrorMsg(errorMsg);
result.setDeleteObjectKeys(objectKeys);
return result;
}
}
package com.schbrain.framework.autoconfigure.oss.bean;
import com.aliyun.oss.model.OSSObject;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author liaozan
* @since 2021/12/3
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DownloadResult extends OssOperationResult {
private static final long serialVersionUID = -9010079317621184645L;
private OSSObject ossObject;
public static DownloadResult success(String bucket, String objectKey, OSSObject ossObject) {
DownloadResult result = new DownloadResult();
result.setSuccess(true);
result.setBucket(bucket);
result.setObjectKey(objectKey);
result.setOssObject(ossObject);
return result;
}
public static DownloadResult fail(String bucket, String objectKey, String errorMsg) {
DownloadResult result = new DownloadResult();
result.setSuccess(false);
result.setBucket(bucket);
result.setObjectKey(objectKey);
result.setErrorMsg(errorMsg);
return result;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.oss.bean;
import lombok.Data;
import java.io.Serializable;
/**
* @author liaozan
* @since 2021/12/19
*/
@Data
public class OssOperationResult implements Serializable {
private static final long serialVersionUID = 3651584115463313214L;
protected boolean success;
protected String bucket;
protected String objectKey;
protected String errorMsg;
public boolean isFailed() {
return !success;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.oss.bean;
import com.aliyuncs.auth.sts.AssumeRoleResponse.Credentials;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.*;
import java.time.format.DateTimeFormatter;
/**
* @author liaozan
* @since 2021/12/4
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class UploadCredentials extends OssOperationResult {
private static final long serialVersionUID = 5546792221041679671L;
private String accessKeyId;
private String accessKeySecret;
private String securityToken;
private LocalDateTime expiration;
// for json deserialize
public UploadCredentials() {
}
public UploadCredentials(Credentials credentials) {
this.success = true;
this.accessKeyId = credentials.getAccessKeyId();
this.accessKeySecret = credentials.getAccessKeySecret();
this.securityToken = credentials.getSecurityToken();
// example: 2021-12-04T11:03:37Z
this.expiration = LocalDateTime.parse(credentials.getExpiration(), DateTimeFormatter.ISO_DATE_TIME)
.toInstant(ZoneOffset.UTC)
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
}
public UploadCredentials(String errorMsg) {
this.success = false;
this.errorMsg = errorMsg;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.oss.bean;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author liaozan
* @since 2021/12/3
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class UploadResult extends OssOperationResult {
private static final long serialVersionUID = 7408490108665799098L;
private String url;
public static UploadResult success(String bucket, String objectKey, String url) {
UploadResult result = new UploadResult();
result.setSuccess(true);
result.setBucket(bucket);
result.setObjectKey(objectKey);
result.setUrl(url);
return result;
}
public static UploadResult fail(String bucket, String objectKey, String errorMsg) {
UploadResult result = new UploadResult();
result.setSuccess(false);
result.setBucket(bucket);
result.setObjectKey(objectKey);
result.setErrorMsg(errorMsg);
return result;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.oss.exception;
import com.schbrain.common.exception.BaseException;
/**
* @author liaozan
* @since 2021/12/3
*/
public class OssException extends BaseException {
private static final long serialVersionUID = 7030196267316583562L;
public OssException(String message) {
this(message, null);
}
public OssException(String message, Throwable cause) {
super(message, cause);
}
}
package com.schbrain.framework.autoconfigure.oss.properties;
import com.schbrain.common.util.support.ConfigurableProperties;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* @author liaozan
* @since 2021/12/3
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties(prefix = "schbrain.oss")
public class OssProperties extends ConfigurableProperties {
private String accessKeyId;
private String secretAccessKey;
private String endpoint;
private String bucketName;
private String directory;
private String domain;
@NestedConfigurationProperty
private StsProperties sts;
public boolean isInValid() {
return accessKeyId == null || secretAccessKey == null;
}
@Override
public String getDefaultNamespace() {
return "oss-common";
}
@Data
public static class StsProperties {
private String endpoint;
private String roleArn;
private String roleSessionName;
private Long durationSeconds = 900L;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.oss.util;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.HttpUtil;
import com.aliyun.oss.*;
import com.aliyun.oss.common.comm.ResponseMessage;
import com.aliyun.oss.model.*;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.auth.sts.AssumeRoleRequest;
import com.aliyuncs.auth.sts.AssumeRoleResponse;
import com.aliyuncs.auth.sts.AssumeRoleResponse.Credentials;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.schbrain.common.constants.DateTimeFormatters;
import com.schbrain.common.util.ApplicationName;
import com.schbrain.common.util.ValidateUtils;
import com.schbrain.framework.autoconfigure.oss.bean.*;
import com.schbrain.framework.autoconfigure.oss.exception.OssException;
import com.schbrain.framework.autoconfigure.oss.properties.OssProperties;
import com.schbrain.framework.autoconfigure.oss.properties.OssProperties.StsProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.util.StringUtils;
import java.io.File;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.*;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author liaozan
* @since 2021/12/3
*/
@SuppressWarnings("unused")
@Slf4j
public class OssUtils {
private static OSSClient ossClient;
private static DefaultAcsClient stsAcsClient;
private static OssProperties ossProperties;
private static StsProperties stsProperties;
private static String directory;
public static void initialize(ConfigurableEnvironment environment, OssProperties properties) {
if (properties == null || properties.isInValid()) {
log.warn("ossProperties is invalid, OssUtils will not available until reinitialize with the correct configuration");
return;
}
try {
ossProperties = properties;
ossClient = (OSSClient) initOssClient(properties);
stsAcsClient = initStsAcsClient(properties);
stsProperties = properties.getSts();
directory = properties.getDirectory();
if (directory == null) {
directory = ApplicationName.get(environment);
}
} catch (Exception e) {
log.warn("oss initialize fail, OssUtils will not available until reinitialize with the correct configuration", e);
}
}
public static OssProperties getOssProperties() {
return ossProperties;
}
public static OSSClient getOssClient() {
if (ossClient == null) {
throw new OssException("Oss client is null");
}
return ossClient;
}
// upload file
public static UploadResult upload(File file) {
return upload(file, file.getName());
}
public static UploadResult upload(File file, String objectKey) {
return upload(file, objectKey, false);
}
public static UploadResult upload(File file, String objectKey, boolean allowOverwrite) {
return upload(file, objectKey, allowOverwrite, true);
}
public static UploadResult upload(File file, String objectKey, boolean allowOverwrite, boolean appendPrefix) {
return upload(file, ossProperties.getBucketName(), objectKey, allowOverwrite, appendPrefix);
}
public static UploadResult upload(File file, String bucket, String objectKey, boolean allowOverwrite, boolean appendPrefix) {
return upload(file, bucket, objectKey, allowOverwrite, appendPrefix, null);
}
public static UploadResult upload(File file, String bucket, String objectKey, boolean allowOverwrite, boolean appendPrefix, ObjectMetadata metadata) {
return upload0(file, bucket, objectKey, allowOverwrite, appendPrefix, metadata);
}
// upload stream
public static UploadResult upload(InputStream inputStream, String objectKey) {
return upload(inputStream, objectKey, false);
}
public static UploadResult upload(InputStream inputStream, String objectKey, boolean appendPrefix) {
return upload(inputStream, objectKey, false, appendPrefix);
}
public static UploadResult upload(InputStream inputStream, String objectKey, boolean allowOverwrite, boolean appendPrefix) {
return upload(inputStream, ossProperties.getBucketName(), objectKey, allowOverwrite, appendPrefix);
}
public static UploadResult upload(InputStream inputStream, String bucket, String objectKey, boolean allowOverwrite, boolean appendPrefix) {
return upload0(inputStream, bucket, objectKey, allowOverwrite, appendPrefix, null);
}
public static UploadResult upload(InputStream inputStream, String bucket, String objectKey, boolean allowOverwrite, boolean appendPrefix, ObjectMetadata metadata) {
return upload0(inputStream, bucket, objectKey, allowOverwrite, appendPrefix, metadata);
}
public static DownloadResult download(String objectKey) {
return download(ossProperties.getBucketName(), objectKey);
}
public static DownloadResult download(String bucket, String objectKey) {
boolean exist = exist(bucket, objectKey);
if (!exist) {
throw new OssException(String.format("object [%s] does not exist in bucket [%s]", objectKey, bucket));
}
return download0(bucket, objectKey);
}
public static String generatePreSignedUrl(String objectKey) {
return generatePreSignedUrl(objectKey, Duration.ofHours(1));
}
public static String generatePreSignedUrl(String objectKey, Duration expiration) {
return generatePreSignedUrl(objectKey, expiration, true);
}
public static String generatePreSignedUrl(String objectKey, Duration expiration, boolean https) {
return generatePreSignedUrl(ossProperties.getBucketName(), objectKey, expiration, https);
}
public static String generatePreSignedUrl(String bucket, String objectKey, Duration expiration, boolean https) {
return generatePreSignedUrl(bucket, objectKey, expiration, https, null);
}
public static String generatePreSignedUrl(String bucket, String objectKey, Duration expiration, boolean https, ResponseHeaderOverrides headerOverrides) {
LocalDateTime expirationTime = LocalDateTime.now().plus(expiration);
Instant instant = expirationTime.atZone(ZoneId.systemDefault()).toInstant();
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucket, objectKey);
request.setMethod(HttpMethod.GET);
request.setExpiration(Date.from(instant));
request.setResponseHeaders(headerOverrides);
String preSignedUrl = getOssClient().generatePresignedUrl(request).toString();
if (HttpUtil.isHttp(preSignedUrl) && https) {
preSignedUrl = preSignedUrl.replaceFirst("http", "https");
}
return replaceWithDefaultDomain(preSignedUrl);
}
public static UploadCredentials generateUploadToken() {
AssumeRoleRequest request = new AssumeRoleRequest();
request.setSysMethod(MethodType.POST);
request.setRoleArn(stsProperties.getRoleArn());
request.setRoleSessionName(stsProperties.getRoleSessionName());
request.setDurationSeconds(stsProperties.getDurationSeconds());
AssumeRoleResponse response;
try {
response = getStsAcsClient().getAcsResponse(request);
} catch (com.aliyuncs.exceptions.ClientException e) {
return new UploadCredentials(e.getErrMsg());
}
Credentials credentials = response.getCredentials();
return new UploadCredentials(credentials);
}
public static String buildUploadPath(String objectKey) {
objectKey = removePossibleSlash(objectKey);
String date = DateTimeFormatters.DATE_WITH_SLASH.format(LocalDate.now());
return String.format("%s/%s/%s", directory, date, objectKey);
}
public static String withBucketPrefix(String objectKey) {
return withBucketPrefix(objectKey, ossProperties.getBucketName());
}
public static String withBucketPrefix(String objectKey, String bucket) {
// oss supports https by default
String prefix = "https://" + bucket + "." + ossProperties.getEndpoint();
return URLUtil.completeUrl(prefix, objectKey);
}
public static String replaceWithDefaultDomain(String ossUrl) {
return replaceWithDomain(ossProperties.getDomain(), ossUrl);
}
public static String replaceWithDomain(String domain, String ossUrl) {
if (!StringUtils.hasText(domain)) {
return ossUrl;
}
domain = URLUtil.normalize(domain);
URL originUrl = URLUtil.url(ossUrl);
URL domainUrl = URLUtil.url(domain);
try {
return new URL(domainUrl.getProtocol(), domainUrl.getHost(), domainUrl.getPort(), originUrl.getPath()).toString();
} catch (MalformedURLException e) {
log.warn("replace domain fail, return the default url instead", e);
return ossUrl;
}
}
public static CopyResult copyObject(String sourceKey, String destinationKey) {
return copyObject(ossProperties.getBucketName(), sourceKey, ossProperties.getBucketName(), destinationKey, false);
}
public static CopyResult copyObject(String sourceKey, String destinationBucket, String destinationKey) {
return copyObject(ossProperties.getBucketName(), sourceKey, destinationBucket, destinationKey, false);
}
public static CopyResult copyObject(String sourceBucket, String sourceKey, String destinationBucket, String destinationKey, boolean allowOverwrite) {
ValidateUtils.notEmpty(destinationKey, "destinationKey can not be empty");
ValidateUtils.notEmpty(destinationBucket, "destinationBucket can not be empty");
ValidateUtils.notEmpty(sourceKey, "sourceKey can not be empty");
ValidateUtils.notEmpty(sourceBucket, "sourceBucket can not be empty");
boolean exist = exist(sourceBucket, sourceKey);
if (!exist) {
String errorMsg = StrFormatter.format("sourceKey:{} at sourceBucket:{} not exist", sourceKey, sourceBucket);
return CopyResult.fail(sourceBucket, sourceKey, destinationBucket, destinationKey, errorMsg);
}
if (!allowOverwrite) {
boolean isExist = exist(destinationBucket, destinationKey);
if (isExist) {
String errorMsg = StrFormatter.format("destinationKey:{} at destinationBucket:{} has already exist");
return CopyResult.fail(sourceBucket, sourceKey, destinationBucket, destinationKey, errorMsg);
}
}
ossClient.copyObject(sourceBucket, sourceKey, destinationBucket, destinationKey);
return CopyResult.success(sourceBucket, sourceKey, destinationBucket, destinationKey);
}
public static DeleteResult deleteObject(String objectKey) {
return deleteObject(List.of(objectKey));
}
public static DeleteResult deleteObject(List<String> objectKeys) {
return deleteObject(ossProperties.getBucketName(), objectKeys);
}
public static DeleteResult deleteObject(String bucket, List<String> objectKeys) {
ValidateUtils.notEmpty(bucket, "bucket can not be empty");
ValidateUtils.notEmpty(objectKeys, "objectKeys can not be empty");
String notExistKeys = objectKeys.stream().filter(key -> !exist(bucket, key)).collect(Collectors.joining(","));
if (StringUtils.hasText(notExistKeys)) {
String errorMsg = StrFormatter.format("objectKeys:[{}] not exist", notExistKeys);
return DeleteResult.fail(bucket, objectKeys, errorMsg);
}
DeleteObjectsRequest request = new DeleteObjectsRequest(bucket).withKeys(objectKeys);
DeleteObjectsResult deleteObjectsResult = getOssClient().deleteObjects(request);
List<String> deletedObjects = deleteObjectsResult.getDeletedObjects();
return DeleteResult.success(bucket, deletedObjects);
}
public static boolean exist(String key) {
return exist(ossProperties.getBucketName(), key);
}
public static boolean exist(String bucket, String key) {
return getOssClient().doesObjectExist(bucket, key);
}
private static DownloadResult download0(String bucket, String objectKey) {
GetObjectRequest request = new GetObjectRequest(bucket, objectKey);
OSSObject ossObject = getOssClient().getObject(request);
return DownloadResult.success(bucket, objectKey, ossObject);
}
private static UploadResult upload0(Object object, String bucket, String objectKey, boolean allowOverwrite, boolean appendPrefix, ObjectMetadata metadata) {
ValidateUtils.notEmpty(objectKey, "objectKey must not be empty");
String uploadPath = objectKey;
if (appendPrefix) {
uploadPath = buildUploadPath(objectKey);
}
if (!allowOverwrite) {
boolean exist = exist(bucket, uploadPath);
if (exist) {
String errorMsg = String.format("object with path [%s] already exist in bucket [%s]", uploadPath, bucket);
return UploadResult.fail(bucket, objectKey, errorMsg);
}
}
if (object instanceof File) {
File fileToUpload = (File) object;
if (fileToUpload.isDirectory()) {
if (FileUtil.isDirEmpty(fileToUpload)) {
return UploadResult.fail(bucket, objectKey, "upload directory is empty");
}
String rootPath = FileUtil.getCanonicalPath(fileToUpload);
List<File> fileList = FileUtil.loopFiles(fileToUpload);
for (File file : fileList) {
String subPath = FileUtil.subPath(rootPath, file);
doUpload(file, bucket, uploadPath + "/" + subPath, metadata);
}
// upload directory has no url to return
return UploadResult.success(bucket, null, null);
}
}
return doUpload(object, bucket, uploadPath, metadata);
}
private static UploadResult doUpload(Object object, String bucket, String uploadPath, ObjectMetadata metadata) {
return doUpload(createPutRequest(object, bucket, uploadPath, metadata));
}
private static UploadResult doUpload(PutObjectRequest request) {
PutObjectResult result;
String bucketName = request.getBucketName();
String objectKey = request.getKey();
try {
result = getOssClient().putObject(request);
} catch (OSSException | ClientException exception) {
log.error("upload object to oss fail", exception);
return UploadResult.fail(bucketName, objectKey, exception.getMessage());
}
ResponseMessage response = result.getResponse();
if (response.isSuccessful()) {
String urlWithBucket = withBucketPrefix(objectKey, bucketName);
String finalUrl = replaceWithDefaultDomain(urlWithBucket);
return UploadResult.success(bucketName, objectKey, finalUrl);
}
String errorMsg = response.getErrorResponseAsString();
return UploadResult.fail(bucketName, objectKey, errorMsg);
}
private static DefaultAcsClient getStsAcsClient() {
if (stsAcsClient == null) {
throw new OssException("stsAcsClient is null");
}
return stsAcsClient;
}
private static OSS initOssClient(OssProperties ossProperties) {
return new OSSClientBuilder().build(ossProperties.getEndpoint(), ossProperties.getAccessKeyId(), ossProperties.getSecretAccessKey());
}
private static DefaultAcsClient initStsAcsClient(OssProperties ossProperties) {
StsProperties stsProperties = ossProperties.getSts();
DefaultProfile.addEndpoint("", "Sts", stsProperties.getEndpoint());
IClientProfile profile = DefaultProfile.getProfile("", ossProperties.getAccessKeyId(), ossProperties.getSecretAccessKey());
return new DefaultAcsClient(profile);
}
private static String removePossibleSlash(String objectKey) {
while (objectKey.startsWith("/")) {
objectKey = objectKey.substring(1);
}
return objectKey;
}
private static PutObjectRequest createPutRequest(Object object, String bucket, String objectKey, ObjectMetadata metadata) {
PutObjectRequest request;
if (object instanceof InputStream) {
request = new PutObjectRequest(bucket, objectKey, (InputStream) object, metadata);
} else if (object instanceof File) {
request = new PutObjectRequest(bucket, objectKey, (File) object, metadata);
} else {
throw new OssException("unknown upload object type");
}
request.setProcess("");
return request;
}
}
\ No newline at end of file
{
"groups": [
{
"name": "schbrain.oss",
"type": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties",
"sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties"
},
{
"name": "schbrain.oss.sts",
"type": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties$StsProperties",
"sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties",
"sourceMethod": "getSts()"
}
],
"properties": [
{
"name": "schbrain.oss.access-key-id",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties"
},
{
"name": "schbrain.oss.bucket-name",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties"
},
{
"name": "schbrain.oss.directory",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties"
},
{
"name": "schbrain.oss.domain",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties"
},
{
"name": "schbrain.oss.endpoint",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties"
},
{
"name": "schbrain.oss.name",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties"
},
{
"name": "schbrain.oss.namespace",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties"
},
{
"name": "schbrain.oss.prefix",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties"
},
{
"name": "schbrain.oss.secret-access-key",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties"
},
{
"name": "schbrain.oss.sts.duration-seconds",
"type": "java.lang.Long",
"sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties$StsProperties",
"defaultValue": 900
},
{
"name": "schbrain.oss.sts.endpoint",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties$StsProperties"
},
{
"name": "schbrain.oss.sts.role-arn",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties$StsProperties"
},
{
"name": "schbrain.oss.sts.role-session-name",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties$StsProperties"
}
],
"hints": []
}
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>schbrain-parent</artifactId>
<version>${revision}</version>
</parent>
<artifactId>starters</artifactId>
<packaging>pom</packaging>
<modules>
<module>apollo-spring-boot-starter</module>
<module>cache-spring-boot-starter</module>
<module>dubbo-spring-boot-starter</module>
<module>logger-spring-boot-starter</module>
<module>mybatis-spring-boot-starter</module>
<module>oss-spring-boot-starter</module>
<module>xxl-job-spring-boot-starter</module>
</modules>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>starters</artifactId>
<version>${revision}</version>
</parent>
<artifactId>xxl-job-spring-boot-starter</artifactId>
<dependencies>
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
</dependency>
<dependency>
<groupId>com.schbrain.framework</groupId>
<artifactId>logger-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package com.schbrain.framework.autoconfigure.xxl;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import com.xxl.job.core.thread.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
/**
* @author liaozan
* @since 2022/4/18
*/
@Slf4j
public class SchbrainXxlJobExecutor extends XxlJobSpringExecutor implements InitializingBean, DisposableBean {
private Field executorRegistryThreadStopField;
private Field jobLogFileCleanThreadStopField;
private Field triggerCallbackThreadStopField;
private volatile boolean started = false;
public SchbrainXxlJobExecutor() {
try {
this.executorRegistryThreadStopField = ExecutorRegistryThread.class.getDeclaredField("toStop");
ReflectionUtils.makeAccessible(executorRegistryThreadStopField);
} catch (Exception e) {
this.executorRegistryThreadStopField = null;
}
try {
this.triggerCallbackThreadStopField = TriggerCallbackThread.class.getDeclaredField("toStop");
ReflectionUtils.makeAccessible(triggerCallbackThreadStopField);
} catch (Exception e) {
this.triggerCallbackThreadStopField = null;
}
try {
this.jobLogFileCleanThreadStopField = JobLogFileCleanThread.class.getDeclaredField("toStop");
ReflectionUtils.makeAccessible(jobLogFileCleanThreadStopField);
} catch (Exception e) {
this.jobLogFileCleanThreadStopField = null;
}
}
@Override
public void start() throws Exception {
afterPropertiesSet();
}
@Override
public void afterPropertiesSet() throws Exception {
if (started) {
return;
}
super.start();
started = true;
log.info("Xxl-job started");
}
@Override
public void destroy() {
if (!started) {
return;
}
super.destroy();
resetThreadsStatus();
started = false;
log.info("Xxl-job destroyed");
}
/**
* Reset xxl-job related thread to make it support restartable
*/
private void resetThreadsStatus() {
ReflectionUtils.setField(executorRegistryThreadStopField, ExecutorRegistryThread.getInstance(), false);
ReflectionUtils.setField(triggerCallbackThreadStopField, TriggerCallbackThread.getInstance(), false);
ReflectionUtils.setField(jobLogFileCleanThreadStopField, JobLogFileCleanThread.getInstance(), false);
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.xxl;
import com.schbrain.common.util.ApplicationName;
import com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils;
import com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties;
import com.schbrain.framework.autoconfigure.xxl.condition.XxlJobShouldAvailableCondition;
import com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties;
import com.xxl.job.core.executor.XxlJobExecutor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.env.ConfigurableEnvironment;
import java.nio.file.Paths;
/**
* @author liaozan
* @since 2022/1/8
*/
@AutoConfiguration
@Conditional(XxlJobShouldAvailableCondition.class)
@EnableConfigurationProperties(XxlJobProperties.class)
public class XxlJobAutoConfiguration {
@Bean
@ConditionalOnMissingBean(XxlJobExecutor.class)
public SchbrainXxlJobExecutor schbrainXxlJobSpringExecutor(ConfigurableEnvironment environment) {
XxlJobProperties xxlJobProperties = ConfigUtils.loadConfig(environment, XxlJobProperties.class);
LoggingFileProperties loggingProperties = ConfigUtils.loadConfig(environment, LoggingFileProperties.class);
String applicationName = ApplicationName.get(environment);
SchbrainXxlJobExecutor executor = new SchbrainXxlJobExecutor();
executor.setAdminAddresses(xxlJobProperties.getAdminAddresses());
executor.setIp(xxlJobProperties.getIp());
executor.setPort(xxlJobProperties.getPort());
executor.setAppName(applicationName);
executor.setAccessToken(xxlJobProperties.getAccessToken());
executor.setLogPath(Paths.get(loggingProperties.getLogPath(), "xxl-job").toString());
executor.setLogRetentionDays(xxlJobProperties.getLogRetentionDays());
return executor;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.xxl.condition;
import com.schbrain.common.util.EnvUtils;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* @author liaozan
* @since 2022/4/20
*/
public class XxlJobShouldAvailableCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment environment = context.getEnvironment();
if (EnvUtils.runningOnCloudPlatform(environment)) {
return ConditionOutcome.match("runningOnCloudPlatform");
}
Boolean registerWithServer = environment.getProperty("schbrain.xxl.register", boolean.class, false);
if (Boolean.TRUE.equals(registerWithServer)) {
return ConditionOutcome.match("schbrain.xxl.register is true");
}
return ConditionOutcome.noMatch("It is neither running on the cloud platform nor enabled");
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.xxl.handler;
import com.fasterxml.jackson.databind.JsonNode;
import com.schbrain.common.util.JacksonUtils;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
import com.xxl.job.core.log.XxlJobLogger;
/**
* @author liaozan
* @since 2022/8/23
*/
public abstract class BaseJobHandler extends IJobHandler {
@Override
public ReturnT<String> execute(String param) throws Exception {
JsonNode jsonNode = JacksonUtils.getJsonNode(param);
return execute(jsonNode);
}
protected ReturnT<String> execute(JsonNode param) throws Exception {
XxlJobLogger.log("{} does not implement the execute method, so return success directly", getClass());
return success();
}
protected ReturnT<String> success() {
return ReturnT.SUCCESS;
}
protected ReturnT<String> failed(String errMsg) {
ReturnT<String> result = ReturnT.FAIL;
result.setMsg(errMsg);
return result;
}
}
\ No newline at end of file
package com.schbrain.framework.autoconfigure.xxl.properties;
import com.schbrain.common.util.InetUtils;
import com.schbrain.common.util.support.ConfigurableProperties;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author liaozan
* @since 2022/1/8
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties(prefix = "schbrain.xxl")
public class XxlJobProperties extends ConfigurableProperties {
private String adminAddresses;
private String ip = InetUtils.findFirstNonLoopBackHostInfo().getIpAddress();
private int port = -1;
private String accessToken;
private int logRetentionDays = 7;
private boolean register = false;
@Override
public String getDefaultNamespace() {
return "xxl-job-common";
}
}
\ No newline at end of file
{
"groups": [
{
"name": "schbrain.xxl",
"type": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties",
"sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties"
}
],
"properties": [
{
"name": "schbrain.xxl.access-token",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties"
},
{
"name": "schbrain.xxl.admin-addresses",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties"
},
{
"name": "schbrain.xxl.ip",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties"
},
{
"name": "schbrain.xxl.log-retention-days",
"type": "java.lang.Integer",
"sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties",
"defaultValue": 7
},
{
"name": "schbrain.xxl.name",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties"
},
{
"name": "schbrain.xxl.namespace",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties"
},
{
"name": "schbrain.xxl.port",
"type": "java.lang.Integer",
"sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties",
"defaultValue": -1
},
{
"name": "schbrain.xxl.prefix",
"type": "java.lang.String",
"sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties"
},
{
"name": "schbrain.xxl.register",
"type": "java.lang.Boolean",
"sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties",
"defaultValue": false
}
],
"hints": []
}
\ No newline at end of file
com.schbrain.framework.autoconfigure.xxl.XxlJobAutoConfiguration
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>schbrain-parent</artifactId>
<version>${revision}</version>
</parent>
<artifactId>support</artifactId>
<packaging>pom</packaging>
<modules>
<module>schbrain-spring-support</module>
<module>schbrain-base-dao</module>
</modules>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>support</artifactId>
<version>${revision}</version>
</parent>
<artifactId>schbrain-base-dao</artifactId>
<dependencies>
<dependency>
<groupId>com.schbrain.common</groupId>
<artifactId>common-util</artifactId>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package com.schbrain.framework.dao;
import com.github.pagehelper.Page;
import java.util.List;
/**
* description
*
* @author liwu on 2019/7/29
*/
public interface BaseDao<T> {
String getTableName();
/**
* 插入单个领域对象
* <p><strong>注意:</strong>插入时只会指定非<tt>null</tt>的领域对象属性对应的列</p>
*
* @param obj 待插入领域对象
* @return 插入是否成功
*/
Boolean add(T obj);
/**
* 批量插入领域对象
*
* @param objList 待插入领域对象列表
* @param fields 插入时指定的领域对象属性列表,如果为空表示对象的所有属性
* @return 影响行数
*/
Integer addList(List<T> objList, String... fields);
T getById(long id);
T getOneByObject(T obj);
T getOneByCondition(String whereClause, Object... objs);
List<T> listByIdList(List<Long> idList);
List<T> listByCondition(String whereClause, Object... objs);
List<T> listByObject(T obj);
Integer getCountByCondition(String whereClause, Object... objs);
Page<T> pageByCondition(int pageNum, int pageSize, String whereClause, Object... objs);
/**
* 分页获取列表
*
* @param pageNum 当前页码
* @param pageSize 当前页记录数
* @param whereClause <tt>where</tt>关键词后的条件语句
* @param orderByClause <tt>order by</tt>关键词后的排序语句,注意:语句中不支持参数
* @param objs <tt>where</tt>关键词后的条件语句中参数对应的值
* @return <tt>page</tt>对象,包含记录及分页信息
*/
Page<T> pageByCondition(int pageNum, int pageSize, String whereClause, String orderByClause, Object... objs);
Boolean deleteById(long id);
Integer deleteByIdList(List<Long> idList);
Integer deleteByCondition(String whereClause, Object... objs);
Boolean updateById(T obj);
Boolean updateByIdWithNull(T obj);
Integer updateByCondition(T obj, String whereClause, Object... objs);
Integer updateByCompleteSql(String completeSql, Object... objs);
}
\ No newline at end of file
package com.schbrain.framework.dao.mybatis;
import com.schbrain.framework.dao.BaseDao;
import com.schbrain.framework.dao.mybatis.mapper.BaseMapper;
import org.apache.ibatis.reflection.ExceptionUtil;
import org.mybatis.spring.SqlSessionTemplate;
import java.io.Serializable;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.*;
/**
* description
*
* @author liwu on 2019/7/29
*/
public class BaseMethodInvocationHandler<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -5077848994288044943L;
private final T originMapperProxy;
private final BaseMapper baseMapper;
public BaseMethodInvocationHandler(T originMapperProxy, SqlSessionTemplate sqlSession, Class<T> mapperInterface) {
this.originMapperProxy = originMapperProxy;
baseMapper = new BaseMapper(sqlSession, mapperInterface);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
return invokeDefaultMethod(proxy, method, args);
} else if (isBaseMethod(method)) {
return invokeBaseMethod(method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
return method.invoke(originMapperProxy, args);
}
private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable {
final Constructor<Lookup> constructor = Lookup.class.getDeclaredConstructor(Class.class, int.class);
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
final Class<?> declaringClass = method.getDeclaringClass();
return constructor
.newInstance(declaringClass, Lookup.PRIVATE | Lookup.PROTECTED | Lookup.PACKAGE | Lookup.PUBLIC)
.unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
}
private boolean isBaseMethod(Method method) {
return BaseDao.class.equals(method.getDeclaringClass());
}
private Object invokeBaseMethod(Method method, Object[] args) {
return baseMapper.invokeBaseMethod(method, args);
}
}
\ No newline at end of file
package com.schbrain.framework.dao.mybatis;
import com.github.pagehelper.PageInterceptor;
import com.schbrain.framework.dao.BaseDao;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.Configuration;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.mapper.MapperFactoryBean;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Properties;
/**
* description
*
* @author liwu on 2019/7/29
*/
public class CustomizeMapperFactoryBean<T> extends MapperFactoryBean<T> {
public CustomizeMapperFactoryBean() {
super();
}
public CustomizeMapperFactoryBean(Class<T> mapperInterface) {
super(mapperInterface);
}
@Override
@SuppressWarnings("unchecked")
public T getObject() throws Exception {
T originMapperProxy = super.getObject();
Class<T> mapperInterface = getMapperInterface();
if (!BaseDao.class.isAssignableFrom(mapperInterface)) {
return originMapperProxy;
}
// 判断是否已经添加分页过滤器
SqlSessionTemplate sqlSession = getSqlSessionTemplate();
Configuration configuration = sqlSession.getConfiguration();
List<Interceptor> interceptorList = configuration.getInterceptors();
boolean hasPageInterceptor = interceptorList.stream().anyMatch(e -> PageInterceptor.class.isAssignableFrom(e.getClass()));
if (!hasPageInterceptor) {
PageInterceptor pageInterceptor = new PageInterceptor();
pageInterceptor.setProperties(new Properties());
configuration.addInterceptor(pageInterceptor);
}
// 创建代理
BaseMethodInvocationHandler<T> handler = new BaseMethodInvocationHandler<>(originMapperProxy, sqlSession, mapperInterface);
return (T) Proxy.newProxyInstance(getMapperInterface().getClassLoader(), new Class[]{mapperInterface}, handler);
}
}
\ No newline at end of file
package com.schbrain.framework.dao.mybatis.annotation;
import java.lang.annotation.*;
/**
* description
*
* @author liwu on 2019/8/1
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface MapperConfig {
Class<?> domainClass();
String tableName();
}
\ No newline at end of file
package com.schbrain.framework.dao.mybatis.exception;
/**
* description
*
* @author liwu on 2019/8/1
*/
public class MapperParseException extends RuntimeException {
private static final long serialVersionUID = -4680549953650814744L;
public MapperParseException() {
super();
}
public MapperParseException(String message) {
super(message);
}
}
\ No newline at end of file
package com.schbrain.framework.dao.mybatis.mapper;
import cn.hutool.core.bean.BeanUtil;
import com.github.pagehelper.PageHelper;
import com.schbrain.framework.dao.mybatis.annotation.MapperConfig;
import com.schbrain.framework.dao.mybatis.exception.MapperParseException;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.mybatis.spring.SqlSessionTemplate;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
/**
* 每一个Mapper实例对应一个BaseMapper实例
*
* @author liwu on 2019/7/31
*/
public class BaseMapper {
private final SqlSessionTemplate sqlSession;
private final Class<?> mapperInterface;
private final BaseMapperStatement bms;
private Class<?> domainClass;
private String tableName;
private Field[] fields;
public BaseMapper(SqlSessionTemplate sqlSession, Class<?> mapperInterface) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
parseMapperClass();
bms = new BaseMapperStatement(sqlSession.getConfiguration(), mapperInterface, domainClass, tableName, fields);
}
public Object invokeBaseMethod(Method method, Object[] args) {
String methodName = method.getName();
switch (methodName) {
case "getTableName":
return tableName;
case "getById":
return getById(args);
case "listByIdList":
return getByIdList(args);
case "getCountByCondition":
return sqlSession.selectOne(bms.getCountByConditionMSId(), args);
case "getOneByCondition":
String sql = (String) args[0];
if (!StringUtils.containsIgnoreCase(sql, "limit")) {
args[0] += " limit 1";
}
return sqlSession.selectOne(bms.getListByConditionMSId(), args);
case "listByCondition":
return sqlSession.selectList(bms.getListByConditionMSId(), args);
case "getOneByObject":
return sqlSession.selectOne(bms.getGetOneByObjectMSId(), args[0]);
case "listByObject":
return sqlSession.selectList(bms.getListByObjectMSId(), args[0]);
case "pageByCondition":
return pageByCondition(args);
case "add":
return sqlSession.insert(bms.getAddMSId(), args[0]) > 0;
case "addList":
return sqlSession.insert(bms.getAddListMSId((String[]) args[1]), args[0]);
case "deleteById":
return sqlSession.delete(bms.getDeleteByIdMSId(), args[0]) > 0;
case "deleteByIdList":
return sqlSession.delete(bms.getDeleteByIdListMSId(), args[0]);
case "deleteByCondition":
return sqlSession.delete(bms.getDeleteByConditionMSId(), args);
case "updateById":
return updateById(args);
case "updateByIdWithNull":
return updateByIdWithNull(args);
case "updateByCondition":
return sqlSession.update(bms.getUpdateByConditionMSId(), args);
case "updateByCompleteSql":
return sqlSession.update(bms.getUpdateByCompleteSqlMSId(), args);
default:
return null;
}
}
private void parseMapperClass() {
MapperConfig mapperConfig = mapperInterface.getAnnotation(MapperConfig.class);
if (null == mapperConfig) {
throw new MapperParseException(String.format("Can not find MapperConfig annotation in mapper class %s ", mapperInterface.getName()));
}
tableName = mapperConfig.tableName();
if (StringUtils.isBlank(tableName)) {
throw new MapperParseException(String.format("Table name is blank in MapperConfig annotation in mapper class %s ", mapperInterface.getName()));
}
domainClass = mapperConfig.domainClass();
if (Class.class.equals(domainClass)) {
throw new MapperParseException(String.format("Domain class is not set in MapperConfig annotation in mapper class %s ", mapperInterface.getName()));
}
fields = domainClass.getDeclaredFields();
if (fields.length < 1) {
throw new MapperParseException(String.format("Domain class %s has no fields", domainClass.getName()));
}
}
private Object getById(Object[] args) {
if (null == args[0]) {
throw new IllegalArgumentException("Parameter id can not be null");
}
return sqlSession.selectOne(bms.getGetByIdMSId(), args[0]);
}
private Object getByIdList(Object[] args) {
if (null == args[0]) {
throw new IllegalArgumentException("Parameter idList can not be null");
}
// noinspection unchecked
List<Long> idList = (List<Long>) args[0];
if (CollectionUtils.isEmpty(idList)) {
throw new IllegalArgumentException("Parameter idList can not be empty");
}
return sqlSession.selectList(bms.getListByIdListMSId(), idList);
}
private Object pageByCondition(Object[] args) {
Object[] conditionArgs;
String orderBy = null;
if (4 == args.length) {
conditionArgs = new Object[]{args[2], args[3]};
} else {
conditionArgs = new Object[]{args[2], args[4]};
orderBy = (String) args[3];
}
if (StringUtils.isNotBlank(orderBy)) {
PageHelper.startPage((int) args[0], (int) args[1], orderBy);
} else {
PageHelper.startPage((int) args[0], (int) args[1]);
}
return sqlSession.selectList(bms.getListByConditionMSId(), conditionArgs);
}
private Object updateById(Object[] args) {
Object updateObj = args[0];
if (BeanUtil.getFieldValue(updateObj, "id") == null) {
throw new IllegalArgumentException("Id can not be null");
}
return sqlSession.update(bms.getUpdateByIdMSId(), updateObj) > 0;
}
private Object updateByIdWithNull(Object[] args) {
Object updateObj = args[0];
if (BeanUtil.getFieldValue(updateObj, "id") == null) {
throw new IllegalArgumentException("Id can not be null");
}
return sqlSession.update(bms.getUpdateByIdWithNullMSId(), updateObj) > 0;
}
}
\ No newline at end of file
package com.schbrain.framework.dao.mybatis.mapper;
import cn.hutool.crypto.SecureUtil;
import com.google.common.base.CaseFormat;
import com.google.common.base.Converter;
import com.schbrain.framework.dao.mybatis.mapper.sqlsource.*;
import org.apache.ibatis.builder.StaticSqlSource;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.Configuration;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
/**
* description
*
* @author liwu on 2019/8/2
*/
public class BaseMapperStatement {
private static final String GMT_UPDATE_FIELD = "gmtUpdate";
private static final String GMT_CREATE_FIELD = "gmtCreate";
private final Configuration configuration;
private final Class<?> mapperInterface;
private final LanguageDriver languageDriver;
private final Map<String, String> fieldColumnMap = new LinkedHashMap<>();
private final String tableName;
private final Class<?> domainClass;
private final Field[] fields;
private final List<ResultMap> objectResultMapList = new ArrayList<>(1);
private final List<ResultMap> intResultMapList = new ArrayList<>(1);
private String selectClause;
private String insertClause;
public BaseMapperStatement(Configuration configuration, Class<?> mapperInterface, Class<?> domainClass, String tableName, Field[] fields) {
this.configuration = configuration;
this.languageDriver = configuration.getDefaultScriptingLanguageInstance();
this.mapperInterface = mapperInterface;
this.tableName = tableName;
this.domainClass = domainClass;
this.fields = fields;
this.objectResultMapList.add(new ResultMap.Builder(configuration, "objectResultMap", domainClass, Collections.emptyList()).build());
this.intResultMapList.add(new ResultMap.Builder(configuration, "intResult", int.class, Collections.emptyList()).build());
setClause();
addAddMS();
addGetByIdMS();
addListByIdListMS();
addListByConditionMS();
addListByObjectMS();
addListOneByObjectMS();
addCountByConditionMS();
addDeleteByIdMS();
addDeleteByIdListMS();
addDeleteByConditionMS();
addUpdateByIdMS();
addUpdateByIdWithNullMS();
addUpdateByConditionMS();
addUpdateByCompleteSqlMS();
}
public String getAddMSId() {
String methodName = "add";
return mapperInterface.getName() + "." + methodName;
}
public String getAddListMSId(String... fields) {
String sql = getInsertListScript(fields);
String msId = mapperInterface.getName() + ".addList" + SecureUtil.md5(sql);
if (!configuration.hasStatement(msId)) {
addAddListMS(msId, sql);
}
return msId;
}
public String getGetByIdMSId() {
String methodName = "getById";
return mapperInterface.getName() + "." + methodName;
}
public String getListByIdListMSId() {
String methodName = "getByIdList";
return mapperInterface.getName() + "." + methodName;
}
public String getListByConditionMSId() {
String methodName = "listByCondition";
return mapperInterface.getName() + "." + methodName;
}
public String getListByObjectMSId() {
String methodName = "listByObject";
return mapperInterface.getName() + "." + methodName;
}
public String getGetOneByObjectMSId() {
String methodName = "getOneByObject";
return mapperInterface.getName() + "." + methodName;
}
public String getCountByConditionMSId() {
String methodName = "getCountByCondition";
return mapperInterface.getName() + "." + methodName;
}
public String getDeleteByIdMSId() {
String methodName = "deleteById";
return mapperInterface.getName() + "." + methodName;
}
public String getDeleteByIdListMSId() {
String methodName = "deleteByIdList";
return mapperInterface.getName() + "." + methodName;
}
public String getDeleteByConditionMSId() {
String methodName = "deleteByCondition";
return mapperInterface.getName() + "." + methodName;
}
public String getUpdateByIdMSId() {
String methodName = "updateById";
return mapperInterface.getName() + "." + methodName;
}
public String getUpdateByIdWithNullMSId() {
String methodName = "updateByIdWithNull";
return mapperInterface.getName() + "." + methodName;
}
public String getUpdateByConditionMSId() {
String methodName = "updateByCondition";
return mapperInterface.getName() + "." + methodName;
}
public String getUpdateByCompleteSqlMSId() {
String methodName = "updateByCompleteSql";
return mapperInterface.getName() + "." + methodName;
}
private void setClause() {
Converter<String, String> converter = CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.LOWER_UNDERSCORE);
StringBuilder selectClause = new StringBuilder("select ");
StringBuilder insertClause = new StringBuilder("insert into ");
insertClause.append(tableName).append("(");
for (Field field : fields) {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
String fieldName = field.getName();
String columnName = converter.convert(fieldName);
fieldColumnMap.put(fieldName, columnName);
selectClause.append(" ").append(columnName).append(",");
insertClause.append(" ").append(columnName).append(",");
}
selectClause.replace(selectClause.length() - 1, selectClause.length(), " from ").append(tableName).append(" ");
insertClause.replace(insertClause.length() - 1, insertClause.length(), ") values ");
this.selectClause = selectClause.toString();
this.insertClause = insertClause.toString();
}
///////////////////add
private void addAddMS() {
String sql = getInsertScript();
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, domainClass);
addUpdateMappedStatement(getAddMSId(), sqlSource, SqlCommandType.INSERT);
}
private String getInsertScript() {
StringBuilder sb = new StringBuilder("<script>");
sb.append("insert into ").append(tableName);
sb.append("<trim prefix='(' suffix=')' suffixOverrides=','>");
fieldColumnMap.forEach((field, column) -> {
if (GMT_UPDATE_FIELD.equals(field) || GMT_CREATE_FIELD.equals(field)) {
sb.append(column).append(",");
} else {
sb.append(String.format("<if test='%s != null'>%s,</if>", field, column));
}
});
sb.append("</trim>");
sb.append("<trim prefix='values (' suffix=')' suffixOverrides=','>");
fieldColumnMap.forEach((field, column) -> {
if (GMT_UPDATE_FIELD.equals(field)) {
sb.append("<choose>");
sb.append(String.format("<when test='%s != null'>#{%s},</when><otherwise>now(),</otherwise></choose>", GMT_UPDATE_FIELD, GMT_UPDATE_FIELD));
} else if (GMT_CREATE_FIELD.equals(field)) {
sb.append("<choose>");
sb.append(String.format("<when test='%s != null'>#{%s},</when><otherwise>now(),</otherwise></choose>", GMT_CREATE_FIELD, GMT_CREATE_FIELD));
} else {
sb.append(String.format("<if test='%s != null'>#{%s},</if>", field, field));
}
});
sb.append("</trim></script>");
return sb.toString();
}
///////////////////addList
private void addAddListMS(String msId, String sql) {
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, List.class);
addUpdateMappedStatement(msId, sqlSource, SqlCommandType.INSERT);
}
private String getInsertListScript(String... fields) {
String tmpInsertClause;
Set<String> fieldSet;
if (null == fields || 0 == fields.length) {
tmpInsertClause = insertClause;
fieldSet = fieldColumnMap.keySet();
} else {
fieldSet = new LinkedHashSet<>();
StringBuilder sb = new StringBuilder("insert into ");
sb.append(tableName).append("(");
for (String field : fields) {
if (GMT_UPDATE_FIELD.equals(field) || GMT_CREATE_FIELD.equals(field)) {
continue;
}
String column = fieldColumnMap.get(field);
if (null == column) {
throw new IllegalArgumentException("Can not find column of field:" + field);
}
fieldSet.add(field);
sb.append(column).append(",");
}
// 判断是否有gmtUpdate和gmtCreate
String gmtUpdate = fieldColumnMap.get(GMT_UPDATE_FIELD);
if (null != gmtUpdate) {
fieldSet.add(GMT_UPDATE_FIELD);
sb.append(gmtUpdate).append(",");
}
String gmtCreate = fieldColumnMap.get(GMT_CREATE_FIELD);
if (null != gmtCreate) {
fieldSet.add(GMT_CREATE_FIELD);
sb.append(gmtCreate).append(",");
}
sb.replace(sb.length() - 1, sb.length(), ") values ");
tmpInsertClause = sb.toString();
}
StringBuilder sql = new StringBuilder("<script>");
sql.append(tmpInsertClause).append("<foreach item = 'obj' collection='list' separator=','>");
sql.append("<trim prefix='(' suffix=')' suffixOverrides=','>");
for (String field : fieldSet) {
if (GMT_UPDATE_FIELD.equals(field)) {
sql.append(String.format("<choose><when test='obj.%s != null'>#{obj.%s},</when>", GMT_UPDATE_FIELD, GMT_UPDATE_FIELD)).append("<otherwise>now(),</otherwise></choose>");
} else if (GMT_CREATE_FIELD.equals(field)) {
sql.append(String.format("<choose><when test='obj.%s != null'>#{obj.%s},</when>", GMT_CREATE_FIELD, GMT_CREATE_FIELD)).append("<otherwise>now(),</otherwise></choose>");
} else {
sql.append(String.format("#{obj.%s},", field));
}
}
sql.append("</trim></foreach></script>");
return sql.toString();
}
////////////////getById
private void addGetByIdMS() {
String sql = selectClause + " where id = ?";
ParameterMapping pm = new ParameterMapping.Builder(configuration, "id", Long.class).build();
StaticSqlSource sqlSource = new StaticSqlSource(configuration, sql, Collections.singletonList(pm));
addSelectMappedStatement(getGetByIdMSId(), sqlSource);
}
///////////////////listByIdList
private void addListByIdListMS() {
String sql = "<script>" + selectClause
+ " where id in <foreach close=')' collection='list' item='id' open='(' separator=','>"
+ "#{id}</foreach></script>";
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, List.class);
addSelectMappedStatement(getListByIdListMSId(), sqlSource);
}
/////////////////listByCondition
private void addListByConditionMS() {
SqlSource sqlSource = new ListByConditionSqlSource(configuration, selectClause);
addSelectMappedStatement(getListByConditionMSId(), sqlSource);
}
/////////////////listByObject
private void addListByObjectMS() {
SqlSource sqlSource = languageDriver.createSqlSource(configuration, getListByObjectSql(false), domainClass);
addSelectMappedStatement(getListByObjectMSId(), sqlSource);
}
private String getListByObjectSql(boolean onlyOne) {
StringBuilder sql = new StringBuilder("<script>");
sql.append(selectClause).append("<where>");
fieldColumnMap.forEach((field, column) -> sql.append(String.format("<if test='%s != null'> and %s=#{%s} </if>", field, column, field)));
sql.append("</where>");
if (onlyOne) {
sql.append("limit 1");
}
sql.append("</script>");
return sql.toString();
}
/////////////////getOneByObject
private void addListOneByObjectMS() {
SqlSource sqlSource = languageDriver.createSqlSource(configuration, getListByObjectSql(true), domainClass);
addSelectMappedStatement(getGetOneByObjectMSId(), sqlSource);
}
/////////////////getCountByCondition
private void addCountByConditionMS() {
SqlSource sqlSource = new CountByConditionSqlSource(configuration, tableName);
addCountMappedStatement(getCountByConditionMSId(), sqlSource);
}
///////////////////deleteById
private void addDeleteByIdMS() {
String sql = "delete from " + tableName + " where id = ?";
ParameterMapping pm = new ParameterMapping.Builder(configuration, "id", Long.class).build();
StaticSqlSource sqlSource = new StaticSqlSource(configuration, sql, Collections.singletonList(pm));
addUpdateMappedStatement(getDeleteByIdMSId(), sqlSource, SqlCommandType.DELETE);
}
///////////////////deleteByIdList
private void addDeleteByIdListMS() {
String sql = "<script>delete from " + tableName
+ " where id in <foreach close=')' collection='list' item='id' open='(' separator=','>"
+ "#{id}</foreach></script>";
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, List.class);
addUpdateMappedStatement(getDeleteByIdListMSId(), sqlSource, SqlCommandType.DELETE);
}
/////////////////deleteByCondition
private void addDeleteByConditionMS() {
SqlSource sqlSource = new DeleteByConditionSqlSource(configuration, tableName);
addUpdateMappedStatement(getDeleteByConditionMSId(), sqlSource, SqlCommandType.DELETE);
}
/////////////////updateById
private void addUpdateByIdMS() {
String sql = getUpdateScript(false) + " where id = #{id}</script>";
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, domainClass);
addUpdateMappedStatement(getUpdateByIdMSId(), sqlSource, SqlCommandType.UPDATE);
}
private String getUpdateScript(boolean withNull) {
StringBuilder sb = new StringBuilder("<script>");
sb.append("update ").append(tableName).append("<set>");
fieldColumnMap.forEach((field, column) -> {
if (!GMT_UPDATE_FIELD.equals(field) && !"id".equals(column)) {
if (withNull) {
sb.append(String.format("%s = #{%s},", column, field));
} else {
sb.append(String.format("<if test='%s != null'>%s = #{%s},</if>", field, column, field));
}
}
});
if (fieldColumnMap.containsKey(GMT_UPDATE_FIELD)) {
sb.append(String.format("<choose><when test='%s != null'>gmt_update = #{%s}</when>", GMT_UPDATE_FIELD, GMT_UPDATE_FIELD)).append("<otherwise>gmt_update = now()</otherwise></choose>");
}
sb.append("</set>");
return sb.toString();
}
/////////////////updateByIdWithNull
private void addUpdateByIdWithNullMS() {
String sql = getUpdateScript(true) + " where id = #{id}</script>";
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, domainClass);
addUpdateMappedStatement(getUpdateByIdWithNullMSId(), sqlSource, SqlCommandType.UPDATE);
}
/////////////////updateByCondition
private void addUpdateByConditionMS() {
SqlSource sqlSource = new UpdateByConditionSqlSource(configuration, languageDriver, getUpdateByConditionScript());
addUpdateMappedStatement(getUpdateByConditionMSId(), sqlSource, SqlCommandType.UPDATE);
}
private String getUpdateByConditionScript() {
StringBuilder sb = new StringBuilder("<script>");
sb.append("update ").append(tableName).append("<set>");
fieldColumnMap.forEach((field, column) -> {
if (!GMT_UPDATE_FIELD.equals(field) && !"id".equals(field)) {
sb.append(String.format("<if test='obj.%s != null'>%s = #{obj.%s},</if>", field, column, field));
}
});
if (fieldColumnMap.containsKey(GMT_UPDATE_FIELD)) {
sb.append(String.format("<choose><when test='obj.%s != null'>gmt_update = #{obj.%s}</when>", GMT_UPDATE_FIELD, GMT_UPDATE_FIELD)).append("<otherwise>gmt_update = now()</otherwise></choose>");
}
sb.append("</set>");
return sb.toString();
}
/////////////////updateByCompleteSql
private void addUpdateByCompleteSqlMS() {
SqlSource sqlSource = new UpdateByCompleteSqlSource(configuration);
addUpdateMappedStatement(getUpdateByCompleteSqlMSId(), sqlSource, SqlCommandType.UPDATE);
}
///////////////////////////////add statement/////////////////////
private synchronized void addSelectMappedStatement(String msId, SqlSource sqlSource) {
if (configuration.hasStatement(msId)) {
return;
}
MappedStatement ms = new MappedStatement.Builder(configuration, msId, sqlSource, SqlCommandType.SELECT)
.resultMaps(objectResultMapList).flushCacheRequired(true).build();
configuration.addMappedStatement(ms);
}
private synchronized void addCountMappedStatement(String msId, SqlSource sqlSource) {
if (configuration.hasStatement(msId)) {
return;
}
MappedStatement ms = new MappedStatement.Builder(configuration, msId, sqlSource, SqlCommandType.SELECT)
.resultMaps(intResultMapList).build();
configuration.addMappedStatement(ms);
}
private synchronized void addUpdateMappedStatement(String msId, SqlSource sqlSource, SqlCommandType sqlCommandType) {
if (configuration.hasStatement(msId)) {
return;
}
MappedStatement ms;
if (SqlCommandType.INSERT == sqlCommandType) {
ms = new MappedStatement.Builder(configuration, msId, sqlSource, sqlCommandType)
.resultMaps(intResultMapList).keyGenerator(Jdbc3KeyGenerator.INSTANCE)
.keyColumn("id").keyProperty("id").build();
} else {
ms = new MappedStatement.Builder(configuration, msId, sqlSource, sqlCommandType)
.resultMaps(intResultMapList).build();
}
configuration.addMappedStatement(ms);
}
}
\ No newline at end of file
package com.schbrain.framework.dao.mybatis.mapper.sqlsource;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.builder.SqlSourceBuilder;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.parsing.PropertyParser;
import org.apache.ibatis.session.Configuration;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* description
*
* @author liwu on 2020/1/2
*/
public abstract class AbstractSqlSource implements SqlSource {
private static final String GENERIC_PARAM_NAME = "param";
private static final Pattern pattern = Pattern.compile("#\\{.+?}");
protected final Configuration configuration;
protected AbstractSqlSource(Configuration configuration) {
this.configuration = configuration;
}
protected String getGenericParamSqlAndCheckParamCount(Object[] args) {
String sql = (String) args[0];
if (StringUtils.isBlank(sql)) {
return "1=1";
}
Object[] params = ((Object[]) args[1]);
int paramCount = null == params ? 0 : params.length;
Matcher matcher = pattern.matcher(sql);
int i = 0;
StringBuilder result = new StringBuilder();
while (matcher.find()) {
matcher.appendReplacement(result, "#{" + GENERIC_PARAM_NAME + i++ + "}");
}
if (i != paramCount) {
throw new IllegalArgumentException("The number of params is not the same as placeholders");
}
if (paramCount == 0) {
return sql;
}
return matcher.appendTail(result).toString();
}
protected Map<String, Object> getSqlCommandParam(Object[] args) {
final Map<String, Object> param = new ParamMap<>();
if (null == args || args.length == 0) {
return param;
}
int i = 0;
for (Object arg : args) {
param.put(GENERIC_PARAM_NAME + i++, arg);
}
return param;
}
protected BoundSql getBoundSql(String sql, Object parameterObject) {
Object[] args = (Object[]) ((Map<?, ?>) parameterObject).get("array");
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Map<String, Object> paramMap = getSqlCommandParam((Object[]) args[1]);
String whereClause = replacePlaceholder(getGenericParamSqlAndCheckParamCount(args));
SqlSource sqlSource = sqlSourceParser.parse(sql + whereClause, Map.class, paramMap);
BoundSql boundSql = sqlSource.getBoundSql(paramMap);
paramMap.forEach(boundSql::setAdditionalParameter);
return boundSql;
}
protected String replacePlaceholder(String sql) {
return PropertyParser.parse(sql, configuration.getVariables());
}
}
\ No newline at end of file
package com.schbrain.framework.dao.mybatis.mapper.sqlsource;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.session.Configuration;
/**
* description
*
* @author liwu on 2020/1/2
*/
public class CountByConditionSqlSource extends AbstractSqlSource {
private final String selectClause;
public CountByConditionSqlSource(Configuration configuration, String tableName) {
super(configuration);
this.selectClause = "select count(*) from " + tableName + " where ";
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return getBoundSql(selectClause, parameterObject);
}
}
\ No newline at end of file
package com.schbrain.framework.dao.mybatis.mapper.sqlsource;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.session.Configuration;
/**
* description
*
* @author liwu on 2020/1/2
*/
public class DeleteByConditionSqlSource extends AbstractSqlSource {
private final String deleteClause;
public DeleteByConditionSqlSource(Configuration configuration, String tableName) {
super(configuration);
this.deleteClause = "delete from " + tableName + " where ";
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return getBoundSql(deleteClause, parameterObject);
}
}
\ No newline at end of file
package com.schbrain.framework.dao.mybatis.mapper.sqlsource;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.session.Configuration;
/**
* description
*
* @author liwu on 2020/1/2
*/
public class ListByConditionSqlSource extends AbstractSqlSource {
private final String selectClause;
public ListByConditionSqlSource(Configuration configuration, String selectClause) {
super(configuration);
this.selectClause = selectClause + " where ";
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return getBoundSql(selectClause, parameterObject);
}
}
\ No newline at end of file
package com.schbrain.framework.dao.mybatis.mapper.sqlsource;
import org.apache.ibatis.builder.SqlSourceBuilder;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.session.Configuration;
import java.util.Map;
/**
* description
*
* @author liwu on 2020/1/2
*/
public class UpdateByCompleteSqlSource extends AbstractSqlSource {
public UpdateByCompleteSqlSource(Configuration configuration) {
super(configuration);
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
Object[] args = (Object[]) ((Map) parameterObject).get("array");
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
String sql = replacePlaceholder(getGenericParamSqlAndCheckParamCount(args));
Map<String, Object> paramMap = getSqlCommandParam((Object[]) args[1]);
SqlSource sqlSource = sqlSourceParser.parse(sql, Map.class, paramMap);
BoundSql boundSql = sqlSource.getBoundSql(paramMap);
paramMap.forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}
\ No newline at end of file
package com.schbrain.framework.dao.mybatis.mapper.sqlsource;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.Configuration;
import java.util.Map;
/**
* description
*
* @author liwu on 2020/1/2
*/
public class UpdateByConditionSqlSource extends AbstractSqlSource {
private final String scriptSql;
private final LanguageDriver languageDriver;
public UpdateByConditionSqlSource(Configuration configuration, LanguageDriver languageDriver, String scriptSql) {
super(configuration);
this.languageDriver = languageDriver;
this.scriptSql = scriptSql;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return getBoundSql(scriptSql, parameterObject);
}
@Override
protected BoundSql getBoundSql(String scriptSql, Object parameterObject) {
Object[] args = (Object[]) ((Map) parameterObject).get("array");
Object[] conditionArgs = new Object[]{args[1], args[2]};
Map<String, Object> paramMap = getSqlCommandParam((Object[]) args[2]);
paramMap.put("obj", args[0]);
String whereClause = replacePlaceholder(getGenericParamSqlAndCheckParamCount(conditionArgs));
String sql = scriptSql + " where " + whereClause + "</script>";
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Map.class);
BoundSql boundSql = sqlSource.getBoundSql(paramMap);
paramMap.forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}
\ No newline at end of file
package com.schbrain.framework.dao.util;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* description
*
* @author liwu on 2019/8/19
*/
public class SQLUtil {
public static String buidInClause(String columnName, Class<?> valueType, List<?> valueList) {
if (CollectionUtils.isEmpty(valueList)) {
throw new IllegalArgumentException("Value list can not be empty.");
}
valueList = valueList.stream().filter(Objects::nonNull).collect(Collectors.toList());
if (CollectionUtils.isEmpty(valueList)) {
throw new IllegalArgumentException("Value list can not be empty.");
}
StringBuilder sb = new StringBuilder(" ");
sb.append(columnName).append(" in (");
if (Integer.class.isAssignableFrom(valueType) || Long.class.isAssignableFrom(valueType) ||
Short.class.isAssignableFrom(valueType) || Byte.class.isAssignableFrom(valueType) ||
Double.class.isAssignableFrom(valueType) || Float.class.isAssignableFrom(valueType)) {
sb.append(StringUtils.join(valueList, ',')).append(")");
} else {
valueList.forEach(e -> {
sb.append("'").append(escapeSql(e)).append("',");
});
sb.deleteCharAt(sb.length() - 1).append(")");
}
sb.append(" ");
return sb.toString();
}
private static String escapeSql(Object o) {
if (o instanceof String) {
return StringUtils.replace((String) o, "'", "''");
} else {
return String.valueOf(o);
}
}
}
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.schbrain.framework</groupId>
<artifactId>support</artifactId>
<version>${revision}</version>
</parent>
<artifactId>schbrain-spring-support</artifactId>
<dependencies>
<dependency>
<groupId>com.schbrain.common</groupId>
<artifactId>common-util</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package com.schbrain.framework.support.spring;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
import org.springframework.context.*;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
/**
* @author liaozan
* @since 2021/11/22
*/
@SuppressWarnings({"unchecked", "unused"})
public abstract class BeanPostProcessorAdapter<T> implements SmartInstantiationAwareBeanPostProcessor, ApplicationContextAware {
private final Class<T> beanType;
protected ConfigurableApplicationContext applicationContext;
protected ConfigurableListableBeanFactory beanFactory;
protected ConfigurableEnvironment environment;
public BeanPostProcessorAdapter() {
ParameterizedType parameterizedType = (ParameterizedType) getClass().getGenericSuperclass();
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
this.beanType = (Class<T>) actualTypeArguments[0];
}
@Override
public final Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
if (ClassUtils.isAssignable(beanType, beanClass)) {
return doPostProcessBeforeInstantiation((Class<T>) beanClass);
}
return null;
}
@Override
public final Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (ClassUtils.isAssignableValue(beanType, bean)) {
return doPostProcessBeforeInitialization((T) bean, beanName);
}
return bean;
}
public final boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
if (ClassUtils.isAssignableValue(beanType, bean)) {
return doPostProcessAfterInstantiation((T) bean);
}
return true;
}
@Override
public final Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (ClassUtils.isAssignableValue(beanType, bean)) {
return doPostProcessAfterInitialization((T) bean, beanName);
}
return bean;
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
Assert.isInstanceOf(ConfigurableApplicationContext.class, context, "require ConfigurableApplicationContext");
this.applicationContext = (ConfigurableApplicationContext) context;
this.beanFactory = this.applicationContext.getBeanFactory();
this.environment = this.applicationContext.getEnvironment();
}
public final Class<T> getBeanType() {
return beanType;
}
// region Instantiation
protected T doPostProcessBeforeInstantiation(Class<T> beanClass) {
return null;
}
protected boolean doPostProcessAfterInstantiation(T bean) {
processAfterInstantiation(bean);
return true;
}
protected void processAfterInstantiation(T bean) throws BeansException {
}
// endregion
// region Initialization
protected T doPostProcessBeforeInitialization(T bean, String beanName) throws BeansException {
processBeforeInitialization(bean, beanName);
return bean;
}
protected void processBeforeInitialization(T bean, String beanName) throws BeansException {
}
protected T doPostProcessAfterInitialization(T bean, String beanName) throws BeansException {
processAfterInitialization(bean, beanName);
return bean;
}
protected void processAfterInitialization(T bean, String beanName) throws BeansException {
}
// endregion
}
\ No newline at end of file
package com.schbrain.framework.support.spring;
import cn.hutool.core.lang.Singleton;
import org.springframework.boot.BootstrapContextClosedEvent;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import java.util.ArrayList;
import java.util.List;
/**
* @author liaozan
* @since 2021/11/22
*/
public class BootstrapContextListenerComposite implements ApplicationListener<BootstrapContextClosedEvent> {
private final List<EnvironmentPostProcessorAdapter> adapters = new ArrayList<>();
public static BootstrapContextListenerComposite getInstance() {
return Singleton.get(BootstrapContextListenerComposite.class);
}
public void addListener(EnvironmentPostProcessorAdapter adapter) {
if (adapters.contains(adapter)) {
return;
}
adapters.add(adapter);
}
@Override
public void onApplicationEvent(BootstrapContextClosedEvent event) {
if (adapters.isEmpty()) {
return;
}
AnnotationAwareOrderComparator.sort(adapters);
ConfigurableBootstrapContext bootstrapContext = (ConfigurableBootstrapContext) event.getBootstrapContext();
ConfigurableApplicationContext applicationContext = event.getApplicationContext();
for (EnvironmentPostProcessorAdapter adapter : adapters) {
adapter.onBootstrapContextClosed(bootstrapContext, applicationContext);
}
}
}
package com.schbrain.framework.support.spring;
import lombok.Getter;
import org.apache.commons.logging.Log;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.context.ConfigurableApplicationContext;
/**
* @author liaozan
* @since 2021/11/22
*/
@Getter
public abstract class EnvironmentPostProcessorAdapter implements EnvironmentPostProcessor {
private final Log log;
private final DeferredLogFactory deferredLogFactory;
private final ConfigurableBootstrapContext bootstrapContext;
public EnvironmentPostProcessorAdapter(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext) {
this.log = logFactory.getLog(getClass());
this.bootstrapContext = bootstrapContext;
this.deferredLogFactory = logFactory;
this.addListener(this.bootstrapContext);
}
protected void onBootstrapContextClosed(ConfigurableBootstrapContext bootstrapContext, ConfigurableApplicationContext applicationContext) {
onBootstrapContextClosed(applicationContext);
}
protected void onBootstrapContextClosed(ConfigurableApplicationContext applicationContext) {
}
private void addListener(ConfigurableBootstrapContext bootstrapContext) {
BootstrapContextListenerComposite listener = BootstrapContextListenerComposite.getInstance();
listener.addListener(this);
bootstrapContext.addCloseListener(listener);
}
}
\ No newline at end of file
package com.schbrain.framework.support.spring.defaults;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.util.ArrayUtil;
import com.schbrain.common.constants.DateTimeFormatters;
import com.schbrain.common.util.EnvUtils;
import com.schbrain.common.util.PortUtils;
import com.schbrain.framework.support.spring.EnvironmentPostProcessorAdapter;
import org.springframework.boot.*;
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Show;
import org.springframework.boot.actuate.info.InfoPropertiesInfoContributor.Mode;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.util.ClassUtils;
import org.springframework.util.unit.DataSize;
import java.util.*;
/**
* @author liaozan
* @since 2021/12/18
*/
public class DefaultPropertiesEnvironmentPostProcessor extends EnvironmentPostProcessorAdapter implements Ordered {
private static final String SPRING_PROFILE_ACTIVE = "spring.profiles.active";
private static final String DUBBO_REGISTER_KEY = "dubbo.registry.register";
public DefaultPropertiesEnvironmentPostProcessor(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext) {
super(logFactory, bootstrapContext);
}
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
Map<String, Object> defaultProperties = new HashMap<>();
// management
defaultProperties.put("management.endpoints.web.exposure.include", "*");
defaultProperties.put("management.endpoints.enabled-by-default", true);
defaultProperties.put("management.endpoint.health.show-details", Show.ALWAYS);
defaultProperties.put("management.endpoint.health.show-components", Show.ALWAYS);
defaultProperties.put("management.info.git.mode", Mode.FULL);
defaultProperties.put("management.server.port", PortUtils.findAvailablePort(1024));
// servlet
defaultProperties.put("spring.servlet.multipart.max-file-size", DataSize.ofBytes(-1));
defaultProperties.put("spring.servlet.multipart.max-request-size", DataSize.ofBytes(-1));
// mvc
defaultProperties.put("spring.mvc.throw-exception-if-no-handler-found", true);
// datetime
defaultProperties.put("spring.mvc.format.date", DateTimeFormatters.DATE_PATTERN);
defaultProperties.put("spring.mvc.format.time", DateTimeFormatters.TIME_PATTERN);
defaultProperties.put("spring.mvc.format.date-time", DateTimeFormatters.DATE_TIME_PATTERN);
defaultProperties.put("spring.jackson.date-format", DateTimeFormatters.DATE_TIME_PATTERN);
defaultProperties.put("spring.jackson.time-zone", TimeZone.getDefault());
// others
defaultProperties.put("spring.web.resources.add-mappings", false);
defaultProperties.put("spring.main.allow-circular-references", true);
defaultProperties.put("spring.main.banner-mode", Banner.Mode.OFF);
defaultProperties.put("server.shutdown", Shutdown.GRACEFUL);
// dubbo
configureDubboRegistrationIfPresent(environment, defaultProperties);
// active profile
configureActiveProfileIfPresent(environment, defaultProperties);
environment.setDefaultProfiles(EnvUtils.DEVELOPMENT);
DefaultPropertiesPropertySource.addOrMerge(defaultProperties, environment.getPropertySources());
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
private void configureActiveProfileIfPresent(ConfigurableEnvironment environment, Map<String, Object> defaultProperties) {
if (ArrayUtil.isEmpty(environment.getActiveProfiles())) {
environment.setActiveProfiles(EnvUtils.DEVELOPMENT);
defaultProperties.put(SPRING_PROFILE_ACTIVE, EnvUtils.DEVELOPMENT);
getLog().info(StrFormatter.format("{} is unset, set to {} by default", SPRING_PROFILE_ACTIVE, EnvUtils.DEVELOPMENT));
}
}
private void configureDubboRegistrationIfPresent(ConfigurableEnvironment environment, Map<String, Object> defaultProperties) {
if (!dubboInClassPath()) {
return;
}
if (EnvUtils.runningOnCloudPlatform(environment)) {
return;
}
if (!environment.containsProperty(DUBBO_REGISTER_KEY)) {
getLog().info(StrFormatter.format("Not running on CloudPlatform, {} is set to false by default", DUBBO_REGISTER_KEY));
getLog().info(StrFormatter.format("If you want force to register with Dubbo Registry, set {} = true", DUBBO_REGISTER_KEY));
defaultProperties.put(DUBBO_REGISTER_KEY, false);
}
}
private boolean dubboInClassPath() {
return ClassUtils.isPresent("org.apache.dubbo.config.bootstrap.DubboBootstrap", getClass().getClassLoader());
}
}
\ No newline at end of file
package com.schbrain.framework.support.spring.elasticsearch;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.impl.nio.reactor.IOReactorConfig;
import org.apache.http.protocol.HttpContext;
import org.elasticsearch.client.RestClientBuilder;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.elasticsearch.RestClientBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
/**
* @author liaozan
* @since 2022/5/6
*/
@AutoConfiguration
@ConditionalOnClass(ElasticsearchRestTemplate.class)
public class ElasticsearchFeatureAutoConfiguration {
@Bean
public RestClientBuilderCustomizer elasticsearchRestClientBuilderCustomizer() {
return new RestClientBuilderCustomizer() {
@Override
public void customize(RestClientBuilder builder) {
}
@Override
public void customize(HttpAsyncClientBuilder builder) {
IOReactorConfig ioReactorConfig = IOReactorConfig.custom().setSoKeepAlive(true).build();
builder.setDefaultIOReactorConfig(ioReactorConfig);
builder.setKeepAliveStrategy(new KeepAliveStrategy());
}
@Override
public void customize(Builder builder) {
builder.setSocketTimeout((int) Duration.ofMinutes(1).toMillis());
}
};
}
private static class KeepAliveStrategy extends DefaultConnectionKeepAliveStrategy {
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
long keepAliveDuration = super.getKeepAliveDuration(response, context);
if (keepAliveDuration < 0) {
keepAliveDuration = TimeUnit.MINUTES.toMillis(10);
}
return keepAliveDuration;
}
}
}
\ No newline at end of file
org.springframework.boot.env.EnvironmentPostProcessor=com.schbrain.framework.support.spring.defaults.DefaultPropertiesEnvironmentPostProcessor
\ No newline at end of file
com.schbrain.framework.support.spring.elasticsearch.ElasticsearchFeatureAutoConfiguration
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment