开发遇到的问题

1、产生多条通话记录

    话务系统对接大唐电信的JS接口,大唐会在电话振铃、接听、挂断等时点回调我们系统的JS接口,使我们在这些时点做一些业务。但是在开发和测试时出现了一个怪异现象:打出去一通电话我们的系统却产生了多条通话记录。使用F12进行调试,发现只会调用一次生成通话记录的接口,这就奇怪了,明明只调用了一次接口,为何会产生两条通话记录呢?想破了脑袋终于想通了,这是因为大唐电信在回调接口时是基于坐席回调的,而我们在开发和测试环境中为了节约成本让多人共用同一个坐席登录我们的系统,这时这些登录同一个坐席的用户中只要有一个用户打电话,那么其他用户的JS接口也会被大唐回调,如此就会在不同环境下或者同一环境下的不同机器上产生多条通话记录。这不是代码的问题,而是坐席使用的规范问题,正确的使用方式应该是在同一时刻同一个坐席只能由一个人使用。

2、打开播放器慢

    对接的阿里云点播功能在视频播放时打开播放器的速度很慢,由于在播放时需要调用两次阿里云的接口以获取播放凭证,所以一开始以为是阿里云的接口慢。其实错了,阿里云这么大的厂商肯定会顾及到客户体验的。在我们获取播放凭证的接口中还有我们系统内部的鉴权和数据校验逻辑,在做数据校验时是根据阿里云返回的videoId(据该videoId获取播放凭证,该videoId是之前在上传视频至阿里云时由阿里云返回的,返回后我们记录在表中)查询档案资源记录,查询到则继续获取凭证,否则不获取。问题就出在查询档案记录这个环节,因为点播功能是后来加的,videoId字段也是后加的,在加该字段之前记录表已经很大了,加的videoId字段又没有加索引,当据videoId查询的时候就会很慢。后来优化为使用Id查询,其实也可以将videoId字段加上索引。

3、联表查询数据在子表数据变更时主表数据未记录

    在对接大唐话务管理功能时,需要大唐提供坐席配置,在坐席配置中又需要中继商提供线路,而一开始不知道线路是由中继提供而非大唐提供,故在建立坐席配置表时将坐席和线路存在了一张表中,在通话记录中根据坐席号关联查询中继线路和厂商信息。后来在使用了一段时间该中继厂商之后,由于电话接通率的问题,又将部分坐席接入了另外一家中继的坐席,这样在联表查询的时候就会影响到历史数据——由于只是线路变了而坐席并没有变,会导致该坐席用老线路的通话也展示成新的线路。解决方案:在存储通话记录的时候讲坐席和线路都存在通话记录表中,而不采用联表查询的方式。

4、JS函数执行顺序不确定引发的矛盾

    在对接大唐电信时,询问外呼失败和挂机回调的执行顺序确定与否,给的回复是确定:先执行外呼失败的回调再执行挂机回调,在测试时发现不是如此,这两个函数的执行顺序并不确定,是两条线互不干扰的执行路径。开始想着让它的执行顺序确定下来,经过尝试发现根本不可能确定其执行顺序:因为这两个函数的执行顺序要想确定下来,要么将这两个函数的执行都放在外呼失败的函数中,要么都放在挂机回调的函数中,此时就需要在外呼失败的函数中获取挂机回调执行后得到的数据,反过来想在挂机回调时执行外呼失败的逻辑也需要原来在外呼失败中返回的数据,而这两个数据的获取的先后是不可预知的,矛盾,不可行。最终的临时解决方案是给挂机回调加了个延时调用。

5、启动服务时的用户权限问题

    在档案系统中,突然有一天文件上传不了了,翻看代码没有什么问题,而且文件上传接口很久都没有动过,后来经过排查是因为发版时在linux系统中启动应用使用的root用户,而我们开发中使用的是另一个权限较小的用户,该小权限的用户在root下是不具有写权限的,所以文件上传不成功。

