refactor(web): 拆分 web 模块

This commit is contained in:
liquor
2025-05-20 14:58:38 +00:00
committed by Charles7c
parent adaf475835
commit 9c6182e028
34 changed files with 123 additions and 49 deletions

View File

@@ -1,35 +0,0 @@
/*
* 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.continew.starter.web.annotation;
import org.springframework.context.annotation.Import;
import top.continew.starter.web.autoconfigure.response.GlobalResponseAutoConfiguration;
import java.lang.annotation.*;
/**
* 全局响应启用注解
*
* @author Charles7c
* @since 1.2.0
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({GlobalResponseAutoConfiguration.class})
public @interface EnableGlobalResponse {}

View File

@@ -1,79 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.cors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.core.constant.StringConstants;
/**
* 跨域自动配置
*
* @author Charles7c
* @since 1.0.0
*/
@Lazy
@AutoConfiguration
@ConditionalOnWebApplication
@ConditionalOnProperty(prefix = PropertiesConstants.WEB_CORS, name = PropertiesConstants.ENABLED, havingValue = "true")
@EnableConfigurationProperties(CorsProperties.class)
public class CorsAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(CorsAutoConfiguration.class);
/**
* 跨域过滤器
*/
@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 'Web-CorsFilter' completed initialization.");
return corsFilter;
}
}

View File

@@ -1,102 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.cors;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.core.constant.StringConstants;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 跨域配置属性
*
* @author Charles7c
* @since 1.0.0
*/
@ConfigurationProperties(PropertiesConstants.WEB_CORS)
public class CorsProperties {
private static final List<String> ALL = Collections.singletonList(StringConstants.ASTERISK);
/**
* 是否启用
*/
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<>();
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public List<String> getAllowedOrigins() {
return allowedOrigins;
}
public void setAllowedOrigins(List<String> allowedOrigins) {
this.allowedOrigins = allowedOrigins;
}
public List<String> getAllowedMethods() {
return allowedMethods;
}
public void setAllowedMethods(List<String> allowedMethods) {
this.allowedMethods = allowedMethods;
}
public List<String> getAllowedHeaders() {
return allowedHeaders;
}
public void setAllowedHeaders(List<String> allowedHeaders) {
this.allowedHeaders = allowedHeaders;
}
public List<String> getExposedHeaders() {
return exposedHeaders;
}
public void setExposedHeaders(List<String> exposedHeaders) {
this.exposedHeaders = exposedHeaders;
}
}

View File

