# 组件(更新中...)

在不增加复杂度的情况下,提供常用开发组件。

# 异步任务

Spring boot 可以通过@EnableAsync 开启异步,但是参数都是默认的,线程池,线程数,任务拒绝,任务超时等都不便于扩展。

setTaskDecorator 实现主线程和异步线程的线程号串起来,方便定位问题。

package com.seezoon.framework.component;

import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import com.alibaba.fastjson.JSON;
import com.seezoon.framework.properties.SeezoonProperties;

import lombok.RequiredArgsConstructor;

/**
 * 异步配置
 *
 * 如需要使用异步,springboot 要求显示的用{@code @EnableAsync}
 */
@Configuration
@RequiredArgsConstructor
public class AsyncCustomConfiguration extends AsyncConfigurerSupport {

    private static Logger logger = LoggerFactory.getLogger(AsyncCustomConfiguration.class);

    private final SeezoonProperties seezoonProperties;

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        // lambda表达式用作异常处理程序
        return (throwable, method, obj) -> {
            logger.error("Async exception method name :{},parmas:{}", method.getName(), JSON.toJSONString(obj),
                throwable);
        };
    }

    @Override
    public Executor getAsyncExecutor() {
        SeezoonProperties.AsyncProperties async = seezoonProperties.getAsync();
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(async.getCorePoolSize());
        executor.setAllowCoreThreadTimeOut(async.isAllowCoreThreadTimeOut());
        executor.setMaxPoolSize(async.getMaxPoolSize());
        executor.setQueueCapacity(async.getQueueCapacity());
        executor.setKeepAliveSeconds(async.getKeepAliveTime());
        executor.setThreadNamePrefix(async.getThreadNamePrefix());
        executor.setWaitForTasksToCompleteOnShutdown(async.isWaitForTasksToCompleteOnShutdown());
        executor.setAwaitTerminationSeconds(async.getAwaitTerminationSeconds());
        executor.setTaskDecorator((runnable) -> {
            Map<String, String> copyOfContextMap = MDC.getCopyOfContextMap();
            return () -> {
                try {
                    if (null != copyOfContextMap) {
                        MDC.setContextMap(copyOfContextMap);
                    }
                    runnable.run();
                } finally {
                    MDC.clear();
                }
            };
        });
        // 线程池对拒绝任务的处理策略
        executor.setRejectedExecutionHandler(new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                logger.error("Async exception:task rejected");
            }
        });
        // 初始化
        executor.initialize();
        return executor;
    }
}

# Redis 组件

主要处理序列化方式。

package com.seezoon.framework.component;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * RedisTemplate 等自定义,主要修改序列化方式
 *
 * @author hdf
 */
@Configuration(proxyBeanMethods = false)
public class RedisCustomConfiguration {

    @Bean
    public RedisTemplate<String, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, ?> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer =
            new GenericJackson2JsonRedisSerializer();
        redisTemplate.setDefaultSerializer(genericJackson2JsonRedisSerializer);
        redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
        return redisTemplate;
    }
}

# RestTemplate

添加连接池,链接检测与回收、证书支持等。

package com.seezoon.framework.component;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.SSLContext;

import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;

import com.seezoon.framework.properties.SeezoonProperties;

import lombok.RequiredArgsConstructor;

/**
 * 优化原生无连接池问题
 *
 * @author hdf
 */
@Configuration
@RequiredArgsConstructor
public class RestTemplateCustomConfiguration {

    private final SeezoonProperties seezoonProperties;

    /**
     * 获取ssl的连接池,适合双向请求,微信开发常用,该对象较重请缓存使用
     *
     * <code>
     *     KeyStore ks = KeyStore.getInstance("PKCS12");
     * 	   ks.load(inputStream, mchId.toCharArray());
     * 	   // Trust own CA and all self-signed certs
     * 	   SSLContext sslcontext = SSLContexts.custom()
     * 					.loadKeyMaterial(ks, mchId.toCharArray())
     * 					.build();
     * </code>
     *
     * @return
     */
    public RestTemplate getSslRestTemplate(SSLContext sslContext) {
        Assert.notNull(sslContext, "sslContext must not null");
        ClientHttpRequestFactory factory = clientHttpRequestFactory(sslContext);
        RestTemplate restTemplate = new RestTemplate(factory);
        restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return restTemplate;
    }

    /**
     * 优化默认连接池
     *
     * @return
     */
    @Primary
    @Bean
    public RestTemplate restTemplate() {
        ClientHttpRequestFactory factory = clientHttpRequestFactory(null);
        RestTemplate restTemplate = new RestTemplate(factory);
        restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return restTemplate;
    }