6、事务嵌套导致SpringBatch无法执行

    在一个银行项目中使用到了定时任务,每天定时将银行给的合同、借据、还款计划等数据解析入库并执行相关的逻辑操作,用定时任务进行跑批操作,在该定时任务中使用到了SpringBatch将文件的下载、解压、数据解析入库、逻辑操作等进行了分步,在该定时任务中SpringBatch的运行是正常的,但是担心在执行定时任务时会在某一步出错,就需要手动干预执行该步及其后续的步骤,那么就需要在action的方法中调用SpringBatch的run(),这时却报错了,大意是让移除方法上的@Transactional注解,很明显是跟事务有关系,但是我的代码中并没有使用到@Transactional注解,后来找到了原因,是因为我们的项目的事务是统一在配置文件中使用xml的方式配置的,只需要在该配置中将该方法剔除掉即可,由于整个类中的方法都不需要事务,因此我就将整个类剔除掉了:

 !execution(* com.bdm.crms.batch.action.*.* (..))

7、VUE中改变对象属性值而页面的感知却总是慢一步

       一般而言,一旦我们改变了vue中的对象或对象的属性值,页面就会立即感知到,从而展示最新的值,但是我在开发时遇到一个问题:通过调用后台接口去改变一个vue的对象属性值时,页面展示总是慢一个节拍,就是调用这一次后台接口却展示上一次的调用结果。一开始以为是异步导致的,所以就想着使用同步的方法解决这个问题,而JS一旦使用同步就有可能导致页面卡死无法操作,所以就尝试使用watch和computed的方式去监听影响该属性值的对象或属性,然而并未奏效。最后发现其实并不是异步导致的,而是vue自身的问题(暂且称为问题):在我的文件中,vue声明的对象是在created请求后台接口后返回的一个对象,而我要修改的那个属性值正是这个对象的一个属性,问题在于返回时由于在后台的时候该属性的值为null,返回到前端经过json的转化之后值为null的属性丢失了,也就是说前端一开始接收的这个对象并没有这个属性,这也正是问题的关键所在,vue中对象和页面展示本是双向绑定的,即对象属性值改变则页面展示改变,页面展示改变则对象属性值也改变,但这里所说的对象属性是对象第一次赋值时所具有的的属性,后续添加的属性则不会双向绑定,这也是导致慢一拍的原因,这也印证了一个数据库设计理念,最好给字段初始化一个值,而不是使其为null。

8、改动分页数据导致的问题

       在做档案归档和借阅流程的时候有这样一个需求,同一个订单下的档案可以进行多次归档,但需将每一次的归档进行汇总,即将同一个订单的次归档申请的结果汇总成一个档案夹。借阅的时候是基于档案归档的文件夹进行借阅的,每次借阅也需要先发送一个申请,申请借阅哪个订单下的档案,当然借阅申请和档案夹也是多对一的关系,给出的需求是查询借阅申请的时间在某个时间段内的所有档案夹,如果要通过sql实现这个需求则需要使用分组,将与档案夹关联的所有申请通过订单编号进行分组,然后获取分组后的最大申请时间和最小申请时间与条件进行比对,很明显sql会很复杂。于是我就想到了一个自认为“简单可行”的方法,先将所有档案夹查出来,在java代码中再根据档案夹的订单编号查询所有的借阅申请,再在代码中使用条件去过滤,将不符合条件的档案夹从分页数据中剔除,其实这是不可行的,因为我忽略了一个问题,仅仅改变分页的数据是不行的,还需要改变分页数据的总量,同时还需要改变该条数据在分页中的具体位置,不然的话,在点击分页的时候可能会出现页码不对,数据对应不上的问题。在尝试的过程中还遇到了一个问题:在遍历集合的时候去移除或者增加该集合的元素都会报错,也就是说不能在遍历集合的时候增删该集合的元素,因为在遍历的时候增删元素会使集合的顺序错乱而发生错误,所以JVM干脆直接抛出异常。考虑到上面的问题后我又老老实实的写了那个复杂的sql,结论是不要轻易改动分页的数据。

