java项目开发 -- 学习记录(一)
项目资源访问路径问题
在本地开发环境下,我们访问资源一般都是使用了 File 类,并且传入相对路径作为构造参数,这个方法虽然方便,但是我在使用过程中,还是遇到了一个麻烦的问题。我需要遍历一个工程文件夹里的所有类,将所有带有特定注解的类找出并反射创建,我使用了 File 类去获取文件夹并遍历得到类文件对象,通过该文件路径去拼接一个全限定名创建出来。但是有一个麻烦问题就是,当我打包项目成jar文件后,放入linux系统执行后,发现使用 File 类根本获取不到这个 jar 包里的路径文件,后面弄了很久,找到了解决办法,1 :PathMatchingResourcePatternResolver 类去获取路径上能匹配到的资源;2 :使用 spring 提供的内置ClassPathScanningCandidateComponentProvider 类去扫描获取类,这样就能直接避免路径的问题。
//获取test文件夹下面的所有文件,文件夹 List<Resource> resources = Arrays.asList(new PathMatchingResourcePatternResolver() .getResources(ResourceUtils.CLASSPATH_URL_PREFIX + "/org/apache/test/*")); /** 下面就是通过遍历这个列表,然后获取资源的名字,将名字拼接成全限定名,使用 Class.forName("xxx") 函数创建 **/将项目打成 jar 包后,放入服务器里,此时我们的资源访问路径发生了变化,所以代码里的访问路径就要修改,这样的话服务器运行和本地运行就需要写代码判断,并且需要写死路径,我就在想解决这个问题,想不管在本地还是服务器,只需要一套代码就可以实现资源访问,而且不用判断是服务器还是本地运行,以下是我想出的方法,仅供参考。
//直接获取该类在系统里面的绝对路径,然后我们只需要规定工程文件夹,就能以最小的限制获得比较方便的操作和简单的代码 //resourcePath是自己将资源放置的路径,这个随自己定,可以写在配置文件里,也可以变量存着动态改变 XXXX.class.getProtectionDomain().getCodeSource().getLocation().getPath().split("MyProject/")[0] + resourcePath;springboot 项目读取 application.properties/yml 文件
- 使用 @Value(“server.port”) 注解的方式获取,在对象属性上使用;
- 使用 @ConfigurationProperties 注解,在组件类上使用,使用方式和实体类一样,适合配置多层级结构;
- 使用Environment抽象或@Configuration类中的@Bean方法配置属性源;
- 通过文件流的形式直接读取内容。
public class Example { @Value("${app.name}") private String appName; } @Component @ConfigurationProperties(prefix = "myapp") public class AppConfigProperties { }如果想要设置配置文件的内容,只能是通过文件流的形式去设置,好像没找到提供的工具类,但是我们可以通过配置文件读取数据来改变内容,也就是占位符写法,但是对于保存配置内容,服务器挂了也能看就没办法了。
扫描某路径下的所有类并找出有特定注解的类或者继承特定类的子类
承接上面的问题,不再详细说明,自己写工具还是有很多问题,没必要重复造轮子,但确实学到了东西。
List<Class<?>> annotatedClasses = new ArrayList<>(); // 创建一个扫描器,不使用默认的过滤器 ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); // 添加自定义的过滤器,例如只扫描特定注解的类 TypeFilter annotationFilter = new AnnotationTypeFilter(TaskFlag.class); scanner.addIncludeFilter(annotationFilter); //或者添加根据类的继承关系进行过滤 TypeFilter assignableFilter = new AssignableTypeFilter(Object.class); scanner.addIncludeFilter(assignableFilter); for (BeanDefinition beanDefinition : scanner.findCandidateComponents("org.apache.test.services")) { annotatedClasses.add(Class.forName(beanDefinition.getBeanClassName())); }热加载字节码文件,实现不用重启服务,直接替换文件就起效
我们知道如果一个字节码文件被加载链接后,我们就算替换了该字节码文件,程序依然创建原来的文件对象,因为该文件被加载器加载了,并且在 jvm 里一直存着,所以只能通过重启服务器才能有效。由于我们创建类都是通过默认的类加载器,这个加载器如果加载了类,那么下次创建就不会重新加载,所以我们需要使用自定义加载器,去手动实现每次加载都是读取最新的字节码文件,这样就可以实现热加载效果了。
public class CustomClassLoader extends ClassLoader { byte[] classData ; //通过字节码数据去加载创建类,必须重写该方法,由于双亲委派机制,如果遇到是该类是子类,由于父类被默认加载器加载了, //所以就不会重新加载了;加载的类数据针对于每个加载器是独立的,所以默认加载器加载的类还是旧的类数据 @Override public Class<?> findClass(String name) { if (classData == null) {return null;} return defineClass(name, classData, 0, classData.length); } //使用 File 对象去加载字节码文件,然后将字节码文件里的数据保存 public void loadClassDataFromFile(File file) throws IOException { //这种写法可以在 try 执行完毕后自动关闭释放输入流对象,是Java新特性 try (FileInputStream fis = new FileInputStream(file)) { byte[] classData = new byte[(int) file.length()]; fis.read(classData); this.classData = classData; } } }//通过自定义加载器去加载某个类 CustomClassLoader customClassLoader = new CustomClassLoader(); customClassLoader.loadClassDataFromFile(directory); Class clazz = customClassLoader.findClass(className); //判断该类是否有某个注解 if (clazz.isAnnotationPresent((Class<? extends Annotation>) annotationClass)) { .... }maven 打 jar 包
在 idea 里面很方便的就可以打包,在右边的 maven 工具栏里,直接选择 lifecycle 下的 clean 和 package 执行就可以了。但是我遇到了打包出现找不到符号的错误,这个错误原因很多,大多数都是依赖没有找到,但是我这个是 lombok 的原因,因为 idea 内置了 lombok 注解器,我们正常运行时是没问题的,但是我们使用 maven 打包时,就会引入 pom 文件里的 lombok 版本,所以在依赖追踪检查机制就会通不过。解决方案如下,在 pom 文件的 build 节点下加上这段代码即可,版本和引入的版本要一致。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.36</version> </path> </annotationProcessorPaths> </configuration> </plugin>如果我们想不打包某些配置文件或者其他资源文件到 jar 包里,因为打包后资源访问路径就变化了,通过路径访问就会有问题,只能通过流的方式访问,而且写回数据到 jar 包里就会非常麻烦,所以可以将配置文件放在 jar 包外面,这样也能支持动态配置了。
//在pom文件里build节点添加以下代码就可以排除不想要打包的文件 <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <excludes> <exclude>**/*.properties</exclude> </excludes> </configuration> </plugin>使用 maven 将别人项目打包的 jar 文件引入本地仓库使用
有些时候项目是多个厂商一起开发的,那么就会遇到这种别人将他们的工程打包成 jar 文件,我们用 maven 导入包后就可以使用他们的类和函数。如果手动直接将包放入 maven 本地仓库会报错,因为缺少了很多信息文件。首先我们将 jar 包放入我们的工程文件最外层目录下,然后使用 mvn install 命令将包引入到本地仓库里就可以了。
//XXX.jar是包的路径,如果是放在外面的就设置绝对路径,后面就是包的厂商,版本信息,Dpackaging 是包的格式 mvn install:install-file -Dfile=XXX.jar -DgroupId=test -DartifactId=test-one -Dversion=0.0 -Dpackaging=jar项目 jar 包方式放置在服务器,远程调试代码
将项目 jar 包放入服务器,执行以下代码
//如果找不到 java 命令,可以去jdk安装目录下的bin里面找;1081是远程连接的端口,不是服务器运行端口;XXXX.jar 是项目包; //后面参数是指定项目启动配置文件目录,也就是 application 那个文件,如果我们没有把配置文件打包则可以指定 java -jar -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=1081 XXXX.jar --spring.config.location=/home/xxx然后进入 IDEA ,打开运行/调试配置窗口,新增加远程jvm调试(remote jvm debug),填写好端口(就是上面的1081)和主机ip(服务器ip),然后选好对应的项目模块,确认调试就可以了。
数字转为字符串时,在数字前补0
通过 String.format 格式化的方式,能够在数字没有足够位数时,在前方自动补0。
String.format("%02d", 5); //结果为 "05"对浮点型保留两位小数
方法多种,DecimalFormat 类 format 方法,String.format 方法,Math.round 方法,BigDecimal 类 setScale 方法。
DecimalFormatdf=newDecimalFormat("#.00");//保留两位小数,格式固定,不会四舍五入Stringresult=df.format(3.1415926);Stringresult=String.format("%.2f",3.1415926);//也是保留两位小数,格式固定,不会四舍五入//记住除法是100.0 ,不能写100,结果会成为整数型doubleresult=Math.round(3.1415926*100)/100.0;//round方法是四舍五入,但是结果是整数,所以乘上100左移2位后还原即可BigDecimalresult=newBigDecimal("3.1415926").setScale(2,BigDecimal.ROUND_HALF_UP);//第一个参数是保留2位,第二个是需要四舍五入求某数字的小数位数
首先最容易考虑到的是将数字转为字符串遍历每个字符,但是JAVA里 BigDecimal 类的 scale 方法可以直接返回该结果,非常方便高效。
// 转为字符串是为了避免转换过程中丢失精度BigDecimalbd=newBigDecimal(String.valueOf(10086.4399));intlength=bd.scale();springboot读取解析excel文件
可以使用 easyexcel 工具的实现,使用非常简单方便,首先在 maven 的 pom 文件里加入依赖。
<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.3.2</version> </dependency>根据excel文件创建一个对应的实体类对象,例如:excel文件存的用户数据,有三列,分别是name,gender,age
//必须要加 lombok 的 @Data 注解,否则需要自己写get,set方法 @Data public class User { //这个写excel文件里实际的列名,列名如果是中文的名字,那就是 "名字" @ExcelProperty("name") private String a; @ExcelProperty("gender") private String b; @ExcelProperty("age") private Integer c; }然后创建一个类去实现 ReadListener< User > 接口,重写 invoke ,doAfterAllAnalysed 方法,invoke 方法会传入一个 User 对象,这个对象保存了解析后的一行excel数据,所以该方法是逐行解析;doAfterAllAnalysed 是解析完excel所有数据后的回调方法。
public class UserExcelListener implements ReadListener<User> { @Override public void invoke(User data, AnalysisContext context) { // 逐行处理数据 System.out.println(data.getA()+"处理完!"); } @Override public void doAfterAllAnalysed(AnalysisContext context) { System.out.println("完成导入!"); } }使用jdbc连接sqlserver数据库
在某些场景下,对方使用了sqlserver数据库,我们需要连接该数据库,但是在所有配置都正确的情况下,连接的时候出现了服务器使用了TLS10,不支持TLS13/TLS12的协议错误,这个一般是都是数据库版本低,而我们使用的jdbc包版本太高导致,有几种解决方案:
- 修改jdk安装目录下的 java.security 文件,找到 jdk.tls.disabledAlgorithms 配置,删除 TLSv1 和 TLSv1.1。
- 在jdbc连接url路径里添加 sslProtocol 参数,但是根据我自己的试验,该做法似乎没有作用。
- 降低jdbc驱动的版本,在mvn里引入低版本。
<dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>6.4.0.jre8</version> </dependency>`idea 启动报错:java.lang.IndexOutOfBoundsException: Range [-1, -1 + 1025) out of bounds for length 1024
遇到标题的错误后,应该是项目在构建的时候电脑突然出现问题导致的,可能是蓝屏可能是宕机等等,可以通过 build 里的 rebuild project 重新构建项目才能启动完成。这个问题不会导致打包错误。
操作Excel文件
一般都是通过 apache 的 POI 包去实现操作,有些时候如果发现引入依赖后导入不了类,可以通过添加 easyexcel 依赖,里面也引入了 POI 包,所以可以变相的使用,下面例子是打开一个 excel 文件模板,然后另存。
try { // 读取Excel模板, templateFilePath 是文件的路径 Workbook workbook = WorkbookFactory.create(new FileInputStream(templateFilePath)); // 获取第一个 sheet 页工作表 Sheet sheet = workbook.getSheetAt(0); // 向模板中写入数据 writeDataToSheet(sheet); // 将修改后的内容写入新文件 try (FileOutputStream fileOut = new FileOutputStream(outputFilePath)) { workbook.write(fileOut); } // 关闭资源 workbook.close(); } catch (IOException e) { e.printStackTrace(); } private static void writeDataToSheet(Sheet sheet) { setCellValue(sheet, 1, 0, "张三"); // 行索引从0开始,1代表第2行;列索引0代表A列 setCellValue(sheet, 1, 1, 30); setCellValue(sheet, 1, 2, "2025-01-01"); mergeCells(sheet, 5, 6, 0, 2, "哈利路亚"); //合并单元格方法 } private static void setCellValue(Sheet sheet, int rowIndex, int cellIndex, Object value) { // 获取行,如果行不存在则创建 Row row = sheet.getRow(rowIndex); if (row == null) { row = sheet.createRow(rowIndex); } // 获取单元格,如果单元格不存在则创建 Cell cell = row.getCell(cellIndex); if (cell == null) { cell = row.createCell(cellIndex); } // 根据值的类型设置单元格内容 if (value instanceof String) { cell.setCellValue((String) value); } else if (value instanceof Number) { cell.setCellValue(((Number) value).doubleValue()); } else if (value instanceof Boolean) { cell.setCellValue((Boolean) value); } else { cell.setCellValue(value.toString()); } } /** * 合并单元格并设置内容 * @param sheet 工作表 * @param firstRow 起始行索引(0-based) * @param lastRow 结束行索引(0-based) * @param firstCol 起始列索引(0-based) * @param lastCol 结束列索引(0-based) * @param content 要写入的内容(写入到合并区域的第一个单元格) */ private static void mergeCells(Sheet sheet, int firstRow, int lastRow, int firstCol, int lastCol, String content) { // 创建合并区域 CellRangeAddress region = new CellRangeAddress( firstRow, // 起始行 lastRow, // 结束行 firstCol, // 起始列 lastCol // 结束列 ); // 添加合并区域到工作表 sheet.addMergedRegion(region); // 在合并区域的第一个单元格写入内容 Row row = sheet.getRow(firstRow); if (row == null) { row = sheet.createRow(firstRow); } Cell cell = row.getCell(firstCol); if (cell == null) { cell = row.createCell(firstCol); } cell.setCellValue(content); // 可选:设置合并单元格的样式(例如居中对齐) CellStyle style = sheet.getWorkbook().createCellStyle(); style.setAlignment(HorizontalAlignment.CENTER); // 水平居中 style.setVerticalAlignment(VerticalAlignment.CENTER); // 垂直居中 style.setBorderBottom(BorderStyle.THIN); style.setBorderRight(BorderStyle.THIN); style.setBorderLeft(BorderStyle.THIN); style.setBorderTop(BorderStyle.THIN); // 设置字体加粗 Font font = sheet.getWorkbook().createFont(); font.setBold(true); style.setFont(font); cell.setCellStyle(style); // 遍历合并区域内的所有单元格,设置边框,合并单元格设置边框,必须全部单元格都设置 for (int r = firstRow; r <= lastRow; r++) { Row rowTmp = sheet.getRow(r); if (rowTmp == null) { rowTmp = sheet.createRow(r); } for (int c = firstCol; c <= lastCol; c++) { Cell cellTmp = rowTmp.getCell(c); if (cellTmp == null) { cellTmp = rowTmp.createCell(c); } cellTmp.setCellStyle(style); } } }同步不同的数据库数据时,保证数据一致性方法
当遇到主数据库需要同步副数据库数据时,我们需要保障两个数据库的数据一致性。
- 当然我们很容易可以想到就是在副数据库的表里面添加一个标识字段,每次更改时修改这个字段,然后同步的时候根据这个字段来同步,同步完再次更改这个字段。
- 对于数据量特别大,而且副数据库不允许任何修改时,我们可以通过时间将数据划分出来,然后根据这个划分区间统计出一个数值出来,对比主数据库和副数据库的数值是否一致,然后再往下划分,直到找到一个合理的区间再全部同步一遍就可以了;示例:对于每天的收款金额,我们首先将两边的数据库里每一年的收款金额统计一个和对比,找到数据不一致的那年,然后再统计这年的每个月的金额对比,找到不同的那个月,然后直接将那个月的数据全部同步一遍就可以了。
使用Spring JPA时,saveALLAndFlush 方法无法持久化数据
在我手动删除pgsql数据库里的某些数据后,创建表相对应的实体类,然后使用 JPA 持久化实体数据,但是查看数据库后发现并没有添加这些数据,之后我反复检查数据是否主键冲突,连接库是否错误,表是否产生死锁,是否报错异常,并没有发现问题,然后我又修改了代码结构,让批量保存换成循环单次保存,或者添加事务手动提交,依然没有解决,既没有报错也没有保存数据,这个不知道是什么问题,我就猜测可能和缓存数据有关系,导致主键冲突,没法保存,所以我将该方法改为以下方法顺序执行,deleteAll -> flush -> saveAll -> flush ,先删除这些数据,然后再保存,试了后果然成功保存数据了,只是原因依然没找到,只是将问题解决了,仅此记录一下,等未来找到原因后更新。
使用Spring JPA时,需要切换数据库的不同 schema
我们使用 JPA 的时候,一般都通过配置 application.yml 文件里数据库连接参数来设置当前读取的数据库模式(currentSchema),如果没有设置则默认是public模式下,但有些时候我们就需要读其他模式下的表数据,我们可以在 currentSchema 后面多添加几个模式,但是在写 sql 的时候就需要指定模式(例如 public.table1),避免同名表的情况;但是有种方法我们可以简单实现切换 schema ,就是我们在表实体类上的 @TABLE 注解里设置 schema 参数,这样就手动指定了该表在哪个模式下,而不影响其他代码;但是这只能用于切换schema,不能用在切换数据库。
//访问inquiry,public 两个模式的表jdbc:postgresql://x.x.x.x:5432/xxx?stringtype=unspecified¤tSchema=inquiry,public参考链接
idea 远程调试 jar 包 :https://blog.csdn.net/zhengge417/article/details/132689277
保留两位小数 :https://shuyeidc.com/wp/71945.html
idea 启动报错 :https://blog.csdn.net/ip36900044/article/details/137105622
scene builder 官方 :https://gluonhq.com/products/scene-builder/#download
java 使用 SSE 技术:https://blog.csdn.net/2302_79840586/article/details/142589072
动态上传代码热部署:https://mp.weixin.qq.com/s/wu6MWzcL8qvzTbXSp9Lt7A