    public HttpClientConnectionManager poolingConnectionManager() {
        // 自定义连接池的话这个更准确,自动已连接池参数需要都在连接池上设置,默认池子 {@link org.apache.http.impl.client.HttpClientBuilder#build}
        // 自定义ssl 策略时候,请查看该方法构造函数
        PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager();

        poolingConnectionManager.setMaxTotal(seezoonProperties.getHttp().getMaxTotal()); // 连接池最大连接数
        poolingConnectionManager.setDefaultMaxPerRoute(seezoonProperties.getHttp().getMaxPerRoute()); // 每个主机的并发
        // 长连接
        poolingConnectionManager.setDefaultSocketConfig(SocketConfig.custom()
            .setSoTimeout(seezoonProperties.getHttp().getSocketTimeout()).setSoKeepAlive(true).build());
        // 连接不活跃多久检查毫秒 并不是100 % 可信
        poolingConnectionManager.setValidateAfterInactivity(seezoonProperties.getHttp().getValidateAfterInactivity());
        // 空闲扫描线程
        HttpClientIdleConnectionMonitor.registerConnectionManager(poolingConnectionManager,
            seezoonProperties.getHttp().getIdleTimeToDead(), seezoonProperties.getHttp().getIdleScanTime());
        return poolingConnectionManager;
    }

    public HttpClientBuilder httpClientBuilder(SSLContext sslContext) {
        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        // 设置HTTP连接管理器
        httpClientBuilder.setConnectionManager(poolingConnectionManager());
        RequestConfig requestConfig = RequestConfig.custom()
            .setConnectionRequestTimeout(seezoonProperties.getHttp().getConnectionRequestTimeout())// 获取连接等待时间
            .setConnectTimeout(seezoonProperties.getHttp().getConnectTimeout())// 连接超时
            .setSocketTimeout(seezoonProperties.getHttp().getSocketTimeout())// 获取数据超时
            .build();
        httpClientBuilder.setDefaultRequestConfig(requestConfig).setSSLContext(sslContext)
            .setUserAgent(seezoonProperties.getHttp().getUserAgent()).disableContentCompression()
            // 收动设置连接池 这个在连接池那里设置,查看该方法注释该,为了避免版本升级导致错误,也冗余设置
            .setConnectionTimeToLive(seezoonProperties.getHttp().getConnTimeToLive(), TimeUnit.MILLISECONDS)// 连接最大存活时间
            // 默认重试次数为0,POST 不重试
            .setRetryHandler(new StandardHttpRequestRetryHandler(seezoonProperties.getHttp().getRetyTimes(), false));
        return httpClientBuilder;
    }

    public ClientHttpRequestFactory clientHttpRequestFactory(SSLContext sslContext) {
        HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
        clientHttpRequestFactory.setHttpClient(httpClientBuilder(sslContext).build());
        return clientHttpRequestFactory;
    }

    /**
     * 来自优秀开源代码
     *
     * @see <a>https://github.com/aliyun/aliyun-oss-java-sdk/blob/master/src/main/java/com/aliyun/oss/common/comm/IdleConnectionReaper.java</a>
     */
    public static final class HttpClientIdleConnectionMonitor extends Thread {

        private static final ArrayList<HttpClientConnectionManager> CONNECTION_MANAGERS = new ArrayList<>();
        private static Logger logger = LoggerFactory.getLogger(HttpClientIdleConnectionMonitor.class);
        private static long idleScanTime;
        private static HttpClientIdleConnectionMonitor instance;

        private static long idleConnectionTime;

        private volatile boolean shuttingDown;

        private HttpClientIdleConnectionMonitor(long idleConnectionTime, long idleScanTime) {
            super("Connection Manager Monitor");
            HttpClientIdleConnectionMonitor.idleConnectionTime = idleConnectionTime;
            HttpClientIdleConnectionMonitor.idleScanTime = idleScanTime;
            setDaemon(true);
        }

        /**
         * 如果 HttpClientIdleConnectionMonitor 已存在则设置的两个时间参数不生效
         *
         * @param connectionManager
         * @param idleConnectionTime
         * @param idleScanTime
         * @return
         */
        public static synchronized boolean registerConnectionManager(HttpClientConnectionManager connectionManager,
            long idleConnectionTime, long idleScanTime) {
            if (instance == null) {
                instance = new HttpClientIdleConnectionMonitor(idleConnectionTime, idleScanTime);
                instance.start();
            }
            return CONNECTION_MANAGERS.add(connectionManager);
        }

        public static synchronized boolean removeConnectionManager(HttpClientConnectionManager connectionManager) {
            boolean b = CONNECTION_MANAGERS.remove(connectionManager);
            if (CONNECTION_MANAGERS.isEmpty()) {
                shutdown();
            }
            return b;
        }