9、使用FastJson将后台对象转换为JSON串之后前端解析不正确

       SpringBoot默认使用的HttpMessageConverters是Jackson,通过Jackson将Http请求的请求报文和响应报文转化为JSON串,但是Jackson对时间和日期的处理在大陆地区看来是有问题的(时间可能会差一个小时),于是将Jackson换为了FastJson:

@SpringBootApplication
public class SpringBootTestApplication {
	
    public static void main(String[] args) {
        SpringApplication.run(SpringBootTestApplication.class, args);
    }
    
    /**
	 * json解析使用fastJson
	 * @return
	 */
	@Bean
	public HttpMessageConverters fastJsonConfigure(){
		FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
		FastJsonConfig fastJsonConfig = new FastJsonConfig();
		fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
		
		//日期格式化
		fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
		converter.setFastJsonConfig(fastJsonConfig);
		converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8));
		return new HttpMessageConverters(converter);
	}
}

       这个时候又出现了新的问题:在一个controller的方法中如果返回的数据包含多个集合(List或Set等),而这些集合中恰巧又有交集的时候(多个集合中都存在同一个对象,不限于这种情况,实际上只要返回的数据有相同的对象时都会如此),FastJson为了节省空间会将第二个及其之后的该对象以引用的方式进行转化,即转化后的json串中会出现:$ref。而$ref被传递到前端时由于前端是没有使用FastJson进行解析的,也就会导致前端解析到$ref这个字符串时解析失败。FastJson也为我们提供了解决方法,那就是不使用对象引用的Json转换方式:只需要将SerializerFeature属性的值改为DisableCircularReferenceDetect即可

@SpringBootApplication
public class SpringBootTestApplication {
	
    public static void main(String[] args) {
        SpringApplication.run(SpringBootTestApplication.class, args);
    }
    
    /**
	 * json解析使用fastJson
	 * @return
	 */
	@Bean
	public HttpMessageConverters fastJsonConfigure(){
		FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
		FastJsonConfig fastJsonConfig = new FastJsonConfig();
		fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect);
		
		//日期格式化
		fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
		converter.setFastJsonConfig(fastJsonConfig);
		converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8));
		return new HttpMessageConverters(converter);
	}
}

10、相同的业务逻辑在多处用到时,应抽成一个的公共方法,不然一是会产生不易察觉和难以修复的Bug,由于业务逻辑本该相同,却分开写了多个方法处理,很可能导致其中一处改了,而另一处没有,或者其中一处使用的判断逻辑,在另一处不能成立等问题;二是代码难以维护,在业务逻辑需要变动的情况下,需要多处修改,改来改去可能就不一致了。

11、使用类加载器加载文件时的缓存问题

    在做档案归档功能的时候,归档档案的条目是固定的,但是在经过一段时间之后又有可能会发生变化,为了省事我将这些条目放在了一个json配置文件中(当然也可以放在数据库中):

{
  "borrower": [
    {
      "itemCode": "ciIdCardPositive",
      "itemName": "身份证",
      "useType": "ciIdCardPositive"
    },
    {
      "itemCode": "ciHousehold",
      "itemName": "户口本",
      "useType": "ciHousehold"
    },
    {
      "itemCode": "ciMarriage",
      "itemName": "婚姻证明",
      "useType": "ciMarriage"
    },
    {
      "itemCode": "ciCreditReporting",
      "itemName": "征信报告",
      "useType": "ciCreditReporting"
    },
    {
      "itemCode": "ciDeathCertificate",
      "itemName": "其他",
      "useType": "ciDeathCertificate"
    }
  ],
  "coll": [
    {
      "itemCode": "PANGULMTCOLL01",
      "itemName": "房产证明",
      "useType": "HouseResult"
    },
    {
      "itemCode": "PANGULMTCOLL02",
      "itemName": "产权证明",
      "useType": "HouseResult"
    },
    {
      "itemCode": "PANGULMTCOLL03",
      "itemName": "评估报告",
      "useType": "HouseResult"
    }
  ]
}

    然后通过类加载器加载这个配置文件到内存,再进行解析,将配置文件解析成一个对象,之后再通过这个对象来获取这些条目信息:对象和配置文件是存在对应关系的

