Java开发必备技巧

1 调试技巧
一般的IDE工具都有以下调试功能,本文以 IntelliJ IDEA 为例

1.1 计算表达式
以下是实际开发中很容易遇到的一种场景:调试下面的代码时,validate() 返回值为true, 正常的逻辑是进入function1,而我们想要查看的结果是function2的返回值

boolean validate = validate();
String s = null;
if(validate) {
    s = function1();
} else {
    s = function2();
}
logger.info(s);

部分不熟悉计算表达式的同事在遇到以下代码时, 可能会直接修改代码如下:

boolean validate = false;
String s = null;
if(validate) {
    s = function1();
} else {
    s = function2();
}
logger.info(s);

这种调试方式虽能满足需求, 但容易忘记还原代码,把调试代码一并提交,从而产生问题。如果这段代码是在源码中,无法直接修改,部分同事还会追本溯源,最终通过修改配置文件等方式, 使 validate() 返回false, 浪费了较多时间。

利用IDE工具的 计算表达式功能, 可直接查看function2的执行结果, 而无需修改代码:

、

计算表达式支持多行模式,可用于合成参数:

在这里插入图片描述

利用计算表达式,可直接测试Spring容器中任意单例bean的方法,无需进入相应的业务模块进行调试:
在这里插入图片描述

注:上面的示例中需保证appContext已实例化,可通过@DependsOn 指定优先实例化appContext

1.2 条件断点
开发时使用断点进行调试是开发中最基础的技能,通过对断点设置条件,最快的定位到目标代码,同样也是开发人员的必备技能。

条件断点最经典的应用是对循环中的代码进行调试,如:

在这里插入图片描述

List集合可以直接通过下标设置条件:

在这里插入图片描述

foreach循环:
在这里插入图片描述

对于无法取下标的集合,可转为数组后再取下标,如:

在这里插入图片描述

1.3 异常断点
异常断点可以认为对条件断点功能的一种扩展,虽然应用不如条件断点那么广泛,但在一些场景下可以更快速的定位到问题代码,常见的使用场景如下:

在这里插入图片描述

注:上面的场景也可使用条件断点来进行定位, 如下:
在这里插入图片描述

但在更复杂的场景中,比如同一行会有多个变量可能报空指针,或是报一些我们平时不是很常见的异常,

使用异常断点的优势就会比较明显。

1.4 远程调试配置
在生产环境遇到问题时,远程调试无疑是定位问题的最佳手段。 IntelliJ IDEA 配置远程调试的步骤如下:

1.点击Edit Configurations,添加remote配置
在这里插入图片描述

在这里插入图片描述

2.修改ip和端口为目标服务的ip和预计监听端口, 修改好之后把 jvm启动参数 添加到目标服务的启动脚本中,如:

在这里插入图片描述

在这里插入图片描述

3.重启服务,待服务监听配置的端口后,启动remote,即可开始远程调试:
在这里插入图片描述

Debug生效:

在这里插入图片描述

需要慎重使用的是 Build Project功能,一些文件改动后能够直接替换到虚拟机内存,但不会改动jar包中的class,重启项目后就会失效
在这里插入图片描述

在这里插入图片描述

1.5 调试"回退"【袁】
有时候在调试复杂的方法嵌套方法的逻辑代码时候,好不容易进到一个断点,一不小心手一抖或者按错了快捷键,断点就过去了,想回过头看看刚才的变量值,只能再跑一遍。

(注意:可能发生的副作用,已经对全局状态进行的更改(如静态变量,对域值的更改等)不会被撤销,只会重置局部变量。网络流量,文件操作,控制台输出等都不能倒回。)
在这里插入图片描述

1.6 动态修改变量【袁】
调试时,动态修改变量的值。在变量上右击,然后选择Set Value。

在这里插入图片描述

1.7 并发测试rest接口【袁】

Apache ab: Apache提供的一款小巧的压力测试工具,用来测试服务能力很方便。

使用教程:https://www.jianshu.com/p/e3793ae91a62

2 问题排查
2.1 jar包冲突处理
jar包冲突是maven项目最常见的问题之一,jar包冲突可能引起的问题有很多,最经典的是下面的异常:

java.lang.NoSuchMethodError
当出现NoSuchMethodError异常时,首先应该怀疑到jar包冲突上。确定是否有jar包冲突比较简单,直接查看目标类是否存在多个版本的jar包中即可。

当确定是jar包冲突引起的问题后,还有以下问题需要解决:

    1. 如何确定在报错环境下使用的jar包版本,从而进行排除?

    2. 确定需要排除的jar包后,如何定位jar包的引入路径?

对于第1个问题,可在调试环境下调用以下代码,查看出现错误的目标类从那个jar包中加载:

Target.class.getProtectionDomain().getCodeSource().getLocation().getPath()

如:

在这里插入图片描述

当定位到需排除的jar包后,接下来就是分析jar包的引入路径。

常规的做法是使用mvn指令打印依赖树,再通过文件编辑工具进行查找:

mvn dependency:tree>D:/tmp/tree.txt

在这里插入图片描述

本文推荐的做法是使用 Maven Helper插件,在IntelliJ IDEA中集成该插件后,可以直接在pom中搜索每个jar包的依赖路径,使用起来十分方便:

在这里插入图片描述

2.2 死锁检测
死锁只有在并发环境下才可能出现,在日常开发中出现较少,一般暴露问题的阶段都是在现场部署之后。

而在代码审核或者自检阶段,多关注以下两类情况,能够一定程度上预防死锁的发生:

    1.Spring容器中两个或多个bean之前相互依赖;

    2. 相互依赖的bean调用对方的加锁方法;

下面的一段代码是根据一个老系统中使用的实际代码简化而成,并通过多线程模拟并发环境,以达到满足死锁的条件。

@Component
public class DeadLock {

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

private class UserService {

    private RoleService roleService;

    public synchronized void boundRole() throws InterruptedException {
        Thread.sleep(1000L);
        logger.info("boundRole");
        roleService.updateRole();
    }

    public synchronized void updateUser() {
        logger.info("updateUser");
    }

    public void setRoleService(RoleService roleService) {
        this.roleService = roleService;
    }
}

private class RoleService {

    private UserService userService;

    public synchronized void boundUser() throws InterruptedException {
        Thread.sleep(1000L);
        logger.info("boundUser");
        userService.updateUser();
    }

    public synchronized void updateRole() {
        logger.info("updateRole");
    }

    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

@PostConstruct
public void DeadLockTest() {
    UserService userService = new UserService();
    RoleService roleService = new RoleService();
    userService.setRoleService(roleService);
    roleService.setUserService(userService);
    new Thread(() -> {
        try {
            userService.boundRole();
        } catch (InterruptedException e) {
            logger.error("", e);
        }
    }).start();
    new Thread(() -> {
        try {
            roleService.boundUser();
        } catch (InterruptedException e) {
            logger.error("", e);
        }
    }).start();
}

}
项目启动后,执行结果如下: boundRole方法和 boundUser 方法中的语句被打印,但 updateRole和updateUser 中的语句却一直没有执行,在现场环境下,则是相应的请求一直阻塞,页面无法得到返回结果。

在这里插入图片描述

定位死锁比较简单,一般直接使用jstack -l pid 即可,如

在这里插入图片描述

执行结果如下:

在这里插入图片描述

考虑到现场环境不一定安装jdk, 对于系统自启动的服务而言,直接使用jstack也可能会有权限问题。在大多数情况下,定位死锁的方式会类似于之前的远程调试,这里介绍两款jdk自带的支持远程分析的图形工具:Jconsole 和 JVisualVM。之所以同时介绍这两种工具,是因为这两种不仅都可以定位死锁, 同时对于内存分析等功能可以起到互补作用, 更重要的是这两种图形工具的远程配置是通用的,相当于添加一遍配置,可同时使用这两种工具。

Jconsole/JVisualVM的远程配置虚拟机参数如下:

-Djava.rmi.server.hostname=10.16.81.35
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.port=22222
跟之前的远程调试一样,把这段配置作为虚拟机的启动参数即可,需要额外注意的有以下两点:

   1. -Djava.rmi.server.hostname=10.16.81.35 这个参数不是必加的,大多数参考资料中也没有提及,但遇到服务器有多个ip的情况下,不加这一句远程工具可能无法连接,笔者之前也在这个点上走过一些弯路,安全起见建议是把这句加上。

   2.在服务器防火墙启用的情况下,之前的远程调试只需要开放remote端口即可, 但Jconsole/JVisualVM 的远程连接不止需要开放指定的监听端口号,同时还需要开放JMXserver随机监听的端口号。

linux下可以通过命令lsof -i | grep 来查看当前java进程需要监听的随机端口号,如:

netstat -tupln | grep 8093
lsof -i | grep 6032

当然,如果能临时关闭防火墙的话,也可以不用考虑端口的问题。

把远程配置的虚拟机参数加入启动脚本后,重启服务:
在这里插入图片描述

选择远程进程,输入ip和连接端口,连接jconsole(同一台机器上没有权限问题时可直接使用本地进程连接):

在这里插入图片描述

连接好之后,在线程窗口可以检测死锁:

在这里插入图片描述

检测结果如下:

在这里插入图片描述

连接JVisualVM:

在这里插入图片描述

在线程页面查看是否检测到死锁,如有死锁可点击线程Dump:
在这里插入图片描述

远程分析的结果与jstack一致:
在这里插入图片描述

2.3 内存分析
很多同事都在开发阶段或者现场反馈的问题中遇到过jvm内存不足的问题。

内存不足的主要表现有:

  1. 系统卡顿、几乎所有的请求响应时间都很长、甚至超时。(这种情况一般是由于频繁的Full GC导致所有线程被挂起)

  2. 抛出相应的内存溢出异常,如 java.lang.OutOfMemoryError: Java heap space

而导致内存不足的原因一般有以下两种:

   1. 内存泄露,无用的对象无法回收,随着系统不断运行,累计占用的空间越来越多,导致内存不足

   2. 程序单次运行需要占有大量的内存空间,分配的空间不足

分配空间不足导致问题的情况相对较少,处理也比较方便,在硬件环境允许的前提下适当加大虚拟机内存即可。而内存泄露无法靠单纯的加大虚拟机内存来处理,必须找出内存泄露的源头,并加以处理,否则无论多大的内存,都会在系统不断运行的环境下耗尽。(在跟一些同事的沟通过程中,有一位同事处理这类问题的方式是编写脚本定时重启系统,从而解决内存泄露导致的问题,在无法定位到内存泄露源头的情况下,也算是一种不太妥当的处理方案)。

  不管是哪种情况导致的内存不足, 进行内存分析都是必不可少的步骤,在 “死锁检测” 这一小节中,已经介绍了 Jconsole 和 JVisualVM 这两种工具的远程配置方式 和 部分功能,对于内存不足的情况,我们同样使用这两种工具进行分析。以下是内存分析的实战:

  使用jpa批量插入数据时,插入数据量达到56W时, 抛出OutOfMemoryError异常:

在这里插入图片描述

关键代码如下:

//创建数据插入对象
EntityManager entityManager = JPAUtil.getEntityManager();
//开始时间
long startTime = Calendar.getInstance().getTimeInMillis();
//已插入条数
int totalSize = 0;
for (DataSource dataSource : effectiveDataSource) {
List dataList = new ArrayList<>();
for (int i = 1; i <= dataSource.getDataSize(); i++) {
dataList.add(dataSource.next());
if(i % config.getBatchSize() == 0) {
totalSize = batchSave(entityManager, dataList, startTime, totalSize, config, dataSource);
}
}
batchSave(entityManager, dataList, startTime, totalSize, config, dataSource);
logger.info(“数据插入完成:” + dataSource.getId());
}
closeEntityManager(entityManager);
重新启动服务,使用Jconsole监控服务内存变化情况,当插入一定的数据后,年老代的变化情况如下:

在这里插入图片描述

垃圾回收情况如下:

在这里插入图片描述

由Jconsole的监控情况可知,年老代收集器收集次数高达41次,但年老代的内存占用却一直递增,并且接近达到上限,至此已经可以推测出随着运行时间的增加,不断产生无法回收的对象,最终导致内存溢出。接下来我们通过JVisualVM来排查消耗最内存的对象,并尝试释放这部分对象占用的空间。

使用JVisualVM监控进程后,本地进程可以直接点击 堆Dump 查看实例, 远程调试点击 堆Dump 可在服务端生成 hprof 文件,将hprof文件复制到本地,再导入JVisualVM中即可。堆Dump情况如下所示:

在这里插入图片描述

int[] 类型的实例数 和 内存占比都居首位,可从这里开始排查,双击查看int[] 的实例,可随机选择实例查看引用,关注项数较大的类型,如:

在这里插入图片描述

上图中Object[] 类型的数组中含有EntityEntryContext$ManagedEntityImpl 类型的实例1048576个,毫无疑问已经是内存泄露的首要嫌疑对象。继续展开这个Object[]类型的引用:

在这里插入图片描述

可以定位到这个数组是被IdentityHashMap类型的实例所引用,而这个实例又被EntityEntryContext 对象的 nonEnhancedEntityXref 属性引用,如下:

private transient IdentityHashMap<Object,ManagedEntity> nonEnhancedEntityXref;

我们的目的是清空 nonEnhancedEntityXref 引用的IdentityHashMap,一般有以下两种选择:一是直接销毁EntityEntryContext 对象,二是调用EntityEntryContext 内部的方法,清空nonEnhancedEntityXref。查看EntityEntryContext 内部方法,发现clear方法可以直接做到这点:

public void clear() {
//…省略代码
if ( nonEnhancedEntityXref != null ) {
//目标方法
nonEnhancedEntityXref.clear();
}
//…省略代码
}
继续顺着EntityEntryContext 对象的引用跟踪到 StatefulPersistenceContext 类:

public class StatefulPersistenceContext implements PersistenceContext {
private EntityEntryContext entityEntryContext;
public void clear() {
//…省略代码
entityEntryContext.clear();
}
}
继续展开引用,StatefulPersistenceContext 被 SessionImpl 的实例所引用,查看源码发现SessionImpl的clear方法会调用StatefulPersistenceContext的clear方法:
在这里插入图片描述

public final class SessionImpl implements SessionImplementor...{
    private transient StatefulPersistenceContext persistenceContext;
     
    public void clear() {
         //..省略代码
        try {
           internalClear();
        }
        catch (RuntimeException e) {
            throw exceptionConverter.convert( e );
        }
   }
 
    private void internalClear() {
        persistenceContext.clear();
        //..省略代码
    }
 
 
 
}

至此,嫌疑对象和处理方案均已找到,修改后的代码如下:

//创建数据插入对象
EntityManager entityManager = JPAUtil.getEntityManager();
//开始时间
long startTime = Calendar.getInstance().getTimeInMillis();
//已插入条数
int totalSize = 0;
int refreshSize = 0;
for (DataSource dataSource : effectiveDataSource) {
    List<Object> dataList = new ArrayList<>();
    for (int i = 1; i <= dataSource.getDataSize(); i++) {
        dataList.add(dataSource.next());
        if(i % config.getBatchSize() == 0) {
            refreshSize += dataList.size();
            //新增超过refreshSize数据刷新entityManager,使被占用的资源能及时回收
            if(refreshSize >= config.getRefreshSize()) {
                logger.info("刷新entityManager");
                entityManager.clear();
                refreshSize = 0;
            }
            totalSize = batchSave(entityManager, dataList, startTime, totalSize, config, dataSource);
        }
    }
    batchSave(entityManager, dataList, startTime, totalSize, config, dataSource);
    logger.info("数据插入完成:" + dataSource.getId());
}
closeEntityManager(entityManager);

refreshSize默认设置为20W,即每新增20W条数据调用entityManager(SessionImpl对象)的clear方法,从而解决内存溢出的问题。代码修改后,
插入数据超过100W条仍在正常执行。

在这里插入图片描述

打开Jconsole监控年老代内存变化,发现垃圾回收时能够清理掉大部分空间,如下:

在这里插入图片描述

3 敏捷开发
区别于企业视角的“用户需求为核心进行的迭代开发”,这章的标题名虽然也是敏捷开发,但定义仅适用于研发人员,即“尽量避免重复开发,使相同的工作内容模板化”。

3.1 模板代码
对开发代码进行模板化是减少重复性工作的一项重要内容。前有新体系下统一使用的脚手架,后有各种现存的插件或者小工具用于创建代码,如比较通用的 mybatis-generator

插件。由于不同项目的差异性,各小组之间也都有个性化的代码创建方式, 如:

      idea + groovy 组合:自定义groovy 文件,置于idea目录下,在idea工具中即可生成目标代码。

      java + vm模板引擎 组合: 自定义代码创建工具,无需依赖开发工具,java研发人员无需了解新语法,但开发成本高于前一种。

无论选择哪种创建代码的方式,只要能满足当前项目需求即可,研发人员可根据自身的偏好和使用习惯进行选择,无需强制使用同一种代码创建工具。但切忌的是无意义的重复开发,

少部分同事甚至连持久化对象都手动开发,但表字段较多时,需耗费大量时间写对应的实体类,且容易出错和不规范。

     除了上面介绍的适用文件级代码的生成方式,一些零散的固定代码、注释等 推荐使用idea自带的  Live Templates 功能。除了 已有的 fori 、for、psvm、sout 等已有的动态模板,

Live Templates 同样也支持自定义模板:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.2 批量编辑
批量编辑功能也是开发工程经常遇到的场景,很多同事遇到需要变量编辑的功能不假思索的就一个个去编辑了,花费了较多时间。其实很多开发工具都支持批量编辑的功能,如

Notepad ++,这里推荐的是 sublime 工具。

下面是一段错误码的国际化配置,根据需求,需要改造成枚举类,如果逐个修改势必花费大量时间。

errorCode.0x17601000=Unknown error
errorCode.0x17601001=Invoke fail
errorCode.0x17601002=Illegal state error
errorCode.0x17601003=Illegal argument error
errorCode.0x17601004=Server is too busy
errorCode.0x17601005=Unsupported operation
errorCode.0x17601006=No permission
errorCode.0x17601007=User not logged in
errorCode.0x17601008=Get user information error
errorCode.0x17601009=Get mac address error
errorCode.0x17601010=Close Stream error
errorCode.0x17601011=Get local ip error
errorCode.0x17601012=Init secure lib error
errorCode.0x17601013=Generate key pair error
errorCode.0x17601014=Generate security token error
errorCode.0x17601015=Shake iac key error
errorCode.0x17601016=Get server path error
errorCode.0x17601017=Encrypt error
errorCode.0x17601018=Encrypt error
errorCode.0x17601019=Decrypt error
把这段配置复制到sublime工具中,使用alt+f3 选中相同内容,或者shift + 鼠标右键按住 拖动光标,效果如下:

在这里插入图片描述

每一行的0x前都有一个光标,修改一行的内容相当于对所有行进行操作,毫不费力就达到了需求,并且花费的时间不会随着数量增多而改变,改动后的结果如下:
在这里插入图片描述

3.3 数据模拟
在开发过程中,模拟数据也是一件比较费事费力的事情,以下是一个基于java开发的数据模拟小工具,解压后可通过使用手册查看各项配置,或者根据手册上的svn地址自行下载源码进行维护。

data-batch-production-1.0-SNAPSHOT-release.zip,见资源《dev-util》

3.4 代码生成(idea + groovy 组合)【袁】
数据库视图:

1、点击右侧的Database图标,要是没有该图标,请去自行百度

2、点击 + 号

3、选择 Data Source

4、选择 一种数据库类型(需要连哪种就选哪种)
在这里插入图片描述

配置数据库连接:

1、根据实际情况填写相关配置即可

2、点击Test Connection测试连接通过,就ok了

在这里插入图片描述

代码生成:

1、选中一张或者多张表,选择生成类型(Entity,Service,Controller。。。可自行扩展)

在这里插入图片描述

2、选择需要保存目录模块,点击生成即可
在这里插入图片描述

3、生成完毕

在这里插入图片描述

4 玩转代码(张桂荣)
4.1 减少缩进
CustomTranferDTO dto = getCustomTranferDTOById(CUSTOM_TRANSFER_DTO_ID);
if(!Objects.isNull(dto)){
String dtoName = dto.getName();
if(!Objects.isNull(dtoName )){
String[] results = dtoName.splite(’,’);
……………………
return results;
}
}
return null;
避免if的多次、深层嵌套,可把异常过程放在if语句块后,而不是把正常过程放在if语句块后。即将if条件为真并执行修改为if条件为假返回或抛异常。上述代码就可以修改为:

CustomTranferDTO dto = getCustomTranferDTOById(CUSTOM_TRANSFER_DTO_ID);
if(Objects.isNull(dto)){
return null;
}
String dtoName = dto.getName();
if(Objects.isNull(dtoName )){
return null;
}
String[] results = dtoName.splite(’,’);
……………………
return results;!
看上去就整齐多了。

4.2 巧用反射
某些特定场合,使用反射能够减少冗余代码。结合组件中的一个函数来说明,这个函数是用于根据不同的归属地,对车流量进行累加:

/**
 * 对正常识别的车牌进行流量累加
 *
 * @param plateNo 车牌号
 * @param DO 车道车流量详情DO
 * @author: zhangguirong 2019-01-22 13:57
 */
private void countFlowForAbnormalPlate(String plateNo, THighwayVfsLaneTypeLocDO DO) {
  char plateLocation = plateNo.charAt(0);
  switch (plateLocation) {
    case '京':
      DO.setLocationBeijing(DO.getLocationBeijing() + 1);
      break;
    case '津':
      DO.setLocationTianjin(DO.getLocationTianjin() + 1);
      break;
    case '冀':
      DO.setLocationHebei(DO.getLocationHebei() + 1);
      break;
    case '蒙':
      DO.setLocationNeimenggu(DO.getLocationNeimenggu() + 1);
      break;
    case '辽':
      DO.setLocationLiaoning(DO.getLocationLiaoning() + 1);
      break;
    case '吉':
      DO.setLocationJilin(DO.getLocationJilin() + 1);
      break;
    case '黑':
      DO.setLocationHeilongjiang(DO.getLocationHeilongjiang() + 1);
      break;
    case '沪':
      DO.setLocationShanghai(DO.getLocationShanghai() + 1);
      break;
    case '苏':
      DO.setLocationJiangsu(DO.getLocationJiangsu() + 1);
      break;
    case '浙':
      DO.setLocationZhejiang(DO.getLocationZhejiang() + 1);
      break;
    case '皖':
      DO.setLocationAnhui(DO.getLocationAnhui() + 1);
      break;
    case '闽':
      DO.setLocationFujian(DO.getLocationFujian() + 1);
      break;
    case '赣':
      DO.setLocationJiangxi(DO.getLocationJiangxi() + 1);
      break;
    case '鲁':
      DO.setLocationShandong(DO.getLocationShandong() + 1);
      break;
    case '豫':
      DO.setLocationHenan(DO.getLocationHenan() + 1);
      break;
    case '鄂':
      DO.setLocationHubei(DO.getLocationHubei() + 1);
      break;
    case '湘':
      DO.setLocationHunan(DO.getLocationHunan() + 1);
      break;
    case '粤':
      DO.setLocationGuangdong(DO.getLocationGuangdong() + 1);
      break;
    case '桂':
      DO.setLocationGuangxi(DO.getLocationGuangxi() + 1);
      break;
    case '琼':
      DO.setLocationHainan(DO.getLocationHainan() + 1);
      break;
    case '渝':
      DO.setLocationChongqing(DO.getLocationChongqing() + 1);
      break;
    case '川':
      DO.setLocationSichuan(DO.getLocationSichuan() + 1);
      break;
    case '贵':
      DO.setLocationGuizhou(DO.getLocationGuizhou() + 1);
      break;
    case '云':
      DO.setLocationYunnan(DO.getLocationYunnan() + 1);
      break;
    case '藏':
      DO.setLocationXizang(DO.getLocationXizang() + 1);
      break;
    case '陕':
      DO.setLocationShanxiShan(DO.getLocationShanxiShan() + 1);
      break;
    case '晋':
      DO.setLocationShanxiJin(DO.getLocationShanxiJin() + 1);
      break;
    case '甘':
      DO.setLocationGansu(DO.getLocationGansu() + 1);
      break;
    case '青':
      DO.setLocationQinghai(DO.getLocationQinghai() + 1);
      break;
    case '宁':
      DO.setLocationNingxia(DO.getLocationNingxia() + 1);
      break;
    case '新':
      DO.setLocationXinjiang(DO.getLocationXinjiang() + 1);
      break;
    case '港':
      DO.setLocationXianggang(DO.getLocationXianggang() + 1);
      break;
    case '澳':
      DO.setLocationAomen(DO.getLocationAomen() + 1);
      break;
    case '台':
      DO.setLocationTaiwan(DO.getLocationTaiwan() + 1);
      break;
    case '使':
      DO.setLocationShi(DO.getLocationShi() + 1);
      break;
    case 'W':
      DO.setLocationWj(DO.getLocationWj() + 1);
      break;
    default:
      logger.info("Plate information is incorrect! Give up statistics!");
  }
}

根据车牌第一个字符,分别对车流量统计对象THighwayVfsLaneTypeLocDO的不同字段进行累加操作。逻辑非常简单,但是写出的代码非常长。可以考虑用反射来解决:

首先,定义一个二维数组:归属地简称-成员变量/字段名,并将其转化为Map(数组遍历效率比Map.get()低):

private static final String[][] locationMapArrays = new String[][] {
    {"京", "locationBeijing"},
    {"津", "locationTianjin"},
    {"冀", "locationHebei"},
    {"蒙","locationNeimenggu" },
    {"辽","locationLiaoning"},
    {"吉","locationJilin"},
    {"黑","locationHeilongjiang"},
    {"沪","locationShanghai"},
    {"苏","locationJiangsu"},
    {"浙","locationZhejiang"},
    {"皖","locationAnhui"},
    {"闽","locationFujian"},
    {"赣","locationJiangxi"},
    {"鲁","locationShandong"},
    {"豫","locationHenan"},
    {"鄂","locationHubei"},
    {"湘","locationHunan"},
    {"粤","locationGuangdong"},
    {"桂","locationGuangxi"},
    {"琼","locationHainan"},
    {"渝","locationChongqing"},
    {"川","locationSichuan"},
    {"贵","locationGuizhou"},
    {"云","locationYunnan"},
    {"藏","locationXizang"},
    {"陕","locationShanxiShan"},
    {"晋","locationShanxiJin"},
    {"甘","locationGansu"},
    {"青","locationQinghai"},
    {"宁","locationNingxia"},
    {"新","locationXinjiang"},
    {"港","locationXianggang"},
    {"澳","locationAomen"},
    {"台","locationTaiwan"},
    {"使","locationShi"},
    {"W","locationWj"}
};
 
private static final Map<Object, Object> locationMap = Collections.unmodifiableMap(ArrayUtils.toMap(locationMapArrays));

然后,利用反射的方式,修改之前的函数:

/**
 * 对正常识别的车牌进行流量累加
 *
 * @param plateNo 车牌号
 * @param DO 车道车流量详情DO
 * @author: zhangguirong 2019-01-22 13:57
 */
private void countFlowForAbnormalPlate(String plateNo, THighwayVfsLaneTypeLocDO DO) {
  String filed = (String) locationMap.get(plateNo.substring(0, 1));
  if (StringUtils.isBlank(filed)){
    return;
  }
  try {
    // 根据名字获取类的字段
    final Field declaredField = THighwayVfsLaneTypeLocDO.class.getDeclaredField(filed);
    // 字段是私有的话,需要设置该属性,这样才能操作
    declaredField.setAccessible(true);
    // 获取对象该字段的值
    final Integer o = (Integer) declaredField.get(DO);
    // 设置对象该字段的值
    declaredField.set(DO, o + 1);
  } catch (NoSuchFieldException | IllegalAccessException e) {
    logger.warn("Plate information is incorrect. Give up statistics. -- {}", plateNo);
  }
}

这种对对象的成员变量进行赋值操作,特别是成员变量比较多的情况下,使用反射还是比较有利的。

4.3 Lombok注解【袁】
使用@Data 注解,可以减少大量的模板代码,让代码更加简洁。但是。。。FindBugs会扫描出漏洞,此问题待统一解决。
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值