mirror of
https://github.com/continew-org/continew-starter.git
synced 2025-09-09 20:57:23 +08:00
feat(web): 新增 Web 模块,从核心模块拆分 Web 相关自动配置
This commit is contained in:
60
continew-starter-web/pom.xml
Normal file
60
continew-starter-web/pom.xml
Normal file
@@ -0,0 +1,60 @@
|
||||
<?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>top.charles7c.continew</groupId>
|
||||
<artifactId>continew-starter</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>continew-starter-web</artifactId>
|
||||
<description>ContiNew Starter Web 模块</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web(提供 Spring MVC Web 开发能力,默认内置 Tomcat 服务器) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<!-- 移除内置 Tomcat 服务器 -->
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Undertow 服务器(采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-undertow</artifactId>
|
||||
<!-- 移除 websocket 依赖,后续使用 websocket 可考虑由 Netty 提供。另可解决日志警告:UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used -->
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>io.undertow</groupId>
|
||||
<artifactId>undertow-websockets-jsr</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Hibernate Validator -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- API 文档模块 -->
|
||||
<dependency>
|
||||
<groupId>top.charles7c.continew</groupId>
|
||||
<artifactId>continew-starter-api-doc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON 模块 - Jackson -->
|
||||
<dependency>
|
||||
<groupId>top.charles7c.continew</groupId>
|
||||
<artifactId>continew-starter-json-jackson</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <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 top.charles7c.continew.starter.web.annotation;
|
||||
|
||||
import org.springframework.context.annotation.Import;
|
||||
import top.charles7c.continew.starter.web.autoconfigure.exception.GlobalExceptionHandlerAutoConfiguration;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 全局异常、错误处理器启用注解
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.2.0
|
||||
*/
|
||||
@Target({ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Import({GlobalExceptionHandlerAutoConfiguration.class})
|
||||
public @interface EnableGlobalExceptionHandler {
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <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 top.charles7c.continew.starter.web.autoconfigure.cors;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
import top.charles7c.continew.starter.core.constant.PropertiesConstants;
|
||||
import top.charles7c.continew.starter.core.constant.StringConstants;
|
||||
|
||||
/**
|
||||
* 跨域自动配置
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Lazy
|
||||
@AutoConfiguration
|
||||
@ConditionalOnWebApplication
|
||||
@ConditionalOnProperty(prefix = PropertiesConstants.CORS, name = PropertiesConstants.ENABLED, havingValue = "true")
|
||||
@EnableConfigurationProperties(CorsProperties.class)
|
||||
public class CorsAutoConfiguration {
|
||||
|
||||
/**
|
||||
* 跨域过滤器
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public CorsFilter corsFilter(CorsProperties properties) {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
// 设置跨域允许时间
|
||||
config.setMaxAge(1800L);
|
||||
// 配置允许跨域的域名
|
||||
if (properties.getAllowedOrigins().contains(StringConstants.ASTERISK)) {
|
||||
config.addAllowedOriginPattern(StringConstants.ASTERISK);
|
||||
} else {
|
||||
// 配置为 true 后则必须配置允许跨域的域名,且不允许配置为 *
|
||||
config.setAllowCredentials(true);
|
||||
properties.getAllowedOrigins().forEach(config::addAllowedOrigin);
|
||||
}
|
||||
// 配置允许跨域的请求方式
|
||||
properties.getAllowedMethods().forEach(config::addAllowedMethod);
|
||||
// 配置允许跨域的请求头
|
||||
properties.getAllowedHeaders().forEach(config::addAllowedHeader);
|
||||
// 配置允许跨域的响应头
|
||||
properties.getExposedHeaders().forEach(config::addExposedHeader);
|
||||
// 添加映射路径,拦截一切请求
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration(StringConstants.PATH_PATTERN, config);
|
||||
CorsFilter corsFilter = new CorsFilter(source);
|
||||
log.debug("[ContiNew Starter] - Auto Configuration 'CorsFilter' completed initialization.");
|
||||
return corsFilter;
|
||||
}
|
||||
}
|
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <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 top.charles7c.continew.starter.web.autoconfigure.cors;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import top.charles7c.continew.starter.core.constant.PropertiesConstants;
|
||||
import top.charles7c.continew.starter.core.constant.StringConstants;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 跨域配置属性
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(PropertiesConstants.CORS)
|
||||
public class CorsProperties {
|
||||
|
||||
/**
|
||||
* 是否启用跨域配置
|
||||
*/
|
||||
private boolean enabled = false;
|
||||
|
||||
/**
|
||||
* 允许跨域的域名
|
||||
*/
|
||||
private List<String> allowedOrigins = new ArrayList<>(ALL);
|
||||
|
||||
/**
|
||||
* 允许跨域的请求方式
|
||||
*/
|
||||
private List<String> allowedMethods = new ArrayList<>(ALL);
|
||||
|
||||
/**
|
||||
* 允许跨域的请求头
|
||||
*/
|
||||
private List<String> allowedHeaders = new ArrayList<>(ALL);
|
||||
|
||||
/**
|
||||
* 允许跨域的响应头
|
||||
*/
|
||||
private List<String> exposedHeaders = new ArrayList<>();
|
||||
|
||||
private static final List<String> ALL = Collections.singletonList(StringConstants.ASTERISK);
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <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 top.charles7c.continew.starter.web.autoconfigure.exception;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import top.charles7c.continew.starter.web.core.exception.GlobalErrorHandler;
|
||||
import top.charles7c.continew.starter.web.core.exception.GlobalExceptionHandler;
|
||||
|
||||
/**
|
||||
* 全局异常处理器自动配置
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Import({GlobalExceptionHandler.class, GlobalErrorHandler.class})
|
||||
@ConditionalOnMissingBean(BasicErrorController.class)
|
||||
public class GlobalExceptionHandlerAutoConfiguration {
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct() {
|
||||
log.debug("[ContiNew Starter] - Auto Configuration 'Extension-Global Exception Handler' completed " + "initialization.");
|
||||
}
|
||||
}
|
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <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 top.charles7c.continew.starter.web.core.exception;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
|
||||
import org.springframework.boot.web.servlet.error.ErrorAttributes;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import top.charles7c.continew.starter.web.model.R;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 全局错误处理器
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
public class GlobalErrorHandler extends BasicErrorController {
|
||||
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
public GlobalErrorHandler(ErrorAttributes errorAttributes,
|
||||
ServerProperties serverProperties,
|
||||
List<ErrorViewResolver> errorViewResolvers) {
|
||||
super(errorAttributes, serverProperties.getError(), errorViewResolvers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
|
||||
Map<String, Object> errorAttributeMap = super.getErrorAttributes(request, super.getErrorAttributeOptions(request, MediaType.TEXT_HTML));
|
||||
String path = (String)errorAttributeMap.get("path");
|
||||
HttpStatus status = super.getStatus(request);
|
||||
R<Object> result = R.fail(status.value(), (String)errorAttributeMap.get("error"));
|
||||
result.setData(path);
|
||||
try {
|
||||
response.setStatus(HttpStatus.OK.value());
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
objectMapper.writeValue(response.getWriter(), result);
|
||||
} catch (IOException e) {
|
||||
log.error("请求地址 [{}],默认错误处理时发生 IO 异常。", path, e);
|
||||
}
|
||||
log.error("请求地址 [{}],发生错误,错误信息:{}。", path, JSONUtil.toJsonStr(errorAttributeMap));
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
|
||||
Map<String, Object> errorAttributeMap = super.getErrorAttributes(request, super.getErrorAttributeOptions(request, MediaType.ALL));
|
||||
String path = (String)errorAttributeMap.get("path");
|
||||
HttpStatus status = super.getStatus(request);
|
||||
R<Object> result = R.fail(status.value(), (String)errorAttributeMap.get("error"));
|
||||
result.setData(path);
|
||||
log.error("请求地址 [{}],发生错误,错误信息:{}。", path, JSONUtil.toJsonStr(errorAttributeMap));
|
||||
return new ResponseEntity<>(BeanUtil.beanToMap(result), HttpStatus.OK);
|
||||
}
|
||||
}
|
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <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 top.charles7c.continew.starter.web.core.exception;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import top.charles7c.continew.starter.core.constant.StringConstants;
|
||||
import top.charles7c.continew.starter.core.exception.BadRequestException;
|
||||
import top.charles7c.continew.starter.core.exception.BusinessException;
|
||||
import top.charles7c.continew.starter.core.util.ExceptionUtils;
|
||||
import top.charles7c.continew.starter.web.model.R;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 拦截自定义验证异常-错误请求
|
||||
*/
|
||||
@ExceptionHandler(BadRequestException.class)
|
||||
public R handleBadRequestException(BadRequestException e, HttpServletRequest request) {
|
||||
log.warn("请求地址 [{}],自定义验证失败。", request.getRequestURI(), e);
|
||||
return R.fail(HttpStatus.BAD_REQUEST.value(), e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截校验异常-违反约束异常
|
||||
*/
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public R constraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
|
||||
log.warn("请求地址 [{}],参数验证失败。", request.getRequestURI(), e);
|
||||
String errorMsg = CollUtil.join(e
|
||||
.getConstraintViolations(), StringConstants.CHINESE_COMMA, ConstraintViolation::getMessage);
|
||||
return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截校验异常-绑定异常
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
public R handleBindException(BindException e, HttpServletRequest request) {
|
||||
log.warn("请求地址 [{}],参数验证失败。", request.getRequestURI(), e);
|
||||
String errorMsg = CollUtil.join(e
|
||||
.getAllErrors(), StringConstants.CHINESE_COMMA, DefaultMessageSourceResolvable::getDefaultMessage);
|
||||
return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截校验异常-方法参数无效异常
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public R handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
|
||||
log.warn("请求地址 [{}],参数验证失败。", request.getRequestURI(), e);
|
||||
String errorMsg = ExceptionUtils.exToNull(() -> Objects.requireNonNull(e.getBindingResult().getFieldError())
|
||||
.getDefaultMessage());
|
||||
return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截校验异常-方法参数类型不匹配异常
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
public R handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e,
|
||||
HttpServletRequest request) {
|
||||
String errorMsg = StrUtil.format("参数名:[{}],期望参数类型:[{}]", e.getName(), e.getParameter().getParameterType());
|
||||
log.warn("请求地址 [{}],参数转换失败,{}。", request.getRequestURI(), errorMsg, e);
|
||||
return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截文件上传异常-超过上传大小限制
|
||||
*/
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public R handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e, HttpServletRequest request) {
|
||||
log.warn("请求地址 [{}],上传文件失败,文件大小超过限制。", request.getRequestURI(), e);
|
||||
String sizeLimit = StrUtil.subBetween(e.getMessage(), "The maximum size ", " for");
|
||||
String errorMsg = String.format("请上传小于 %sMB 的文件", NumberUtil.parseLong(sizeLimit) / 1024 / 1024);
|
||||
return R.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截校验异常-请求方式不支持异常
|
||||
*/
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
public R handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
|
||||
log.error("请求地址 [{}],不支持 [{}] 请求。", request.getRequestURI(), e.getMethod());
|
||||
return R.fail(HttpStatus.METHOD_NOT_ALLOWED.value(), e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截业务异常
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public R handleServiceException(BusinessException e, HttpServletRequest request) {
|
||||
log.error("请求地址 [{}],发生业务异常。", request.getRequestURI(), e);
|
||||
return R.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截未知的运行时异常
|
||||
*/
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public R handleRuntimeException(RuntimeException e, HttpServletRequest request) {
|
||||
log.error("请求地址 [{}],发生系统异常。", request.getRequestURI(), e);
|
||||
return R.fail(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截未知的系统异常
|
||||
*/
|
||||
@ExceptionHandler(Throwable.class)
|
||||
public R handleException(Throwable e, HttpServletRequest request) {
|
||||
log.error("请求地址 [{}],发生未知异常。", request.getRequestURI(), e);
|
||||
return R.fail(e.getMessage());
|
||||
}
|
||||
}
|
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <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 top.charles7c.continew.starter.web.model;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 响应信息
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@Schema(description = "响应信息")
|
||||
public class R<T> implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final int SUCCESS_CODE = HttpStatus.OK.value();
|
||||
private static final int FAIL_CODE = HttpStatus.INTERNAL_SERVER_ERROR.value();
|
||||
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
@Schema(description = "是否成功", example = "true")
|
||||
private boolean success;
|
||||
|
||||
/**
|
||||
* 业务状态码
|
||||
*/
|
||||
@Schema(description = "业务状态码", example = "200")
|
||||
private int code;
|
||||
|
||||
/**
|
||||
* 业务状态信息
|
||||
*/
|
||||
@Schema(description = "业务状态信息", example = "操作成功")
|
||||
private String msg;
|
||||
|
||||
/**
|
||||
* 响应数据
|
||||
*/
|
||||
@Schema(description = "响应数据")
|
||||
private T data;
|
||||
|
||||
/**
|
||||
* 时间戳
|
||||
*/
|
||||
@Schema(description = "时间戳", example = "1691453288")
|
||||
private long timestamp = DateUtil.currentSeconds();
|
||||
|
||||
private R(boolean success, int code, String msg, T data) {
|
||||
this.success = success;
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作成功
|
||||
*
|
||||
* @param <T> 响应数据类型
|
||||
* @return R /
|
||||
*/
|
||||
public static <T> R<T> ok() {
|
||||
return new R<>(true, SUCCESS_CODE, "操作成功", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作成功
|
||||
*
|
||||
* @param data 响应数据
|
||||
* @param <T> 响应数据类型
|
||||
* @return R /
|
||||
*/
|
||||
public static <T> R<T> ok(T data) {
|
||||
return new R<>(true, SUCCESS_CODE, "操作成功", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作成功
|
||||
*
|
||||
* @param msg 业务状态信息
|
||||
* @param <T> 响应数据类型
|
||||
* @return R /
|
||||
*/
|
||||
public static <T> R<T> ok(String msg) {
|
||||
return new R<>(true, SUCCESS_CODE, msg, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作成功
|
||||
*
|
||||
* @param msg 业务状态信息
|
||||
* @param data 响应数据
|
||||
* @param <T> 响应数据类型
|
||||
* @return R /
|
||||
*/
|
||||
public static <T> R<T> ok(String msg, T data) {
|
||||
return new R<>(true, SUCCESS_CODE, msg, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作失败
|
||||
*
|
||||
* @param <T> 响应数据类型
|
||||
* @return R /
|
||||
*/
|
||||
public static <T> R<T> fail() {
|
||||
return new R<>(false, FAIL_CODE, "操作失败", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作失败
|
||||
*
|
||||
* @param msg 业务状态信息
|
||||
* @param <T> 响应数据类型
|
||||
* @return R /
|
||||
*/
|
||||
public static <T> R<T> fail(String msg) {
|
||||
return new R<>(false, FAIL_CODE, msg, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作失败
|
||||
*
|
||||
* @param data 响应数据
|
||||
* @param <T> 响应数据类型
|
||||
* @return R /
|
||||
*/
|
||||
public static <T> R<T> fail(T data) {
|
||||
return new R<>(false, FAIL_CODE, "操作失败", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作失败
|
||||
*
|
||||
* @param msg 业务状态信息
|
||||
* @param data 响应数据
|
||||
* @param <T> 响应数据类型
|
||||
* @return R /
|
||||
*/
|
||||
public static <T> R<T> fail(String msg, T data) {
|
||||
return new R<>(false, FAIL_CODE, msg, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作失败
|
||||
*
|
||||
* @param code 业务状态码
|
||||
* @param msg 业务状态信息
|
||||
* @param <T> 响应数据类型
|
||||
* @return R /
|
||||
*/
|
||||
public static <T> R<T> fail(int code, String msg) {
|
||||
return new R<>(false, code, msg, null);
|
||||
}
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <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 top.charles7c.continew.starter.web.util;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.file.FileNameUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 文件上传工具类
|
||||
*
|
||||
* @author Zheng Jie(<a href="https://gitee.com/elunez/eladmin">ELADMIN</a>)
|
||||
* @author Charles7c
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class FileUploadUtils {
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*
|
||||
* @param multipartFile 源文件对象
|
||||
* @param filePath 文件路径
|
||||
* @param isKeepOriginalFilename 是否保留原文件名
|
||||
* @return 目标文件对象
|
||||
*/
|
||||
public static File upload(MultipartFile multipartFile, String filePath, boolean isKeepOriginalFilename) {
|
||||
String originalFilename = multipartFile.getOriginalFilename();
|
||||
String extensionName = FileNameUtil.extName(originalFilename);
|
||||
|
||||
String fileName;
|
||||
if (isKeepOriginalFilename) {
|
||||
fileName = String.format("%s-%s.%s", FileNameUtil.getPrefix(originalFilename), DateUtil.format(LocalDateTime
|
||||
.now(), DatePattern.PURE_DATETIME_MS_PATTERN), extensionName);
|
||||
} else {
|
||||
fileName = String.format("%s.%s", IdUtil.fastSimpleUUID(), extensionName);
|
||||
}
|
||||
|
||||
try {
|
||||
String pathname = filePath + fileName;
|
||||
File dest = new File(pathname).getCanonicalFile();
|
||||
// 如果父路径不存在,自动创建
|
||||
if (!dest.getParentFile().exists()) {
|
||||
if (!dest.getParentFile().mkdirs()) {
|
||||
log.error("Create upload file parent path failed.");
|
||||
}
|
||||
}
|
||||
// 文件写入
|
||||
multipartFile.transferTo(dest);
|
||||
return dest;
|
||||
} catch (Exception e) {
|
||||
log.error("Upload file occurred an error: {}. fileName: {}.", e.getMessage(), fileName, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <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 top.charles7c.continew.starter.web.util;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.http.useragent.UserAgent;
|
||||
import cn.hutool.http.useragent.UserAgentUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import top.charles7c.continew.starter.core.constant.StringConstants;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Servlet 工具类
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class ServletUtils {
|
||||
|
||||
/**
|
||||
* 获取请求对象
|
||||
*
|
||||
* @return /
|
||||
*/
|
||||
public static HttpServletRequest getRequest() {
|
||||
return getServletRequestAttributes().getRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取响应对象
|
||||
*
|
||||
* @return /
|
||||
*/
|
||||
public static HttpServletResponse getResponse() {
|
||||
return getServletRequestAttributes().getResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器及其版本信息
|
||||
*
|
||||
* @param request 请求对象
|
||||
* @return 浏览器及其版本信息
|
||||
*/
|
||||
public static String getBrowser(HttpServletRequest request) {
|
||||
if (null == request) {
|
||||
return null;
|
||||
}
|
||||
return getBrowser(request.getHeader("User-Agent"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器及其版本信息
|
||||
*
|
||||
* @param userAgentString User-Agent 字符串
|
||||
* @return 浏览器及其版本信息
|
||||
*/
|
||||
public static String getBrowser(String userAgentString) {
|
||||
UserAgent userAgent = UserAgentUtil.parse(userAgentString);
|
||||
return userAgent.getBrowser().getName() + StringConstants.SPACE + userAgent.getVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作系统
|
||||
*
|
||||
* @param request 请求对象
|
||||
* @return 操作系统
|
||||
*/
|
||||
public static String getOs(HttpServletRequest request) {
|
||||
if (null == request) {
|
||||
return null;
|
||||
}
|
||||
return getOs(request.getHeader("User-Agent"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作系统
|
||||
*
|
||||
* @param userAgentString User-Agent 字符串
|
||||
* @return 操作系统
|
||||
*/
|
||||
public static String getOs(String userAgentString) {
|
||||
UserAgent userAgent = UserAgentUtil.parse(userAgentString);
|
||||
return userAgent.getOs().getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取响应所有的头(header)信息
|
||||
*
|
||||
* @param response 响应对象{@link HttpServletResponse}
|
||||
* @return header值
|
||||
*/
|
||||
public static Map<String, String> getHeaderMap(HttpServletResponse response) {
|
||||
final Collection<String> headerNames = response.getHeaderNames();
|
||||
final Map<String, String> headerMap = MapUtil.newHashMap(headerNames.size(), true);
|
||||
for (String name : headerNames) {
|
||||
headerMap.put(name, response.getHeader(name));
|
||||
}
|
||||
return headerMap;
|
||||
}
|
||||
|
||||
private static ServletRequestAttributes getServletRequestAttributes() {
|
||||
return (ServletRequestAttributes)Objects.requireNonNull(RequestContextHolder.getRequestAttributes());
|
||||
}
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <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 top.charles7c.continew.starter.web.util;
|
||||
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.web.accept.ContentNegotiationManager;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
|
||||
import org.springframework.web.util.UrlPathHelper;
|
||||
import top.charles7c.continew.starter.core.constant.StringConstants;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Spring 工具类
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.1.1
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class SpringUtils {
|
||||
|
||||
/**
|
||||
* 取消注册静态资源映射
|
||||
*
|
||||
* @param handlerMap 静态资源映射
|
||||
*/
|
||||
public static void deRegisterResourceHandler(Map<String, String> handlerMap) {
|
||||
ApplicationContext applicationContext = SpringUtil.getApplicationContext();
|
||||
// 获取已经注册的映射
|
||||
final HandlerMapping resourceHandlerMapping = applicationContext
|
||||
.getBean("resourceHandlerMapping", HandlerMapping.class);
|
||||
final Map<String, Object> oldHandlerMap = (Map<String, Object>)ReflectUtil
|
||||
.getFieldValue(resourceHandlerMapping, "handlerMap");
|
||||
// 移除之前注册的映射
|
||||
for (Map.Entry<String, String> entry : handlerMap.entrySet()) {
|
||||
String pathPattern = StrUtil.appendIfMissing(entry.getKey(), StringConstants.PATH_PATTERN);
|
||||
oldHandlerMap.remove(pathPattern);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册静态资源映射
|
||||
*
|
||||
* @param handlerMap 静态资源映射
|
||||
*/
|
||||
public static void registerResourceHandler(Map<String, String> handlerMap) {
|
||||
ApplicationContext applicationContext = SpringUtil.getApplicationContext();
|
||||
// 获取已经注册的映射
|
||||
final HandlerMapping resourceHandlerMapping = applicationContext
|
||||
.getBean("resourceHandlerMapping", HandlerMapping.class);
|
||||
final Map<String, Object> oldHandlerMap = (Map<String, Object>)ReflectUtil
|
||||
.getFieldValue(resourceHandlerMapping, "handlerMap");
|
||||
// 重新注册映射
|
||||
final ServletContext servletContext = applicationContext.getBean(ServletContext.class);
|
||||
final ContentNegotiationManager contentNegotiationManager = applicationContext
|
||||
.getBean("mvcContentNegotiationManager", ContentNegotiationManager.class);
|
||||
final UrlPathHelper urlPathHelper = applicationContext.getBean("mvcUrlPathHelper", UrlPathHelper.class);
|
||||
final ResourceHandlerRegistry resourceHandlerRegistry = new ResourceHandlerRegistry(applicationContext, servletContext, contentNegotiationManager, urlPathHelper);
|
||||
for (Map.Entry<String, String> entry : handlerMap.entrySet()) {
|
||||
// 移除之前注册的映射
|
||||
String pathPattern = StrUtil.appendIfMissing(entry.getKey(), StringConstants.PATH_PATTERN);
|
||||
oldHandlerMap.remove(pathPattern);
|
||||
// 重新注册映射
|
||||
String resourceLocations = StrUtil.appendIfMissing(entry.getValue(), StringConstants.SLASH);
|
||||
resourceHandlerRegistry.addResourceHandler(pathPattern).addResourceLocations("file:" + resourceLocations);
|
||||
}
|
||||
final Map<String, ?> additionalUrlMap = ReflectUtil
|
||||
.<SimpleUrlHandlerMapping>invoke(resourceHandlerRegistry, "getHandlerMapping")
|
||||
.getUrlMap();
|
||||
ReflectUtil.<Void>invoke(resourceHandlerMapping, "registerHandlers", additionalUrlMap);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user