关于java:十二条后端开发经验分享纯干货

1次阅读

共计 29903 个字符,预计需要花费 75 分钟才能阅读完成。

前言

本文是博主从事后端开发以来,对公司、集体我的项目的经验总结,蕴含代码编写、性能举荐、第三方库应用及优雅配置等,心愿大家看到都能有所播种

  • 博主 github 地址: https://github.com/wayn111

一. 优雅的进行线程池异样解决

在 Java 开发中,线程池的应用必不可少,应用无返回值 execute() 办法时,线程执行产生异样的话,须要记录日志,不便回溯,个别做法是在线程执行办法内 try/catch 解决,如下:

@Test
public void test() throws Exception {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(100000));
    Future<Integer> submit = 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 List<String> strings = 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);  
    }  
  
}

四. 应用 ip2region 实现申请地址解析

在博主之前公司得我的项目中,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 配置多数据源形式

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("classpath*:mapper/slave/*.xml"));
        log.info("------------------------------------------SlaveDataSource 配置胜利");
        return sessionFactoryBean.getObject();}
}

数据源 yml 配置

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    # 主库数据源
    master:
      url: jdbc:mysql://localhost:3306/db1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
      username: root
      password:
    slave:
      url: jdbc:mysql://localhost:3306/db2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
      username: root
      password:

博主刚开始编码一、两年得时候一个我的项目中遇到了多数据源应用得问题,那时候题主便在网上搜寻 Spring 多数据源 得帖子,大多数都是基于 Spring 提供得AbstractRoutingDataSource + AOP + 注解 来做动静切换,包含当初风行得 Mybatis plus 官网得多数据源解决方案也是这种做法,这种做法解决了博主过后得多数据源应用问题,起初加了一个需要,在一个定时工作中,查问两个数据源得数据,才发现动静切换在单个办法中不好用了,最初应用得原生 jdbc 数据源解决。多年后,博主在另一家公司得我的项目中又遇到了多数据源问题,然而这次博主在网上搜寻得是Mybatis 多数据源,才发现了这个优雅得解决方案,进而举荐给大家

六. Spring Security 我的项目中,应用 MDC 实现接口申请调用追踪,以及用户 ID 记录

MDC 介绍

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j、logback 及 log4j2 提供的一种不便在多线程条件下记录日志的性能。MDC 能够看成是一个 与以后线程绑定的哈希表 ,能够往其中增加键值对。MDC 中蕴含的内容能够 被同一线程中执行的代码所拜访。以后线程的子线程会继承其父线程中的 MDC 的内容。当须要记录日志时,只须要从 MDC 中获取所需的信息即可。

尽管 MDC 可能不便得实现接口申请调用追踪性能,然而它在子线程中会失落父线程中增加得键值对信息,解决办法是通过父线程中调用线程池前调用 MDC.getCopyOfContextMap(),而后在子线程中第一个调用 MDC.setConextMap() 获取键值对信息,残缺实现代码如下:

/**
 * 自定义 Spring 线程池,解决子线程失落 reqest_id 问题
 */
public class ThreadPoolExecutorMdcWrapper extends ThreadPoolTaskExecutor {