@@ -1,88 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.mvc;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.continew.starter.web.autoconfigure.mvc.converter.BaseEnumConverterFactory;
import top.continew.starter.web.autoconfigure.mvc.converter.time.DateConverter;
import top.continew.starter.web.autoconfigure.mvc.converter.time.LocalDateConverter;
import top.continew.starter.web.autoconfigure.mvc.converter.time.LocalDateTimeConverter;
import top.continew.starter.web.autoconfigure.mvc.converter.time.LocalTimeConverter;
import java.util.List;
import java.util.Objects;
/**
* Web MVC 自动配置
*
* @author Charles7c
* @since 2.4.0
*/
@EnableWebMvc
@AutoConfiguration
public class WebMvcAutoConfiguration implements WebMvcConfigurer {
private static final Logger log = LoggerFactory.getLogger(WebMvcAutoConfiguration.class);
private final MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter;
public WebMvcAutoConfiguration(MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter) {
this.mappingJackson2HttpMessageConverter = mappingJackson2HttpMessageConverter;
}
/**
* 解决 Jackson2ObjectMapperBuilderCustomizer 配置不生效的问题
* <p>
* MappingJackson2HttpMessageConverter 对象在程序启动时创建了多个,移除多余的,保证只有一个
* </p>
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(MappingJackson2HttpMessageConverter.class::isInstance);
if (Objects.isNull(mappingJackson2HttpMessageConverter)) {
converters.add(0, new MappingJackson2HttpMessageConverter());
} else {
converters.add(0, mappingJackson2HttpMessageConverter);
}
// 自定义 converters 时,需要手动在最前面添加 ByteArrayHttpMessageConverter
// 否则 Spring Doc OpenAPI 的 /*/api-docs/**(例如:/v3/api-docs/default接口响应内容会变为 Base64 编码后的内容,最终导致接口文档解析失败
// 详情请参阅https://github.com/springdoc/springdoc-openapi/issues/2143
converters.add(0, new ByteArrayHttpMessageConverter());
}
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(new BaseEnumConverterFactory());
registry.addConverter(new DateConverter());
registry.addConverter(new LocalDateTimeConverter());
registry.addConverter(new LocalDateConverter());
registry.addConverter(new LocalTimeConverter());
}
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'Web MVC' completed initialization.");
}
}

View File

@@ -1,46 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.mvc.converter;
import org.springframework.core.convert.converter.Converter;
import top.continew.starter.core.enums.BaseEnum;
import java.util.HashMap;
import java.util.Map;
/**
* BaseEnum 参数转换器
*
* @author Charles7c
* @since 2.4.0
*/
public class BaseEnumConverter<T extends BaseEnum> implements Converter<String, T> {
private final Map<String, T> enumMap = new HashMap<>();
public BaseEnumConverter(Class<T> enumType) {
T[] enums = enumType.getEnumConstants();
for (T e : enums) {
enumMap.put(String.valueOf(e.getValue()), e);
}
}
@Override
public T convert(String source) {
return enumMap.get(source);
}
}

View File

@@ -1,40 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.mvc.converter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import top.continew.starter.core.enums.BaseEnum;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* BaseEnum 参数转换器工厂
*
* @author Charles7c
* @since 2.4.0
*/
public class BaseEnumConverterFactory implements ConverterFactory<String, BaseEnum> {
private static final Map<Class, Converter> CONVERTER_CACHE = new ConcurrentHashMap<>();
@Override
public <T extends BaseEnum> Converter<String, T> getConverter(Class<T> targetType) {
return CONVERTER_CACHE.computeIfAbsent(targetType, key -> new BaseEnumConverter<>(targetType));
}
}

View File

@@ -1,36 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.mvc.converter.time;
import cn.hutool.core.date.DateUtil;
import org.springframework.core.convert.converter.Converter;
import java.util.Date;
/**
* Date 参数转换器
*
* @author Charles7c
* @since 2.10.0
*/
public class DateConverter implements Converter<String, Date> {
@Override
public Date convert(String source) {
return DateUtil.parse(source);
}
}

View File

@@ -1,36 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.mvc.converter.time;
import cn.hutool.core.date.DateUtil;
import org.springframework.core.convert.converter.Converter;
import java.time.LocalDate;
/**
* LocalDate 参数转换器
*
* @author Charles7c
* @since 2.10.0
*/
public class LocalDateConverter implements Converter<String, LocalDate> {
@Override
public LocalDate convert(String source) {
return DateUtil.parse(source).toLocalDateTime().toLocalDate();
}
}

View File

@@ -1,36 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.mvc.converter.time;
import cn.hutool.core.date.DateUtil;
import org.springframework.core.convert.converter.Converter;
import java.time.LocalDateTime;
/**
* LocalDateTime 参数转换器
*
* @author Charles7c
* @since 2.10.0
*/
public class LocalDateTimeConverter implements Converter<String, LocalDateTime> {
@Override
public LocalDateTime convert(String source) {
return DateUtil.parse(source).toLocalDateTime();
}
}

View File

@@ -1,36 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.mvc.converter.time;
import cn.hutool.core.date.DateUtil;
import org.springframework.core.convert.converter.Converter;
import java.time.LocalTime;
/**
* LocalTime 参数转换器
*
* @author Charles7c
* @since 2.10.0
*/
public class LocalTimeConverter implements Converter<String, LocalTime> {
@Override
public LocalTime convert(String source) {
return DateUtil.parse(source).toLocalDateTime().toLocalTime();
}
}

View File

@@ -1,71 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.response;
import cn.hutool.core.util.ClassUtil;
import org.apache.commons.lang3.reflect.TypeUtils;
import org.springdoc.core.parsers.ReturnTypeParser;
import org.springframework.core.MethodParameter;
import top.continew.starter.apidoc.util.DocUtils;
import java.lang.reflect.Type;
/**
* SpringDoc 全局响应处理器
* <p>
* 接口文档全局添加响应格式 {@link com.feiniaojin.gracefulresponse.data.Response}
* </p>
*
* @author echo
* @since 2.5.2
*/
public class ApiDocGlobalResponseHandler implements ReturnTypeParser {
private final GlobalResponseProperties globalResponseProperties;
private final Class<Object> responseClass;
public ApiDocGlobalResponseHandler(GlobalResponseProperties globalResponseProperties) {
this.globalResponseProperties = globalResponseProperties;
this.responseClass = ClassUtil.loadClass(globalResponseProperties.getResponseClassFullName());
}
/**
* 获取返回类型
*
* @param methodParameter 方法参数
* @return {@link Type }
*/
@Override
public Type getReturnType(MethodParameter methodParameter) {
// 获取返回类型
Type returnType = ReturnTypeParser.super.getReturnType(methodParameter);
// 判断是否具有 RestController 注解
if (!DocUtils.hasRestControllerAnnotation(methodParameter.getContainingClass())) {
return returnType;
}
// 如果为响应类型,则直接返回
if (returnType.getTypeName().contains(globalResponseProperties.getResponseClassFullName())) {
return returnType;
}
// 如果是 void类型则返回 R<Void>
if (returnType == void.class || returnType == Void.class) {
return TypeUtils.parameterize(responseClass, Void.class);
}
// 返回 R<T>
return TypeUtils.parameterize(responseClass, returnType);
}
}

View File

@@ -1,47 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.response;
import com.feiniaojin.gracefulresponse.advice.lifecycle.exception.BeforeControllerAdviceProcess;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.Nullable;
/**
* 默认回调处理器实现
*
* @author Charles7c
* @since 2.6.0
*/
public class DefaultBeforeControllerAdviceProcessImpl implements BeforeControllerAdviceProcess {
private final Logger log = LoggerFactory.getLogger(DefaultBeforeControllerAdviceProcessImpl.class);
private final GlobalResponseProperties globalResponseProperties;
public DefaultBeforeControllerAdviceProcessImpl(GlobalResponseProperties globalResponseProperties) {
this.globalResponseProperties = globalResponseProperties;
}
@Override
public void call(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception e) {
if (globalResponseProperties.isPrintExceptionInGlobalAdvice()) {
log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e);
}
}
}