public class FmArchivesFileFlowConstants {

    public static ArchiveItemRelShip archiveItemRelShips;

    static {
        InputStream resourceAsStream = null;
        BufferedReader br = null;
        try {
            //resourceAsStream = FmArchivesCommonInitJavaCard.class.getResourceAsStream("/json/archivesAndItem.json");
            // 档案条目映射关系:仅在类加载时读取一次最新配置文件
            resourceAsStream = FmArchivesCommonInitJavaCard.class.getClassLoader().getResource("json/archivesAndItem.json").openStream();
            br = new BufferedReader(new InputStreamReader(resourceAsStream));
            StringBuilder jsonStr = new StringBuilder();
            String json = "";
            while ((json = br.readLine()) != null) {
                jsonStr.append(json);
            }
            archiveItemRelShips = JsonUtils.jsonToObj(jsonStr.toString(), ArchiveItemRelShip.class);
        } catch (Exception e) {
            log.error("读取archivesAndItem.json文件出错,FmArchivesFileFlowConstants.static:", e);
        } finally {
            try {
                if (null != resourceAsStream)
                    resourceAsStream.close();
                if (null != br)
                    br.close();
            } catch (Exception e) {
                log.error("关闭流异常,FmArchivesFileFlowConstants.static:", e);
            }
        }
    }
}

    为了尽可能的少读取该配置文件,将文件的加载和解析放在了static静态代码块(注意不是静态方法)中,这样就只会在类加载的时候读取配置文件,从而减少IO,但是遇到了一个问题,就是使用FmArchivesCommonInitJavaCard.class.getResourceAsStream("/json/archivesAndItem.json");时会出现缓存问题,就是在我改了archivesAndItem.json文件之后再次发版(按说SpringBoot项目再次发版后,会重新加载类的定义等信息,但是以防万一,还是做了这个改动),读取到的依然是改动之前的配置信息,百度说是因为this.class.getResourceAsStream(path);方法是存在缓存的,就是如果之前加载过该文件,那下次会直接从内存中获取,而不会重新加载,这就导致不能获取到最新的配置,另外这种方式在传入绝对路径的时候需要加上"/";由于缓存(也可能是我们合并分支时出现问题导致的,具体原因不好考证,暂且认为是缓存的原因)的存在将文件加载的方式改为了FmArchivesCommonInitJavaCard.class.getClassLoader().getResource("json/archivesAndItem.json").openStream();这种方式每次都会新开一个流去读取最新的文件(也因为此开销要比getResourceAsStream大,所以要尽可能的减少读取的次数,这也真是将这段代码放在了静态代码块中的原因),这种方式默认就会从类路径下读取文件,不需要以"/"开头。

12、切换环境测试

       公司的系统架构是分布式的,有很多个系统,而且是前后端分离的。测试人员在测试出来一个问题之后,需要修复,本想着修复之后自己先在开发环境测试,没问题之后再提测。但是遇到了一个问题,由于是分布式的系统,又是前后端分离的,修改的前端项目依赖于另一个系统,但是另一个系统的开发环境宕机了,没法在开发环境测试,也因为此不好定位问题,而修改的代码自己又没有多大的把握,只能硬着头皮让测试人员发到测试去测,一测不过,再改,再提测,还是不过。就不好意思再让测试人员发版测试了。后来就把前端的项目直接怼到测试环境的后台去,修改了前端,通过xshell到测试环境的后台查看日志,这样就容易定位问题了,方便了很多。