    @Override
    public void execute(Runnable task) {super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public Future<?> submit(Runnable task) {return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
}

/**
 * MDC 帮忙类,增加 reqest_id
 */
public class ThreadMdcUtil {


    public static final String REQUEST_ID = "request_id";

    /**
     * 设置申请惟一 ID
     */
    public static void setTraceIdIfAbsent() {if (MDC.get(REQUEST_ID) == null) {MDC.put(REQUEST_ID, IdUtil.getUid());
        }
    }

    /**
     * 存在 userId 则增加到 REQUEST_ID 中
     * @param userId
     */
    public static void setUserId(String userId) {String s = MDC.get(REQUEST_ID);
        if (s != null) {MDC.put(REQUEST_ID, s + "_" + userId);
        }
    }

    public static void removeTraceId() {MDC.remove(REQUEST_ID);
    }

    public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {return () -> {if (context == null) {MDC.clear();
            } else {MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {return callable.call();
            } finally {MDC.clear();
            }
        };
    }

    public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {return () -> {if (context == null) {MDC.clear();
            } else {MDC.setContextMap(context);
            }
            // 设置 traceId
            setTraceIdIfAbsent();
            try {runnable.run();
            } finally {MDC.clear();
            }
        };
    }
}

Spring Security 中增加 token 过滤器

/**
 * token 过滤器 验证 token 有效性
 *
 * @author ruoyi
 */
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private TokenService tokenService;


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        try {
            // 入口传入申请 ID
            ThreadMdcUtil.setTraceIdIfAbsent();
            LoginUserDetail loginUser = tokenService.getLoginUser(request);
            if (Objects.nonNull(loginUser) && Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {
                // 记录 userId
                ThreadMdcUtil.setUserId(String.valueOf(loginUser.getMember().getId()));
                tokenService.verifyToken(loginUser);
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
            chain.doFilter(request, response);
        } finally {
            // 进口移除申请 ID
            ThreadMdcUtil.removeTraceId();}
    }

}

最初在 logback.xml 中增加 %X{request_id}

<property name="pattern"
          value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{request_id}] [%thread] [%-5level] %logger{36}:%L %M - %msg%n"/>

日志打印成果如下:

2022-11-27 21:29:48.008 [86c76336100c414dbe9217aeb099ccd5_12] [http-nio-82-exec-2] [INFO] c.w.m.a.s.impl.IHomeServiceImpl:56 getHomeIndexDataCompletableFuture - getHomeIndexDataCompletableFuture:com.wayn.common.util.R@701f7b8e, newGoodsList=[{"actualSales":0,"brandId":0,"brief":"酥脆奶香,甜酸回味","categoryId":1008015,"counterPrice":56.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1116011","id":1116011,"isHot":true,"isNew":true,"isOnSale":true,"keywords":"","name":" 蔓越莓曲奇 200 克 ","picUrl":"http://yanxuan.nosdn.127.net/767b370d07f3973500db54900bcbd2a7.png","retailPrice":36.00,"shareUrl":"","sort":5,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":0,"brandId":0,"brief":"粉彩色泽,记录生存","categoryId":1012003,"counterPrice":49.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1127047","id":1127047,"isHot":false,"isNew":true,"isOnSale":true,"keywords":"","name":" 趣味粉彩系列笔记本 ","picUrl":"http://yanxuan.nosdn.127.net/6c03ca93d8fe404faa266ea86f3f1e43.png","retailPrice":29.00,"shareUrl":"","sort":2,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":0,"brandId":0,"brief":"慢回弹海绵,时尚设计。","categoryId":1008002,"counterPrice":66.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1134030","id":1134030,"isHot":false,"isNew":true,"isOnSale":true,"keywords":"","name":" 简洁知性记忆棉坐垫 ","picUrl":"http://yanxuan.nosdn.127.net/aa49dfe878becf768eddc4c1636643a6.png","retailPrice":46.00,"shareUrl":"","sort":12,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":0,"brandId":0,"brief":"慢回弹海绵的呵护,萌趣添彩。","categoryId":1008002,"counterPrice":69.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1134032","id":1134032,"isHot":false,"isNew":true,"isOnSale":true,"keywords":"","name":" 趣味粉彩系列记忆棉坐垫 ","picUrl":"http://yanxuan.nosdn.127.net/8b30eeb17c831eba08b97bdcb4c46a8e.png","retailPrice":49.00,"shareUrl":"","sort":13,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":0,"brandId":0,"brief":"100% 桑蚕丝,丝滑润肤","categoryId":1008009,"counterPrice":2619.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1135002","id":1135002,"isHot":false,"isNew":true,"isOnSale":true,"keywords":"","name":" 宫廷豪华真丝四件套 ","picUrl":"http://yanxuan.nosdn.127.net/45548f26cfd0c7c41e0afc3709d48286.png","retailPrice":2599.00,"shareUrl":"","sort":1,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":0,"brandId":0,"brief":"自在海军领摸索将来梦","categoryId":1020003,"counterPrice":89.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1135072","id":1135072,"isHot":false,"isNew":true,"isOnSale":true,"keywords":"","name":" 经典海魂纹水手裙(婴童)","picUrl":"http://yanxuan.nosdn.127.net/43e57d4208cdc78ac9c088f9b3e798f5.png","retailPrice":69.00,"shareUrl":"","sort":3,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":0,"brandId":0,"brief":"经典海魂纹自在海军领","categoryId":1020003,"counterPrice":89.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1135073","id":1135073,"isHot":false,"isNew":true,"isOnSale":true,"keywords":"","name":" 海魂纹哈衣水手服(婴童)","picUrl":"http://yanxuan.nosdn.127.net/53052b04ae001d289c040e09ea92231c.png","retailPrice":69.00,"shareUrl":"","sort":4,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":5,"brandId":0,"brief":"差旅好伴侣","categoryId":1032000,"counterPrice":119.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1152031","id":1152031,"isHot":true,"isNew":true,"isOnSale":true,"keywords":"","name":" 魔兽世界 - 伊利丹颈枕眼罩套装 ","picUrl":"http://yanxuan.nosdn.127.net/fd6e78a397bd9e9804116a36f0270b0a.png","retailPrice":99.00,"shareUrl":"","sort":4,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":5,"brandId":0,"brief":"桌面整顿神器","categoryId":1032000,"counterPrice":519.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1152095","id":1152095,"isHot":false,"isNew":true,"isOnSale":true,"keywords":"","name":" 魔兽世界 联盟·暴风城 堡垒收纳盒 ","picUrl":"http://yanxuan.nosdn.127.net/c86b49f635fa141decebabbd0966a6ef.png","retailPrice":499.00,"shareUrl":"","sort":6,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":0,"brandId":0,"brief":"3 重透气,清新柔滑","categoryId":1008009,"counterPrice":479.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1152161","id":1152161,"isHot":false,"isNew":true,"isOnSale":true,"keywords":"","name":" 竹语丝麻印花四件套 ","picUrl":"http://yanxuan.nosdn.127.net/977401e75113f7c8334c4fb5b4bf6215.png","retailPrice":459.00,"shareUrl":"","sort":6,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10}], categoryList=[{"createTime":"2020-12-08 23:09:12","delFlag":false,"iconUrl":"http://cdn.wayn.xin/9fc3c52571aa38a1466f114c2dc892fc.png","id":2,"jumpType":0,"name":"大牌手机","picUrl":"http://cdn.wayn.xin/2545dd00ca4575759024af2811949a9d.jpg","sort":1,"status":0,"updateTime":"2020-12-12 19:26:48","valueId":5,"valueUrl":"http://baidu.com"},{"createTime":"2020-12-06 13:27:54","delFlag":false,"iconUrl":"http://82.157.141.70/upload/2022/02/21/a23fa32c8f4004a8c9fbbb1784462163.jpg","id":1,"jumpType":0,"name":"滋补保健 2","picUrl":"http://cdn.wayn.xin/d4de7172eb7ae4178ae4dafd6a973d8f.jpg","sort":2,"status":0,"updateTime":"2022-06-19 09:17:20","valueId":2},{"createTime":"2020-12-08 23:26:15","delFlag":false,"iconUrl":"http://cdn.wayn.xin/6bc0f8a131d2d16b8fc2004d4aa4860c.png","id":3,"jumpType":1,"name":"不锈钢锅","picUrl":"http://cdn.wayn.xin/314d87257f7a2ff03d5f4c5183797912.jpg","sort":2,"status":0,"updateTime":"2020-12-12 19:27:24","valueId":1036000},{"createTime":"2020-12-12 19:28:08","delFlag":false,"iconUrl":"http://cdn.wayn.xin/5a90531d901529967885279d7dc826e1.png","id":14,"jumpType":0,"name":"进口牛奶","picUrl":"http://cdn.wayn.xin/0b1f6ab63d5e222c52c83a2d0581e44c.jpg","sort":3,"status":0,"valueId":5},{"createTime":"2020-12-12 19:28:33","delFlag":false,"iconUrl":"http://cdn.wayn.xin/33530951827ca7e59940d51cda537d84.png","id":15,"jumpType":0,"name":"量贩囤货","picUrl":"http://cdn.wayn.xin/bb6daee3b3e51c3008db97585249f513.jpg","sort":4,"status":0,"valueId":2},{"createTime":"2020-12-12 19:28:50","delFlag":false,"iconUrl":"http://cdn.wayn.xin/7d337f25111b263b29d5d12589015c46.png","id":16,"jumpType":0,"name":"清洁用品","picUrl":"http://cdn.wayn.xin/be8995bda39d03b17349b8ec0dcab3d5.jpg","sort":5,"status":0,"valueId":2},{"createTime":"2020-12-12 19:29:10","delFlag":false,"iconUrl":"http://cdn.wayn.xin/2e632ec0173bb477dcdb601495e0412a.png","id":17,"jumpType":0,"name":"洗护用品","picUrl":"http://cdn.wayn.xin/53fb88c9d1245caa882aa3fc29187d0b.jpg","sort":6,"status":0,"valueId":4},{"createTime":"2020-12-12 19:29:28","delFlag":false,"iconUrl":"http://cdn.wayn.xin/942323c0e74677cf2aa15f09a1e63bca.png","id":18,"jumpType":0,"name":"日用百货","picUrl":"http://cdn.wayn.xin/8587f91db2edcb43e57da9835cc7ec76.jpg","sort":7,"status":0,"valueId":2},{"createTime":"2020-12-12 19:29:46","delFlag":false,"iconUrl":"http://cdn.wayn.xin/18d9d860ba9b8b28522e050f11a8a8e0.png","id":19,"jumpType":0,"name":"明星乳胶","picUrl":"http://cdn.wayn.xin/65273c7fb2273e90958e92626248a90a.jpg","sort":8,"status":0,"valueId":6},{"createTime":"2020-12-12 19:30:15","delFlag":false,"iconUrl":"http://cdn.wayn.xin/7c790577afda91eebc3c95586e190957.png","id":20,"jumpType":0,"name":"口碑好物","picUrl":"http://cdn.wayn.xin/210011b35be4ceee39e6a466b40b8e22.jpg","sort":9,"status":0,"updateTime":"2021-04-01 20:13:08","valueId":5}], expire_time=1669549170235, hotGoodsList=[{"actualSales":1,"brandId":1001045,"brief":"一级桑蚕丝,吸湿透气柔软","categoryId":1036000,"counterPrice":719.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1006013","id":1006013,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":" 双宫茧桑蚕丝被 空调被 ","picUrl":"http://yanxuan.nosdn.127.net/583812520c68ca7995b6fac4c67ae2c7.png","retailPrice":699.00,"shareUrl":"","sort":7,"unit":"件","updateTime":"2021-08-08 11:19:36","virtualSales":10},{"actualSales":1,"brandId":1001045,"brief":"双层子母被,四季皆可应用","categoryId":1008008,"counterPrice":14199.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1006014","id":1006014,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":" 双宫茧桑蚕丝被 子母被 ","picUrl":"http://yanxuan.nosdn.127.net/2b537159f0f789034bf8c4b339c43750.png","retailPrice":1399.00,"shareUrl":"","sort":15,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":6,"brandId":1001000,"brief":"加大加厚,双色精彩","categoryId":1036000,"counterPrice":219.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1011004","id":1011004,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":" 色织精梳 AB 纱格纹空调被 ","picUrl":"http://yanxuan.nosdn.127.net/0984c9388a2c3fd2335779da904be393.png","retailPrice":199.00,"shareUrl":"","sort":2,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":6,"brandId":0,"brief":"共享密切 2 人时光","categoryId":1008008,"counterPrice":219.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1019002","id":1019002,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":" 降级款护颈双人记忆枕 ","picUrl":"http://yanxuan.nosdn.127.net/0118039f7cda342651595d994ed09567.png","retailPrice":199.00,"shareUrl":"","sort":10,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":6,"brandId":0,"brief":"衰弱爱护枕","categoryId":1008008,"counterPrice":119.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1019006","id":1019006,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":" 动物填充护颈夜交藤枕 ","picUrl":"http://yanxuan.nosdn.127.net/60c3707837c97a21715ecc3986a744ce.png","retailPrice":99.00,"shareUrl":"","sort":7,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":6,"brandId":0,"brief":"厚实舒服","categoryId":1008001,"counterPrice":59.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1021000","id":1021000,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"被","name":"埃及进口长绒棉毛巾","picUrl":"http://yanxuan.nosdn.127.net/7191f2599c7fe44ed4cff7a76e853154.png","retailPrice":39.00,"shareUrl":"","sort":7,"unit":" 条 ","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":6,"brandId":1001020,"brief":" 浪漫毛线绣球,简洁而不简略 ","categoryId":1008009,"counterPrice":319.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1022000","id":1022000,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":"意式毛线绣球四件套","picUrl":"http://yanxuan.nosdn.127.net/5350e35e6f22165f38928f3c2c52ac57.png","retailPrice":299.00,"shareUrl":"","sort":18,"unit":" 件 ","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":32,"brandId":1001000,"brief":" 柔软纱布,婴童可用 ","categoryId":1036000,"counterPrice":269.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1027004","id":1027004,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":"色织六层纱布夏凉被","picUrl":"http://yanxuan.nosdn.127.net/6252f53aaf36c072b6678f3d8c635132.png","retailPrice":249.00,"shareUrl":"","sort":3,"unit":" 件 ","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":32,"brandId":0,"brief":" 原生苦荞,衰弱护颈 ","categoryId":1008008,"counterPrice":119.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1036002","id":1036002,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":"平地苦荞麦枕","picUrl":"http://yanxuan.nosdn.127.net/ffd7efe9d5225dff9f36d5110b027caa.png","retailPrice":99.00,"shareUrl":"","sort":5,"unit":" 件 ","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":32,"brandId":0,"brief":"5cm 记忆绵的密切包裹 ","categoryId":1008008,"counterPrice":619.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1037011","id":1037011,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":"安睡慢回弹记忆绵床垫","picUrl":"http://yanxuan.nosdn.127.net/a03ea6f4509439acdafcb7ceba1debe0.png","retailPrice":599.00,"shareUrl":"","sort":22,"unit":" 件 ","updateTime":"2018-02-01 00:00:00","virtualSales":10}]}]
2022-11-27 21:29:48.336 [9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage_mpCount:137 debug - ==>  Preparing: SELECT COUNT(*) AS total FROM shop_goods WHERE del_flag = 0 AND is_on_sale = 1
2022-11-27 21:29:48.387 [9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage_mpCount:137 debug - ==> Parameters: 
2022-11-27 21:29:48.426 [9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage_mpCount:137 debug - <==      Total: 1
2022-11-27 21:29:48.430 [9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage:137 debug - ==>  Preparing: select id, goods_sn, name, pic_url, counter_price, retail_price, actual_sales, virtual_sales from shop_goods WHERE del_flag = 0 and is_on_sale = 1 order by create_time desc LIMIT ?
2022-11-27 21:29:48.452 [9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage:137 debug - ==> Parameters: 6(Long)
2022-11-27 21:29:48.476 [9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage:137 debug - <==      Total: 6

最初剖析上诉日志:通过 86c76336100c414dbe9217aeb099ccd5 实现接口调用追踪,通过 12 用户 ID,实现用户调用追踪

七. alibaba excel 导出时自定义格局转换优雅实现

官网介绍:EasyExcel 是一个基于 Java 的简略、省内存的读写 Excel 的开源我的项目。在尽可能节约内存的状况下反对读写百 M 的 Excel。

EasyExcelalibaba 出的一个基于 java poi 得 excel 通用解决类库,他的劣势在于内存耗费。比照 easypoi 计划,EasyExcel 在内存耗费、知名度(大厂光环)上更出众些。

博主在应用过程中发现导出 excel,官网对自定义格局字段提供了 converter 接口,但只简略提供了CustomStringStringConverter 类代码,达不到博主想要得优雅要求,如下:

public class CustomStringStringConverter implements Converter<String> {
    @Override
    public Class<?> supportJavaTypeKey() {return String.class;}

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {return CellDataTypeEnum.STRING;}

    /**
     * 这里读的时候会调用
     *
     * @param context
     * @return
     */
    @Override
    public String convertToJavaData(ReadConverterContext<?> context) {return "自定义:" + context.getReadCellData().getStringValue();}

    /**
     * 这里是写的时候会调用 不必管
     *
     * @return
     */
    @Override
    public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {return new WriteCellData<>(context.getValue());
    }

}

在以上代码中,打个比方想要实现性别字段得自定义格局转换,就须要在 convertToExcelData 办法中,增加如下代码

@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {String value = context.getValue();
    if ("man".equals(value)) {return new WriteCellData<>("男");
    } else {return new WriteCellData<>("女");
    }
}

能够看到,十分得不优雅,对于这种类型字段,博主习惯应用枚举类来定义字段所有类型,而后将枚举类转换为 map(value,desc) 构造,就能够优雅得实现这个自定义格局得需要

/**
 * 一、先定义 int 字段形象转换类,实现通用转换逻辑
 */
public abstract class AbstractIntConverter implements Converter<Integer> {abstract List<ConverterDTO> getArr();

    public WriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {List<ConverterDTO> values = getArr();
        Map<Integer, String> map = values.stream().collect(toMap(ConverterDTO::getType, ConverterDTO::getDesc));
        String result = map.getOrDefault(value, "");
        return new WriteCellData<>(result);
    }

    static class ConverterDTO {
        private Integer type;
        private String desc;

        public Integer getType() {return type;}

        public void setType(Integer type) {this.type = type;}

        public String getDesc() {return desc;}

        public void setDesc(String desc) {this.desc = desc;}

        public ConverterDTO(Integer type, String desc) {
            this.type = type;
            this.desc = desc;
        }
    }
}

/**
 * 二、定义通用状态字段转换类
 */
public class StatusConverter extends AbstractIntConverter {

    @Override
    List<ConverterDTO> getArr() {StatusEnum[] values = StatusEnum.values();
        return Arrays.stream(values).map(sexEnum -> new ConverterDTO(sexEnum.getType(), sexEnum.getDesc())).toList();}

    /**
     * 状态枚举
     */
    enum StatusEnum {MAN(0, "启用"),
        WOMAN(1, "禁用");

        private Integer type;
        private String desc;

        StatusEnum(Integer type, String desc) {
            this.type = type;
            this.desc = desc;
        }
        public Integer getType() {return type;}
        public String getDesc() {return desc;}
    }
}

最初再导出 ExcelProperty 中甜腻加 StatusConverter,就优雅得实现了自定义格局得需要

public class User extends BaseEntity {
    ...
    /**
     * 用户状态 0 启用 1 禁用
     */
    @ExcelProperty(value = "用户状态", converter = StatusConverter.class)
    private Integer userStatus;
    ...

}

八. Springboot 默认 redis 客户端 lettuce 常常连贯超时解决方案

不晓得大家有没有遇到这种状况,线上我的项目应用 lettuce 客户端,当操作 redis 得接口一段时间没有调用后(比方 30 分钟),再次调用 redis 操作后,就会遇到连贯超时得问题,导致接口异样。博主间接给出剖析过程:

  1. 通过 wireshark 抓包工具,发现我的项目中 redis 连贯创立后,一段时间未传输数据后,客户端发送 psh 包,未收到服务端 ack 包,触发 tcp 得超时重传机制,在重传次数重试完后,最终客户端被动敞开了连贯。

到这里咱们就晓得这个问题,次要起因在于服务端没有回复客户端(比方 tcp 参数设置、防火墙被动敞开等,都是针对一段时间内没有数据传输得 tcp 连贯会做敞开解决),造成了客户端得连贯超时

面对这个问题有三种解决方案:

  • redis 操作异样后进行重试,这篇文章有介绍 生产环境 Redis 连贯,长时间无响应被服务器断开问题
  • 启用一个心跳定时工作,定时拜访 redis,放弃 redis 连贯不被敞开,简而言之,就是写一个定时工作,定时调用 redisget 命令,进而保活 redis 连贯
  • 基于 Springboot 提供得 LettuceClientConfigurationBuilderCustomizer 自定义客户端配置,博主这里次要针对第三种自定义客户端配置来解说一种优雅得形式

Springboot 我的项目中对于 lettuce 客户端得主动配置是没有启用保活配置得,要启用得话代码如下:

/**
 * 自定义 lettuce 客户端配置
 *
 * @return LettuceClientConfigurationBuilderCustomizer
 */
@Bean
public LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer() {
    return clientConfigurationBuilder -> {LettuceClientConfiguration clientConfiguration = clientConfigurationBuilder.build();
        ClientOptions clientOptions = clientConfiguration.getClientOptions().orElseGet(ClientOptions::create);
        ClientOptions build = clientOptions.mutate().build();
        SocketOptions.KeepAliveOptions.Builder builder = build.getSocketOptions().getKeepAlive().mutate();
        // 保活配置
        builder.enable(true);
        builder.idle(Duration.ofSeconds(30));
        SocketOptions.Builder socketOptionsBuilder = clientOptions.getSocketOptions().mutate();
        SocketOptions.KeepAliveOptions keepAliveOptions = builder.build();
        socketOptionsBuilder.keepAlive(keepAliveOptions);
        SocketOptions socketOptions = socketOptionsBuilder.build();
        ClientOptions clientOptions1 = ClientOptions.builder().socketOptions(socketOptions).build();
        clientConfigurationBuilder.clientOptions(clientOptions1);
    };
}

增加 lettuce 客户端的自定义配置,在 KeepAliveOptions 中启用 enable,这样 lettuce 客户端就会在 tcp 协定标准上启用 keep alive 机制主动发送心跳包

九. redis 客户端 lettuce 启用 epoll

间接给 官网连贯,配置很简略,增加一个 netty-all 得依赖,lettuce 会自动检测我的项目零碎是否反对 epolllinux 零碎反对),并且是否有 netty-transport-native-epoll 依赖(netty-all 蕴含 netty-transport-native-epoll),都满足得话就会主动启用 epoll 事件循环,进一步晋升零碎性能

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
</dependency>

十. Springboot web 我的项目优雅停机

web 我的项目配置了优雅停机后,在重启 jar 包,或者容器时能够避免正在流动得线程被忽然进行(kill -9 无解,请不要应用这个参数杀线上过程,docker compose 我的项目尽量不要用 docker-compose down 命令敞开我的项目,应用 docker-compose rm -svf 能够触发优雅停机),造成用户申请失败,在此期间容许实现现有申请但不容许新申请,配置如下:

server: shutdown: "graceful"

十一. nginx 配置通用申请后缀

先说下这个配置产生得前提,博主公司 pc 客户我的项目是基于 electron 打包得网页我的项目,每次我的项目大版本更新时,为了做好兼容性,避免客户端网页缓存等,会应用一个新网页地址,打个比方:

老网页地址,v1.1.0 版本网页拜访地址:http://api.dev.com/pageV110

新网页地址,v1.2.0 版本网页拜访地址:http://api.dev.com/pageV120

那么我的项目得 nginx 配置则则须要新加一个 v1.2.0 得配置如下:

server {
   listen 80;
   server_name api.dev.com;
   client_max_body_size 10m;

   # 老网页 v1.1.0 配置
   location ~ ^/pageV110 {
               alias  /home/wwwroot/api.dev.com/pageV110;
               index  index.html index.htm;
       }
       
   # 新网页 v1.2.0 配置
   location ~ ^/pageV120 {
               alias  /home/wwwroot/api.dev.com/pageV120;
               index  index.html index.htm;
       }

}

那么博主在每次我的项目公布得时候就须要配合前端发版,配置一个新网页,故产生了这个通用配置得需要,如下:

server {
   listen 80;
   server_name api.dev.com;
   client_max_body_size 10m;

   # 配置正则 localtion
   location ~ ^/pageV(.*) {
               set $s $1; # 定义后缀变量
               alias  /home/wwwroot/api.dev.com/pageV$s;
               index  index.html index.htm;
       }

}

nginx 配置文件语法中,location 语句能够应用正则表达式,定义 set $s $1 变量,实现了通用配置

十二. 对于开发人员的自我晋升和冲破

博主这里次要总结了四点:

  1. 多和别人沟通,沟通能把简单问题简单化,有时候开发阶段一个需要多问几句,能够缩小因为集体了解差别导致的需要不统一问题,进而缩小开发工夫
  2. 建设长短期指标,观看技术视频、书籍给本人充电,比方 7 天利用业余时间看完一本电子书,三十天从零开始一个新我的项目等
  3. 长于总结,对于我的项目中的疑难 bug,踩坑点要有记录,避免下次遇到再掉坑里
  4. 敢于尝试、担责,对我的项目、代码里明确不合理的中央要敢于跟别人沟通,批改问题代码,达到优化目标。对于本人造成的问题要承当,不要推卸责任。对于线上问题要器重,优先解决线上问题。
正文完
 0