View File

@@ -1,242 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.response;
import com.feiniaojin.gracefulresponse.ExceptionAliasRegister;
import com.feiniaojin.gracefulresponse.advice.*;
import com.feiniaojin.gracefulresponse.advice.lifecycle.exception.BeforeControllerAdviceProcess;
import com.feiniaojin.gracefulresponse.advice.lifecycle.exception.ControllerAdvicePredicate;
import com.feiniaojin.gracefulresponse.advice.lifecycle.exception.RejectStrategy;
import com.feiniaojin.gracefulresponse.api.ResponseFactory;
import com.feiniaojin.gracefulresponse.api.ResponseStatusFactory;
import com.feiniaojin.gracefulresponse.defaults.DefaultResponseFactory;
import com.feiniaojin.gracefulresponse.defaults.DefaultResponseStatusFactoryImpl;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springdoc.core.parsers.ReturnTypeParser;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.core.util.GeneralPropertySourceFactory;
import java.util.Locale;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 全局响应自动配置
*
* @author Charles7c
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(GlobalResponseProperties.class)
@PropertySource(value = "classpath:default-web.yml", factory = GeneralPropertySourceFactory.class)
public class GlobalResponseAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(GlobalResponseAutoConfiguration.class);
private final GlobalResponseProperties globalResponseProperties;
public GlobalResponseAutoConfiguration(GlobalResponseProperties globalResponseProperties) {
this.globalResponseProperties = globalResponseProperties;
}
/**
* 全局响应体处理(非 void
*/
@Bean
@ConditionalOnMissingBean
public GrNotVoidResponseBodyAdvice grNotVoidResponseBodyAdvice() {
return new GrNotVoidResponseBodyAdvice();
}
/**
* 全局响应体处理void
*/
@Bean
@ConditionalOnMissingBean
public GrVoidResponseBodyAdvice grVoidResponseBodyAdvice() {
return new GrVoidResponseBodyAdvice();
}
/**
* 处理前回调(目前仅打印异常日志)
*/
@Bean
@ConditionalOnMissingBean
public BeforeControllerAdviceProcess beforeControllerAdviceProcess() {
return new DefaultBeforeControllerAdviceProcessImpl(globalResponseProperties);
}
/**
* 框架异常处理器
*/
@Bean
public FrameworkExceptionAdvice frameworkExceptionAdvice(BeforeControllerAdviceProcess beforeControllerAdviceProcess,
@Lazy RejectStrategy rejectStrategy) {
FrameworkExceptionAdvice frameworkExceptionAdvice = new FrameworkExceptionAdvice();
frameworkExceptionAdvice.setRejectStrategy(rejectStrategy);
frameworkExceptionAdvice.setControllerAdviceProcessor(frameworkExceptionAdvice);
frameworkExceptionAdvice.setBeforeControllerAdviceProcess(beforeControllerAdviceProcess);
frameworkExceptionAdvice.setControllerAdviceHttpProcessor(frameworkExceptionAdvice);
return frameworkExceptionAdvice;
}
/**
* 数据校验异常处理器
*/
@Bean
public DataExceptionAdvice dataExceptionAdvice(BeforeControllerAdviceProcess beforeControllerAdviceProcess,
@Lazy RejectStrategy rejectStrategy) {
DataExceptionAdvice dataExceptionAdvice = new DataExceptionAdvice();
dataExceptionAdvice.setRejectStrategy(rejectStrategy);
dataExceptionAdvice.setControllerAdviceProcessor(dataExceptionAdvice);
dataExceptionAdvice.setBeforeControllerAdviceProcess(beforeControllerAdviceProcess);
dataExceptionAdvice.setControllerAdviceHttpProcessor(dataExceptionAdvice);
return dataExceptionAdvice;
}
/**
* 默认全局异常处理器
*/
@Bean
public DefaultGlobalExceptionAdvice defaultGlobalExceptionAdvice(BeforeControllerAdviceProcess beforeControllerAdviceProcess,
@Lazy RejectStrategy rejectStrategy) {
DefaultGlobalExceptionAdvice advice = new DefaultGlobalExceptionAdvice();
advice.setRejectStrategy(rejectStrategy);
CopyOnWriteArrayList<ControllerAdvicePredicate> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
copyOnWriteArrayList.add(advice);
advice.setPredicates(copyOnWriteArrayList);
advice.setControllerAdviceProcessor(advice);
advice.setBeforeControllerAdviceProcess(beforeControllerAdviceProcess);
advice.setControllerAdviceHttpProcessor(advice);
return advice;
}
/**
* 默认参数校验异常处理器
*/
@Bean
public DefaultValidationExceptionAdvice defaultValidationExceptionAdvice(BeforeControllerAdviceProcess beforeControllerAdviceProcess,
@Lazy RejectStrategy rejectStrategy) {
DefaultValidationExceptionAdvice advice = new DefaultValidationExceptionAdvice();
advice.setRejectStrategy(rejectStrategy);
advice.setControllerAdviceProcessor(advice);
advice.setBeforeControllerAdviceProcess(beforeControllerAdviceProcess);
// 设置默认参数校验异常http处理器
advice.setControllerAdviceHttpProcessor(advice);
return advice;
}
/**
* 拒绝策略
*/
@Bean
public RejectStrategy rejectStrategy() {
return new DefaultRejectStrategyImpl();
}
/**
* 释放异常处理器
*/
@Bean
public ExceptionHandlerExceptionResolver releaseExceptionHandlerExceptionResolver() {
return new ReleaseExceptionHandlerExceptionResolver();
}
/**
* 国际化支持
*/
@Bean
@ConditionalOnProperty(prefix = PropertiesConstants.WEB_RESPONSE, name = "i18n", havingValue = "true")
public GrI18nResponseBodyAdvice grI18nResponseBodyAdvice() {
return new GrI18nResponseBodyAdvice();
}
/**
* 国际化配置
*/
@Bean
@ConditionalOnProperty(prefix = PropertiesConstants.WEB_RESPONSE, name = "i18n", havingValue = "true")
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("i18n", "i18n/messages");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setDefaultLocale(Locale.CHINA);
return messageSource;
}
/**
* 响应工厂
*/
@Bean
@ConditionalOnMissingBean
public ResponseFactory responseBeanFactory() {
return new DefaultResponseFactory();
}
/**
* 响应状态工厂
*/
@Bean
@ConditionalOnMissingBean
public ResponseStatusFactory responseStatusFactory() {
return new DefaultResponseStatusFactoryImpl();
}
/**
* 异常别名注册
*/
@Bean
public ExceptionAliasRegister exceptionAliasRegister() {
return new ExceptionAliasRegister();
}
/**
* 响应支持
*/
@Bean
public AdviceSupport adviceSupport() {
return new AdviceSupport();
}
/**
* SpringDoc 全局响应处理器
*
* @return {@link ApiDocGlobalResponseHandler }
*/
@Bean
@ConditionalOnClass(ReturnTypeParser.class)
@ConditionalOnMissingBean
public ApiDocGlobalResponseHandler apiDocGlobalResponseHandler() {
return new ApiDocGlobalResponseHandler(globalResponseProperties);
}
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'Web-Global Response' completed initialization.");
}
}

