mirror of
https://github.com/201206030/novel-cloud.git
synced 2025-06-24 05:56:38 +00:00
refactor: 基于 novel 项目 & Spring Cloud 2022 & Spring Cloud Alibaba 2022 重构
This commit is contained in:
159
novel-core/novel-config/pom.xml
Normal file
159
novel-core/novel-config/pom.xml
Normal file
@ -0,0 +1,159 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>novel-core</artifactId>
|
||||
<groupId>io.github.xxyopen</groupId>
|
||||
<version>2.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>novel-config</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<!-- Exclude the Tomcat dependency -->
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Use Undertow instead -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-undertow</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- mybatis-plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 缓存相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 请求参数校验相关 -->
|
||||
<dependency>
|
||||
<groupId>org.hibernate.validator</groupId>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MQ 相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-amqp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ShardingSphere-JDBC -->
|
||||
<dependency>
|
||||
<groupId>org.apache.shardingsphere</groupId>
|
||||
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
|
||||
<version>${shardingsphere-jdbc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot 管理和监控 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redisson 相关 -->
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||
<version>${redisson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Aop 相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenAPI 3 -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>${springdoc-openapi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-bootstrap</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.github.xxyopen</groupId>
|
||||
<artifactId>novel-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,44 @@
|
||||
package io.github.xxyopen.novel.config;
|
||||
|
||||
import io.github.xxyopen.novel.common.constant.AmqpConsts;
|
||||
import org.springframework.amqp.core.Binding;
|
||||
import org.springframework.amqp.core.BindingBuilder;
|
||||
import org.springframework.amqp.core.FanoutExchange;
|
||||
import org.springframework.amqp.core.Queue;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* AMQP 配置类
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/25
|
||||
*/
|
||||
@Configuration
|
||||
public class AmqpConfig {
|
||||
|
||||
/**
|
||||
* 小说信息改变交换机
|
||||
*/
|
||||
@Bean
|
||||
public FanoutExchange bookChangeExchange() {
|
||||
return new FanoutExchange(AmqpConsts.BookChangeMq.EXCHANGE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Elasticsearch book 索引更新队列
|
||||
*/
|
||||
@Bean
|
||||
public Queue esBookUpdateQueue() {
|
||||
return new Queue(AmqpConsts.BookChangeMq.QUEUE_ES_UPDATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Elasticsearch book 索引更新队列绑定到小说信息改变交换机
|
||||
*/
|
||||
@Bean
|
||||
public Binding esBookUpdateQueueBinding() {
|
||||
return BindingBuilder.bind(esBookUpdateQueue()).to(bookChangeExchange());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package io.github.xxyopen.novel.config;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import io.github.xxyopen.novel.common.constant.CacheConsts;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.caffeine.CaffeineCache;
|
||||
import org.springframework.cache.support.SimpleCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||
import org.springframework.data.redis.cache.RedisCacheWriter;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
|
||||
/**
|
||||
* 缓存配置类
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/12
|
||||
*/
|
||||
@Configuration
|
||||
public class CacheConfig {
|
||||
|
||||
/**
|
||||
* Caffeine 缓存管理器
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
public CacheManager caffeineCacheManager() {
|
||||
SimpleCacheManager cacheManager = new SimpleCacheManager();
|
||||
|
||||
List<CaffeineCache> caches = new ArrayList<>(CacheConsts.CacheEnum.values().length);
|
||||
// 类型推断 var 非常适合 for 循环,JDK 10 引入,JDK 11 改进
|
||||
for (var c : CacheConsts.CacheEnum.values()) {
|
||||
if (c.isLocal()) {
|
||||
Caffeine<Object, Object> caffeine = Caffeine.newBuilder().recordStats()
|
||||
.maximumSize(c.getMaxSize());
|
||||
if (c.getTtl() > 0) {
|
||||
caffeine.expireAfterWrite(Duration.ofSeconds(c.getTtl()));
|
||||
}
|
||||
caches.add(new CaffeineCache(c.getName(), caffeine.build()));
|
||||
}
|
||||
}
|
||||
|
||||
cacheManager.setCaches(caches);
|
||||
return cacheManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis 缓存管理器
|
||||
*/
|
||||
@Bean
|
||||
public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
|
||||
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(
|
||||
connectionFactory);
|
||||
|
||||
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
|
||||
.disableCachingNullValues().prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX);
|
||||
|
||||
Map<String, RedisCacheConfiguration> cacheMap = new LinkedHashMap<>(
|
||||
CacheConsts.CacheEnum.values().length);
|
||||
// 类型推断 var 非常适合 for 循环,JDK 10 引入,JDK 11 改进
|
||||
for (var c : CacheConsts.CacheEnum.values()) {
|
||||
if (c.isRemote()) {
|
||||
if (c.getTtl() > 0) {
|
||||
cacheMap.put(c.getName(),
|
||||
RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
|
||||
.prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX)
|
||||
.entryTtl(Duration.ofSeconds(c.getTtl())));
|
||||
} else {
|
||||
cacheMap.put(c.getName(),
|
||||
RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
|
||||
.prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter,
|
||||
defaultCacheConfig, cacheMap);
|
||||
redisCacheManager.setTransactionAware(true);
|
||||
redisCacheManager.initializeCaches();
|
||||
return redisCacheManager;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package io.github.xxyopen.novel.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Mybatis-Plus 配置类
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/16
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
/**
|
||||
* 分页插件
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package io.github.xxyopen.novel.config;
|
||||
|
||||
import io.github.xxyopen.novel.common.constant.SystemConfigConsts;
|
||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.info.Info;
|
||||
import io.swagger.v3.oas.annotations.info.License;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
|
||||
/**
|
||||
* OpenApi 配置类
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/9/1
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("dev")
|
||||
@OpenAPIDefinition(info = @Info(title = "novel 项目接口文档", version = "v3.2.0", license = @License(name = "Apache 2.0", url = "https://www.apache.org/licenses/LICENSE-2.0")))
|
||||
@SecurityScheme(type = SecuritySchemeType.APIKEY, in = SecuritySchemeIn.HEADER, name = SystemConfigConsts.HTTP_AUTH_HEADER_NAME, description = "登录 token")
|
||||
public class OpenApiConfig {
|
||||
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package io.github.xxyopen.novel.config;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Xss 过滤配置属性
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/17
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "novel.xss")
|
||||
public record XssProperties(Boolean enabled, List<String> excludes) {
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package io.github.xxyopen.novel.config.annotation;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 分布式锁-Key 注解
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/6/20
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
|
||||
public @interface Key {
|
||||
|
||||
String expr() default "";
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package io.github.xxyopen.novel.config.annotation;
|
||||
|
||||
import io.github.xxyopen.novel.common.constant.ErrorCodeEnum;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
/**
|
||||
* 分布式锁 注解
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/6/20
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target(METHOD)
|
||||
public @interface Lock {
|
||||
|
||||
String prefix();
|
||||
|
||||
boolean isWait() default false;
|
||||
|
||||
long waitTime() default 3L;
|
||||
|
||||
ErrorCodeEnum failCode() default ErrorCodeEnum.OK;
|
||||
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package io.github.xxyopen.novel.config.aspect;
|
||||
|
||||
import io.github.xxyopen.novel.config.annotation.Key;
|
||||
import io.github.xxyopen.novel.config.annotation.Lock;
|
||||
import io.github.xxyopen.novel.config.exception.BusinessException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.common.TemplateParserContext;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 分布式锁 切面
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/6/20
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LockAspect {
|
||||
|
||||
private final RedissonClient redissonClient;
|
||||
|
||||
private static final String KEY_PREFIX = "Lock";
|
||||
|
||||
private static final String KEY_SEPARATOR = "::";
|
||||
|
||||
@Around(value = "@annotation(io.github.xxyopen.novel.config.annotation.Lock)")
|
||||
@SneakyThrows
|
||||
public Object doAround(ProceedingJoinPoint joinPoint) {
|
||||
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
|
||||
Method targetMethod = methodSignature.getMethod();
|
||||
Lock lock = targetMethod.getAnnotation(Lock.class);
|
||||
String lockKey = KEY_PREFIX + buildLockKey(lock.prefix(), targetMethod,
|
||||
joinPoint.getArgs());
|
||||
RLock rLock = redissonClient.getLock(lockKey);
|
||||
if (lock.isWait() ? rLock.tryLock(lock.waitTime(), TimeUnit.SECONDS) : rLock.tryLock()) {
|
||||
try {
|
||||
return joinPoint.proceed();
|
||||
} finally {
|
||||
rLock.unlock();
|
||||
}
|
||||
}
|
||||
throw new BusinessException(lock.failCode());
|
||||
}
|
||||
|
||||
private String buildLockKey(String prefix, Method method, Object[] args) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (StringUtils.hasText(prefix)) {
|
||||
builder.append(KEY_SEPARATOR).append(prefix);
|
||||
}
|
||||
Parameter[] parameters = method.getParameters();
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
builder.append(KEY_SEPARATOR);
|
||||
if (parameters[i].isAnnotationPresent(Key.class)) {
|
||||
Key key = parameters[i].getAnnotation(Key.class);
|
||||
builder.append(parseKeyExpr(key.expr(), args[i]));
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String parseKeyExpr(String expr, Object arg) {
|
||||
if (!StringUtils.hasText(expr)) {
|
||||
return arg.toString();
|
||||
}
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
Expression expression = parser.parseExpression(expr, new TemplateParserContext());
|
||||
return expression.getValue(arg, String.class);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package io.github.xxyopen.novel.config.exception;
|
||||
|
||||
import io.github.xxyopen.novel.common.constant.ErrorCodeEnum;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 自定义业务异常,用于处理用户请求时,业务错误时抛出
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/11
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
private final ErrorCodeEnum errorCodeEnum;
|
||||
|
||||
public BusinessException(ErrorCodeEnum errorCodeEnum) {
|
||||
// 不调用父类 Throwable的fillInStackTrace() 方法生成栈追踪信息,提高应用性能
|
||||
// 构造器之间的调用必须在第一行
|
||||
super(errorCodeEnum.getMessage(), null, false, false);
|
||||
this.errorCodeEnum = errorCodeEnum;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package io.github.xxyopen.novel.config.exception;
|
||||
|
||||
import io.github.xxyopen.novel.common.constant.ErrorCodeEnum;
|
||||
import io.github.xxyopen.novel.common.resp.RestResp;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* 通用的异常处理器
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/11
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class CommonExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理数据校验异常
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
public RestResp<Void> handlerBindException(BindException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return RestResp.fail(ErrorCodeEnum.USER_REQUEST_PARAM_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务异常
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public RestResp<Void> handlerBusinessException(BusinessException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return RestResp.fail(e.getErrorCodeEnum());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统异常
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public RestResp<Void> handlerException(Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return RestResp.error();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package io.github.xxyopen.novel.config.filter;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
|
||||
import io.github.xxyopen.novel.config.XssProperties;
|
||||
import io.github.xxyopen.novel.config.wrapper.XssHttpServletRequestWrapper;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.FilterConfig;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.annotation.WebFilter;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 防止 XSS 攻击的过滤器
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/17
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(value = "novel.xss.enabled", havingValue = "true")
|
||||
@WebFilter(urlPatterns = "/*", filterName = "xssFilter")
|
||||
@EnableConfigurationProperties(value = {XssProperties.class})
|
||||
@RequiredArgsConstructor
|
||||
public class XssFilter implements Filter {
|
||||
|
||||
private final XssProperties xssProperties;
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
Filter.super.init(filterConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
|
||||
FilterChain filterChain) throws IOException, ServletException {
|
||||
HttpServletRequest req = (HttpServletRequest) servletRequest;
|
||||
if (handleExcludeUrl(req)) {
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
return;
|
||||
}
|
||||
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(
|
||||
(HttpServletRequest) servletRequest);
|
||||
filterChain.doFilter(xssRequest, servletResponse);
|
||||
}
|
||||
|
||||
private boolean handleExcludeUrl(HttpServletRequest request) {
|
||||
if (CollectionUtils.isEmpty(xssProperties.excludes())) {
|
||||
return false;
|
||||
}
|
||||
String url = request.getServletPath();
|
||||
for (String pattern : xssProperties.excludes()) {
|
||||
Pattern p = Pattern.compile("^" + pattern);
|
||||
Matcher m = p.matcher(url);
|
||||
if (m.find()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
Filter.super.destroy();
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package io.github.xxyopen.novel.config.interceptor;
|
||||
|
||||
import io.github.xxyopen.novel.common.auth.JwtUtils;
|
||||
import io.github.xxyopen.novel.common.auth.UserHolder;
|
||||
import io.github.xxyopen.novel.common.constant.SystemConfigConsts;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
/**
|
||||
* Token 解析拦截器
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/27
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class TokenParseInterceptor implements HandlerInterceptor {
|
||||
|
||||
@SuppressWarnings("NullableProblems")
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
|
||||
Object handler) throws Exception {
|
||||
// 获取登录 JWT
|
||||
String token = request.getHeader(SystemConfigConsts.HTTP_AUTH_HEADER_NAME);
|
||||
if (StringUtils.hasText(token)) {
|
||||
// 解析 token 并保存
|
||||
UserHolder.setUserId(JwtUtils.parseToken(token, SystemConfigConsts.NOVEL_FRONT_KEY));
|
||||
}
|
||||
return HandlerInterceptor.super.preHandle(request, response, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* DispatcherServlet 完全处理完请求后调用,出现异常照常调用
|
||||
*/
|
||||
@SuppressWarnings("NullableProblems")
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
|
||||
throws Exception {
|
||||
// 清理当前线程保存的用户数据
|
||||
UserHolder.clear();
|
||||
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package io.github.xxyopen.novel.config.wrapper;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* XSS 过滤处理
|
||||
*
|
||||
* @author xiongxiaoyang
|
||||
* @date 2022/5/17
|
||||
*/
|
||||
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private static final Map<String, String> REPLACE_RULE = new HashMap<>();
|
||||
|
||||
static {
|
||||
REPLACE_RULE.put("<", "<");
|
||||
REPLACE_RULE.put(">", ">");
|
||||
}
|
||||
|
||||
public XssHttpServletRequestWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getParameterValues(String name) {
|
||||
String[] values = super.getParameterValues(name);
|
||||
if (values != null) {
|
||||
int length = values.length;
|
||||
String[] escapeValues = new String[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
escapeValues[i] = values[i];
|
||||
int index = i;
|
||||
REPLACE_RULE.forEach(
|
||||
(k, v) -> escapeValues[index] = escapeValues[index].replaceAll(k, v));
|
||||
}
|
||||
return escapeValues;
|
||||
}
|
||||
return new String[0];
|
||||
}
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
#--------------------- Spring Cloud 配置-------------------
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: 192.168.10.110:8848
|
||||
openfeign:
|
||||
lazy-attributes-resolution: true
|
||||
|
||||
feign:
|
||||
sentinel:
|
||||
enabled: true
|
||||
|
||||
--- #--------------------------通用配置-------------------------
|
||||
spring:
|
||||
jackson:
|
||||
generator:
|
||||
# JSON 序列化时,将所有 Number 类型的属性都转为 String 类型返回,避免前端数据精度丢失的问题。
|
||||
# 由于 Javascript 标准规定所有数字处理都应使用 64 位 IEEE 754 浮点值完成,
|
||||
# 结果是某些 64 位整数值无法准确表示(尾数只有 51 位宽)
|
||||
write-numbers-as-strings: true
|
||||
|
||||
--- #---------------------数据库配置---------------------------
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/novel_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: test123456
|
||||
# ShardingSphere-JDBC 配置
|
||||
# 配置是 ShardingSphere-JDBC 中唯一与应用开发者交互的模块,
|
||||
# 通过它可以快速清晰的理解 ShardingSphere-JDBC 所提供的功能。
|
||||
shardingsphere:
|
||||
# 是否开启分库分表
|
||||
enabled: false
|
||||
props:
|
||||
# 是否在日志中打印 SQL
|
||||
sql-show: true
|
||||
# 模式配置
|
||||
mode:
|
||||
# 单机模式
|
||||
type: Standalone
|
||||
repository:
|
||||
# 文件持久化
|
||||
type: File
|
||||
props:
|
||||
# 元数据存储路径
|
||||
path: .shardingsphere
|
||||
# 使用本地配置覆盖持久化配置
|
||||
overwrite: true
|
||||
# 数据源配置
|
||||
datasource:
|
||||
names: ds_0
|
||||
ds_0:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
driverClassName: com.mysql.cj.jdbc.Driver
|
||||
jdbcUrl: jdbc:mysql://localhost:3306/novel_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: test123456
|
||||
# 规则配置
|
||||
rules:
|
||||
# 数据分片
|
||||
sharding:
|
||||
tables:
|
||||
# book_content 表
|
||||
book_content:
|
||||
# 数据节点
|
||||
actual-data-nodes: ds_$->{0}.book_content$->{0..9}
|
||||
# 分表策略
|
||||
table-strategy:
|
||||
standard:
|
||||
# 分片列名称
|
||||
sharding-column: chapter_id
|
||||
# 分片算法名称
|
||||
sharding-algorithm-name: bookContentSharding
|
||||
sharding-algorithms:
|
||||
bookContentSharding:
|
||||
# 行表达式分片算法,使用 Groovy 的表达式,提供对 SQL 语句中的 = 和 IN 的分片操作支持
|
||||
type: INLINE
|
||||
props:
|
||||
# 分片算法的行表达式
|
||||
algorithm-expression: book_content$->{chapter_id % 10}
|
||||
|
||||
--- #---------------------中间件配置---------------------------
|
||||
spring:
|
||||
data:
|
||||
# Redis 配置
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
# password: 123456
|
||||
|
||||
# RabbitMQ 配置
|
||||
rabbitmq:
|
||||
addresses: "amqp://xxyopen:test123456@192.168.10.110"
|
||||
virtual-host: novel
|
||||
template:
|
||||
retry:
|
||||
# 开启重试
|
||||
enabled: true
|
||||
# 最大重试次数
|
||||
max-attempts: 3
|
||||
# 第一次和第二次重试之间的持续时间
|
||||
initial-interval: "3s"
|
||||
|
||||
--- #----------------------安全配置---------------------------
|
||||
# Actuator 端点管理
|
||||
management:
|
||||
# 端点公开配置
|
||||
endpoints:
|
||||
# 通过 HTTP 公开的 Web 端点
|
||||
web:
|
||||
exposure:
|
||||
# 公开所有的 Web 端点
|
||||
include: "*"
|
||||
info:
|
||||
env:
|
||||
# 公开所有以 info. 开头的环境属性
|
||||
enabled: true
|
||||
health:
|
||||
rabbit:
|
||||
# 关闭 rabbitmq 的健康检查
|
||||
enabled: true
|
||||
elasticsearch:
|
||||
# 关闭 elasticsearch 的健康检查
|
||||
enabled: true
|
||||
|
||||
--- #---------------------自定义配置----------------------------
|
||||
novel:
|
||||
# XSS 过滤配置
|
||||
xss:
|
||||
# 过滤开关
|
||||
enabled: true
|
||||
# 排除链接
|
||||
excludes:
|
||||
- /system/notice/*
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,15 @@
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
config:
|
||||
server-addr: 192.168.10.110:8848
|
||||
file-extension: yml
|
||||
extension-configs[0]:
|
||||
data‐id: novel-mysql.yml
|
||||
refresh: true
|
||||
extension-configs[1]:
|
||||
data‐id: novel-redis.yml
|
||||
refresh: true
|
||||
extension-configs[2]:
|
||||
data‐id: novel-rabbitmq.yml
|
||||
refresh: true
|
Reference in New Issue
Block a user