解决Docker容器中MySQL连接因LANG环境变量缺失导致的问题
解决Docker容器中MySQL连接因LANG环境变量缺失导致的问题问题描述在使用Docker容器部署Spring Boot应用时，遇到以下错误：java.lang.IllegalStateException: DBAppender cannot function if the JDBC driver does not support getGeneratedKeys method *and* without a specific SQL dialect环境信息：Docker容器：基于Ubuntu 22.04，运行宝塔面板数据库：MySQL 8.0.26（独立容器）应用：Spring Boot + Logback + MySQL Connector 8.0.16关键发现：同样的JAR包在虚拟机上运行正常，在容器中无法启动快速解决方案根本解决方案（推荐）：apt-get update && apt-get install -y locales && locale-gen en_US.UTF-8 && update-locale LANG=en_US.UTF-8 && export LANG=en_US.UTF-8 && export LC_ALL=en_US.UTF-8 && echo 'export LANG=en_US.UTF-8' >> ~/.bashrc && echo 'export LC_ALL=en_US.UTF-8' >> ~/.bashrc && source ~/.bashrc临时解决方案（连接参数）：在数据库连接URL中添加：allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8spring: datasource: url: jdbc:mysql://127.0.0.1:3306/your_database?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8或者在启动容器时添加环境变量：docker run -d \ --name your-container \ --net=host \ --restart always \ --privileged \ -e LANG=C.UTF-8 \ -e LC_ALL=C.UTF-8 \ your-image:tag详细排查过程初期错误理解最初看到错误信息，以为是Logback配置问题，尝试了多种方案：取消注释SQL方言配置 ❌<sqlDialect class="ch.qos.logback.core.db.dialect.MySQLDialect"/>使用DriverManagerConnectionSource ❌升级/降级Logback版本 ❌使用HikariCP替代Commons DBCP ❌暂时禁用DBAppender ✅（临时方案，不是根本解决）转换思路：环境差异分析经过两天的配置调试无果后，开始从环境角度分析问题。1. Java版本对比# 容器和虚拟机都是相同版本 java version "1.8.0_381" Java(TM) SE Runtime Environment (build 1.8.0_381-b09)2. 环境变量对比容器环境：JAVA_HOME=/home/root/soft/jdk1.8.0_381 PATH=/home/root/soft/jdk1.8.0_381/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin # 缺失 LANG 和 LC_* 变量虚拟机环境：JAVA_HOME=/home/root/soft/jdk1.8.0_381 LANG=en_US.UTF-8 # 关键差异！ PATH=/home/root/soft/jdk1.8.0_381/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin3. MySQL驱动行为测试创建测试程序验证MySQL驱动的getGeneratedKeys支持：import java.sql.*; public class TestMySQLDriver { public static void main(String[] args) { try { Class.forName("com.mysql.cj.jdbc.Driver"); Connection conn = DriverManager.getConnection( "jdbc:mysql://127.0.0.1:3306/your_database?useSSL=false", "username", "password"); DatabaseMetaData meta = conn.getMetaData(); System.out.println("supportsGetGeneratedKeys: " + meta.supportsGetGeneratedKeys()); System.out.println("Driver version: " + meta.getDriverVersion()); System.out.println("Database version: " + meta.getDatabaseProductVersion()); conn.close(); } catch (Exception e) { e.printStackTrace(); } } }测试结果对比：容器中：java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:110) at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:835) ... at TestMySQLDriver.main(TestMySQLDriver.java:7)虚拟机中：supportsGetGeneratedKeys: true Driver version: mysql-connector-java-8.0.16 Database version: 8.0.26关键发现： 容器环境尝试设置LANG环境变量时出现警告：export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8 -bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)根因分析核心问题： 容器环境缺少LANG和LC_ALL环境变量，导致：Public Key Retrieval错误 - 这是MySQL 8.0的安全特性，在字符编码异常时更容易触发字符编码处理异常 - MySQL连接器在处理字符编码时出现问题SSL/TLS握手失败 - 编码问题影响了安全连接的建立getGeneratedKeys方法识别失败 - 驱动无法正确识别数据库功能支持本质问题： Public Key Retrieval is not allowed 错误在MySQL 8.0中很常见，但通常在环境正常的情况下可以通过连接参数解决。然而在缺少locale的容器环境中，这个错误变得更加顽固。解决方案详解方案1：修复LANG环境变量（根本解决方案，推荐）# 1. 安装locale支持 apt-get update apt-get install -y locales # 2. 生成UTF-8 locale locale-gen en_US.UTF-8 update-locale LANG=en_US.UTF-8 # 3. 设置环境变量 export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8 # 4. 永久化配置 echo 'export LANG=en_US.UTF-8' >> ~/.bashrc echo 'export LC_ALL=en_US.UTF-8' >> ~/.bashrc source ~/.bashrc # 5. 验证配置 locale方案2：连接字符串参数解决（临时方案）如果无法修改容器环境，可以通过调整MySQL连接参数来绕过这个问题：// 添加 allowPublicKeyRetrieval=true 参数 String url = "jdbc:mysql://127.0.0.1:3306/your_database?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8";完整连接参数建议：String url = "jdbc:mysql://127.0.0.1:3306/your_database?" + "useSSL=false&" + "allowPublicKeyRetrieval=true&" + "useUnicode=true&" + "characterEncoding=UTF-8&" + "serverTimezone=Asia/Shanghai";Spring Boot配置文件：spring: datasource: url: jdbc:mysql://127.0.0.1:3306/your_database?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: username password: password driver-class-name: com.mysql.cj.jdbc.Driver注意： 方案2虽然能解决连接问题，但不能根本解决locale缺失问题，可能在其他功能上仍有隐患。推荐优先使用方案1。方案3：Docker启动时配置docker run -d \ --name baota \ --net=host \ --restart always \ --privileged \ -e LANG=C.UTF-8 \ -e LC_ALL=C.UTF-8 \ -v /path/to/data:/data \ your-image:tag方案4：Dockerfile中预设FROM ubuntu:22.04 # 安装locale并设置环境变量 RUN apt-get update && \ apt-get install -y locales && \ locale-gen en_US.UTF-8 && \ update-locale LANG=en_US.UTF-8 ENV LANG=C.UTF-8 ENV LC_ALL=C.UTF-8 # 其他配置...验证解决效果修复后重新测试MySQL连接：java -cp .:mysql-connector-java-8.0.16.jar TestMySQLDriver期望输出：supportsGetGeneratedKeys: true Driver version: mysql-connector-java-8.0.16 Database version: 8.0.26经验总结环境一致性的重要性 - 看似相同的环境可能存在关键差异字符编码的影响范围 - LANG环境变量不仅影响显示，还会影响网络通信和数据库连接问题定位思路 - 当配置层面无法解决时，要从环境层面分析Docker容器的注意事项 - 容器环境通常是精简的，可能缺少一些基础的系统配置相关问题和预防类似问题可能出现在：其他需要字符编码的Java应用Python应用的数据库连接文件上传/下载功能国际化(i18n)应用预防措施：构建Docker镜像时主动设置LANG环境变量在CI/CD流程中添加环境一致性检查制作标准化的基础镜像，包含必要的locale配置关键要点：根本问题是locale缺失：Public Key Retrieval is not allowed 在MySQL 8.0中很常见，但在locale正常的环境中通常可以通过连接参数解决。在Docker容器的精简环境中，locale缺失使这个问题变得更加复杂。两种解决思路：治本：修复容器的locale环境，这样应用的各个方面都能正常工作治标：通过连接参数绕过验证，但可能在其他功能上仍有隐患环境一致性：Docker容器环境的精简性可能导致一些看似无关的系统配置缺失，而这些配置对应用的正常运行至关重要。在排查此类问题时，环境差异分析往往比配置调优更有效。
面试概况本次面试为快手商业化部门日常实习生的一轮面试，面试时长约为40分钟。面试内容主要集中在Java基础知识、数据库、并发编程、算法等方面。以下是详细的面试问题及回答整理。面试问题及回答1. Java 的集合类知道哪些？Java 的集合类主要分为以下几类：List：有序集合，允许重复元素。常见实现类有 ArrayList、LinkedList、Vector。Set：不允许重复元素的集合。常见实现类有 HashSet、LinkedHashSet、TreeSet。Map：键值对集合，键唯一但值可以重复。常见实现类有 HashMap、LinkedHashMap、TreeMap、Hashtable、ConcurrentHashMap。Queue：队列集合，支持 FIFO（先进先出）操作。常见实现类有 LinkedList、PriorityQueue、ArrayDeque。2. 说说 HashMap？HashMap 是 Java 中常用的集合类，基于哈希表实现，允许存储键值对。其主要特点如下：无序存储：HashMap 不保证元素的顺序。允许空键和空值：但最多只能有一个空键。线程不安全：在多线程环境下使用 HashMap 可能会导致数据不一致。时间复杂度：插入、删除、查找操作的时间复杂度均为 O(1)（理想情况下）。3. HashMap 线程安全吗，如何变线程安全？HashMap 本身是线程不安全的。可以通过以下几种方式使其线程安全：使用 Collections.synchronizedMap：将 HashMap 包装成线程安全的 Map。Map<String, String> map = Collections.synchronizedMap(new HashMap<>());使用 ConcurrentHashMap：ConcurrentHashMap 是线程安全的 Map 实现，性能优于 Hashtable。Map<String, String> map = new ConcurrentHashMap<>();手动同步：在访问 HashMap 的地方手动加锁。synchronized (map) { map.put(key, value); }4. ConcurrentHashMap 如何实现的？ConcurrentHashMap 通过分段锁（Segment）机制实现线程安全。具体实现如下：分段锁：将整个哈希表分成多个段（Segment），每个段内部是一个小的哈希表。锁粒度：每次操作只锁定当前段，而不是整个哈希表，提高了并发性能。JDK 1.8 改进：取消了 Segment，改为使用 CAS 操作和锁竞争机制，进一步提高了性能。5. synchronized 原理以及升级过程synchronized 是 Java 中的内置锁，用于实现线程同步。其原理如下：锁的状态：锁有四种状态，依次是无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁升级：锁可以从较低状态升级到较高状态，但不能降级。无锁状态：没有任何线程持有锁。偏向锁：偏向于第一个获取锁的线程，减少锁的竞争。轻量级锁：使用 CAS 操作尝试获取锁，如果失败则升级为重量级锁。重量级锁：使用操作系统互斥量实现，性能较差。6. 线程池的参数？ThreadPoolExecutor 是 Java 中创建线程池的类，其构造方法参数如下：corePoolSize：核心线程数。maximumPoolSize：最大线程数。keepAliveTime：线程空闲时间。unit：keepAliveTime 的时间单位。workQueue：任务队列，用于存储等待执行的任务。threadFactory：线程工厂，用于创建线程。handler：拒绝策略，当任务队列满且线程数达到最大值时的处理策略。7. full gc, minor gc?Minor GC：回收年轻代（Young Generation）的垃圾收集操作。当年轻代空间不足时触发。Full GC：回收整个堆（包括年轻代和老年代）的垃圾收集操作。通常在以下情况下触发：老年代空间不足。系统显式调用 System.gc()。Minor GC 时发现老年代空间不足。8. 垃圾回收算法？常见的垃圾回收算法有：标记-清除：标记所有需要回收的对象，然后清除这些对象。标记-整理：标记所有需要回收的对象，然后将存活对象移动到一端，清除边界外的对象。复制：将内存分为两个区域，每次只使用其中一个区域，将存活对象复制到另一个区域。分代收集：将内存分为年轻代和老年代，分别使用不同的回收算法。9. CPU 密集型和 IO 密集型下如何设置核心线程数？CPU 密集型：任务主要消耗 CPU 资源，核心线程数一般设置为 CPU 核心数 + 1。int corePoolSize = Runtime.getRuntime().availableProcessors() + 1;IO 密集型：任务主要消耗 I/O 资源，核心线程数一般设置为 CPU 核心数 * 2。int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;10. 聚簇索引和非聚簇索引区别聚簇索引：数据行的物理存储顺序与索引顺序一致。一个表只能有一个聚簇索引。查询速度快，但插入、删除、更新操作较慢。非聚簇索引：数据行的物理存储顺序与索引顺序无关。一个表可以有多个非聚簇索引。查询速度相对较慢，但插入、删除、更新操作较快。11. MySQL 的各个隔离级别，以及分别解决了什么问题MySQL 的事务隔离级别有四种：读未提交（Read Uncommitted）：最低隔离级别，允许脏读。读已提交（Read Committed）：允许不可重复读，但不允许脏读。可重复读（Repeatable Read）：默认隔离级别，允许幻读，但不允许脏读和不可重复读。串行化（Serializable）：最高隔离级别，不允许脏读、不可重复读和幻读。12. Redis 有什么数据类型Redis 支持多种数据类型：字符串（String）：最基本的类型，可以存储字符串、数字等。列表（List）：有序集合，支持从两端插入和删除操作。集合（Set）：无序集合，不允许重复元素。有序集合（Sorted Set）：有序集合，每个元素关联一个分数。哈希表（Hash）：键值对集合，键唯一。位图（Bitmap）：用于处理位级别的操作。HyperLogLog：用于估算集合的基数。13. Redis 分布式锁实现？Redis 分布式锁可以通过以下几种方式实现：SETNX 命令：使用 SETNX 命令尝试获取锁。SETNX key valueEXPIRE 命令：设置锁的超时时间，防止死锁。EXPIRE key secondsLua 脚本：使用 Lua 脚本保证原子性。local key = KEYS[1] local value = ARGV[1] local expire = tonumber(ARGV[2]) if redis.call("setnx", key, value) == 1 then redis.call("expire", key, expire) return 1 else return 0 end14. 项目中的前缀树，自定义注解限频，网站优化过程前缀树（Trie）：用于快速查找字符串，常用于搜索引擎、拼写检查等。自定义注解限频：通过自定义注解和 AOP 切面实现接口限流。@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RateLimit { int limit() default 100; int time() default 1; } @Aspect @Component public class RateLimitAspect { @Around("@annotation(rateLimit)") public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { // 限流逻辑 return joinPoint.proceed(); } }网站优化过程：前端优化：使用 CDN、压缩资源、合并文件、懒加载等。后端优化：优化数据库查询、使用缓存、减少网络请求等。服务器优化：调整 JVM 参数、优化网络配置、使用负载均衡等。15. 算法：反转链表反转链表的实现如下：public ListNode reverseList(ListNode head) { ListNode prev = null; ListNode current = head; while (current != null) { ListNode next = current.next; current.next = prev; prev = current; current = next; } return prev; }总结本次面试涉及的知识点较为广泛，涵盖了 Java 基础、并发编程、数据库、算法等多个方面。准备面试时，建议重点复习这些知识点，并结合实际项目经验进行准备。希望这篇博客对大家有所帮助！
JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home PATH=$JAVA_HOME/bin:$PATH //给环境变量赋值 export JAVA_HOME //导出使其生效 export PATH 参考链接：https://blog.csdn.net/m0_51520179/article/details/131295356
set PATH=%PATH%;"D:\Program Files\Java\jre1.8.0_25\bin" keytool -genkey -alias testapp -keyalg RSA -keysize 2048 -validity 36500 -keystore testapp.keystore Enter keystore password: //testapp1992 Re-enter new password: //testapp1992 What is your first and last name? [Unknown]: //testapp What is the name of your organizational unit? [Unknown]: //starg What is the name of your organization? [Unknown]: //starg What is the name of your City or Locality? [Unknown]: //xiamen What is the name of your State or Province? [Unknown]: //xiamen What is the two-letter country code for this unit? [Unknown]: //CN Is CN=XX, OU=XX, O=XX, L=XX, ST=XX, C=XX correct? [no]: //确认上面输入的内容是否正确，输入y，回车 Enter key password for <testalias> (RETURN if same as keystore password): //确认证书密码与证书文件密码一样（HBuilder|HBuilderX要求这两个密码一致），直接回车就可以 keytool -list -v -keystore testapp.keystore 别名: testapp 创建日期: 2021-8-4 条目类型: PrivateKeyEntry 证书链长度: 1 证书[1]: 所有者: CN=testapp, OU=starg, O=starg, L=xiamen, ST=xiamen, C=CN 发布者: CN=testapp, OU=starg, O=starg, L=xiamen, ST=xiamen, C=CN 序列号: 18728bc0 有效期开始日期: Wed Aug 04 10:30:32 CST 2021, 截止日期: Fri Jul 11 10:30:32 CST 2121 证书指纹: MD5: B4:14:19:EA:7A:D4:8F:E4:AB:D7:E9:04:11:C7:EB:7D SHA1: 46:6E:CA:FA:58:45:05:29:5E:FF:00:D0:53:A0:D6:45:B4:63:B4:CB SHA256: 91:8C:A9:14:E2:77:27:C6:36:EE:AC:DD:D6:08:22:2F:95:77:9A:63:E2:CE:34:A1:38:6C:F5:CF:13:31:9B:22 签名算法名称: SHA256withRSA 版本: 3 扩展: #1: ObjectId: 2.5.29.14 Criticality=false SubjectKeyIdentifier [ KeyIdentifier [ 0000: FF ED 61 5C 25 46 D5 40 30 1C 5D FA B8 D9 8C 8A ..a\%F.@0.]..... 0010: AD 72 4D 0D .rM. ] ] ******************************************* *******************************************
（一）引言在实际开发工作中，通常需要搭建maven私有仓库。（二）Nexus介绍 Nexus 是Maven仓库管理器，如果你使用Maven，你可以从Maven中央仓库 下载所需要的构件（artifact），但这通常不是一个好的做法，你应该在本地架设一个Maven仓库服务器，在代理远程仓库的同时维护本地仓库，以节省带宽和时间，Nexus就可以满足这样的需要。此外，他还提供了强大的仓库管理功能，构件搜索功能，它基于REST，友好的UI是一个extjs的REST客户端，它占用较少的内存，基于简单文件系统而非数据库。这些优点使其日趋成为最流行的Maven仓库管理器。（三）安装docker请参考 Docker安装_Icoolkj的博客（四）docker中安装nexus31、拉取镜像## 通过docker search nexus 命令搜索一下docker公有库在的 nexus相关的镜像 [root@icoolkj ~]# docker search nexus## 拉取nexus3镜像 [root@icoolkj /]# docker pull sonatype/nexus3 Using default tag: latest latest: Pulling from sonatype/nexus3 f70d60810c69: Pull complete 545277d80005: Pull complete 10b49635409a: Pull complete Digest: sha256:3fd7e90bcf49fb55d87d852cab854e5669ed115b09bdb25f47c45ee0797231aa 147.6MB/295.3MB Status: Downloaded newer image for sonatype/nexus3:latest docker.io/sonatype/nexus3:latest [root@icoolkj /]# 2、建立数据储存文件夹## 建立数据存放文件夹，用于docker中nexus的数据与本地物理机映射 [root@icoolkj /]# mkdir -p /usr/local/nexus3/nexus-data [root@icoolkj /]# ll /usr/local/nexus3/ 总用量 4 drwxr-xr-x 2 root root 4096 6月 3 18:06 nexus-data ## 更改权限 [root@icoolkj /]# chmod 777 /usr/local/nexus3/nexus-data/ [root@icoolkj /]# ll /usr/local/nexus3/ 总用量 4 drwxr-xr-x 2 777 root 4096 6月 3 18:06 nexus-data [root@icoolkj /]# 3、安装并运行容器## 编写启动脚本docker-nexus3-start.sh docker rm -f docker-nexus3 || true docker run --name docker-nexus3 \ -p 8081:8081 \ -v /usr/local/nexus3/nexus-data:/nexus-data \ --restart=always \ -d sonatype/nexus3 ## 参数说明 --name nexus #启动该容器的名字，可以自己更改为自己想要的名字 -p 8081:8081 #端口映射，将本地8081端口映射为容器的8081端口,第一个8081可以改成自己想要放开的端口 -v /docker/nexus/nexus-data:/nexus-data # 将容器的/nexus-data地址 代理到 本地/docker/nexus/nexus-data文件夹下 --restart=always #在重启docker时，自动重启改容器。 -d sonatype/nexus3 #即为后台运行一直sonatype/nexus34、获取容器的日志[root@icoolkj nexus3]# docker logs -f --tail 20 docker-nexus3（五）使用nexus31、浏览器访问2、配置Nexus提示输入密码，并告知你的密码储存位置Your admin user password is located in /nexus-data/admin.password on the server.因为docker中nexus3的数据储存位置与本地物理机建立了映射关系，所有在物理机上的地址应该是/usr/local/nexus3/nexus-data/admin.password登录成功后需要更改密码，更改密码需要记住（浏览器都有记住密码的功能，顺⼿点保存⾯，下次你直接登录就好了）；更改密码完成之后，admin.password⽂件⾃动删除！！！## 默认仓库说明 maven-central：maven中央库，默认从https://repo1.maven.org/maven2/拉取jar maven-releases：私库发行版jar，初次安装请将Deployment policy设置为Allow redeploy maven-snapshots：私库快照（调试版本）jar maven-public：仓库分组，把上面三个仓库组合在一起对外提供服务，在本地maven基础配置settings.xml或项目pom.xml中使用## Nexus仓库类型介绍 hosted：本地仓库，通常我们会部署自己的构件到这一类型的仓库。比如公司的第二方库。 proxy：代理仓库，它们被用来代理远程的公共仓库，如maven中央仓库。 group：仓库组，用来合并多个hosted/proxy仓库，当你的项目希望在多个repository使用资源时就不需要多次引用了，只需要引用一个group即可。如图所示，代理仓库负责代理远程中央仓库，托管仓库负责本地资源，组资源库 = 代理资源库 + 托管资源库3、配置阿里云代理仓库1）、新建仓库(Create repository)Repository-->Repositories-->Create repository-->maven2(proxy)填写仓库名称——maven-aliyun，并填入仓库urlhttps://maven.aliyun.com/repository/public2）、配置仓库组(默认已有一个maven-public) 注：注意仓库顺序。maven查找依赖时会依次遍历仓库组中的仓库。## 官方文档中建议： It is recommended practice to place hosted repositories higher in the list than proxy repositories. For proxy repositories, the repository manager needs to check the remote repository which will incur more overhead than a hosted repository lookup. 希望将hosted repositories【托管资源库】的顺序放在proxy repositories【代理资源库】之前，因为一个group【组资源库】中可以涵括这些托管资源库和代理资源库。而一整个的group是作为一个public，一个接口给别人使用的。所以当查找架包的时候，如果代理资源库在前面，那就是先从远程去查找jar，而不是先从托管资源库（本地仓库）去查找是否有jar。这样访问外网的消耗比起来在本地查找，当然是将托管资源库放在代理资源库之前的优先位置了。4、创建角色创建角色（develop），并分配nx-all权限Security-->Roles-->Create注：创建角色的同时可以为当前创建的角色分配权限。5、创建用户创建用户（developer），并授予develop角色Security-->Users-->Create注：创建用户并为创建的用户挂上相应的角色。（六）maven配置文件Maven下的setting.xml文件和项目中的pom.xml文件的关系是：settting.xml文件是全局设置，而pom.xml文件是局部设置。pom.xml文件对于项目来说，是优先使用的。而pom.xml文件中如果没有配置镜像地址的话，就按照settting.xml中定义的地址去查找。修改本地maven配置文件(conf/setting.xml)servers节点下添加以下内容(username和password为刚刚在nexus3中添加的用户和其密码) <servers> <server> <id>maven-public</id> <username>developer</username> <password>icoolkj</password> </server> <server> <id>maven-releases</id> <username>developer</username> <password>icoolkj</password> </server> <server> <id>maven-snapshots</id> <username>developer</username> <password>icoolkj</password> </server> </servers>mirrors节点下添加以下内容 <mirrors> <mirror> <id>maven-public</id> <name>maven-public</name> <url>http://182.92.199.85:8081/repository/maven-public/</url> <mirrorOf>central</mirrorOf> </mirror> </mirrors>（七）项目中发布pom.xml配置实际使用中distributionManagement可以配置在parent项目中，子项目无需重复配置。上述配置全部完成后就可以在项目中使用mven clean deploy将项目的jar包上传到自己的私服上了。 <repositories> <repository> <id>maven-public</id> <name>Nexus Repository</name> <url>http://192.168.1.188:8081/repository/maven-public/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>maven-public</id> <name>Nexus Plugin Repository</name> <url>http://192.168.1.188:8081/repository/maven-public/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </pluginRepository> </pluginRepositories> <distributionManagement> <repository> <id>maven-releases</id> <name>Nexus Release Repository</name> <url>http://192.168.1.188:8081/repository/maven-releases/</url> </repository> <snapshotRepository> <id>maven-snapshots</id> <name>Nexus Snapshot Repository</name> <url>http://192.168.1.188:8081/repository/maven-snapshots/</url> </snapshotRepository> </distributionManagement>至此，nexus搭建完毕，支持本地部署依赖jar包。
WebSocket服务端配置类import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Service; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * websocket配置类 * @author daiyg * @date 2021/8/24 17:19 */ @Service @Configuration @EnableWebSocket public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); } }服务端代码import com.caso.common.core.utils.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; /** * @author daiyg * @date 2021/8/24 18:15 */ @Component @ServerEndpoint(value = "/websocket/{id}/{key}", subprotocols = {"protocol"}) public class WebSocketServer { private static int onlineCount = 0; private static ConcurrentHashMap<String, WebSocketServer> webSocketSet = new ConcurrentHashMap<>(); private static ConcurrentHashMap<String, List<String>> map = new ConcurrentHashMap<>(); //与某个客户端的连接会话，需要通过它来给客户端发送数据 private Session session; private static Logger log = LogManager.getLogger(WebSocketServer.class); private String id = ""; private String key= ""; /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(@PathParam(value = "id") String id,@PathParam("key") String key, Session session) { this.session = session; this.id = id;//接收到发送消息的人员编号 this.key = key; if ("1".equals(id)) { log.info("非门岗用户连接！"); } else { List<String> ids = new ArrayList<>(); ids = map.get(id); if(StringUtils.isEmpty(ids)){ ids = new ArrayList<>(); } ids.add(key); map.put(id,ids); webSocketSet.put(key, this); //加入set中 } addOnlineCount(); //在线数加1 log.info("用户" + id + "连接成功！当前在线人数为" + getOnlineCount()); try { sendMessage("连接成功"); } catch (IOException e) { log.error("websocket IO异常"); } } /** * 连接关闭调用的方法 */ @OnClose public void onClose() { //删除map中值 List<String> keys = map.get(this.id); for (int i = 0; i < keys.size(); i++) { if(this.key.equals(keys.get(i))){ keys.remove(i); i--; } } webSocketSet.remove(this.key); //从set中删除 subOnlineCount(); //在线数减1 log.info("有一连接关闭！当前在线人数为" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 */ @OnMessage public void onMessage(String message) { log.info("来自客户端的消息:" + message); } /** * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("发生错误"); try { session.close(); } catch (IOException e) { e.printStackTrace(); } error.printStackTrace(); } /** * 服务端主动推送消息 * * @param message * @throws IOException */ public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } /** * 发送信息给指定ID用户，如果用户不在线则返回不在线信息给自己 * * @param message * @param personKeys * @throws IOException */ public static void sendtoUser(String message, String[] personKeys) throws IOException { //获取人员集合 for (String personKey : personKeys) { //获取map中对应的链接 if (map.get(personKey) != null && StringUtils.isNotEmpty(map.get(personKey))) { //遍历给连接人发送信息 for(String s : map.get(personKey)){ webSocketSet.get(s).sendMessage(message); } } else { //如果用户不在线则返回不在线信息给自己 log.error("当前用户不在线：" + personKey); } } } /** * 发送信息给所有人 * * @param * @throws IOException */ public void sendtoAll(String message) throws IOException { for (String key : webSocketSet.keySet()) { try { webSocketSet.get(key).sendMessage(message); } catch (IOException e) { e.printStackTrace(); } } } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketServer.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketServer.onlineCount--; } }
一、使用Tomcat提供的WebSocket库 Java可以使用Tomcat提供的WebSocket库接口实现WebSocket服务，代码编写也非常的简单。现在的H5联网游戏基本上都是使用WebSocket协议，基于长连接，服务器可以主动推送消息，而不是传统的网页采用客户端轮询的方式获取服务器的消息。下面给出简单使用Tomcat的WebSocket服务的基本代码结构。@ServerEndpoint("/webSocket") public class WebSocket { @OnOpen public void onOpen(Session session) throws IOException{ logger.debug("新连接"); } @OnClose public void onClose(){ logger.debug("连接关闭"); } @OnMessage public void onMessage(String message, Session session) throws IOException { logger.debug("收到消息"); } @OnError public void onError(Session session, Throwable error){ error.printStackTrace(); } }二、WebSocket协议的整个流程1. 基于TCP协议WebSocket本质是基于TCP协议的，采用Java编写WebSocket服务时可以使用NIO或者AIO实现高并发的服务。2. 握手过程客户端采用TCP协议连接服务器指定端口后，首先需要发送一条HTTP的握手协议GET /web HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: 127.0.0.1:8001 Origin: http://127.0.0.1:8001 Sec-WebSocket-Key: hj0eNqbhE/A0GkBXDRrYYw== Sec-WebSocket-Version: 13请求的头里面必须包含以下内容：Connection 其值为Upgrade，表示升级协议Upgrade 其值为websocket，表示升级为WebSocket协议Sec-WebSocket-Key 客户端发送给服务器的密钥，用于标识每个客户端，其值是16位的随机base64编码。Sec-WebSocket-Version WebSocket的协议版本服务器收到这条协议验证成功后进行协议升级，并且不会关闭Socket连接，并发送给客户端响应升级握手成功的HTTP协议包。HTTP/1.1 101 Switching Protocols Content-Length: 0 Upgrade: websocket Sec-Websocket-Accept: ZEs+c+VBk8Aj01+wJGN7Y15796g= Connection: Upgrade Date: Wed, 21 Jun 2017 03:29:14 GMT 响应的协议包里面，首先是101的状态码，更换协议；其中最重要的就是Sec-WebSocket-Accept字段。其值是通过客户端的Key加上固定的"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"密钥，通过采用16位的base64编码后发送给客户端验证，如果客户端也验证成功就表示握手完成。String acc = secKey + WEBSOCK_MAGIC_TAG; MessageDigest sh1 = MessageDigest.getInstance("SHA1"); String key = Base64.getEncoder().encodeToString(sh1.digest(acc.getBytes()));3. 数据的读写握手成功后就可以进行数据发送和读取，WebSocket的数据可以是二进制或者纯文本。每次读取和发送数据需要打包成帧数据，需要按照其标准的格式进行发送或读取才能够正常的进行数据通信。上图就是帧数据的结构图，解析帧数据的代码如下，由于是摘录的部分代码，所以只能作为理解和参考，不可直接使用。protected WebSocketFrameData ParseFrame(NetPacketBuffer bytes){ bytes.mark(); WebSocketFrameData frame = new WebSocketFrameData(); int opData = bytes.readByte(); frame.UnPackOpCodeHeader(opData); // 第一步 int length = frame.UnPackMaskHeader(bytes.readByte()); // 第二步 // 读取长度 if (length == 126) { length = bytes.readShort(); } else if (length == 127){ length = (int) bytes.readInt64(); } // 数据不足,进来的是半包 if(length + 4 > bytes.remaining()){ bytes.reset(); // return null; } // 读取mask if frame.mMasked byte[] masks = new byte[4]; // 第三步 for (int i = 0; i < 4; i++) { masks[i] = (byte) bytes.readByte(); } frame.mLength = length; frame.mData = bytes.readMulitBytes(length); frame.MaskData(masks); // 第四步 return frame; }上面代码中第一步是解析出当前帧是否是最后帧mFin标记、操作码mOpCode，采用位处理，具体的实现如下。public void UnPackOpCodeHeader(int opData){ mRsv1 = (opData & 64) == 64; mRsv2 = (opData & 32) == 32; mRsv3 = (opData & 16) == 16; mFin = (opData & 128) == 128; mOpCode = (opData & 15); }第二步在读取长度前，先解析当前帧是否有采用Mask掩码加密处理，并且里面有可能包含整个帧的长度信息，具体看上面的判断代码。public int UnPackMaskHeader(int mkData){ mMasked = (mkData & 128) == 128; return (mkData & 127); // 这里返回的是长度信息 }接下来就是读取Mask内容，注意只有客户端发送给服务端时需要采用Mask对数据做处理，服务端发送给客户端时不需要做处理。最后通过Mask掩码解析出真实数据。public void MaskData(byte[] masks){ if (!mMasked or masks.length == 0) return ; for (int i = 0; i < mLength; i++) { mData[i] = (byte) (mData[i] ^ masks[i % 4]); } }以上就解析出单帧的数据，帧数据可以分为消息数据(细分为文本数据和二进制数据)、PING包、PONG包、CLOSE包、CONTINUATION包(数据未发送完成包)。而且帧数据又有mFin标记数据是否完整，否则需要将多个帧数据合成一个完整的消息数据。// 读取帧数据，可能存在多帧数据，因此需要手动拆分 WebSocketFrameData frame = ParseFrame(mCachePacket); if(frame == null){ break; // 说明数据不完整，暂不处理。 } // 不完整的帧的时候，只有第一帧会标记帧的类型 opCode = opCode == -1? frame.mOpCode: opCode; mCacheFrame.append(frame.mData, 0, frame.mLength); if(!frame.mFin) // 非完整的数据不处理。 { continue; } // 处理完整的数据 switch(opCode) { case WebSocketFrameData.OP_TEXT: case WebSocketFrameData.OP_BINARY: mCacheFrame.flip(); this.OnMessage(mCacheFrame, opCode); break; case WebSocketFrameData.OP_PING: this.OnPing(mCacheFrame); break; case WebSocketFrameData.OP_PONG: this.OnPong(mCacheFrame); break; case WebSocketFrameData.OP_CLOSE: this.OnClosed(); break; case WebSocketFrameData.OP_CONTINUATION: this.Close(); break; } opCode = -1; mCacheFrame.clear();读取整个客户端的协议数据流程就已经完成了，服务端发送回去的数据就只需要注意两点：大的数据包需要分帧数据发送。不需要采用Mask掩码加密，因此Mask位置设置为0，并且不写入掩码数据。原文摘自
String sn = “1234,567,8910”; List<String> snList = Arrays.asList(sn.split(","));
package com.landicorp.cash.util; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.DecimalFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.apache.commons.beanutils.BeanMap; import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFDateUtil; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.DataFormat; import org.apache.poi.ss.usermodel.Font; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.WorkbookFactory; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import com.landicorp.core.util.common.DateUtil; import com.landicorp.core.util.export.annotations.CollectionExcelTarget; import com.landicorp.core.util.export.annotations.ExcelField; public class ExcelUtil { private Workbook workbook; private OutputStream os; private String pattern;// 日期格式 public void setPattern(String pattern) { this.pattern = pattern; } public ExcelUtil(Workbook workboook) { this.workbook = workboook; } public ExcelUtil(InputStream is, String version) throws FileNotFoundException, IOException { if ("2003".equals(version)) { workbook = new HSSFWorkbook(is); } else { workbook = new XSSFWorkbook(is); } } public String toString() { return "共有 " + getSheetCount() + "个sheet 页！"; } public String toString(int sheetIx) throws IOException { return "第 " + (sheetIx + 1) + "个sheet 页，名称： " + getSheetName(sheetIx) + "，共 " + getRowCount(sheetIx) + "行！"; } /** * * 根据后缀判断是否为 Excel 文件，后缀匹配xls和xlsx * * @param pathname * @return * */ public static boolean isExcel(String pathname) { if (pathname == null) { return false; } return pathname.endsWith(".xls") || pathname.endsWith(".xlsx"); } /** * 对象转map * @param obj * @return */ public static Map<String, Object> obj2Map(Object obj) { Map<String, Object> map = new HashMap<String, Object>(); try { Class<?> cls = obj.getClass(); while (!"java.lang.Object".equals(cls.getName())) { Field[] fields = cls.getDeclaredFields(); Field.setAccessible(fields, true); for (int i = 0; i < fields.length; i++) { try { String fieldName = fields[i].getName(); // 获取原来的访问控制权限 boolean accessFlag = fields[i].isAccessible(); // 修改访问控制权限 fields[i].setAccessible(true); // 获取在对象f中属性fields[i]对应的对象中的变量 if (!"serialVersionUID".equals(fieldName) && !"calss".equals(fieldName)) { String methodName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); Object o = obj.getClass().getMethod(methodName).invoke(obj); if(null == map.get(fieldName)){ map.put(fieldName, o); } } // 恢复访问控制权限 fields[i].setAccessible(accessFlag); } catch (Exception e) { e.printStackTrace(); } } cls = cls.getSuperclass(); } } catch (Exception e) { e.printStackTrace(); } return map; } /** * 对象转map * @param obj * @return */ public static Map<String, Object> obj2Map2(Object obj) { Map<String, Object> map = new HashMap<String, Object>(); try { Map<String, Object> objMap = new BeanMap(obj); for (Entry<String, Object> entry : objMap.entrySet()) map.put(entry.getKey(), entry.getValue()); } catch (SecurityException e) { e.printStackTrace(); } return map; } /** * 对象转map，适用于没有继承属性的对象 * @param obj * @return */ public static Map<String, Object> object2Map(Object obj) { Map<String, Object> map = new HashMap<String, Object>(); Class<? extends Object> clazz = obj.getClass(); Field[] fds = clazz.getFields(); Field[] fields = clazz.getDeclaredFields(); for (int i = 0, len = fields.length; i < len; i++) { String varName = fields[i].getName(); try { // 获取原来的访问控制权限 boolean accessFlag = fields[i].isAccessible(); // 修改访问控制权限 fields[i].setAccessible(true); // 获取在对象f中属性fields[i]对应的对象中的变量 Object o = fields[i].get(obj); if (o != null) map.put(varName, o); // 恢复访问控制权限 fields[i].setAccessible(accessFlag); } catch (IllegalArgumentException ex) { ex.printStackTrace(); } catch (IllegalAccessException ex) { ex.printStackTrace(); } } return map; } /** * * 读取 Excel 第一页所有数据 * * @return * @throws Exception * */ public List<List<String>> read() throws Exception { return read(0, 0, getRowCount(0) - 1); } /** * * 读取指定sheet 页所有数据 * * @param sheetIx * 指定 sheet 页，从 0 开始 * @return * @throws Exception */ public List<List<String>> read(int sheetIx) throws Exception { return read(sheetIx, 0, getRowCount(sheetIx) - 1); } /** * * 读取指定sheet 页指定行数据 * * @param sheetIx * 指定 sheet 页，从 0 开始 * @param start * 指定开始行，从 0 开始 * @param end * 指定结束行，从 0 开始 * @return * @throws Exception */ public List<List<String>> read(int sheetIx, int start, int end) throws Exception { Sheet sheet = workbook.getSheetAt(sheetIx); List<List<String>> list = new ArrayList<List<String>>(); if (end > getRowCount(sheetIx)) { end = getRowCount(sheetIx); } int cols = sheet.getRow(0).getLastCellNum(); // 第一行总列数 for (int i = start; i <= end; i++) { List<String> rowList = new ArrayList<String>(); Row row = sheet.getRow(i); for (int j = 0; j < cols; j++) { if (row == null) { rowList.add(null); continue; } rowList.add(getCellValueToString(row.getCell(j))); } list.add(rowList); } return list; } public static <T> List<T> importExcel(Class<T> clazz, InputStream stream, int startRow) throws Exception { Workbook workbook = WorkbookFactory.create(stream); Sheet sheet = workbook.getSheetAt(0); List<T> dataList = new ArrayList<T>(); // 获取工作表 for (int i = 0; i <= sheet.getLastRowNum(); i++) { if (i < startRow) { continue; } Row row = sheet.getRow(i); Field[] fields = clazz.getFields(); T data = clazz.newInstance(); boolean hasValue = false; for (Field fd : fields) { ExcelField ef = fd.getAnnotation(ExcelField.class); if (ef != null) { Cell cell = row.getCell(ef.cindex()); if (cell == null) { continue; } Object value = getCellValue(cell, fd.getType(), ef.defaultValue()); fd.set(data, value); if (cell.getCellTypeEnum() != CellType.BLANK) { hasValue = true; } } } if (hasValue) { Method method = clazz.getMethod("setRowNum", new Class[] { int.class }); method.invoke(data, i); dataList.add(data); } } workbook.close(); return dataList; } private static Object getCellValue(Cell cell, Class<?> fieldType, String defaultValue) throws ParseException { if (cell == null) { return null; } Object cellValue = null; boolean isDouble = false; boolean isString = false; switch (cell.getCellTypeEnum()) { case NUMERIC: cellValue = cell.getNumericCellValue(); isDouble = true; // if ((double) cellValue == 0) { // return null; // } break; case STRING: cellValue = cell.getStringCellValue(); isString = true; break; case BLANK: if (defaultValue != null && !"".equals(defaultValue)) { cellValue = defaultValue; isString = true; } break; default: { cellValue = cell.getStringCellValue(); break; } } if (cellValue == null || cellValue == "") { return null; } String typeName = fieldType.getName(); if (typeName.equals("int")) { if (!isString) { DecimalFormat df = new DecimalFormat("0"); return new Double(df.format(cellValue)).intValue(); } else { return new Double(cellValue.toString()).intValue(); } } if (typeName.equals("double")) { return new Double(cellValue + ""); } if (typeName.equals("long")) { if (!isString) { DecimalFormat df = new DecimalFormat("0"); return new Double(df.format(cellValue)).longValue(); } else { return new Double(cellValue.toString()).longValue(); } } if (typeName.equals("java.util.Date")) { return DateUtil.toDate(cellValue + "", "yyyy-MM-dd HH:mm:ss"); } if (isDouble) { DecimalFormat df = new DecimalFormat("0"); return df.format(cellValue); } return (cellValue + "").trim(); } /** * 过滤全空的行 * * @param list * @return * @throws Exception */ public List<List<String>> filterEmptyRow(List<List<String>> list) throws Exception { for (int i = 0; i < list.size(); i++) { boolean isAllEmpty = true; for (String str : list.get(i)) { if (null != str && !str.equals("")) { isAllEmpty = false; break; } } if (isAllEmpty) { list.remove(i); i--; } } return list; } /** * * 将数据写入到 Excel 默认第一页中，从第1行开始写入 * * @param rowData * 数据 * @return * @throws IOException * */ public boolean write(List<List<String>> rowData) throws IOException { return write(0, rowData, 0); } /** * * 将数据写入到 Excel 新创建的 Sheet 页 * * @param rowData * 数据 * @param sheetName * 长度为1-31，不能包含后面任一字符: ：\ / ? * [ ] * @return * @throws IOException */ public boolean write(List<List<String>> rowData, String sheetName, boolean isNewSheet) throws IOException { Sheet sheet = null; if (isNewSheet) { sheet = workbook.createSheet(sheetName); } else { sheet = workbook.createSheet(); } int sheetIx = workbook.getSheetIndex(sheet); return write(sheetIx, rowData, 0); } /** * * 将数据追加到sheet页最后 * * @param rowData * 数据 * @param sheetIx * 指定 Sheet 页，从 0 开始 * @param isAppend * 是否追加,true 追加，false 重置sheet再添加 * @return * @throws IOException */ public boolean write(int sheetIx, List<List<String>> rowData, boolean isAppend) throws IOException { if (isAppend) { return write(sheetIx, rowData, getRowCount(sheetIx)); } else {// 清空再添加 clearSheet(sheetIx); return write(sheetIx, rowData, 0); } } /** * * 将数据写入到 Excel 指定 Sheet 页指定开始行中,指定行后面数据向后移动 * * @param rowData * 数据 * @param sheetIx * 指定 Sheet 页，从 0 开始 * @param startRow * 指定开始行，从 0 开始 * @return * @throws IOException */ public boolean write(int sheetIx, List<List<String>> rowData, int startRow) throws IOException { Sheet sheet = workbook.getSheetAt(sheetIx); int dataSize = rowData.size(); if (getRowCount(sheetIx) > 0) {// 如果小于等于0，则一行都不存在 sheet.shiftRows(startRow, getRowCount(sheetIx), dataSize); } for (int i = 0; i < dataSize; i++) { Row row = sheet.createRow(i + startRow); for (int j = 0; j < rowData.get(i).size(); j++) { Cell cell = row.createCell(j); cell.setCellValue(rowData.get(i).get(j) + ""); } } return true; } /** * * 设置cell 样式 * * @param sheetIx * 指定 Sheet 页，从 0 开始 * @param colIndex * 指定列，从 0 开始 * @return * @throws IOException */ public boolean setStyle(int sheetIx, int rowIndex, int colIndex, CellStyle style) throws IOException { Sheet sheet = workbook.getSheetAt(sheetIx); // sheet.autoSizeColumn(colIndex, true);// 设置列宽度自适应 sheet.setColumnWidth(colIndex, 4000); Cell cell = sheet.getRow(rowIndex).getCell(colIndex); cell.setCellStyle(style); return true; } /** * * 设置样式 * * @param type * 1：标题 2：第一行 * @return */ public CellStyle makeStyle(int type) { CellStyle style = workbook.createCellStyle(); DataFormat format = workbook.createDataFormat(); style.setDataFormat(format.getFormat("@"));// // 内容样式 设置单元格内容格式是文本 style.setAlignment(CellStyle.ALIGN_CENTER);// 内容居中 // style.setBorderTop(CellStyle.BORDER_THIN);// 边框样式 // style.setBorderRight(CellStyle.BORDER_THIN); // style.setBorderBottom(CellStyle.BORDER_THIN); // style.setBorderLeft(CellStyle.BORDER_THIN); Font font = workbook.createFont();// 文字样式 if (type == 1) { // style.setFillForegroundColor(HSSFColor.LIGHT_BLUE.index);//颜色样式 // 前景颜色 // style.setFillBackgroundColor(HSSFColor.LIGHT_BLUE.index);//背景色 // style.setFillPattern(CellStyle.ALIGN_FILL);// 填充方式 font.setBold(true); font.setFontHeight((short) 500); } if (type == 2) { font.setBold(true); font.setFontHeight((short) 300); } style.setFont(font); return style; } /** * * 合并单元格 * * @param sheetIx * 指定 Sheet 页，从 0 开始 * @param firstRow * 开始行 * @param lastRow * 结束行 * @param firstCol * 开始列 * @param lastCol * 结束列 */ public void region(int sheetIx, int firstRow, int lastRow, int firstCol, int lastCol) { Sheet sheet = workbook.getSheetAt(sheetIx); sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, firstCol, lastCol)); } /** * * 指定行是否为空 * * @param sheetIx * 指定 Sheet 页，从 0 开始 * @param rowIndex * 指定开始行，从 0 开始 * @return true 不为空，false 不行为空 * @throws IOException */ public boolean isRowNull(int sheetIx, int rowIndex) throws IOException { Sheet sheet = workbook.getSheetAt(sheetIx); return sheet.getRow(rowIndex) == null; } /** * * 创建行，若行存在，则清空 * * @param sheetIx * 指定 sheet 页，从 0 开始 * @param rownum * 指定创建行，从 0 开始 * @return * @throws IOException */ public boolean createRow(int sheetIx, int rowIndex) throws IOException { Sheet sheet = workbook.getSheetAt(sheetIx); sheet.createRow(rowIndex); return true; } /** * * 指定单元格是否为空 * * @param sheetIx * 指定 Sheet 页，从 0 开始 * @param rowIndex * 指定开始行，从 0 开始 * @param colIndex * 指定开始列，从 0 开始 * @return true 行不为空，false 行为空 * @throws IOException */ public boolean isCellNull(int sheetIx, int rowIndex, int colIndex) throws IOException { Sheet sheet = workbook.getSheetAt(sheetIx); if (!isRowNull(sheetIx, rowIndex)) { return false; } Row row = sheet.getRow(rowIndex); return row.getCell(colIndex) == null; } /** * * 创建单元格 * * @param sheetIx * 指定 sheet 页，从 0 开始 * @param rowIndex * 指定行，从 0 开始 * @param colIndex * 指定创建列，从 0 开始 * @return true 列为空，false 行不为空 * @throws IOException */ public boolean createCell(int sheetIx, int rowIndex, int colIndex) throws IOException { Sheet sheet = workbook.getSheetAt(sheetIx); Row row = sheet.getRow(rowIndex); row.createCell(colIndex); return true; } /** * 返回sheet 中的行数 * * * @param sheetIx * 指定 Sheet 页，从 0 开始 * @return */ public int getRowCount(int sheetIx) { Sheet sheet = workbook.getSheetAt(sheetIx); if (sheet.getPhysicalNumberOfRows() == 0) { return 0; } return sheet.getLastRowNum() + 1; } /** * * 返回所在行的列数 * * @param sheetIx * 指定 Sheet 页，从 0 开始 * @param rowIndex * 指定行，从0开始 * @return 返回-1 表示所在行为空 */ public int getColumnCount(int sheetIx, int rowIndex) { Sheet sheet = workbook.getSheetAt(sheetIx); Row row = sheet.getRow(rowIndex); return row == null ? -1 : row.getLastCellNum(); } /** * * 设置row 和 column 位置的单元格值 * * @param sheetIx * 指定 Sheet 页，从 0 开始 * @param rowIndex * 指定行，从0开始 * @param colIndex * 指定列，从0开始 * @param value * 值 * @return * @throws IOException */ public boolean setValueAt(int sheetIx, int rowIndex, int colIndex, String value) throws IOException { Sheet sheet = workbook.getSheetAt(sheetIx); sheet.getRow(rowIndex).getCell(colIndex).setCellValue(value); return true; } /** * 返回第一页row和column位置单元格值 * * @param rowIndex * @param colIndex * @return */ public String getValueAt(int rowIndex, int colIndex) { Sheet sheet = workbook.getSheetAt(0); return getCellValueToString(sheet.getRow(rowIndex).getCell(colIndex)); } /** * * 返回 row 和 column 位置的单元格值 * * @param sheetIx * 指定 Sheet 页，从 0 开始 * @param rowIndex * 指定行，从0开始 * @param colIndex * 指定列，从0开始 * @return * */ public String getValueAt(int sheetIx, int rowIndex, int colIndex) { Sheet sheet = workbook.getSheetAt(sheetIx); return getCellValueToString(sheet.getRow(rowIndex).getCell(colIndex)); } /** * * 重置指定行的值 * * @param rowData * 数据 * @param sheetIx * 指定 Sheet 页，从 0 开始 * @param rowIndex * 指定行，从0开始 * @return * @throws IOException */ public boolean setRowValue(int sheetIx, List<String> rowData, int rowIndex) throws IOException { Sheet sheet = workbook.getSheetAt(sheetIx); Row row = sheet.getRow(rowIndex); for (int i = 0; i < rowData.size(); i++) { row.getCell(i).setCellValue(rowData.get(i)); } return true; } /** * * 返回指定行的值的集合 * * @param sheetIx * 指定 Sheet 页，从 0 开始 * @param rowIndex * 指定行，从0开始 * @return */ public List<String> getRowValue(int sheetIx, int rowIndex) { Sheet sheet = workbook.getSheetAt(sheetIx); Row row = sheet.getRow(rowIndex); List<String> list = new ArrayList<String>(); if (row == null) { list.add(null); } else { for (int i = 0; i < row.getLastCellNum(); i++) { list.add(getCellValueToString(row.getCell(i))); } } return list; } /** * * 返回列的值的集合 * * @param sheetIx * 指定 Sheet 页，从 0 开始 * @param rowIndex * 指定行，从0开始 * @param colIndex * 指定列，从0开始 * @return */ public List<String> getColumnValue(int sheetIx, int rowIndex, int colIndex) { Sheet sheet = workbook.getSheetAt(sheetIx); List<String> list = new ArrayList<String>(); for (int i = rowIndex; i < getRowCount(sheetIx); i++) { Row row = sheet.getRow(i); if (row == null) { list.add(null); continue; } list.add(getCellValueToString(sheet.getRow(i).getCell(colIndex))); } return list; } /** * * 获取excel 中sheet 总页数 * * @return */ public int getSheetCount() { return workbook.getNumberOfSheets(); } public void createSheet() { workbook.createSheet(); } /** * * 设置sheet名称，长度为1-31，不能包含后面任一字符: ：\ / ? * [ ] * * @param sheetIx * 指定 Sheet 页，从 0 开始，// * @param name * @return * @throws IOException */ public boolean setSheetName(int sheetIx, String name) throws IOException { workbook.setSheetName(sheetIx, name); return true; } /** * * 获取 sheet名称 * * @param sheetIx * 指定 Sheet 页，从 0 开始 * @return * @throws IOException */ public String getSheetName(int sheetIx) throws IOException { Sheet sheet = workbook.getSheetAt(sheetIx); return sheet.getSheetName(); } /** * 获取sheet的索引，从0开始 * * @param name * sheet 名称 * @return -1表示该未找到名称对应的sheet */ public int getSheetIndex(String name) { return workbook.getSheetIndex(name); } /** * * 删除指定sheet * * @param sheetIx * 指定 Sheet 页，从 0 开始 * @return * @throws IOException */ public boolean removeSheetAt(int sheetIx) throws IOException { workbook.removeSheetAt(sheetIx); return true; } /** * * 删除指定sheet中行，改变该行之后行的索引 * * @param sheetIx * 指定 Sheet 页，从 0 开始 * @param rowIndex * 指定行，从0开始 * @return * @throws IOException */ public boolean removeRow(int sheetIx, int rowIndex) throws IOException { Sheet sheet = workbook.getSheetAt(sheetIx); sheet.shiftRows(rowIndex + 1, getRowCount(sheetIx), -1); Row row = sheet.getRow(getRowCount(sheetIx) - 1); sheet.removeRow(row); return true; } /** * * 设置sheet 页的索引 * * @param sheetname * Sheet 名称 * @param pos * Sheet 索引，从0开始 */ public void setSheetOrder(String sheetname, int sheetIx) { workbook.setSheetOrder(sheetname, sheetIx); } /** * * 清空指定sheet页（先删除后添加并指定sheetIx） * * @param sheetIx * 指定 Sheet 页，从 0 开始 * @return * @throws IOException */ public boolean clearSheet(int sheetIx) throws IOException { String sheetname = getSheetName(sheetIx); removeSheetAt(sheetIx); workbook.createSheet(sheetname); setSheetOrder(sheetname, sheetIx); return true; } public Workbook getWorkbook() { return workbook; } /** * * 关闭流 * * @throws IOException */ public void close() throws IOException { if (os != null) { os.close(); } workbook.close(); } /** * * 转换单元格的类型为String 默认的 * 默认的数据类型：CELL_TYPE_BLANK(3), CELL_TYPE_BOOLEAN(4), * CELL_TYPE_ERROR(5),CELL_TYPE_FORMULA(2), CELL_TYPE_NUMERIC(0), * CELL_TYPE_STRING(1) * * @param cell * @return * */ private String getCellValueToString(Cell cell) { String strCell = ""; if (cell == null) { return null; } switch (cell.getCellType()) { case Cell.CELL_TYPE_BOOLEAN: strCell = String.valueOf(cell.getBooleanCellValue()); break; case Cell.CELL_TYPE_NUMERIC: if (HSSFDateUtil.isCellDateFormatted(cell)) { Date date = cell.getDateCellValue(); if (pattern != null) { SimpleDateFormat sdf = new SimpleDateFormat(pattern); strCell = sdf.format(date); } else { strCell = date.toString(); } break; } // 不是日期格式，则防止当数字过长时以科学计数法显示 cell.setCellType(HSSFCell.CELL_TYPE_STRING); strCell = cell.toString(); break; case Cell.CELL_TYPE_STRING: strCell = cell.getStringCellValue(); break; default: break; } return strCell; } }
1.选择gradleAndroid Studio安装后可能会出现gradle sync failed错误首先打开设置界面（Ctrl+Alt+S）选择 Build,Execution,Deployment选择Gradle重新设置你的Service directory path目录安装Android Studio时默认会安装Gradle在 "C:/Users/用户名/.gradle" 此路径下，你只需要把 Service directory path 的路径修改为更详细点就可以了（eg:C:/Users/EraJi/.gradle/wrapper/dists/gradle-4.4-all）。直接指定路径中的 gradle 文件夹。你也可以自己下载对应版本的Gradle，然后把这个路径指向你下载好的Gradle目录。下载地址：这里就不细讲了，其实我弄不太懂重新编译下工程就可以了2.检查Jdk选择自带的jdk11或者默认的jdk83.sdk设置在目录 D:\\{YOUR_SDK_PATH}\Android\Sdk\tools\bin 执行命令sdkmanager --licenses一路选择 yes