View File

@@ -1,30 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.response;
import com.feiniaojin.gracefulresponse.GracefulResponseProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.continew.starter.core.constant.PropertiesConstants;
/**
* 全局响应配置属性
*
* @author Charles7c
* @since 2.5.0
*/
@ConfigurationProperties(PropertiesConstants.WEB_RESPONSE)
public class GlobalResponseProperties extends GracefulResponseProperties {}

View File

@@ -1,63 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.server;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
/**
* 服务器配置属性
*
* @author Charles7c
* @since 2.11.0
*/
@ConfigurationProperties("server.extension")
public class ServerExtensionProperties {
/**
* 默认禁止三个不安全的 HTTP 方法(如 CONNECT、TRACE、TRACK
*/
private static final List<String> DEFAULT_ALLOWED_METHODS = List.of("CONNECT", "TRACE", "TRACK");
/**
* 是否启用
*/
private boolean enabled = true;
/**
* 不允许的请求方式
*/
private List<String> disallowedMethods = new ArrayList<>(DEFAULT_ALLOWED_METHODS);
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public List<String> getDisallowedMethods() {
return disallowedMethods;
}
public void setDisallowedMethods(List<String> disallowedMethods) {
this.disallowedMethods = disallowedMethods;
}
}

View File

@@ -1,70 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import io.undertow.Undertow;
import io.undertow.server.handlers.DisallowedMethodsHandler;
import io.undertow.util.HttpString;
import top.continew.starter.core.constant.PropertiesConstants;
import java.util.stream.Collectors;
/**
* Undertow 自动配置
*
* @author Jasmine
* @author Charles7c
* @since 2.11.0
*/
@AutoConfiguration
@ConditionalOnWebApplication
@ConditionalOnClass(Undertow.class)
@EnableConfigurationProperties(ServerExtensionProperties.class)
@ConditionalOnProperty(prefix = "server.extension", name = PropertiesConstants.ENABLED, havingValue = "true")
public class UndertowAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(UndertowAutoConfiguration.class);
/**
* Undertow 自定义配置
*/
@Bean
public WebServerFactoryCustomizer<UndertowServletWebServerFactory> customize(ServerExtensionProperties properties) {
return factory -> {
factory.addDeploymentInfoCustomizers(deploymentInfo -> deploymentInfo
.addInitialHandlerChainWrapper(handler -> new DisallowedMethodsHandler(handler, properties
.getDisallowedMethods()
.stream()
.map(HttpString::tryFromString)
.collect(Collectors.toSet()))));
log.debug("[ContiNew Starter] - Disallowed HTTP methods on Server Undertow: {}.", properties
.getDisallowedMethods());
log.debug("[ContiNew Starter] - Auto Configuration 'Web-Server Undertow' completed initialization.");
};
}
}

