异步任务的管理器 | 教你如何优雅打印日志

一、前言

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

二、线程池配置

执行异步人任务时,需要将执行的任务放入到线程池中,所以需配置好我们的线程池,比如 核心线程大小最大可创建的线程数队列长度 等。

2.1 基本参数配置


@Configuration
public class ThreadPoolConfig {

    /**
     * 核心线程池大小
     */
    private int corePoolSize = 50;

    /**
     * 最大可创建的线程数
     */
    private int maxPoolSize = 200;

    /**
     * 队列最大长度
     */
    private int queueCapacity = 1000;

    /**
     * 线程池维护线程所允许的空闲时间
     */
    private int keepAliveSeconds = 300;
    
    ......
    ......
}
复制代码

配置好基本的参数后,我们需要用这些参数初始化 ThreadPoolTaskExecutor 线程池任务执行器:

2.2 初始化任务执行器


@Bean(name = "threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
    // 创建线程池任务执行器对象
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    
    // 设置核心线程池大小
    executor.setCorePoolSize(corePoolSize);
    // 设置最大可创建的线程数
    executor.setMaxPoolSize(maxPoolSize);
    // 设置队列最大长度
    executor.setQueueCapacity(queueCapacity);
    // 设置线程池维护线程所允许的空闲时间
    executor.setKeepAliveSeconds(keepAliveSeconds);
    // 线程池对拒绝任务(无线程可用时)的处理策略
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    
    return executor;
}
复制代码

注意:@Bean 注解不能少,直接将其交 Spring 容器管理,后面可以通过 getBean() 方法拿到方法返回的实例。


@Bean(name = "scheduledExecutorService")
protected ScheduledExecutorService scheduledExecutorService() {
    return new ScheduledThreadPoolExecutor
            (
                    corePoolSize,
                    new BasicThreadFactory.Builder()
                            .namingPattern("schedule-pool-%d").daemon(true)
                            .build()
            ) {
        @Override
        protected void afterExecute(Runnable r, Throwable t) {
            super.afterExecute(r, t);
            ThreadUtil.printException(r, t);
        }
    };
}

/**
 * 打印线程异常信息
 */
public static void printException(Runnable r, Throwable t) {
    if (t == null && r instanceof Future<?>) {
        try {
            Future<?> future = (Future<?>) r;
            if (future.isDone()) {
                future.get();
            }
        } catch (CancellationException ce) {
            t = ce;
        } catch (ExecutionException ee) {
            t = ee.getCause();
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
        }
    }
    if (t != null) {
        logger.error(t.getMessage(), t);
    }
}
复制代码

这个 Bean 实例是用来创建一个调度线程池执行器,并需重写 afterExecute() 方法,去处理执行任务过程中产生的异常或执行完成后的下一步操作流程,主要是用于 执行周期性或定时任务 ,后面会用到它去异步执行记录登录日志任务。

三、异步任务管理器

顾名思义,就是用来对异步任务进行统一的管理,并提供了一种访问其唯一对象的方式,这样做得好处就是,在内存中有且仅有一个实例,减少了内存的开销,尤其对于频繁的创建和销毁实例,用这种方式来频繁执行多个异步任务性能是相对比较好的。

3.1 设计模式

想必,聪明的各位已经想到了 —— 设计模式之单例模式(Singleton Pattern),这种模式也被称之为 Java 中最简单的设计模式。


public class AsyncTaskManger {

    private AsyncTaskManger() {
    }

    private static AsyncTaskManger me = new AsyncTaskManger();

    public static AsyncTaskManger me() {
        return me;
    }
    
    ......
    ......
}
复制代码

这里采用最常用的线程安全的饿汉式,没有加锁执行效率会比较高,就是在类加载就会初始化,比较浪费内存了  ̄□ ̄||

3.2 暴露实例方法


    /**
     * 操作延迟 10 毫秒
     */
    private static final int OPERATE_DELAY_TIME = 10;

    /**
     * 异步操作任务调度线程池
     */
    private ScheduledExecutorService executor = SpringUtil.getBean("scheduledExecutorService");

    /**
     * 执行任务
     *
     * @param task 任务
     */
    public void execute(TimerTask task) {
        executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
    }