13、数据修复的步骤

       项目中的一个A功能依赖于另外几个功能,但是这几个功能模块又给了不同的人开发,不幸的是把A功能分给了我。A功能是档案归档,就是把订单在各个时点上传过的附件展示到一个页面上,并且可以在这个页面上继续上传附件,而且上传的附件在其他各个功能节点上还要都能看得到。另外几个功能上传附件的形式不尽相同,所以搜集到一起的逻辑也不一样。其中有一个功能是双方约定好的附件条目代码,但是那家伙在没有告知我的情况下自己把条目代码改了,而且还上了生产,这就导致在他那边上传的附件,在我这看不到,在我这上传的附件他那也看不到。生产数据库数据出现了问题,我不但得依着他改代码,还得依着他修改生产数据。这里说一下修复生产数据的思路:由于是分布式的架构,数据存放在不同的数据库,我们使用的是阿里云的云数据库,只支持跨库联表查询,但不支持跨库联表修改等操作,即使支持跨库联表修改建议也不要直接在生产库上直接操作,万一出现差错就无力回天了。方案是将涉及到的几张表复制到开发环境的数据库中,可以放在同一个数据库中,也可以放在多个数据库中(因为开发环境通常是支持跨库联表修改操作的),然后记录受影响的数据的id,在修复后只在生产上执行这些数据的update语句即可,而不是整张表覆盖,那样会产生问题——在你修复的过程中产生的数据会丢失。

14、外网应用访问内网应用接口的网络配置

       在做档案系统的RFID档案柜管理时,需要通过档案系统访问档案柜和标签仪的接口来进行档案扫描和标签制作等。我们生产环境的档案系统是部署在阿里云上的,分配的网络对于公司内部来说必然是外网;而档案柜和标签仪是放在公司内部使用的,一个档案柜连接一台公司的电脑,并在这些电脑上部署控制档案柜的应用服务,来实现通过电脑程序控制档案柜,标签仪也是如此,为了方便将标签仪直接安装在了我们公司档案管理员员工自己的电脑上,并在他的电脑上部署标签仪的应用服务,来驱动标签仪工作,这些应用服务必然是在我们公司内部的局域网中。这就产生一个问题,部署在外网的档案系统要直接访问内网中档案柜和标签仪的应用服务接口是访问不了的,需要一个跳板机做端口映射。我们的解决方案是在跳板机上给每个档案柜和标签仪分配一个端口,并给这些分配的端口做端口映射,映射到相应档案柜和标签仪的应用服务上,然后档案系统访问跳板机即可。也就是说我们在表中给每个档案柜配置一个唯一标识,用来区分是哪个柜子,在程序中根据这个标识来确定访问的是哪个柜子,那么访问这个柜子的端口也就由这个标识决定,当请求到跳板机上时,跳板机再根据端口映射映射到相应的档案柜或标签仪的服务上去。举例来说,柜子1在内网的IP是10.105.53.123,端口是8989;柜子2在内网的ip是10.105.53.124,端口也是8989;跳板机的ip为10.105.65.73,在跳板机上配置两个端口映射:8081映射到10.105.53.123:8989,8082映射到10.105.53.124:8989,这样就可以让生产环境的档案系统通过访问10.105.65.73:8081来访问柜子1的服务,通过访问10.105.65.73:8082来访问柜子2的服务,那我们怎么知道档案系统是应该访问8081还是8082呢,这个我们可以通过在表中配置来解决,这样我们只需要在跳板机上给档案系统加上白名单即可,注意一定要记住加白名单或者关闭网络防火墙。