View File

@@ -1,203 +0,0 @@
/*
* 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.continew.starter.web.model;
import cn.hutool.extra.spring.SpringUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.feiniaojin.gracefulresponse.api.ResponseStatusFactory;
import com.feiniaojin.gracefulresponse.data.Response;
import com.feiniaojin.gracefulresponse.data.ResponseStatus;
import com.feiniaojin.gracefulresponse.defaults.DefaultResponseStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Objects;
/**
* 响应信息
*
* @author Charles7c
* @since 1.0.0
*/
@Schema(description = "响应信息")
public class R<T> implements Response {
private static final ResponseStatusFactory RESPONSE_STATUS_FACTORY = SpringUtil
.getBean(ResponseStatusFactory.class);
/**
* 状态码
*/
@Schema(description = "状态码", example = "0")
private String code;
/**
* 状态信息
*/
@Schema(description = "状态信息", example = "ok")
private String msg;
/**
* 是否成功
*/
@Schema(description = "是否成功", example = "true")
private boolean success;
/**
* 时间戳
*/
@Schema(description = "时间戳", example = "1691453288000")
private Long timestamp;
/**
* 响应数据
*/
@Schema(description = "响应数据")
private T data;
/**
* 状态信息
*/
private ResponseStatus status = new DefaultResponseStatus();
public R() {
}
public R(ResponseStatus status) {
this.status = status;
}
public R(String code, String msg) {
this.setCode(code);
this.setMsg(msg);
}
public R(ResponseStatus status, T data) {
this(status);
this.setData(data);
}
public R(String code, String msg, T data) {
this(code, msg);
this.setData(data);
}
@Override
public void setStatus(ResponseStatus status) {
this.status = status;
}
@Override
@JsonIgnore
public ResponseStatus getStatus() {
return status;
}
@Override
public void setPayload(Object payload) {
this.data = (T)payload;
}
@Override
@JsonIgnore
public Object getPayload() {
return data;
}
public String getCode() {
return status.getCode();
}
public void setCode(String code) {
status.setCode(code);
}
public String getMsg() {
return status.getMsg();
}
public void setMsg(String msg) {
status.setMsg(msg);
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public boolean isSuccess() {
return Objects.equals(RESPONSE_STATUS_FACTORY.defaultSuccess().getCode(), status.getCode());
}
public Long getTimestamp() {
return System.currentTimeMillis();
}
/**
* 操作成功
*
* @return R /
*/
public static R ok() {
return new R(RESPONSE_STATUS_FACTORY.defaultSuccess());
}
/**
* 操作成功
*
* @param data 响应数据
* @return R /
*/
public static R ok(Object data) {
return new R(RESPONSE_STATUS_FACTORY.defaultSuccess(), data);
}
/**
* 操作成功
*
* @param msg 业务状态信息
* @param data 响应数据
* @return R /
*/
public static R ok(String msg, Object data) {
R r = ok(data);
r.setMsg(msg);
return r;
}
/**
* 操作失败
*
* @return R /
*/
public static R fail() {
return new R(RESPONSE_STATUS_FACTORY.defaultError());
}
/**
* 操作失败
*
* @param code 业务状态码
* @param msg 业务状态信息
* @return R /
*/
public static R fail(String code, String msg) {
return new R(code, msg);
}
}

View File

@@ -1,113 +0,0 @@
/*
* 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.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 cn.hutool.core.util.URLUtil;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
/**
* 文件工具类
*
* @author Zheng Jie<a href="https://gitee.com/elunez/eladmin">ELADMIN</a>
* @author Charles7c
* @since 1.0.0
*/
public class FileUploadUtils {
private static final Logger log = LoggerFactory.getLogger(FileUploadUtils.class);
private 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 = "%s-%s.%s".formatted(FileNameUtil.getPrefix(originalFilename), DateUtil.format(LocalDateTime
.now(), DatePattern.PURE_DATETIME_MS_PATTERN), extensionName);
} else {
fileName = "%s.%s".formatted(IdUtil.fastSimpleUUID(), extensionName);
}
try {
String pathname = filePath + fileName;
File dest = new File(pathname).getCanonicalFile();
// 如果父路径不存在,自动创建
if (!dest.getParentFile().exists() && (!dest.getParentFile().mkdirs())) {
log.error("Create upload file parent path failed.");
}
// 文件写入
multipartFile.transferTo(dest);
return dest;
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return null;
}
/**
* 下载
*
* @param response 响应对象
* @param file 文件
*/
public static void download(HttpServletResponse response, File file) throws IOException {
download(response, new FileInputStream(file), file.getName());
}
/**
* 下载
*
* @param response 响应对象
* @param inputStream 文件流
* @param fileName 文件名
* @since 2.5.0
*/
public static void download(HttpServletResponse response,
InputStream inputStream,
String fileName) throws IOException {
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLUtil.encode(fileName));
try (inputStream; var outputStream = response.getOutputStream()) {
response.setContentLengthLong(inputStream.transferTo(outputStream));
}
}
}

