本文是博主从事后端开发以来,对公司、个人项目的经验总结,包含代码编写、功能推荐、第三方库使用及优雅配置等,希望大家看到都能有所收获
一. 优雅的进行线程池异常处理
在Java开发中,线程池的使用必不可少,使用无返回值 execute() 方法时,线程执行发生异常的话,需要记录日志,方便回溯,一般做法是在线程执行方法内 try/catch 处理,如下:
@Test public void test() throws Exception { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100000)); Futuresubmit = threadPoolExecutor.execute(() -> { try { int i = 1 / 0; return i; } catch (Exception e) { log.error(e.getMessage(), e); return null; } }); } 复制代码
但是当线程池调用方法很多时,那么每个线程执行方法内都要 try/catch 处理,这就不优雅了,其实ThreadPoolExecutor类还支持传入 ThreadFactory 参数,自定义线程工厂,在创建 thread 时,指定 setUncaughtExceptionHandler 异常处理方法,这样就可以做到全局处理异常了,代码如下:
ThreadFactory threadFactory = r -> { Thread thread = new Thread(r); thread.setUncaughtExceptionHandler((t, e) -> { // 记录线程异常 log.error(e.getMessage(), e); }); return thread; }; ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100000), threadFactory); threadPoolExecutor.execute(() -> { log.info("---------------------"); int i = 1 / 0; }); 复制代码
先介绍下线程池得四种决绝策略
AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,这是线程池默认的拒绝策略
DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。 使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略
DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
CallerRunsPolicy:由调用线程处理该任务
如下是一个线上业务接口使用得线程池配置,决绝策略采用 CallerRunsPolicy
// 某个线上线程池配置如下 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 50, // 最小核心线程数 50, // 最大线程数,当队列满时,能创建的最大线程数 60L, TimeUnit.SECONDS, // 空闲线程超过核心线程时,回收该线程的最大等待时间 new LinkedBlockingQueue<>(5000), // 阻塞队列大小,当核心线程使用满时,新的线程会放进队列 new CustomizableThreadFactory("task"), // 自定义线程名 new ThreadPoolExecutor.CallerRunsPolicy() // 线程执行的拒绝策略 ); 复制代码
在某些情况下,子线程任务调用第三方接口超时,导致核心线程数、最大线程数占满、阻塞队列占满的情况下执行拒绝策略时,由于使用 CallerRunsPolicy 策略,导致业务线程执行子任务时继续超时,进而导致接口执行异常,这种情况下,考虑到子线程任务得重要性,不是很重要得话,可以使用 DiscardPolicy 策略,要是很重要,可以发送到消息队列中持久化子线程任务数据待后续处理
博主推荐通过静态内部类实现单例模式,并实现懒加载效果,代码如下
// 使用静态内部类完成单例模式封装,避免线程安全问题,避免重复初始化成员属性 @Slf4j public class FilterIpUtil { private FilterIpUtil() { } private Liststrings = new ArrayList<>(); // 代码块在FilterIpUtil实例初始化时才会执行 { // 在代码块中完成文件的第一次读写操作,后续不再读这个文件 System.out.println("FilterIpUtil init"); try (InputStream resourceAsStream = FilterIpUtil.class.getClassLoader().getResourceAsStream("filterIp.txt")) { // 将文件内容放到string集合中 IoUtil.readUtf8Lines(resourceAsStream, strings); } catch (IOException e) { log.error(e.getMessage(), e); } } public static FilterIpUtil getInstance() { return InnerClassInstance.instance; } // 使用内部类完成单例模式,由jvm保证线程安全 private static class InnerClassInstance { private static final FilterIpUtil instance = new FilterIpUtil(); } // 判断集合中是否包含目标参数 public boolean isFilter(String arg) { return strings.contains(arg); } } 复制代码
在博主之前公司得项目中,ip解析是调用淘宝IP还有聚合IP接口获取结果,通常耗时200毫秒左右,并且接口不稳定时而会挂。都会影响业务接口耗时,后来在 github 上了解到 ip2region 这个项目,使用本地ip库查询,查询速度微秒级别, 精准度能达到90%,但是ip库还是有少部分ip信息不准,建议数据库中把请求ip地址保存下来。简介如下:
ip2region v2.0 - 是一个离线IP地址定位库和IP定位数据管理框架,10微秒级别的查询效率,提供了众多主流编程语言的 xdb 数据生成和查询客户端实现基于 xdb 文件的查询,下面是一个 Spring 项目中 ip2region 帮助类来实现ip地址解析
/** * ip2region工具类 */ @Slf4j @Component public class Ip2region { private Searcher searcher = null; @Value("${ip2region.path:}") private String ip2regionPath = ""; @PostConstruct private void init() { // 1、从 dbPath 加载整个 xdb 到内存。 String dbPath = ip2regionPath; // 1、从 dbPath 加载整个 xdb 到内存。 byte[] cBuff; try { cBuff = Searcher.loadContentFromFile(dbPath); searcher = Searcher.newWithBuffer(cBuff); } catch (Exception e) { log.error("failed to create content cached searcher: {}", e.getMessage(), e); } } public IpInfoBean getIpInfo(String ip) { if (StringUtils.isBlank(ip)) { return null; } // 3、查询 try { long sTime = System.nanoTime(); // 国家|区域|省份|城市|ISP String region = searcher.search(ip); long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime)); log.info("{region: {}, ioCount: {}, took: {} μs}", region, searcher.getIOCount(), cost); if (StringUtils.isNotBlank(region)) { String[] split = region.split("\|"); IpInfoBean ipInfo = new IpInfoBean(); ipInfo.setIp(ip); if (!"".equals(split[0])) { ipInfo.setCountry(split[0]); } if (!"".equals(split[2])) { ipInfo.setProvince(split[2]); } if (!"".equals(split[3])) { ipInfo.setCity(split[3]); } if (!"".equals(split[4])) { ipInfo.setIsp(split[4]); } return ipInfo; } } catch (Exception e) { log.error("failed to search({}): {}", ip, e); return null; } // 4、关闭资源 - 该 searcher 对象可以安全用于并发,等整个服务关闭的时候再关闭 searcher // searcher.close(); // 备注:并发使用,用整个 xdb 数据缓存创建的查询对象可以安全的用于并发,也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。 return null; } } 复制代码
要注意得就是 ip2region v2.0 版本使用的xdb文件不建议放在项目 resources 下一起打包,存在编码格式问题,建议通过指定路径加载得方式单独放在服务器目录下
Springboot + mybatis 得项目中一般通过 @MapperScan 注解配置 dao 层包目录,来实现 dao 层增强,其实项目中配置一个@MapperScan 是指定一个数据源,配置两个@MapperScan就可以指定两个数据源,通过不同得 dao 层包目录区分,来实现不同数据源得访问隔离。
比如下面代码中,com.xxx.dao.master 目录下为主数据源 dao 文件,com.xxx.dao.slave 为从数据源 dao 文件,这个方式比网上得基于 aop 加注解得方式更加简洁好用,也没有单个方法中使用不同数据源切换得问题,因此推荐这种写法
/** * 主数据源 */ @Slf4j @Configuration @MapperScan(basePackages = {"com.xxx.dao.master"}, sqlSessionFactoryRef = "MasterSqlSessionFactory") public class MasterDataSourceConfig { @Bean(name = "MasterDataSource") @Qualifier("MasterDataSource") @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource clickHouseDataSource() { return DruidDataSourceBuilder.create().build(); } @Bean(name = "MasterSqlSessionFactory") public SqlSessionFactory getSqlSessionFactory(@Qualifier("MasterDataSource") DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath*:mapper/master/*.xml")); log.info("------------------------------------------MasterDataSource 配置成功"); return sessionFactoryBean.getObject(); } } /** * 从数据源 */ @Slf4j @Configuration @MapperScan(basePackages = {"com.xxx.dao.slave"}, sqlSessionFactoryRef = "SlaveSqlSessionFactory") public class MasterDataSourceConfig { @Bean(name = "SlaveDataSource") @Qualifier("SlaveDataSource") @ConfigurationProperties(prefix = "spring.datasource.slave") public DataSource clickHouseDataSource() { return DruidDataSourceBuilder.create().build(); } @Bean(name = "SlaveSqlSessionFactory") public SqlSessionFactory getSqlSessionFactory(@Qualifier("SlaveDataSource") DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("clas