        public static synchronized boolean shutdown() {
            if (instance != null) {
                instance.markShuttingDown();
                instance.interrupt();
                CONNECTION_MANAGERS.clear();
                instance = null;
                return true;
            }
            return false;
        }

        public static synchronized int size() {
            return CONNECTION_MANAGERS.size();
        }

        public static synchronized void setIdleConnectionTime(long idletime) {
            idleConnectionTime = idletime;
        }

        private void markShuttingDown() {
            shuttingDown = true;
        }

        @SuppressWarnings("unchecked")
        @Override
        public void run() {
            while (true) {
                if (shuttingDown) {
                    logger.debug("Shutting down Connection Manager Monitor thread.");
                    return;
                }

                try {
                    Thread.sleep(idleScanTime);
                } catch (InterruptedException e) {
                }

                try {
                    List<HttpClientConnectionManager> connectionManagers = null;
                    synchronized (HttpClientIdleConnectionMonitor.class) {
                        connectionManagers = (List<
                            HttpClientConnectionManager>)HttpClientIdleConnectionMonitor.CONNECTION_MANAGERS.clone();
                    }
                    for (HttpClientConnectionManager connectionManager : connectionManagers) {
                        try {
                            connectionManager.closeExpiredConnections();
                            connectionManager.closeIdleConnections(idleConnectionTime, TimeUnit.MILLISECONDS);
                        } catch (Exception ex) {
                            logger.warn("Unable to close idle connections", ex);
                        }
                    }
                } catch (Throwable t) {
                    logger.debug("Connection Manager Monitor thread: ", t);
                }
            }
        }
    }
}

# Task Execution and Scheduling (opens new window)

连接池,链接拒绝等支持,默认的也够用但是开启后线程会增多,需要手动配置,所以选择自定义实现下。

package com.seezoon.framework.component;

import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import com.seezoon.framework.properties.SeezoonProperties;
import com.seezoon.framework.properties.SeezoonProperties.ScheduledProperties;

import lombok.RequiredArgsConstructor;

/**
 * 定时配置可选
 * 
 * @see <a>https://docs.spring.io/spring-boot/docs/2.4.5/reference/htmlsingle/#boot-features-task-execution-sch</a>
 *      如需要使用异步,springboot 要求显示的用{@code @EnableScheduling}
 */
@Configuration
@RequiredArgsConstructor
public class ScheduledCustomConfiguration implements SchedulingConfigurer {

    private static Logger logger = LoggerFactory.getLogger(ScheduledCustomConfiguration.class);

    private final SeezoonProperties seezoonProperties;

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        ScheduledProperties scheduled = seezoonProperties.getScheduled();
        ScheduledThreadPoolExecutor scheduledThreadPoolExecutor =
            new ScheduledThreadPoolExecutor(scheduled.getCorePoolSize());
        scheduledThreadPoolExecutor.setMaximumPoolSize(scheduled.getMaxPoolSize());
        scheduledThreadPoolExecutor.setKeepAliveTime(scheduled.getKeepAliveTime(), TimeUnit.SECONDS);
        scheduledThreadPoolExecutor.setRejectedExecutionHandler((r, executor) -> {
            logger.error("Scheduled exception:scheduled rejected");
        });
        taskRegistrar.setScheduler(scheduledThreadPoolExecutor);
    }
}

# Caching (opens new window)(caffeine)

简易内存缓存,适合对分布式场景不一致不敏感的场景,如字典,非关键参数。

该框架默认配置了。

spring.cache.type=caffeine
spring.cache.caffeine.spec=maximumSize=1000,expireAfterAccess=300s

缓存用法,具体请参考参考文档。

@Cacheable(cacheNames = "SysParam", key = "#paramKey")

# 文件存储

提供通过配置实现文件存储类型更换,使用其他存储可以自行扩展。

com.seezoon.framework.component.file.handler.FileHandler
com.seezoon.framework.component.file.FileHandlerConfiguration
  • 本地文件(多实例可以使用NFS)

    seezoon.file.store-type=local
    seezoon.file.url-prefix=http://127.0.0.1:3001/static
    # 本地适用,其他部署环境写真实目录
    seezoon.file.local.directory=@local.upload.directory@
    
  • 阿里云OSS

    seezoon.file.store-type=aliyun_oss
    seezoon.file.url-prefix=https://seezoon-file.oss-cn-hangzhou.aliyuncs.com
    seezoon.file.aliyun.bucket-name=seezoon-file
    seezoon.file.aliyun.endpoint=oss-cn-hangzhou.aliyuncs.com
    seezoon.file.aliyun.access-key-id=xxxx
    seezoon.file.aliyun.access-key-secret=xxx