View File

@@ -1,104 +0,0 @@
/*
* 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.continew.starter.web.util;
import cn.hutool.core.io.IoUtil;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/**
* 可重复读取请求体的包装器
* 支持文件流直接透传,非文件流可重复读取
*
* @author echo
* @since 2.10.0
*/
public class RepeatReadRequestWrapper extends HttpServletRequestWrapper {
private byte[] cachedBody;
private final HttpServletRequest originalRequest;
public RepeatReadRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.originalRequest = request;
// 判断是否为文件上传请求
if (!isMultipartContent(request)) {
this.cachedBody = IoUtil.readBytes(request.getInputStream(), false);
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
// 如果是文件上传,直接返回原始输入流
if (isMultipartContent(originalRequest)) {
return originalRequest.getInputStream();
}
// 非文件上传,返回可重复读取的输入流
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
// 非阻塞I/O这里可以根据需要实现
}
@Override
public int read() {
return byteArrayInputStream.read();
}
};
}
@Override
public BufferedReader getReader() throws IOException {
// 如果是文件上传直接返回原始Reader
if (isMultipartContent(originalRequest)) {
new BufferedReader(new InputStreamReader(originalRequest.getInputStream(), StandardCharsets.UTF_8));
}
return new BufferedReader(new InputStreamReader(getInputStream()));
}
/**
* 检查是否为文件上传请求
*
* @param request 请求对象
* @return 是否为文件上传请求
*/
public boolean isMultipartContent(HttpServletRequest request) {
return request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart/");
}
}

View File

@@ -1,146 +0,0 @@
/*
* 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.continew.starter.web.util;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.WriteListener;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
import org.springframework.http.MediaType;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
/**
* 可重复读取响应内容的包装器
* 支持缓存响应内容,便于日志记录和后续处理 (不缓存SSE)
*
* @author echo
* @author Charles7c
* @since 2.10.0
*/
public class RepeatReadResponseWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream cachedOutputStream = new ByteArrayOutputStream();
private final PrintWriter writer = new PrintWriter(cachedOutputStream, true);
/**
* 是否为流式响应
*/
private boolean isStreamingResponse = false;
public RepeatReadResponseWrapper(HttpServletResponse response) {
super(response);
checkStreamingResponse();
}
@Override
public void setContentType(String type) {
super.setContentType(type);
// 根据 Content-Type 判断是否为流式响应
if (type != null) {
String lowerType = type.toLowerCase();
isStreamingResponse = lowerType.contains(MediaType.TEXT_EVENT_STREAM_VALUE);
}
}
private void checkStreamingResponse() {
String contentType = getContentType();
if (contentType != null) {
String lowerType = contentType.toLowerCase();
isStreamingResponse = lowerType.contains(MediaType.TEXT_EVENT_STREAM_VALUE);
}
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
checkStreamingResponse();
// 对于 SSE 流式响应,直接返回原始响应流,不做额外处理
if (isStreamingResponse) {
return super.getOutputStream();
}
return new ServletOutputStream() {
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
@Override
public void write(int b) throws IOException {
cachedOutputStream.write(b);
}
@Override
public void write(byte[] b) throws IOException {
cachedOutputStream.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
cachedOutputStream.write(b, off, len);
}
};
}
@Override
public PrintWriter getWriter() throws IOException {
checkStreamingResponse();
if (isStreamingResponse) {
// 对于 SSE 流式响应,直接返回原始响应写入器,不做额外处理
return super.getWriter();
}
return writer;
}
/**
* 获取缓存的响应内容
*
* @return 缓存的响应内容
*/
public String getResponseContent() {
if (!isStreamingResponse) {
writer.flush();
return cachedOutputStream.toString(StandardCharsets.UTF_8);
}
return null;
}
/**
* 将缓存的响应内容复制到原始响应中
*
* @throws IOException IO 异常
*/
public void copyBodyToResponse() throws IOException {
if (!isStreamingResponse && cachedOutputStream.size() > 0) {
getResponse().getOutputStream().write(cachedOutputStream.toByteArray());
}
}
/**
* 是否为流式响应
*
* @return 是否为流式响应
*/
public boolean isStreamingResponse() {
return isStreamingResponse;
}
}

View File