复制代码

通过前面种下的豆,都到豆,这也就是人们常说的种瓜得瓜、种豆得豆,这里的豆指的是调度线程池执行器通过 scheduledExecutorService 获取,拿来执行周期性或定时任务。

四、异步任务工厂

设计这个类主要是用来产生 TimerTask 的,比如用户访问登录页面,登录判断的同时,还需记录下用户登录的日志情况,是登录成功了呢?还是失败了!退出登录也可以记录到日志表中,并且完全不用担心后端处理不过来的问题,因为它是放在异步操作任务调度线程池当中。

4.1 具体代码实现


public class AsyncTaskFactory {

    private static final Logger loginUserLogger = LoggerFactory.getLogger("user-login");
    
    ......
    ......
}
复制代码

获取日志打印对象,用于控制台的日志的打印输出,便于下一步的开发调试。


/**
 * 记录用户登录日志
 *
 * @param username 用户名
 * @param status 状态
 * @param message 提示消息
 * @param args 参数列表
 * @return 定时器任务
 */
public static TimerTask recordUserLoginLog(final String username, final String status, final String message,
                                           final Object... args) {
    // 获取请求对象
    HttpServletRequest request = ServletUtil.getRequest();

    // 请求表头
    final String header = ServletUtil.getHeader(request, "User-Agent", CharsetUtil.UTF_8);
    
    final UserAgent userAgent = UserAgentParser.parse(header);
    // 获取客户端 IP 地址
    String ip = ServletUtil.getClientIP(request);
    return new TimerTask() {
        @Override
        public void run() {
            // 通过 ip 地址获取现实地址
            String address = AddressUtil.getRealAddressByIP(ip);
            
            StringBuilder builder = new StringBuilder();
            final boolean ignoreNull = true;
            builder.append(StrUtil.format("[ {} ]", ip, ignoreNull));
            builder.append(StrUtil.format("[ {} ]", address, ignoreNull));
            builder.append(StrUtil.format("[ {} ]", username, ignoreNull));
            builder.append(StrUtil.format("[ {} ]", status, ignoreNull));
            builder.append(StrUtil.format("[ {} ]", message, ignoreNull));
            // 打印信息到日志
            loginUserLogger.info(builder.toString(), args);
            
            // 获取客户端操作系统
            String os = UserAgentUtil.getOperatingSystem();
            // 获取客户端浏览器
            String browser = UserAgentUtil.getBrowser();
            // 封装对象
            UserLoginLog userLoginLog = new UserLoginLog();
            userLoginLog.setUserName(username);
            userLoginLog.setIpaddr(ip);
            userLoginLog.setLoginLocation(address);
            userLoginLog.setBrowser(browser);
            userLoginLog.setOs(os);
            userLoginLog.setMessage(message);
            
            // 设置日志状态
            if (StrUtil.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) {
                userLoginLog.setStatus(Constants.SUCCESS);
            } else if (Constants.LOGIN_FAIL.equals(status)) {
                userLoginLog.setStatus(Constants.FAIL);
            }
            
            // 调用 Service 层方法,记录日志
            SpringUtil.getBean(UserLoginLogService.class).insertUserLoginLog(userLoginLog);
        }
    };
}
复制代码

除了一些记录常规信息之外,通过解析 User Agent 中文名为用户代理这个特殊字符串头获取并记录下客户使用的 操作系统及版本浏览器及版本浏览器渲染引擎 等。

具体解析方法,我使用了 HutoolUserAgentParser.parse(请求头) 二次封装了一下去获取的(推荐),如果不使用该方法,那可能要引入相关包依赖去解析获取了。

五、调用


// 执行异步任务
AsyncTaskManger.me().execute(AsyncTaskFactory.recordUserLoginLog(username, Constants.LOGIN_SUCCESS, "User login successful."));
复制代码

大功告成,链式调用美观优雅,skr ~

六、结尾

撰文不易,欢迎大家点赞、评论,你的关注、点赞是我坚持的不懈动力,感谢大家能够看到这里!Peace & Love。

猜你喜欢

转载自juejin.im/post/7036387006431100936