15、后台使用url.openConnection()发送请求遇到的问题

       在档案系统中有这样一个需求,各个周围系统要调用档案系统的接口查询附件上传情况等,为了安全起见,在档案系统中增加了二次校验,即回调各个周围系统的Http接口将这些系统传递过来的validCode传回去然后各个周围系统进行校验,以避免档案系统的接口被滥调,这里出现的问题是在生产上档案系统调用周围系统的回调接口时时常出现不成功的情况,但也不是每次都不成功,而测试环境是正常的,究其原因是档案系统中使用的是url.openConnection()的方式来模拟浏览器访问,但是却没有设置User-Agent请求头,即周围系统不认为来自档案系统的后台请求是浏览器的请求,因此调用的时候有时会响应403(不知道为什么有时候会请求成功),加上该设置后就没有问题了:测试环境正常的原因是测试环境使用的是http协议,而生产环境则是https

public class MainTest {

	public static void main(String args[]) {
		URL url;
		HttpURLConnection conn = null;
		InputStream inStream = null;
		String result = "";
		try {
			url = new URL("https://xxx/accessValidCode!check.xhtml?validCode=48a8598a-c841-42ba-9e3e-3ddbb0240400");
			conn = (HttpURLConnection) url.openConnection();
			conn.setConnectTimeout(300);
			conn.setReadTimeout(300);
			conn.setRequestMethod("GET");
			//模拟浏览器请求
			conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
			inStream = conn.getInputStream();
			byte[] data = input2byte(inStream);
			result = new String(data, "UTF-8");
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println(result);
	}

	public static final byte[] input2byte(InputStream inStream) throws IOException {
		ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
		try {

			byte[] buff = new byte[100];
			int rc = 0;
			while ((rc = inStream.read(buff, 0, 100)) > 0) {
				swapStream.write(buff, 0, rc);
			}
			byte[] in2b = swapStream.toByteArray();

			return in2b;
		} catch (Exception e) {
			throw new IOException(e);
		} finally {

			swapStream.flush();
			swapStream.close();

		}
	}
}

16、事务和异常的处理

    企业开发中大多会使用Spring进行事务管理,而大多数情况下我们会把事务加在service层,需要注意的有两点:

       1️⃣Spring默认只在遇到运行时异常时才会回滚,因此默认情况下throw new Exception()事务是不会回滚的,除非配置rollback=Exception,此时遇到任何异常都会回滚;

       2️⃣service层的异常最好不要try catch处理,而是向上抛出,因为如果try catch了,则Spring捕捉不到异常,也不会回滚;

17、应用之间通信连接超时

       部署在两台机器上的应用要想完成通信,除了相互能够 ping 通之外,还需要开放应用的端口,否则也是无法进行通信的。对于Linux系统而言,最暴力的方式就是关闭防火墙,当然这只能在开发或测试环境这么做,生产上还是需要配置开放的端口。

18、引用jar包包扫描的问题

    在工作中新开发了一个组件,该组件使用SpringBoot开发,然后打成jar包,在其他项目中使用maven或者gradle引入,但在引入后发现其他项目中无法获取到jar包中置于Spring容器中的对象,jar包中开启的定时任务等也没有生效。究其原因是因为其他项目也是使用SpringBoot开发,SpringBoot应用扫描的包默认为主启动类所在的包及其子包,而作为jar包引入到SpringBoot应用的包并不在这个应用扫描的范围,也就是说jar包中所有标注了@Component和@Service等注解的类并没有起作用,解决方法有两种:一是在主启动类上使用@ComponentScan注解将jar包中的包纳入扫描的范畴;二是将jar包编写为一个starter,在项目中开启,原理都是一样的,都是将jar包中的包纳入到项目的包扫描范畴。

19、HTTP连接不断开的问题

       在一个循环中使用HttpClient发送GET请求到另外一个应用中,出现HTTP连接持续增长却不断开,原因是在创建Http连接的时候采用的如下方式:org.apache.httpcomponent:httpclient 包提供的方法

CloseableHttpClient client = HttpClients.createDefault();

       这种方式创建的CloseableHttpClient发送请求时是keep alive的,不会自动关闭连接,需要手动关闭。或者将这种方法创建的CloseableHttpClient对象放在全局范围内供多次调用,这样重复使用这个连接也不会造成连接数量的暴增,导致程序挂掉。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值