@@ -1,336 +0,0 @@
/*
* 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.continew.starter.web.util;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.util.UriUtils;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.json.jackson.util.JSONUtils;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* Servlet 工具类
*
* @author Charles7c
* @author echo
* @since 1.0.0
*/
public class ServletUtils extends JakartaServletUtil {
private ServletUtils() {
}
/**
* 获取浏览器及其版本信息
*
* @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();
}
/**
* 获取请求方法
*
* @return {@link String }
* @since 2.11.0
*/
public static String getRequestMethod() {
HttpServletRequest request = getRequest();
return request != null ? request.getMethod() : null;
}
/**
* 获取请求参数
*
* @param name 参数名
* @return {@link String }
* @since 2.11.0
*/
public static String getRequestParameter(String name) {
HttpServletRequest request = getRequest();
return request != null ? request.getParameter(name) : null;
}
/**
* 获取请求 Ip
*
* @return {@link String }
* @since 2.11.0
*/
public static String getRequestIp() {
HttpServletRequest request = getRequest();
return request != null ? getClientIP(request) : null;
}
/**
* 获取请求头信息
*
* @return {@link Map }<{@link String }, {@link String }>
* @since 2.11.0
*/
public static Map<String, String> getRequestHeaders() {
HttpServletRequest request = getRequest();
return request != null ? getHeaderMap(request) : Collections.emptyMap();
}
/**
* 获取请求 URL包含 query 参数)
* <p>{@code http://localhost:8000/system/user?page=1&size=10}</p>
*
* @return {@link URI }
* @since 2.11.0
*/
public static URI getRequestUrl() {
HttpServletRequest request = getRequest();
if (request == null) {
return null;
}
String queryString = request.getQueryString();
if (CharSequenceUtil.isBlank(queryString)) {
return URI.create(request.getRequestURL().toString());
}
try {
StringBuilder urlBuilder = appendQueryString(queryString);
return new URI(urlBuilder.toString());
} catch (URISyntaxException e) {
String encoded = UriUtils.encodeQuery(queryString, StandardCharsets.UTF_8);
StringBuilder urlBuilder = appendQueryString(encoded);
return URI.create(urlBuilder.toString());
}
}
/**
* 获取请求路径
*
* @return {@link URI }
* @since 2.11.0
*/
public static String getRequestPath() {
HttpServletRequest request = getRequest();
return request != null ? request.getRequestURI() : null;
}
/**
* 获取请求 body 参数
*
* @return {@link String }
* @since 2.11.0
*/
public static String getRequestBody() {
HttpServletRequest request = getRequest();
if (request instanceof RepeatReadRequestWrapper wrapper && !wrapper.isMultipartContent(request)) {
String body = JakartaServletUtil.getBody(request);
return JSONUtils.isTypeJSON(body) ? body : null;
}
return null;
}
/**
* 获取请求参数
*
* @return {@link Map }<{@link String }, {@link Object }>
* @since 2.11.0
*/
public static Map<String, Object> getRequestParams() {
String body = getRequestBody();
return CharSequenceUtil.isNotBlank(body) && JSONUtils.isTypeJSON(body)
? JSONUtils.toBean(body, Map.class)
: Collections.unmodifiableMap(JakartaServletUtil.getParamMap(Objects.requireNonNull(getRequest())));
}
/**
* 获取响应状态
*
* @return int
* @since 2.11.0
*/
public static int getResponseStatus() {
HttpServletResponse response = getResponse();
return response != null ? response.getStatus() : -1;
}
/**
* 获取响应所有的头header信息
*
* @return header值
* @since 2.11.0
*/
public static Map<String, String> getResponseHeaders() {
HttpServletResponse response = getResponse();
if (response == null) {
return Collections.emptyMap();
}
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;
}
/**
* 获取响应 body 参数
*
* @return {@link String }
* @since 2.11.0
*/
public static String getResponseBody() {
HttpServletResponse response = getResponse();
if (response instanceof RepeatReadResponseWrapper wrapper && !wrapper.isStreamingResponse()) {
String body = wrapper.getResponseContent();
return JSONUtils.isTypeJSON(body) ? body : null;
}
return null;
}
/**
* 获取响应参数
*
* @return {@link Map }<{@link String }, {@link Object }>
* @since 2.11.0
*/
public static Map<String, Object> getResponseParams() {
String body = getResponseBody();
return CharSequenceUtil.isNotBlank(body) && JSONUtils.isTypeJSON(body)
? JSONUtils.toBean(body, Map.class)
: null;
}
/**
* 获取 HTTP Session
*
* @return HttpSession
* @since 2.11.0
*/
public static HttpSession getSession() {
HttpServletRequest request = getRequest();
return request != null ? request.getSession() : null;
}
/**
* 获取 HTTP Request
*
* @return HttpServletRequest
* @since 2.11.0
*/
public static HttpServletRequest getRequest() {
ServletRequestAttributes attributes = getRequestAttributes();
if (attributes == null) {
return null;
}
return attributes.getRequest();
}
/**
* 获取 HTTP Response
*
* @return HttpServletResponse
* @since 2.11.0
*/
public static HttpServletResponse getResponse() {
ServletRequestAttributes attributes = getRequestAttributes();
if (attributes == null) {
return null;
}
return attributes.getResponse();
}
/**
* 获取请求属性
*
* @return {@link ServletRequestAttributes }
* @since 2.11.0
*/
public static ServletRequestAttributes getRequestAttributes() {
try {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes)attributes;
} catch (Exception e) {
return null;
}
}
/**
* 追加查询字符串
*
* @param queryString 查询字符串
* @return {@link StringBuilder }
*/
private static StringBuilder appendQueryString(String queryString) {
HttpServletRequest request = getRequest();
if (request == null) {
return new StringBuilder();
}
return new StringBuilder().append(request.getRequestURL())
.append(StringConstants.QUESTION_MARK)
.append(queryString);
}
}

