SpringBoot 实现全局统一请求日志处理相关工具类内容类型判断工具类import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 内容类型工具类 * @author TheEnd */ public class ContentTypeUtil { public static boolean isBinaryContent(HttpServletRequest request) { String contentType = request.getContentType(); return isBinaryContent(contentType); } public static boolean isBinaryContent(HttpServletResponse response) { String contentType = response.getContentType(); return isBinaryContent(contentType); } public static boolean isBinaryContent(String contentType) { if (contentType == null || contentType.isEmpty()) { return false; } return contentType.startsWith("image/") || contentType.startsWith("multipart/form-data") || contentType.startsWith("audio/") || contentType.startsWith("video/") || contentType.startsWith("application/octet-stream") || contentType.startsWith("application/vnd.ms-excel") || contentType.startsWith("application/vnd.ms-powerpoint") || contentType.startsWith("application/msword") || contentType.startsWith("application/vnd.openxmlformats-officedocument") || contentType.startsWith("application/zip") || contentType.startsWith("application/x-"); } }客户端IP获取工具类import javax.servlet.http.HttpServletRequest; import java.util.*; /** * 用户获取客户端IP的工具类 * @author TheEnd */ public class IpUtil { /** * 获取客户端IP * @param request 请求对象 * @return 客户端IP */ public static String getIp(HttpServletRequest request) { if (request == null) { return ""; } ArrayList<String> headerNames = Collections.list(request.getHeaderNames()); List<String> ipHeaderNames = Arrays.asList("X-Forwarded-For", "Proxy-Client-IP", "X-Real-IP"); for (String ipHeaderName : ipHeaderNames) { String ip = getHeaderValueIgnoreCase(ipHeaderName, headerNames, request); if (ip != null && !ip.isEmpty()) { return ip; } } return request.getRemoteAddr(); } private static String getHeaderValueIgnoreCase(String headerName, List<String> requestHeaderNames, HttpServletRequest request) { for (String requestHeaderName : requestHeaderNames) { if (requestHeaderName.equalsIgnoreCase(headerName)) { return request.getHeader(requestHeaderName); } } return ""; } }获取已登录用户信息封装此信息属于业务信息, 需要按照业务实际情况进行编写, 所以此处只提供接口和一个空实现获取已登录用户信息接口import org.springframework.lang.NonNull; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 已登录用户信息获取工具 * @author TheEnd */ public interface LoginUserInfoHelper { /** * 用户ID * @param request 请求对象 * @param response 响应对象 * @return 用户ID */ String userId(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response); /** * 用户名 * @param request 请求对象 * @param response 响应对象 * @return 用户名 */ String username(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response); }默认空实现, 如不需要记录登录人信息, 使用此类即可import org.springframework.lang.NonNull; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 返回空字符串的用户信息获取器 * @author The */ public class EmptyLoginUserInfoHelper implements LoginUserInfoHelper { @Override public String userId(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response) { return ""; } @Override public String username(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response) { return ""; } }可以根据实际业务需求自定义实现类.编写日志类import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import java.io.Serializable; import java.util.Date; /** * 请求日志 * @author TheEnd */ @Data @Accessors(chain = true) @NoArgsConstructor @AllArgsConstructor public class RequestLog implements Serializable { /** * 请求方法 */ private String method; /** * 请求路径 */ private String url; /** * 客户端Ip */ private String ip; /** * 携带Cookie */ private String cookie; /** * 请求参数 */ private String requestParams; /** * 请求体 */ private String requestBody; /** * 响应HTTP状态码 */ private Integer responseCode; /** * 响应内容 */ private String responseBody; /** * 用户ID */ private String userId; /** * 用户名 */ private String username; /** * 模块名称 */ private String modelName; /** * 接口名称 */ private String interfaceName; /** * 备注 */ private String remarks; /** * 请求时间 */ private Date requestTime; /** * 响应时间 */ private Date responseTime; /** * 使用毫秒 */ private Integer useTime; }编写相关注解日志额外信息注解用于通过注解给日志记录增加额外信息可以标注在类上和方法上, 标注在类上时类中所有的方法的日志都有此信息. 类和方法上都有标注时,优先生效方法上的.import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 接口额外信息 * @author TheEnd */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface LogExtra { /** * 模块名 * @return 模块名 */ String modelName() default ""; /** * 接口名 * @return 接口名 */ String interfaceName() default ""; /** * 备注 * @return 备注 */ String remarks() default ""; }不记录日志注解标注了此注解的类和方法不会进行日志记录import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 表名此接口不需要记录日志 * @author TheEnd */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface NotLog { }编写序列化器提供了序列化器接口LogSerializer和默认实现, 可以根据业务需求自定义序列化器, 创建一个类并实现LogSerializer接口即可.序列化器接口import org.springframework.lang.NonNull; /** * 日志序列化器 * @author TheEnd */ public interface LogSerializer { /** * 序列化 * @param log 日志对象 * @return 序列化数据 */ byte[] serializer(@NonNull RequestLog log); }JDK ToString 序列化import org.springframework.lang.NonNull; /** * 将日志对象通过ToString进行序列化的序列化器 * @author TheEnd */ public class ToStringLogSerializer implements LogSerializer { @Override public byte[] serializer(@NonNull RequestLog log) { return log.toString().getBytes(); } }Jackson ToJson 序列化import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.lang.NonNull; /** * 将日志对象转为JSON的序列化器 * @author TheEnd */ public class JsonLogSerializer implements LogSerializer { private static final ObjectMapper MAPPER = new ObjectMapper(); @Override public byte[] serializer(@NonNull RequestLog log) { try { return MAPPER.writeValueAsBytes(log); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } }编写日志打印器日志打印器用于将日志输出至指定位置, 提供打印器接口和默认实现, 可以根据业务需求, 自定义打印器. 可以将日志输出至文件,控制台,消息中间件,数据库,网络流等打印器接口import org.springframework.lang.NonNull; /** * 日志打印器 * @author TheEnd */ public interface LogPrinter { /** * 打印日志 * @param log 日志对象 */ void print(@NonNull RequestLog log); }Logback 打印器通过Logback打印import lombok.Data; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.lang.NonNull; /** * 将日志写到文件的持久化器 * @author TheEnd */ @Data @RequiredArgsConstructor public class LogbackLogPrinter implements LogPrinter { private final Logger log = LoggerFactory.getLogger(LogbackLogPrinter.class); /** * 序列化器 */ private final LogSerializer serializer; @Override public void print(@NonNull RequestLog requestLog) { byte[] bytes = serializer.serializer(requestLog); log.info(new String(bytes)); } }编写日志拦截器类import java.lang.annotation.Annotation; import java.util.Date; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.lang.NonNull; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.io.Serializable; import java.lang.reflect.Method; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; /** * 日志序列化拦截器 * @author TheEnd */ @Slf4j @ControllerAdvice @RequiredArgsConstructor public class LogPersistenceInterceptor implements HandlerInterceptor, ResponseBodyAdvice<Object> { /** * 请示时间Key */ private static final String API_START_TIME = "ApiLogSerializableInterceptor:preHandle:start:millis"; /** * 请求体Key */ private static final String API_REQUEST_BODY = "ApiLogSerializableInterceptor:preHandle:request:body"; /** * 处理器key */ private static final String API_HANDLER = "ApiLogSerializableInterceptor:preHandle:handler"; /** * JSON 工具对象 */ private final ObjectMapper logMapper = new ObjectMapper(); /** * 日志打印器 */ private final List<LogPrinter> logPrinters; /** * 当前登录用户信息获取器 */ private final LoginUserInfoHelper loginUserInfoHelper; @Override public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) { return true; } @Override public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception { request.setAttribute(API_HANDLER, handler); if (!isRecord(request, response, handler)) { return true; } Date startTime = new Date(); request.setAttribute(API_START_TIME, startTime); String body = readBody(request); request.setAttribute(API_REQUEST_BODY, body); return true; } @Override public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType, @NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType, @NonNull ServerHttpRequest request, @NonNull ServerHttpResponse response) { HttpServletRequest req = ((ServletServerHttpRequest) request).getServletRequest(); HttpServletResponse res = ((ServletServerHttpResponse) response).getServletResponse(); Object handler = req.getAttribute(API_HANDLER); if (isRecord(req, res, handler)) { return body; } String result = ""; if (!isBinaryContent(res) && body instanceof Serializable) { try { result = logMapper.writeValueAsString(body); } catch (JsonProcessingException e) { log.error("HTTP响应体JSON转换失败", e); } } RequestLog requestLog = buildLog(req, res, handler, result); for (LogPrinter printer : logPrinters) { printer.print(requestLog); } return body; } private String readBody(HttpServletRequest request) { if (request.getContentLength() <= 0) { return ""; } BufferedReader reader = null; try { reader = request.getReader(); } catch (IOException e) { throw new RuntimeException(e); } Stream<String> lines = reader.lines(); return lines.collect(Collectors.joining(System.lineSeparator())); } private boolean isRecord(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) { if (!(handler instanceof HandlerMethod)) { return false; } HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); if (method.isAnnotationPresent(NotLog.class)) { return false; } Class<?> declaringClass = method.getDeclaringClass(); if (declaringClass.isAnnotationPresent(NotLog.class)) { return false; } if (isBinaryContent(request)) { return false; } return true; } private RequestLog buildLog(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, @NonNull String result) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); LogExtra methodExtra = method.getAnnotation(LogExtra.class); Class<?> declaringClass = method.getDeclaringClass(); LogExtra classExtra = declaringClass.getAnnotation(LogExtra.class); LogExtra merge = mergeLogExtra(methodExtra, classExtra); RequestLog requestLog = new RequestLog(); requestLog.setMethod(request.getMethod()); requestLog.setUrl(request.getRequestURI()); requestLog.setIp(IpUtil.getIp(request)); requestLog.setCookie(cookieSerializable(request)); requestLog.setRequestParams(parameterSerializable(request)); requestLog.setRequestBody((String) request.getAttribute(API_REQUEST_BODY)); requestLog.setResponseCode(response.getStatus()); requestLog.setResponseBody(result); requestLog.setUserId(loginUserInfoHelper.userId(request, response)); requestLog.setUsername(loginUserInfoHelper.username(request, response)); requestLog.setModelName(merge.modelName()); requestLog.setInterfaceName(merge.interfaceName()); requestLog.setRemarks(merge.remarks()); Date startTime = (Date) request.getAttribute(API_START_TIME); requestLog.setRequestTime(startTime); Date responseTime = new Date(); requestLog.setResponseTime(responseTime); requestLog.setUseTime(Math.toIntExact(responseTime.getTime() - startTime.getTime())); return requestLog; } private String cookieSerializable(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); try { return logMapper.writeValueAsString(cookies); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } private String parameterSerializable(HttpServletRequest request) { Map<String, String[]> parameterMap = request.getParameterMap(); try { return logMapper.writeValueAsString(parameterMap); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } private LogExtra mergeLogExtra(LogExtra methodExtra, LogExtra classExtra) { return new LogExtra(){ @Override public String modelName() { if (methodExtra != null && methodExtra.modelName() != null && !methodExtra.modelName().isEmpty()) { return methodExtra.modelName(); } if (classExtra != null && classExtra.modelName() != null && !classExtra.modelName().isEmpty()) { return classExtra.modelName(); } return ""; } @Override public String interfaceName() { if (methodExtra != null && methodExtra.interfaceName() != null && !methodExtra.interfaceName().isEmpty()) { return methodExtra.interfaceName(); } if (classExtra != null && classExtra.interfaceName() != null && !classExtra.interfaceName().isEmpty()) { return classExtra.interfaceName(); } return ""; } @Override public String remarks() { if (methodExtra != null && methodExtra.remarks() != null && !methodExtra.remarks().isEmpty()) { return methodExtra.remarks(); } if (classExtra != null && classExtra.remarks() != null && !classExtra.remarks().isEmpty()) { return classExtra.remarks(); } return ""; } @Override public Class<? extends Annotation> annotationType() { return LogExtra.class; } }; } }配置拦截器import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.Collections; import java.util.List; /** * @author TheEnd */ @Configuration public class MvcConfigurer implements WebMvcConfigurer { /** * 日志持久化拦截器 */ private LogPersistenceInterceptor logPersistenceInterceptor; @Autowired public MvcConfigurer setLogPersistenceInterceptor(LogPersistenceInterceptor logPersistenceInterceptor) { this.logPersistenceInterceptor = logPersistenceInterceptor; return this; } /** * 日志打印器 * @return 日志打印器集合 */ @Bean public List<LogPrinter> logPrinters() { // 根据业务实际情况创建并实例化日志打印器, 并注册至IOC容器. LogbackLogPrinter printer = new LogbackLogPrinter(new JsonLogSerializer()); return Collections.singletonList(printer); } /** * 用户信息获取器 * @return 用户信息获取器 */ @Bean public LoginUserInfoHelper loginUserInfoHelper() { // 根据业务实际情况创建并实例化用户信息获取器, 并注册至IOC容器. return new EmptyLoginUserInfoHelper(); } /** * 添加拦截器 * @param registry 拦截器注册 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(logPersistenceInterceptor); } }解决请求体无法重复读取在Filter中,替换原始的请求对象,实现请求体可以重读读取创建RequestWrapper对象, 用于包装原始Request对象import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringReader; /** * 请求包装器 * 用于解决请求体无法重复读取问题 * @author TheEnd */ public class RequestWrapper extends HttpServletRequestWrapper { public RequestWrapper(HttpServletRequest request) { super(request); } private volatile String requestBody; @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new StringReader(getRequestBody())); } @Override public ServletInputStream getInputStream() throws IOException { return new ServletInputStreamWrapper(requestBody.getBytes()); } private static class ServletInputStreamWrapper extends ServletInputStream { private final ByteArrayInputStream inputStream; public ServletInputStreamWrapper(byte[] body) { inputStream = new ByteArrayInputStream(body); } @Override public int read() throws IOException { return inputStream.read(); } @Override public boolean isFinished() { return inputStream.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener listener) { } } private String getRequestBody() throws IOException { if (requestBody == null) { synchronized (this) { if (requestBody == null) { StringBuilder requestBodyBuilder = new StringBuilder(); BufferedReader reader = super.getReader(); String line; while ((line = reader.readLine()) != null) { requestBodyBuilder.append(line); } requestBody = requestBodyBuilder.toString(); } } } return requestBody; } }编写过滤器并替换原始Request对象import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * 用于替换请求对象的过滤器 * 用于解决请求体无法重复读取问题 * @author TheEnd */ public class HttpServletRequestWrapperFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { if (!(servletRequest instanceof HttpServletRequest)) { filterChain.doFilter(servletRequest, servletResponse); return; } HttpServletRequest request = (HttpServletRequest) servletRequest; if (ContentTypeUtil.isBinaryContent(request) || request.getContentLength() < 1) { filterChain.doFilter(request, servletResponse); return; } filterChain.doFilter(new RequestWrapper(request), servletResponse); } }
SpringBoot 常用功能和封装代码小计一. Spring框架通过静态方式获取上下文对象(ApplicationContext)创建一个名为ApplicationContextProvider的类，实现ApplicationContextAware接口。 实现这个接口后，Spring会在ApplicationContext初始化时调用setApplicationContext方法，在这个方法中将ApplicationContext赋值给静态变量context。然后即可在任何地方通过ApplicationContextProvider类的静态方法getApplicationContext()来获取ApplicationContext对象import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component public class ApplicationContextProvider implements ApplicationContextAware { private static ApplicationContext context; @Override public void setApplicationContext(ApplicationContext applicationContext) { context = applicationContext; } public static ApplicationContext getApplicationContext() { return context; } }获取示例:ApplicationContext context = ApplicationContextProvider.getApplicationContext();通过静态方式获取请求对象(HttpServletRequest)SpringBoot 项目中, 可以在非Controller类中使用RequestContextHolder来获取当前请求的HttpServletRequest对象。但是请注意，这种方式不是推荐的做法，因为它会使代码与Spring框架耦合度较高。示例import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; public class MyService { public void doSomething() { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 使用request对象获取请求信息 String userAgent = request.getHeader("User-Agent"); System.out.println("User-Agent: " + userAgent); } }SpringBoot 项目打包为可执行 Jar 包添加POM依赖<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <!-- <version>2.5.2</version> --> <configuration> <executable>true</executable> </configuration> </plugin> </plugins> </build>执行打包命令mvn clean package测试是否正常运行java -jar app.jarSpringBoot 项目加载外部配置文件通过命令行参数指定配置文件路径在启动应用程序时，可以使用--spring.config.location参数来指定配置文件的路径，例如：java -jar app.jar --spring.config.location=file:/custom.properties通过配置文件配置在application.properties或application.yml中指定配置文件路径：在application.properties文件中添加如下配置：spring.config.location=file:/custom.properties或者在application.yml文件中添加如下配置：spring: config: location: file:/custom.properties二. SpringMVC全局统一响应对象封装创建 Result<T> 类import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; /** * 全局统一响应对象 * @author TheEnd */ @Data @Accessors(chain = true) @NoArgsConstructor @AllArgsConstructor public class Result<T> implements Serializable { /** * 响应码 */ private Integer code; /** * 消息 */ private String msg; /** * 数据 */ private T data; public Result(Integer code) { this.code = code; } public Result(String msg) { this.msg = msg; } public Result(Integer code, String msg) { this.code = code; this.msg = msg; } public Result(T data) { this.data = data; } public static Result<String> success() { return new Result<>(Code.SUCCESS); } public static <T> Result<T> success(T t) { return new Result<>(Code.SUCCESS, "", t); } public static Result<String> fail() { return new Result<>(Code.FAIL, "发生异常"); } public static Result<String> fail(Integer code) { return new Result<>(code, "发生异常"); } public static Result<String> fail(String msg) { return new Result<>(Code.FAIL, msg); } public static Result<String> fail(Integer code, String msg) { return new Result<>(code, msg); } public static Result<String> authFail() { return new Result<>(Code.AUTH, "权限不足"); } public static Result<String> businessFail() { return new Result<>(Code.BUSINESS, "业务异常"); } public static Result<String> remoteFail() { return new Result<>(Code.REMOTE, "业务异常"); } public static class Code { public static final Integer SUCCESS = 200; public static final Integer FAIL = 0; public static final Integer AUTH = 400; public static final Integer BUSINESS = 500; public static final Integer REMOTE = 600; } }全局统一异常处理创建自定义异常默认异常/** * 默认异常 * @author TheEnd */ public class DefaultException extends RuntimeException { public DefaultException() { } public DefaultException(String message) { super(message); } public DefaultException(String message, Throwable cause) { super(message, cause); } public DefaultException(Throwable cause) { super(cause); } public DefaultException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } }业务异常/** * 业务异常 * @author TheEnd */ public class BusinessException extends RuntimeException { public BusinessException() { } public BusinessException(String message) { super(message); } public BusinessException(String message, Throwable cause) { super(message, cause); } public BusinessException(Throwable cause) { super(cause); } public BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } }远程服务异常/** * 远程服务异常 * @author TheEnd */ public class RemoteServiceException extends RuntimeException { public RemoteServiceException() { } public RemoteServiceException(String message) { super(message); } public RemoteServiceException(String message, Throwable cause) { super(message, cause); } public RemoteServiceException(Throwable cause) { super(cause); } public RemoteServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } }配置全局异常处理器创建 GlobalExceptionHandler 类import lombok.extern.slf4j.Slf4j; import org.example.blog.common.Result; import org.example.blog.exception.BusinessException; import org.example.blog.exception.DefaultException; import org.example.blog.exception.RemoteServiceException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理器 * @author Lenovo */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(RemoteServiceException.class) public Result<String> handleException(RemoteServiceException e) { log.error("远程服务发生异常", e); return Result.remoteFail().setMsg(e.getMessage()); } @ExceptionHandler(BusinessException.class) public Result<String> handleException(BusinessException e) { log.error("发生业务异常", e); return Result.businessFail().setMsg(e.getMessage()); } @ExceptionHandler(DefaultException.class) public Result<String> handleException(DefaultException e) { log.error("发生异常", e); return Result.fail(e.getMessage()); } @ExceptionHandler(Exception.class) public Result<String> handleException(Exception e) { log.error("发生异常", e); return Result.fail(e.getMessage()); } }三. Spring-Data-JdbcSpringBoot 项目配置动态数据源通过AbstractRoutingDataSource实现动态数据源创建数据源类型枚举根据项目数据源实际情况修改/** * 数据源类型枚举 * @author TheEnd */ public enum DatasourceType { /** * 主库 */ PRIMARY, /** * 三方库 */ SECONDARY } 创建DynamicDataSourceContextHolder对象用于切换数据源/** * 动态数据源切换器 * @author TheEnd */ public class DynamicDataSourceContextHolder { private static final ThreadLocal<DatasourceType> CONTEXT_HOLDER = new ThreadLocal<>(); /** * 设置当前线程使用的数据源类型 * @param dataSourceType 数据源类型 */ public static void setDataSourceType(DatasourceType dataSourceType) { CONTEXT_HOLDER.set(dataSourceType); } /** * 获取当前线程使用的数据源类型 * @return 数据源类型 */ public static DatasourceType getDataSourceType() { return CONTEXT_HOLDER.get(); } /** * 清除当前线程使用的数据源类型 */ public static void clearDataSourceType() { CONTEXT_HOLDER.remove(); } }创建DynamicDataSource对象动态数据源对象, 继承至AbstractRoutingDataSourceimport org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * 动态数据源对象 * @author TheEnd */ public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { // 返回当前线程的数据源类型 return DynamicDataSourceContextHolder.getDataSourceType(); } }创建DataSourceConfig对象在SpringBoot中配置动态数据源import org.example.datasource.DatasourceType; import org.example.datasource.DynamicDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.core.JdbcTemplate; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * 动态数据源配置 * @author TheEnd */ @Configuration public class DataSourceConfig { /** * 主数据源 * @return 主数据源对象 */ @Bean @ConfigurationProperties(prefix = "datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } /** * 三方数据源 * @return 三方数据源对象 */ @Bean @ConfigurationProperties(prefix = "datasource.secondary") public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } /** * 动态数据源 * @param primaryDataSource 主数据源 * @param secondaryDataSource 三方数据源 * @return 动态数据源对象 */ @Bean @Primary public DynamicDataSource dataSource(DataSource primaryDataSource, DataSource secondaryDataSource) { // 将数据源封装为 Map Map<Object, Object> targetDataSources = new HashMap<>(4); targetDataSources.put(DatasourceType.PRIMARY, primaryDataSource); targetDataSources.put(DatasourceType.SECONDARY, secondaryDataSource); // 配置动态数据源对象 DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSources); // 设置默认数据源为主数据源 dataSource.setDefaultTargetDataSource(primaryDataSource); return dataSource; } @Bean public JdbcTemplate jdbcTemplate(DynamicDataSource dataSource) { return new JdbcTemplate(dataSource); } }数据源切换示例使用DynamicDataSourceContextHolder切换数据源import org.example.datasource.DatasourceType; import org.example.datasource.DynamicDataSourceContextHolder; /** * @author TheEnd */ public class DatasourceService { public void datasourceChange() { // 设置使用主数据源 DynamicDataSourceContextHolder.setDataSourceType(DatasourceType.PRIMARY); // 设置使用三方数据源 DynamicDataSourceContextHolder.setDataSourceType(DatasourceType.SECONDARY); // 清除数据源配置 DynamicDataSourceContextHolder.clearDataSourceType(); } }如果发生循环依赖异常, 取消SpringBoot中数据源自动配置即可@SpringBootApplication(exclude = DataSourceAutoConfiguration.class);基于AOP实现通过注解自动切换数据源创建自定义注解DataSourceSwitch注解可以标注在类上或方法上。在方法上标注注解后, 在方法执行前自动切换数据源，并在方法执行后还原数据源。同时，也可以在类级别上使用该注解，使类中的所有方法都具有相同的切换数据源行为。import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author TheEnd */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface DataSourceSwitch { DatasourceType value(); }创建切面类DataSourceSwitchAspectimport org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.reflect.MethodSignature; import org.example.datasource.DatasourceType; import org.example.datasource.DynamicDataSourceContextHolder; import org.springframework.stereotype.Component; import java.lang.reflect.Method; /** * @author Lenovo */ @Aspect @Component public class DataSourceSwitchAspect { private final ThreadLocal<DatasourceType> previousDataSourceType = new ThreadLocal<>(); @Before("@annotation(dataSourceSwitch) || @within(dataSourceSwitch)") public void switchDataSource(JoinPoint joinPoint, DataSourceSwitch dataSourceSwitch) { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); DataSourceSwitch methodAnnotation = method.getAnnotation(DataSourceSwitch.class); if (methodAnnotation != null) { DatasourceType dataSourceType = methodAnnotation.value(); previousDataSourceType.set(DynamicDataSourceContextHolder.getDataSourceType()); DynamicDataSourceContextHolder.setDataSourceType(dataSourceType); } else { DatasourceType classDataSourceType = dataSourceSwitch.value(); previousDataSourceType.set(DynamicDataSourceContextHolder.getDataSourceType()); DynamicDataSourceContextHolder.setDataSourceType(classDataSourceType); } } @After("@annotation(dataSourceSwitch) || @within(dataSourceSwitch)") public void restoreDataSource(JoinPoint joinPoint, DataSourceSwitch dataSourceSwitch) { DynamicDataSourceContextHolder.setDataSourceType(previousDataSourceType.get()); previousDataSourceType.remove(); } }使用注解DataSourceSwitch实现自动切换数据源import org.example.datasource.DatasourceType; import org.example.datasource.DynamicDataSourceContextHolder; import org.example.ds.DataSourceSwitch; import org.springframework.stereotype.Service; /** * @author Lenovo */ @Service @DataSourceSwitch(DatasourceType.SECONDARY) public class DsService { @DataSourceSwitch(DatasourceType.PRIMARY) public DatasourceType test() { return DynamicDataSourceContextHolder.getDataSourceType(); } @DataSourceSwitch(DatasourceType.SECONDARY) public DatasourceType test2() { return DynamicDataSourceContextHolder.getDataSourceType(); } public DatasourceType test3() { return DynamicDataSourceContextHolder.getDataSourceType(); } }三. 整合三方框架
Java 调用打印机打印PDF文件添加依赖<dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> <version>3.0.2</version> </dependency>编写代码package cn.zzzykj.demo; import cn.hutool.cache.CacheUtil; import cn.hutool.cache.impl.TimedCache; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.printing.PDFPageable; import sun.print.PageableDoc; import javax.print.*; import javax.print.attribute.standard.PrinterIsAcceptingJobs; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.file.Files; import java.util.Arrays; import java.util.Optional; import java.util.concurrent.TimeUnit; /** * @author Lenovo */ @Slf4j public class PrintHelper { public static final String PRINT_NAME = "xxx"; public static final String HTTP_PREFIX = "http"; private static final TimedCache<String, PrintJobState> JOB_STATE_CACHE = CacheUtil.newTimedCache(TimeUnit.HOURS.toMillis(1)); public static int status() { PrintService service = findPrint(); if (service == null) { return 6; } PrinterIsAcceptingJobs attribute = service.getAttribute(PrinterIsAcceptingJobs.class); return attribute.getValue() == 1 ? 0 : 7; } public static boolean printFile(String path, String fileName) { PrintService service = findPrint(); try ( PDDocument pdDocument = Loader.loadPDF(getBytes(path)); ) { PDFPageable pageable = new PDFPageable(pdDocument); int pages = pageable.getNumberOfPages(); PrintJobState jobState = new PrintJobState(pages, 1, path, fileName); JOB_STATE_CACHE.put(StrUtil.isNotEmpty(fileName) ? fileName : "", jobState); PageableDoc doc = new PageableDoc(pageable); DocPrintJob job = service.createPrintJob(); job.print(doc, null); return true; } catch (IOException e) { log.error("Failed to load PDF file..."); return false; } catch (PrintException e) { log.error("Abnormal submission of printing task..."); return false; } } public static PrintJobState jobState(String fileName) { if (fileName == null) { fileName = ""; } return JOB_STATE_CACHE.get(fileName); } private static PrintService findPrint() { PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null); Optional<PrintService> hpPrint = Arrays.stream(services).filter(s -> s.getName().contains(PRINT_NAME)).findFirst(); return hpPrint.orElse(null); } private static InputStream getInputStream(String path) throws IOException { InputStream stream; if (path.toLowerCase().startsWith(HTTP_PREFIX)) { URL url = new URL(path); stream = url.openStream(); } else { stream = Files.newInputStream(new File(path).toPath()); } return stream; } private static byte[] getBytes(String path) throws IOException { if (path.toLowerCase().startsWith(HTTP_PREFIX)) { URL url = new URL(path); return IoUtil.readBytes(url.openStream()); } else { return FileUtil.readBytes(path); } } @Data @Accessors(chain = true) @NoArgsConstructor @AllArgsConstructor public static class PrintJobState { private Integer total; private Integer curr; private String name; private String path; } }
Java 实现录音并写出到文件Java 实现录音package cn.zzzykj.demo; import cn.hutool.core.date.DateUtil; import cn.hutool.core.io.file.PathUtil; import cn.hutool.core.util.RandomUtil; import lombok.AllArgsConstructor; import lombok.Data; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import javax.sound.sampled.*; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Date; import java.util.concurrent.*; /** * @author TheEnd */ @Slf4j public class AudioHelper { private static final TargetDataLine MICROPHONE; private static final ExecutorService executorService = Executors.newSingleThreadExecutor(); private static Future<AudioTaskInfo> future; static { try { MICROPHONE = AudioSystem.getTargetDataLine(new AudioFormat(44100.0F, 16, 2, true, false)); } catch (LineUnavailableException e) { log.error("Audio Init: ", e); throw new RuntimeException(e); } } public static void open() { try { log.info("Audio Open..."); MICROPHONE.open(new AudioFormat(44100.0F, 16, 2, true, false)); } catch (LineUnavailableException e) { log.error("Audio Open: ", e); } } public static synchronized void start() { if (future == null) { future = executorService.submit(new AudioTask()); } } public static synchronized String stop() { if (future == null) { throw new RuntimeException(); } log.info("Audio Stop..."); MICROPHONE.stop(); try { AudioTaskInfo info = future.get(); return info.getFile().getAbsoluteFile().getAbsolutePath(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } public static void close() { MICROPHONE.close(); } @Data @Getter public static class AudioTask implements Callable<AudioTaskInfo> { private AudioTaskInfo info; private static File newWav() { Path audioTmpDir = Paths.get("tmp", "audio", "wav"); PathUtil.mkdir(audioTmpDir); String audioTmpName = DateUtil.format(new Date(), "yyyyMMddHHmmss") + RandomUtil.randomNumbers(3) + ".wav"; Path audioTmpPath = Paths.get(audioTmpDir.toString(), audioTmpName); return audioTmpPath.toFile(); } private static File saveWav() { File outputFile = newWav(); AudioFileFormat.Type fileType = AudioFileFormat.Type.WAVE; try { log.info("Write Wav File: {}", outputFile.getPath()); AudioSystem.write(new AudioInputStream(AudioHelper.MICROPHONE), fileType, outputFile); log.info("Write Wav File Success: {}", outputFile.getPath()); } catch (IOException e) { log.info("Write Wav File Fail: {}", outputFile.getPath()); log.error(e.getMessage(), e); throw new RuntimeException(e); } return outputFile; } @Override public AudioTaskInfo call() throws Exception { log.info("Audio Start..."); MICROPHONE.start(); File file = saveWav(); return new AudioTaskInfo(file); } } @Getter @NoArgsConstructor @AllArgsConstructor public static class AudioTaskInfo { private File file; } }
SpringSession会话管理一. Session详情实体类package cn.zzzykj.session.vo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import java.util.Date; /** * @author TheEnd */ @Data @Accessors(chain = true) @NoArgsConstructor @AllArgsConstructor public class SessionInfoVO { private String id; private String sessionId; private String name; private Date creationTime; private Date lastAccessedTime; } 二. 会话管理器1. 创建会话管理器接口package cn.zzzykj.session; import java.util.List; /** * @author TheEnd */ public interface ISessionManager { /** * 获取所有会话信息 * @return 会话信息列表 */ List<SessionInfoVO> getAllSessionInfo(); /** * 使指定会话失效 * @param sessionId 会话id */ void invalidateSession(String sessionId); } 2.编写实现类并注入到Spring容器中 package cn.zzzykj.session; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ScanOptions; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.session.Session; import org.springframework.session.SessionRepository; import org.springframework.session.data.redis.RedisIndexedSessionRepository; import org.springframework.session.jdbc.JdbcIndexedSessionRepository; import org.springframework.stereotype.Component; import java.io.IOException; import java.time.Instant; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * @author Lenovo */ @Slf4j @Component public class DefaultSessionManager implements SessionManager { /** * 会话仓库 */ private SessionRepository<?> sessionRepository; @Autowired public DefaultSessionManager setSessionRepository(SessionRepository<?> sessionRepository) { this.sessionRepository = sessionRepository; return this; } /** * jdbc模板 */ private JdbcTemplate jdbcTemplate; @Autowired public DefaultSessionManager setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; return this; } /** * redis模板 */ private RedisTemplate<String, String> redisTemplate; @Autowired public DefaultSessionManager setRedisTemplate(RedisTemplate<String, String> redisTemplate) { this.redisTemplate = redisTemplate; return this; } @Override public List<SessionInfoVO> getAllSessionInfo() { ArrayList<Session> sessions; if (sessionRepository instanceof JdbcIndexedSessionRepository) { sessions = getJdbcSessions(); } else if (sessionRepository instanceof RedisIndexedSessionRepository) { sessions = getRedisSessions(); } else { log.error("不支持的SessionRepository类型: {}", sessionRepository.getClass().getName()); throw new RuntimeException("不支持的SessionRepository类型: " + sessionRepository.getClass().getName()); } ArrayList<SessionInfoVO> sessionInfos = new ArrayList<>(); for (Session session : sessions) { SessionInfoVO sessionInfo = new SessionInfoVO(); String id = session.getId(); sessionInfo.setId(id); sessionInfo.setSessionId(id); String username = getPrincipalNameFromSession(session); sessionInfo.setName(username); Instant creationTime = session.getCreationTime(); sessionInfo.setCreationTime(new Date(creationTime.toEpochMilli())); Instant lastAccessedTime = session.getLastAccessedTime(); sessionInfo.setLastAccessedTime(new Date(lastAccessedTime.toEpochMilli())); sessionInfos.add(sessionInfo); } return sessionInfos; } @Override public void invalidateSession(String sessionId) { sessionRepository.deleteById(sessionId); } private ArrayList<Session> getJdbcSessions() { ArrayList<Session> sessions = new ArrayList<>(); jdbcTemplate.query("SELECT SESSION_ID FROM SPRING_SESSION", resultSet -> { String sessionId = resultSet.getString("SESSION_ID"); Session session = sessionRepository.findById(sessionId); sessions.add(session); }); return sessions; } private ArrayList<Session> getRedisSessions() { ArrayList<Session> sessions = new ArrayList<>(); ScanOptions options = ScanOptions.scanOptions() .match(RedisIndexedSessionRepository.DEFAULT_NAMESPACE + ":sessions:*") .build(); RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory(); if (connectionFactory != null) { try (RedisConnection connection = connectionFactory.getConnection()) { try (Cursor<byte[]> cursor = connection.scan(options)) { while (cursor.hasNext()) { String key = new String(cursor.next()); String sessionId = key.substring(RedisIndexedSessionRepository.DEFAULT_NAMESPACE.length() + 10); if (sessionId.contains(":")) { continue; } Session session = sessionRepository.findById(sessionId); if (session != null) { sessions.add(session); } } } catch (IOException e) { throw new RuntimeException(e); } } } return sessions; } private String getPrincipalNameFromSession(Session session) { Object springSecurityContext = session.getAttribute("SPRING_SECURITY_CONTEXT"); if (springSecurityContext instanceof SecurityContextImpl) { Authentication authentication = ((SecurityContextImpl) springSecurityContext).getAuthentication(); if (authentication != null) { return authentication.getName(); } } return null; } }
SpringBoot 基于 SSE(Server-Send-Event)实现日志文件实时监控
SpringBoot 基于 SSE(Server-Send-Event)实现日志文件实时监控一. 后端接口实现:@GetMapping(value = "/log") @ResponseBody public SseEmitter realtimeLog() { SseEmitter emitter = new SseEmitter(0L); String file = "./log.log"; Tailer tailer = new Tailer(FileUtil.newFile(file), StandardCharsets.UTF_8, line -> { try { emitter.send(line); } catch (IOException e) { log.error("SSE响应日志异常", e); throw new RuntimeException(e); } }, 10, DateUnit.SECOND.getMillis()); tailer.start(true); emitter.onCompletion(tailer::stop); return emitter; }二. 前端实现 实时日志监控
HTML中使用 Monaco Editor 编辑器Monaco Editor 是一个由 Microsoft 开发的基于 Web 的代码编辑器，它最初是为 Visual Studio Code 编辑器开发的。Monaco Editor 提供了许多强大的功能，包括语法高亮、智能代码补全、代码折叠、错误提示、代码片段等。作为一个基于 Web 的编辑器，Monaco Editor 可以很容易地嵌入到任何支持 Web 技术的应用程序中，使得开发人员可以在他们的应用程序中使用类似 Visual Studio Code 的编辑体验。Monaco Editor 也是 Visual Studio Code 的核心编辑器，因此它与 Visual Studio Code 共享了许多功能和特性，包括相似的外观和行为。如果需要在 Web 应用程序中集成一个功能强大的代码编辑器，Monaco Editor 可能是一个很好的选择。一. 下载 Monaco Editor下载链接: monaco-editor-0.45.0.tgzGitHubAPI文档项目首页下载后解压至项目静态资源目录中二. 使用 Monaco Editor1. 引入 Monaco Editor 2. 创建编辑器容器 3. 初始化编辑器const options = { value: [ 'function x() {', '\tconsole.log("Hello, world!");', '}' ].join('\n'), // 设置编辑器初始内容 language: 'javascript', // 设置编辑器语言 automaticLayout: true, // 自动调整布局 readOnly: false, // 是否只读 theme: 'vs-dark', // 设置编辑器主题 fontSize: 14, // 设置字体大小 fontFamily: 'Courier New', // 设置字体 wordWrap: 'on', // 设置自动换行 minimap: { enabled: true // 是否显示迷你地图 }, scrollbar: { useShadows: false, // 是否使用阴影 verticalHasArrows: true, // 垂直滚动条是否显示箭头 horizontal: 'auto', // 设置水平滚动条 vertical: 'auto' // 设置垂直滚动条 }, lineNumbers: 'on', // 是否显示行号 lineDecorationsWidth: 2, // 设置行号装饰宽度 renderWhitespace: 'all', // 显示空格和制表符 renderControlCharacters: true, // 显示控制字符 rulers: [80, 120], // 设置标尺 glyphMargin: true, // 是否显示字形边距 lightbulb: { enabled: true // 是否显示灯泡建议 }, contextmenu: true, // 是否启用右键菜单 folding: true, // 是否启用代码折叠 suggest: { showIcons: true, // 是否显示建议的图标 filteredTypes: { keyword: false, snippet: true, color: true, reference: true } // 过滤建议类型 }, quickSuggestions: { other: true, comments: false, strings: true } }; // 创建 Monaco Editor 实例 monaco.editor.create(document.getElementById('editor'), options);三. 常用功能// 创建编辑器实例：使用 monaco.editor.create 方法可以创建一个新的编辑器实例。 var editor = monaco.editor.create(document.getElementById('container'), { value: "function hello() {\n\talert('Hello, world!');\n}", language: "javascript" }); // 获取和设置文本内容：使用 editor.getValue() 方法可以获取编辑器中的文本内容，使用 editor.setValue(newValue) 方法可以设置编辑器的文本内容。 var currentText = editor.getValue(); editor.setValue("function goodbye() {\n\tconsole.log('Goodbye, world!');\n}"); // 语法高亮：可以通过 editor.deltaDecorations 方法添加或移除行内装饰，以实现自定义的语法高亮。 var decorations = editor.deltaDecorations([], [ { range: new monaco.Range(1, 1, 1, 20), options: { isWholeLine: true, linesDecorationsClassName: 'myLineDecoration' } } ]); // 设置语言：使用 editor.setModelLanguage 方法可以设置编辑器的语言，以便进行相应语法高亮和代码提示。 monaco.editor.setModelLanguage(editor.getModel(), 'typescript'); // 注册事件监听器：可以通过 editor.onDidChangeModelContent 方法注册事件监听器，以便在内容变化时执行相应的操作。 editor.onDidChangeModelContent(function(event) { console.log("Editor content changed: " + editor.getValue()); }); // 代码折叠：使用 editor.setFoldingStrategy 方法可以设置代码折叠策略，实现代码折叠功能。 editor.setFoldingStrategy(function(context, firstLineNumber, line, lastLineNumber) { // Custom folding logic here }); // 自动完成和智能提示：可以通过 editor.createContextKey 方法创建上下文键，以便根据特定的上下文显示自动完成和智能提示。 var myContextKey = editor.createContextKey('isReadOnly', false); myContextKey.set(true); // 设置主题：使用 monaco.editor.defineTheme 方法可以定义新的编辑器主题，以及 editor.setTheme 方法可以设置编辑器的主题。 monaco.editor.defineTheme('myCustomTheme', { base: 'vs', inherit: true, rules: [{ background: 'EDF2F7' }], colors: { 'editor.foreground': '#000000' } }); editor.setTheme('myCustomTheme'); // 布局控制：可以使用 editor.layout 方法手动触发编辑器的重新布局。 editor.layout(); // 获取光标位置和选中文本：通过 editor.getPosition 和 editor.getSelection 方法可以获取光标位置和选中的文本内容。 var currentPosition = editor.getPosition(); var selectedText = editor.getModel().getValueInRange(editor.getSelection()); // 代码格式化：可以使用 editor.getAction 方法获取格式化代码的动作，以便执行代码格式化操作。 var formatAction = editor.getAction('editor.action.formatDocument'); formatAction.run(); // 获取编辑器配置：可以通过 editor.getRawOptions 方法获取编辑器的原始配置信息。 var editorOptions = editor.getRawOptions(); // 设置编辑器配置：可以使用 editor.updateOptions 方法动态更新编辑器的配置选项。 editor.updateOptions({ tabSize: 4 }); // 执行命令：可以通过 editor.executeEdits 方法执行编辑器的编辑操作，比如插入文本、替换文本等。 editor.executeEdits("my-source", [{ range: new monaco.Range(1, 1, 1, 1), text: "Hello, World!" }]); // 获取模型：可以通过 editor.getModel 方法获取编辑器当前的模型（包含文本内容、语言等信息）。 var model = editor.getModel(); // 设置模型：可以使用 editor.setModel 方法设置编辑器的模型，以显示不同的文本内容或切换语言。 var newModel = monaco.editor.createModel("console.log('Hello, Monaco Editor!')", "javascript"); editor.setModel(newModel); // 撤销和重做操作：可以通过 editor.trigger 方法执行编辑器的撤销和重做操作。 editor.trigger('keyboard', 'redo', null); // 获取编辑器实例：可以通过 monaco.editor.getEditor 方法获取现有的编辑器实例，以便对其进行操作。 var editor = monaco.editor.getEditor('container-id'); // 设置编辑器的尺寸：可以使用 editor.layout 方法手动触发编辑器的重新布局，以适应容器的尺寸变化。 editor.layout(); // 获取光标位置和选中文本：可以通过 editor.getPosition 和 editor.getSelection 方法获取光标位置和选中的文本内容。 var currentPosition = editor.getPosition(); var selectedText = editor.getModel().getValueInRange(editor.getSelection()); // 获取编辑器内容范围：可以使用 editor.getModel().getFullModelRange() 获取编辑器内容的完整范围。 var fullRange = editor.getModel().getFullModelRange(); // 格式化选定的文本：可以使用 editor.getAction 方法获取格式化选定文本的动作，以便执行选定文本的格式化操作。 var formatSelectionAction = editor.getAction('editor.action.formatSelection'); formatSelectionAction.run(); // 设置编辑器的语言：可以使用 editor.setModelLanguage 方法设置编辑器的语言。 monaco.editor.setModelLanguage(editor.getModel(), 'html'); // 获取编辑器的所有装饰器：可以使用 editor.getAllDecorations 方法获取编辑器的所有装饰器。 var allDecorations = editor.getAllDecorations(); // 设置编辑器的装饰器：可以使用 editor.deltaDecorations 方法添加或移除装饰器。 var newDecorations = editor.deltaDecorations([], [ { range: new monaco.Range(2, 1, 2, 20), options: { isWholeLine: true, linesDecorationsClassName: 'myLineDecoration' } } ]); // 获取编辑器的所有动作：可以使用 editor.getActions 方法获取编辑器的所有动作。 var allActions = editor.getActions(); // 设置编辑器的内容状态：可以使用 editor.changeDecorations 方法改变编辑器的内容状态。 editor.changeDecorations(callback); // 设置编辑器的焦点：可以使用 editor.focus 方法设置编辑器的焦点。 editor.focus(); // 获取编辑器的所有模型：可以使用 monaco.editor.getModels 方法获取所有当前存在的编辑器模型。 var allModels = monaco.editor.getModels(); // 注销模型：可以使用 model.dispose 方法注销不再需要的编辑器模型。 model.dispose(); // 获取编辑器的所有语言：可以使用 monaco.languages.getLanguages 方法获取所有支持的语言。 var allLanguages = monaco.languages.getLanguages(); // 注册新的语言：可以使用 monaco.languages.register 方法注册新的语言支持。 monaco.languages.register({ id: 'myCustomLanguage' }); // 获取编辑器的所有主题：可以使用 monaco.editor.getThemes 方法获取所有可用的主题。 var allThemes = monaco.editor.getThemes(); // 注册新的主题：可以使用 monaco.editor.defineTheme 方法注册新的编辑器主题。 monaco.editor.defineTheme('myCustomTheme', { base: 'vs-dark', inherit: true, rules: [{ background: 'EDF2F7' }], colors: { 'editor.foreground': '#000000' } }); // 获取编辑器的所有扩展：可以使用 monaco.extensions.getExtensions 方法获取所有可用的扩展。 var allExtensions = monaco.extensions.getExtensions(); // 加载扩展：可以使用 monaco.extensions.loadExtension 方法加载新的扩展。 monaco.extensions.loadExtension('myExtension'); // 获取编辑器的所有快捷键：可以使用 editor.getActions 方法获取编辑器的所有快捷键动作。 var allKeybindings = editor.getActions(); // 设置编辑器的快捷键：可以使用 editor.addCommand 方法添加新的快捷键命令。 editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S, function() { console.log('Save command triggered'); }); // 获取编辑器的所有语言配置：可以使用 monaco.languages.getLanguageConfiguration 方法获取所有语言的配置信息。 var allLanguageConfigurations = monaco.languages.getLanguageConfiguration(); // 设置语言配置：可以使用 monaco.languages.setLanguageConfiguration 方法设置特定语言的配置信息。 monaco.languages.setLanguageConfiguration('javascript', { comments: { lineComment: '//', blockComment: ['/*', '*/'] } });
SpringBoot读取多次RequestBody一. 编写相关类1. Request包装器import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringReader; /** * @author TheEnd */ public class RequestWrapper extends HttpServletRequestWrapper { private String requestBody; public RequestWrapper(HttpServletRequest request) { super(request); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new StringReader(getRequestBody())); } @Override public ServletInputStream getInputStream() throws IOException { return new ServletInputStreamWrapper(requestBody.getBytes()); } private static class ServletInputStreamWrapper extends ServletInputStream { private final ByteArrayInputStream inputStream; public ServletInputStreamWrapper(byte[] body) { inputStream = new ByteArrayInputStream(body); } @Override public int read() throws IOException { return inputStream.read(); } @Override public boolean isFinished() { return inputStream.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener listener) { } } private String getRequestBody() throws IOException { if (requestBody == null) { StringBuilder requestBodyBuilder = new StringBuilder(); BufferedReader reader = super.getReader(); String line; while ((line = reader.readLine()) != null) { requestBodyBuilder.append(line); } requestBody = requestBodyBuilder.toString(); } return requestBody; } }2. 编写工具类 import cn.hutool.core.util.StrUtil; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author TheEnd */ public class ContentTypeUtil { public static boolean isBinaryContent(HttpServletRequest request) { String contentType = request.getContentType(); return isBinaryContent(contentType); } public static boolean isBinaryContent(HttpServletResponse response) { String contentType = response.getContentType(); return isBinaryContent(contentType); } public static boolean isBinaryContent(String contentType) { if (StrUtil.isEmpty(contentType)) { return false; } return contentType.startsWith("image/") || contentType.startsWith("audio/") || contentType.startsWith("video/") || contentType.startsWith("application/octet-stream"); } }3. 编写过滤器, 并在过滤器中替换原始Request对象import cn.hncht.plug.config.util.ContentTypeUtil; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * @author TheEnd */ @Component @Order(1) public class HttpServletRequestWrapperFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { if (!(servletRequest instanceof HttpServletRequest)) { filterChain.doFilter(servletRequest, servletResponse); return; } HttpServletRequest request = (HttpServletRequest) servletRequest; if (ContentTypeUtil.isBinaryContent(request) || request.getContentLength() < 1) { filterChain.doFilter(request, servletResponse); return; } filterChain.doFilter(new RequestWrapper(request), servletResponse); } }4. 编写拦截器,读取请求体数据@Slf4j public class ApiLogSerializableInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String body = readBody(request); log.info("{}", body); return true; } private String readBody(HttpServletRequest request) { // 如果请求体是二进制流，不读取 if (isBinaryContent(request)) { return ""; } // 如果请求体是空的，不读取 if (request.getContentLength() <= 0) { return ""; } BufferedReader reader = null; try { reader = request.getReader(); } catch (IOException e) { throw new RuntimeException(e); } Stream<String> lines = reader.lines(); return lines.collect(Collectors.joining(System.lineSeparator())); } }二. 配置拦截器生效@Configuration public class ConfigWebMvc implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new ApiLogSerializableInterceptor(sysLogService)); WebMvcConfigurer.super.addInterceptors(registry); } }
SpringBoot 多数据源配置一. 创建动态数据源和数据源切换器创建一个继承自AbstractRoutingDataSource的动态数据源类，用于根据当前线程的上下文选择数据源：import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSourceKey(); } }创建一个DataSourceContextHolder类，用于保存当前线程使用的数据源标识：public class DataSourceContextHolder { private static final ThreadLocal<String> dataSourceKey = new ThreadLocal<>(); public static void setDataSourceKey(String key) { dataSourceKey.set(key); } public static String getDataSourceKey() { return dataSourceKey.get(); } public static void clearDataSourceKey() { dataSourceKey.remove(); } }二. 数据源注入在Spring Boot的配置类中配置多个数据源，并将它们添加到DynamicDataSource中：import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; @Configuration public class DataSourceConfig { @Bean(name = "dataSource1") @ConfigurationProperties("spring.datasource.dataSource1") public DataSource dataSource1() { // 配置第一个数据源 // ... } @Bean(name = "dataSource2") @ConfigurationProperties("spring.datasource.dataSource2") public DataSource dataSource2() { // 配置第二个数据源 // ... } @Primary @Bean(name = "datasource") public AbstractRoutingDataSource routingDataSource() { DynamicDataSource dynamicDataSource = new DynamicDataSource(); Map<Object, Object> dataSourceMap = new HashMap<>(); dataSourceMap.put("dataSource1", dataSource1()); dataSourceMap.put("dataSource2", dataSource2()); dynamicDataSource.setTargetDataSources(dataSourceMap); dynamicDataSource.setDefaultTargetDataSource(dataSource1()); return dynamicDataSource; } }移除SpringBoot中关于DataSource的自动配置, 解决循环依赖问题@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }三. 在业务中切换数据源在需要切换数据源的地方，使用DataSourceContextHolder.setDataSourceKey("dataSource1")来设置当前线程使用的数据源标识：DataSourceContextHolder.setDataSourceKey("dataSource1"); ds1Service.findAll(); DataSourceContextHolder.setDataSourceKey("dataSource2"); ds2Service.findAll(); DataSourceContextHolder.clearDataSourceKey();确保在使用完数据源后，及时调用DataSourceContextHolder.clearDataSourceKey()来清除当前线程的数据源标识，以避免影响其他线程。四. 拓展: 使用Interceptor为请求动态注入数据源定义DataSourceInterceptor拦截器, 用来设置当前线程的数据源.public class DataSourceInterceptor extends HandlerInterceptorAdapter { private DynamicDataSource dynamicDataSource; public DataSourceInterceptor(DynamicDataSource dynamicDataSource) { this.dynamicDataSource = dynamicDataSource; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String dataSourceKey = determineDataSourceKey(request); DataSourceContextHolder.setDataSourceKey(dataSourceKey); return true; } private String determineDataSourceKey(HttpServletRequest request) { // 根据请求信息来确定数据源的标识 // 这里可以根据需要自行实现逻辑，例如根据请求参数、请求头等来决定使用哪个数据源 // 返回的数据源标识需要与之前配置的数据源名称对应 return "dataSource1"; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { DataSourceContextHolder.clearDataSourceKey(); } }在preHandle()方法中，可以根据请求信息来确定当前请求应该使用的数据源，然后通过DataSourceContextHolder.setDataSourceKey(dataSourceKey)方法将数据源标识设置到DataSourceContextHolder中。在afterCompletion()方法中，清除了DataSourceContextHolder中的数据源标识，以确保每个请求结束后都能正确地清除数据源设置。注册数据源切换拦截器该拦截器用于在每个请求前设置当前线程的数据源。@Configuration public class AppConfig implements WebMvcConfigurer { @Autowired private DynamicDataSource dynamicDataSource; @Bean public DataSourceTransactionManager transactionManager() { return new DataSourceTransactionManager(dynamicDataSource); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new DataSourceInterceptor(dynamicDataSource)); } }
SpringCloud Gateway提示端口占用检查端口是否已使用检查 server.address 是否正确