原理讲解
- AOP 切面获取当前请求的注解值,并 异步 发送时间,减少日志操作的性能损耗
- 监听器在接收到日志事件后进行调用feign入口处理
@EventListener注解的应用
在开发工作中,会遇到一种场景,做完某一件事情以后,需要广播一些消息或者通知,告诉其他的模块进行一些事件处理,一般来说,可以一个一个发送请求去通知,但是有一种更好的方式,那就是事件监听,事件监听也是设计模式中 发布-订阅模式、观察者模式的一种实现。
观察者模式:简单的来讲就是你在做事情的时候身边有人在盯着你,当你做的某一件事情是旁边观察的人感兴趣的事情的时候,他会根据这个事情做一些其他的事,但是盯着你看的人必须要到你这里来登记,否则你无法通知到他(或者说他没有资格来盯着你做事情)。
对于 Spring 容器的一些事件,可以监听并且触发相应的方法。通常的方法有 2 种,ApplicationListener 接口和@EventListener 注解。
实现接口自行搜索如何使用,这里只讲解注解应用
使用@EventListener 注解,实现对任意的方法都能监听事件。
在任意方法上标注@EventListener 注解,指定 classes,即需要处理的事件类型,一般就是 ApplicationEven 及其子类,可以设置多项。
@Configuration
public class Config {
@EventListener(classes = {ApplicationEvent.class})
public void listen(ApplicationEvent event) {
System.out.println("事件触发:" + event.getClass().getName());
}
}
定义事件
首先,我们需要定义一个时间(MyTestEvent),需要继承Spring的ApplicationEvent
public class MyTestEvent extends ApplicationEvent{
/**
*
*/
private static final long serialVersionUID = 1L;
private String msg ;
public MyTestEvent(Object source,String msg) {
super(source);
this.msg = msg;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
其实上面添加@EventListener
注解的方法被包装成了ApplicationListener
对象。
@Component
public class MyAnnotationListener implements ApplicationListener<MyTestEvent> {
@Override
public void onApplicationEvent(MyTestEvent event) {
System.out.println("注解监听器1:" + event.getMsg());
}
}
@Async注解使用
开启异步支持
@Configuration
@EnableAsync
public class SpringAsyncConfig { ... }
默认情况下,@EnableAsync检测Spring的@Async注释和EJB 3.1 javax. EJB .异步;此选项还可用于检测其他用户定义的注释类型。
@Async注解使用
1.无返回值
@Async
public void asyncMethodWithVoidReturnType() {
System.out.println("Execute method asynchronously. "
+ Thread.currentThread().getName());
}
2.有返回值
@Async
public Future<String> asyncMethodWithReturnType() {
System.out.println("Execute method asynchronously - "
+ Thread.currentThread().getName());
try {
Thread.sleep(5000);
return new AsyncResult<String>("hello world !!!!");
} catch (InterruptedException e) {
//
}
return null;
}
执行器
默认情况下,Spring 使用SimpleAsyncTaskExecutor去执行这些异步方法(此执行器没有限制线程数)。此默认值可以从两个层级进行覆盖:
-
方法级别
-
应用级别
1. 方法级别覆盖
@Async("threadPoolTaskExecutor")
public void asyncMethodWithConfiguredExecutor() {
System.out.println("Execute method with configured executor - "
+ Thread.currentThread().getName());
}
2. 应用级别覆盖
配置类应该实现AsyncConfigurer接口——这意味着它拥有getAsyncExecutor()方法的实现。在这里,我们将返回整个应用程序的执行器——这现在成为运行带有@Async注释的方法的默认执行器:
@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.initialize();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
return executor;
}
}
异常处理
当方法返回值是Future的时候,异常捕获是没问题的 - Future.get()方法会抛出异常。
但是,如果返回类型是Void,那么异常在当前线程就捕获不到。因此,我们需要添加额外的配置来处理异常。
我们将通过实现AsyncUncaughtExceptionHandler接口创建一个定制的async异常处理程序。handleUncaughtException()方法在存在任何未捕获的异步异常时调用:
public class CustomAsyncExceptionHandler
implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(
Throwable throwable, Method method, Object... obj) {
System.out.println("Exception message - " + throwable.getMessage());
System.out.println("Method name - " + method.getName());
for (Object param : obj) {
System.out.println("Parameter value - " + param);
}
}
}
在上一节中,我们研究了由configuration类实现的AsyncConfigurer接口。作为其中的一部分,我们还需要覆盖getAsyncUncaughtExceptionHandler()方法来返回我们自定义的异步异常处理程序:
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new CustomAsyncExceptionHandler();
}
监听异步处理日志应用
为了模块的通用性,新建一个通用日志模块
新建事件及监听类
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
@Slf4j
@RequiredArgsConstructor
public class SysLogListener {
//日志处理业务类 自定义 syslog 日志类
private final RemoteLogService remoteLogService;
@Async
@Order
@EventListener(SysLogEvent.class)
public void saveSysLog(SysLogEvent event) {
SysLog sysLog = (SysLog) event.getSource();
remoteLogService.saveLog(sysLog, "来源,app wy");
}
}
import org.springframework.context.ApplicationEvent;
public class SysLogEvent extends ApplicationEvent {
public SysLogEvent(SysLog source) {
super(source);
}
}
@Data
public class SysLog implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 编号
*/
@TableId(value = "id", type = IdType.AUTO)
@ApiModelProperty(value = "日志编号")
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/**
* 日志类型
*/
@NotBlank(message = "日志类型不能为空")
@ApiModelProperty(value = "日志类型")
private String type;
/**
* 日志标题
*/
@NotBlank(message = "日志标题不能为空")
@ApiModelProperty(value = "日志标题")
private String title;
/**
* 创建者
*/
@ApiModelProperty(value = "创建人")
private String createBy;
/**
* 创建时间
*/
@ApiModelProperty(value = "创建时间")
private LocalDateTime createTime;
/**
* 更新时间
*/
@ApiModelProperty(value = "更新时间")
private LocalDateTime updateTime;
/**
* 操作IP地址
*/
@ApiModelProperty(value = "操作ip地址")
private String remoteAddr;
/**
* 用户浏览器
*/
@ApiModelProperty(value = "用户代理")
private String userAgent;
/**
* 请求URI
*/
@ApiModelProperty(value = "请求uri")
private String requestUri;
/**
* 操作方式
*/
@ApiModelProperty(value = "操作方式")
private String method;
/**
* 操作提交的数据
*/
@ApiModelProperty(value = "数据")
private String params;
/**
* 执行时间
*/
@ApiModelProperty(value = "方法执行时间")
private Long time;
/**
* 异常信息
*/
@ApiModelProperty(value = "异常信息")
private String exception;
/**
* 服务ID
*/
@ApiModelProperty(value = "应用标识")
private String serviceId;
/**
* 删除标记
*/
@TableLogic
private String delFlag;
}
@FeignClient(contextId = "remoteLogService", value = "log-service",
fallbackFactory = RemoteLogServiceFallbackFactory.class)
public interface RemoteLogService {
/**
* 保存日志
* @param sysLog 日志实体
* @param from 内部调用标志
* @return succes、false
*/
@PostMapping("/log")
R<Boolean> saveLog(@RequestBody SysLog sysLog, @RequestHeader(SecurityConstants.FROM) String from);
}
@Component
public class RemoteLogServiceFallbackFactory implements FallbackFactory<RemoteLogService> {
@Override
public RemoteLogService create(Throwable throwable) {
RemoteLogServiceFallbackImpl remoteLogServiceFallback = new RemoteLogServiceFallbackImpl();
remoteLogServiceFallback.setCause(throwable);
return remoteLogServiceFallback;
}
}
切面类处理日志 发布监听
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
/**
* 操作日志使用spring event异步入库
*/
@Aspect
@Slf4j
public class SysLogAspect {
@Around("@annotation(sysLog)")
@SneakyThrows
public Object around(ProceedingJoinPoint point, com.pig4cloud.pig.common.log.annotation.SysLog sysLog) {
String strClassName = point.getTarget().getClass().getName();
String strMethodName = point.getSignature().getName();
log.debug("[类名]:{},[方法]:{}", strClassName, strMethodName);
SysLog logVo = SysLogUtils.getSysLog();
logVo.setTitle(sysLog.value());
// 发送异步日志事件
Long startTime = System.currentTimeMillis();
Object obj;
try {
obj = point.proceed();
}
catch (Exception e) {
logVo.setType(LogTypeEnum.ERROR.getType());
logVo.setException(e.getMessage());
throw e;
}
finally {
Long endTime = System.currentTimeMillis();
logVo.setTime(endTime - startTime);
SpringContextHolder.publishEvent(new SysLogEvent(logVo));
}
return obj;
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
/**
* 描述
* @return {String}
*/
String value();
}
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
/**
* <p>
* 通过环境变量的形式注入 logging.file 自动维护 Spring Boot Admin Logger Viewer
*/
public class ApplicationLoggerInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
ConfigurableEnvironment environment = applicationContext.getEnvironment();
String appName = environment.getProperty("spring.application.name");
String logBase = environment.getProperty("LOGGING_PATH", "logs");
// spring boot admin 直接加载日志
System.setProperty("logging.file.name", String.format("%s/%s/debug.log", logBase, appName));
}
}
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
/**
日志自动配置
*/
@EnableAsync
@RequiredArgsConstructor
@ConditionalOnWebApplication
@Configuration(proxyBeanMethods = false)
public class LogAutoConfiguration {
private final RemoteLogService remoteLogService;
@Bean
public SysLogListener sysLogListener() {
return new SysLogListener(remoteLogService);
}
@Bean
public SysLogAspect sysLogAspect() {
return new SysLogAspect();
}
}
新建文件
META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.liu.common.log.LogAutoConfiguration
org.springframework.context.ApplicationContextInitializer=\
com.liu.common.log.init.ApplicationLoggerInitializer
辅助类
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.HttpUtil;
import lombok.experimental.UtilityClass;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
/**
* 系统日志工具类
*/
@UtilityClass
public class SysLogUtils {
public SysLog getSysLog() {
HttpServletRequest request = ((ServletRequestAttributes) Objects
.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
SysLog sysLog = new SysLog();
sysLog.setCreateBy(Objects.requireNonNull(getUsername()));
sysLog.setType(LogTypeEnum.NORMAL.getType());
sysLog.setRemoteAddr(ServletUtil.getClientIP(request));
sysLog.setRequestUri(URLUtil.getPath(request.getRequestURI()));
sysLog.setMethod(request.getMethod());
sysLog.setUserAgent(request.getHeader("user-agent"));
sysLog.setParams(HttpUtil.toParams(request.getParameterMap()));
sysLog.setServiceId(getClientId());
return sysLog;
}
/**
* 获取客户端
* @return clientId
*/
private String getClientId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof OAuth2Authentication) {
OAuth2Authentication auth2Authentication = (OAuth2Authentication) authentication;
return auth2Authentication.getOAuth2Request().getClientId();
}
return null;
}
/**
* 获取用户名称
* @return username
*/
private String getUsername() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return null;
}
return authentication.getName();
}
}
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* <p>
* 日志类型
*/
@Getter
@RequiredArgsConstructor
public enum LogTypeEnum {
/**
* 正常日志类型
*/
NORMAL("0", "正常日志"),
/**
* 错误日志类型
*/
ERROR("9", "错误日志");
/**
* 类型
*/
private final String type;
/**
* 描述
*/
private final String description;
}
用法
引入这个模块
在所要处理日志的方法中加入注解
@SysLog("添加终端")
@PostMapping
public R add(@Valid @RequestBody SysOauthClientDetails sysOauthClientDetails) {
return new R<>(sysOauthClientDetailsService.save(sysOauthClientDetails));
}