View File

@@ -1,138 +0,0 @@
/*
* 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.continew.starter.web.util;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.extra.spring.SpringUtil;
import jakarta.servlet.ServletContext;
import org.springframework.context.ApplicationContext;
import org.springframework.http.server.PathContainer;
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 org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
import top.continew.starter.core.constant.StringConstants;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* Spring Web 工具类
*
* @author Charles7c
* @since 1.1.1
*/
public class SpringWebUtils {
private SpringWebUtils() {
}
/**
* 路径是否匹配
*
* @param path 路径
* @param patterns 匹配模式列表
* @return 是否匹配
* @since 2.6.0
*/
public static boolean isMatch(String path, List<String> patterns) {
return patterns.stream().anyMatch(pattern -> isMatch(path, pattern));
}
/**
* 路径是否匹配
*
* @param path 路径
* @param patterns 匹配模式列表
* @return 是否匹配
* @since 2.6.0
*/
public static boolean isMatch(String path, String... patterns) {
return Arrays.stream(patterns).anyMatch(pattern -> isMatch(path, pattern));
}
/**
* 路径是否匹配
*
* @param path 路径
* @param pattern 匹配模式
* @return 是否匹配
* @since 2.4.0
*/
public static boolean isMatch(String path, String pattern) {
PathPattern pathPattern = PathPatternParser.defaultInstance.parse(pattern);
PathContainer pathContainer = PathContainer.parsePath(path);
return pathPattern.matches(pathContainer);
}
/**
* 取消注册静态资源映射
*
* @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 = CharSequenceUtil.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 = CharSequenceUtil.appendIfMissing(CharSequenceUtil.removeSuffix(entry
.getKey(), StringConstants.SLASH), StringConstants.PATH_PATTERN);
oldHandlerMap.remove(pathPattern);
// 重新注册映射
String resourceLocations = CharSequenceUtil.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);
}
}

View File

@@ -1,3 +0,0 @@
top.continew.starter.web.autoconfigure.mvc.WebMvcAutoConfiguration
top.continew.starter.web.autoconfigure.cors.CorsAutoConfiguration
top.continew.starter.web.autoconfigure.server.UndertowAutoConfiguration

View File

@@ -1,42 +0,0 @@
--- ### 响应配置
continew-starter.web.response:
# 是否开启国际化默认false
i18n: false
# 响应类全名(配置后 response-style 将不再生效)
response-class-full-name: top.continew.starter.web.model.R
# 自定义失败 HTTP 状态码默认200建议业务和通信状态码区分
default-http-status-code-on-error: 200
# 自定义成功响应码默认0
default-success-code: 0
# 自定义成功提示默认ok
default-success-msg: ok
# 自定义失败响应码默认1
default-error-code: 1
# 自定义失败提示默认error
default-error-msg: error
# 是否打印异常日志默认false
print-exception-in-global-advice: true
# 是否将原生异常错误信息填充到状态信息中默认false
origin-exception-using-detail-message: true
# 例外包路径(支持数字, * 和 ** 通配符匹配),该包路径下的 Controller 将被忽略处理
exclude-packages:
- io.swagger.**
- org.springdoc.**
- org.springframework.boot.actuate.*
--- ### 服务器配置
server:
## Undertow 服务器配置
undertow:
# HTTP POST 请求内容的大小上限(默认 -1不限制
max-http-post-size: -1
# 以下的配置会影响 buffer这些 buffer 会用于服务器连接的 IO 操作,有点类似 Netty 的池化内存管理
# 每块 buffer的空间大小越小的空间被利用越充分不要设置太大以免影响其他应用合适即可
buffer-size: 512
# 是否分配的直接内存NIO 直接分配的堆外内存)
direct-buffers: true
threads:
# 设置 IO 线程数,它主要执行非阻塞的任务,它们会负责多个连接(默认每个 CPU 核心一个线程)
io: 8
# 阻塞任务线程池,当执行类似 Servlet 请求阻塞操作Undertow 会从这个线程池中取得线程(它的值设置取决于系统的负载)
worker: 256