分享十条Java后端开发实战经验,干货满满!
前沿
借助本篇文章,旨在对自己在公司、个人项目的实战经验总结,包括JAVA常遇到的业务场景技术栈、第三方库以及可复用的代码编写,希望能给大家带来帮助。
目前,我总结了10条常见的业务开发经验,毫无保留的分享给大家。。。,主要涵盖内容如下:悟耕开源EasypoiExcel导入导出最佳实践AlibabaExcel导出时自定义格式转换优雅实现不建议直接使用Async实现异步,需自定义线程池解决Java行业常见业务开发数值计算丢失精度问题HutoolTreeUtil快速构造返回树形结构事务Transactional的失效场景SpringEvent实现异步业务开发中通用的策略模式模板使用ip2region获取用户地址位置信息利用好Java现有优秀的开发库一、悟耕开源easypoiExcel导入导出最佳实践
Excel导入导出几乎在很多中后台项目都会用到,特别是一些CRM、OA、商城、企业应用等系统都十分常见,在开发的过程中我也遇到过很多Excel大数据导入导出的功能,一直以来,使用easypoi做了不少导入导出的需求,导入导出数据量从10万级到现在百万级(Excel峰值103万数据量),整理了一下easypoi的导入导出的基础和高级用法。大数据导出数据转换以及数据加密Excel导入数据校验,并提供错误日志下载1。1注解说明
常见的5个注解类分别是:Excel:作用到filed上面,是对Excel列的一个描述;ExcelCollection:表示一个集合,主要针对一对多的导出,比如一个老师对应多个科目,科目就可以用集合表示;ExcelEntity:表示一个继续深入导出的实体,但他没有太多的实际意义,只是告诉系统这个对象里面同样有导出的字段;ExcelIgnore:和名字一样表示这个字段被忽略跳过这个导导出;ExcelTarget:这个是作用于最外层的对象,描述这个对象的id,以便支持一个对象可以针对不同导出做出不同处理。1。2定义easypoi实体类importcn。afterturn。easypoi。excel。annotation。Excel;importcn。afterturn。easypoi。handler。inter。IExcelDataModel;importcn。afterturn。easypoi。handler。inter。IExcelModel;importlombok。Data;importjavax。validation。constraints。NotBlank;importjava。io。Serializable;DatapublicclassSdSchoolSysUserVerifyimplementsIExcelModel,IExcelDataModel,Serializable{privatestaticfinallongserialVersionUID1L;Excel(name行号)privateIntegerrowNum;Excel(name错误信息)privateStringerrorMsg;真实姓名Excel(name姓名(必填),width25)NotBlank(message姓名不能为空)privateStringrealname;部门编码,需要和用户导入模板名称对应Excel(name部门编码(必填),width30)NotBlank(message部门编码不能为空)privateStringdeptOrgCode;角色编码Excel(name角色编码(必填),width15)NotBlank(message角色编码不能为空)privateStringroleCode;手机号码Excel(name手机号码(选填),width15)privateStringphone;电子邮件Excel(name电子邮件(选填),width15)privateStringemail;性别(1:男2:女)Excel(name性别(选填),width15)privateStringsexName;工号(选填)Excel(name工号(选填),width15)privateStringworkNo;商户IDprivateIntegertenantId;}1。3基础的导入导出逻辑(数据校验)
easyPoi导入校验使用起来也很简单,以导入系统优化为例:
第一步,定义一个检验类SdSchoolSysUserVerify,通过实现IExcelModel、IExcelDataModel,当我们需要输出导入校验错误信息的时候,它们两个就显的很重要了,IExcelModel负责设置错误信息,IExcelDataModel负责设置行号。packagecn。afterturn。easypoi。handler。inter;Excel本身数据文件publicinterfaceIExcelDataModel{获取行号publicIntegergetRowNum();设置行号publicvoidsetRowNum(IntegerrowNum);}
第二步,定义完实体之后,那么如何实现我们的校验逻辑呢,接着自定义一个系统用户导入校验处理器SdSchoolSysUserVerifyHandler,通过实现IExcelVerifyHandler,处理器里编写我们的校验逻辑:系统用户批量导入校验处理器author:jacklinsince:202133111:47ComponentpublicclassSdSchoolSysUserVerifyHandlerimplementsIExcelVerifyHandlerSdSchoolSysUserVerify{privatestaticfinalStringPREFIX【;privatestaticfinalStringSUFFIX】;AutowiredprivateISysBaseAPIsysBaseAPI;OverridepublicExcelVerifyHandlerResultverifyHandler(SdSchoolSysUserVerifyuserVerify){LoginUserloginUser(LoginUser)SecurityUtils。getSubject()。getPrincipal();userVerify。setTenantId(Integer。valueOf(loginUser。getRelTenantIds()));StringJoinerjoinernewStringJoiner(,,PREFIX,SUFFIX);if(StringUtils。isBlank(userVerify。getRealname())){joiner。add(用户姓名不能为空);}根据用户姓名和商户ID查询用户记录,大于0则提示该姓名用户已存在intrealNameCountsysBaseAPI。countByRealName(userVerify。getRealname(),userVerify。getTenantId());if(realNameCount0){joiner。add(该姓名用户已存在,如需添加该用户请在页面添加);}if(StringUtils。isBlank(userVerify。getDeptOrgCode())){joiner。add(部门编码不能为空);}else{查询系统是否存在该部门编码intdeptOrgCodeCountsysBaseAPI。queryDepartCountByDepartSysCodeTenantId(userVerify。getDeptOrgCode(),userVerify。getTenantId());if(deptOrgCodeCount0){joiner。add(部门编码不存在);}}if(oConvertUtils。isEmpty(userVerify。getRoleCode())){joiner。add(用户角色编码不能为空);}else{查询系统是否存在该角色intcountsysBaseAPI。queryRoleCountByRoleCodeTenantId(userVerify。getRoleCode(),userVerify。getTenantId());if(count0){joiner。add(该用户角色编码不存在);}else{查询配置是否用户支持导入该角色intsupportUserImportCountsysBaseAPI。queryIsSupportUserImportByRoleCode(userVerify。getRoleCode(),userVerify。getTenantId());if(supportUserImportCount0){joiner。add(该用户角色编码不支持导入);}}}if(oConvertUtils。isNotEmpty(userVerify。getPhone())){booleanisPhoneValidator。isMobile(userVerify。getPhone());if(!isPhone){joiner。add(手机号填写格式不正确);}}if(oConvertUtils。isNotEmpty(userVerify。getEmail())){booleanisEmailValidator。isEmail(userVerify。getEmail());if(!isEmail){joiner。add(邮箱填写格式不正确);}}if(!【】。equals(joiner。toString())){returnnewExcelVerifyHandlerResult(false,joiner。toString());}returnnewExcelVerifyHandlerResult(true);}}
第三步,在完成第一、二步之后,我们只需要在导入的时候通过params。setVerifyHandler(userVerifyHandler)、params。setNeedVerfiy(true)即可以实现导入校验了。1。4不同类型数据的导入和导出(MapObject)
在某些复杂的场景,我们导入的时候不想直接构造一个bean然后标记注解,但是中间需要处理一些字段逻辑没办法直接导入到数据库,这是用可以用map的形式导入,下面我以一个客户导入的需求演示一下如何通过map的方式导入数据:核心方法:Map数据格式导入ExcelImportResultMapString,ObjectimportResultExcelImportUtil。importExcelMore(inputStream,Map。class,params);
获取导入检验通过的数据
ListMapString,ObjectrightMapListimportResult。getList();
获取导入检验失败的数据
ListMapString,ObjectfailMapListimportResult。getFailList();
最后可以将校验失败的数据,通过excel错误日志输出,非常的方便。1。5基于多线程ForkJoin实现导入优化
在4。0后的版本,easypoi导入支持了forkjoin的多线程支持,使用方法很简单ImportParams新加了两个参数,设置为true就可以了,多线程导入处理可以提高了导入的处理效率,比如:params。setConcurrentTask(true);4。1版本都支持基于forkjoin的线程1。6自定义导入数据处理
这里列举说明一下easypoi的几个比较重要的接口和类:IExcelDataHandler:当存在一下比较特殊的需求场景,easypoi基础服务无法满足客户的需求时,可以通过实现IExcelDataHandler去自定义数据处理,比如数值转换器处理。IExcelVerifyHandler:一般都是通过实现IExcelVerifyHandler接口实现自己的校验逻辑。IExcelModel:自定义实体校验类,主要用于输出错误日志,IExcelModel负责错误信息。IExcelDataModel:自定义实体校验类,主要用于输出错误日志,IExcelDataModel负责设置行号。IExcelDataHandlerExcel导入导出数据处理接口publicinterfaceIExcelDataHandlerT{导出处理方法paramobj当前对象paramname前字段名称paramvalue当前值returnpublicObjectexportHandler(Tobj,Stringname,Objectvalue);}1。7导入组内数据重复校验实现
可以通过ThreadLocal来实现组内校验,可以定位输出每一个错误数据的具体是哪一行,方便我们做导入排错:IM批量推送用户导入校验处理器author:jacklinsince:202211810:45Slf4jComponentpublicclassSdSchoolBatchPushCustomerVerifyHandlerimplementsIExcelVerifyHandlerSdSchoolBatchPushCustomerVerify{AutowiredprivateISdSchoolCustomerServicesdSchoolCustomerService;privatefinalThreadLocalListSdSchoolBatchPushCustomerVerifythreadLocalnewThreadLocal();privatestaticfinalStringPREFIX【;privatestaticfinalStringSUFFIX】;最新采用ThreadLocal线程本地内存变量方式实现组内校验,效果可以author:jacklinsince:202221116:26OverridepublicExcelVerifyHandlerResultverifyHandler(SdSchoolBatchPushCustomerVerifycustomerVerify){StringJoinerjoinernewStringJoiner(,,PREFIX,SUFFIX);StringregisterUserPhonecustomerVerify。getRegisterUserPhone();if(StringUtils。isBlank(registerUserPhone)){joiner。add(注册手机号不能为空);}else{手机号格式校验booleanmobileValidator。isMobile(registerUserPhone);if(!mobile){joiner。add(手机号格式不正确);}}ListSdSchoolBatchPushCustomerVerifythreadLocalValuethreadLocal。get();if(threadLocalValuenull){threadLocalValuenewArrayList();}threadLocalValue。forEach(e{if(e。getRegisterUserPhone()。equals(customerVerify。getRegisterUserPhone())){intlineNumbere。getRowNum()1;joiner。add(数据与第lineNumber行重复);}});添加本行数据对象到ThreadLocal中threadLocalValue。add(customerVerify);threadLocal。set(threadLocalValue);if(!【】。equals(joiner。toString())){returnnewExcelVerifyHandlerResult(false,joiner。toString());}returnnewExcelVerifyHandlerResult(true);}publicThreadLocalListSdSchoolBatchPushCustomerVerifygetThreadLocal(){returnthreadLocal;}}
核心代码:threadLocalValue。forEach(e{if(e。getRegisterUserPhone()。equals(customerVerify。getRegisterUserPhone())){intlineNumbere。getRowNum()1;joiner。add(数据与第lineNumber行重复);}});添加本行数据对象到ThreadLocal中threadLocalValue。add(customerVerify);threadLocal。set(threadLocalValue);二、Alibabaexcel导出时自定义格式转换优雅实现
EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。
Java解析、生成Excel比较有名的框架有Apachepoi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。EasyExcel是alibaba出的一个基于javapoi得Excel通用处理类库,它的优势在于内存消耗。对比easypoi方案,EasyExcel在内存消耗、知名度上更出众些。
博主在使用过程中发现导出Excel,官网对自定义格式字段提供了converter接口,当我们的Excel导入需要将是否文字转成数据库10的时候,这时候就需要自定义转换器WhetherConverter实现了:importcom。alibaba。excel。converters。Converter;importcom。alibaba。excel。enums。CellDataTypeEnum;importcom。alibaba。excel。metadata。GlobalConfiguration;importcom。alibaba。excel。metadata。data。ReadCellData;importcom。alibaba。excel。metadata。data。WriteCellData;importcom。alibaba。excel。metadata。property。ExcelContentProperty;importcom。dragonpass。global。modules。agent。enumreate。Whether;importjava。util。Objects;自定义Excel导入导出转换器authorLinbzsince202211249:55publicclassWhetherConverterimplementsConverterInteger{OverridepublicClasslt;?supportJavaTypeKey(){returnInteger。class;}OverridepublicCellDataTypeEnumsupportExcelTypeKey(){returnCellDataTypeEnum。STRING;}导入,文字转数字,是否10OverridepublicIntegerconvertToJavaData(ReadCellDatalt;?cellData,ExcelContentPropertycontentProperty,GlobalConfigurationglobalConfiguration)throwsException{IntegerresultWhether。NO。getCode();resultWhether。YES。getDesc()。equals(cellData。getStringValue())?Whether。YES。getCode():Whether。NO。getCode();returnresult;}导出,数字转文字,10是否OverridepublicWriteCellDatalt;?convertToExcelData(Integervalue,ExcelContentPropertycontentProperty,GlobalConfigurationglobalConfiguration)throwsException{returnnewWriteCellData(Objects。equals(value,Whether。YES。getCode())?Whether。YES。getDesc():Whether。NO。getDesc());}}导入导出实体类
在导出ExcelProperty中添加WhetherConverter,就优雅得实现了自定义格式得需求:publicstaticclassExportTemplate{DatapublicstaticclassExcelInput{privateStringagentId;}DatapublicstaticclassExcelOutput{ExcelProperty(value类型名称)ColumnWidth(12)privateStringtypeName;ExcelProperty(value权益扣取后额外扣费(是否),converterWhetherConverter。class)ColumnWidth(24)privateIntegerneedPay;ExcelProperty(value扣费金额)ColumnWidth(12)privateBigDecimalprice;ExcelProperty(value是否为默认项(是否),converterWhetherConverter。class)ColumnWidth(24)privateIntegerisDefault;ExcelProperty(valueNcode)ColumnWidth(12)privateStringloungeCode;}DataNoArgsConstructorpublicstaticclassDataCheckResult{ExcelProperty(value结果)privateBooleancheckResultBoolean。TRUE;ExcelProperty(value备注)privateStringremark;publicDataCheckResult(BooleancheckResult,Stringremark){this。checkResultcheckResult;this。remarkremark;}}}ExcelUtil。importByEasyExcel导入OverrideTransactional(rollbackForException。class)publicAgtAgentDiffPriceRuleDTO。ImportExcelDataDTO。OutputimportExcelData(AgtAgentDiffPriceRuleDTO。ImportExcelDataDTO。Inputinput){ExcelUtil。importByEasyExcelListdataListExcelUtil。importByEasyExcel(input。getFile()。getInputStream(),AgtAgentDiffPriceRuleDTO。ExportTemplate。ExcelOutput。class,Integer。MAXVALUE,true);导入数据校验AgtAgentDiffPriceRuleDTO。ExportTemplate。DataCheckResultdataCheckResultdataCheckForResult(dataList,input);if(dataCheckResult。getCheckResult()){TODO校验成功,插入数据。。。}}三、不建议直接使用Async实现异步,需自定义线程池Async应用默认线程池
Spring应用默认的线程池,指在Async注解在使用时,不指定线程池的名称。查看源码,Async的默认线程池为SimpleAsyncTaskExecutor。无返回值的异步调用OverrideAsync(taskExecutor)publicvoidpageExportOrderBigExcel(HttpServletResponseresponse,JSONObjectqueryConditionDataJson,SdSchoolFilterConfigsdSchoolFilterConfig,LoginUserloginUser,SdSchoolDataExportTaskRecordexportTask,HttpServletRequestrequest,StringtenantId){try{Thread。sleep(1000);exportTask。setExportTaskStartTime(newDate());sdSchoolOrderService。exportOrderBigExcelPage(response,queryConditionDataJson,exportTask,sdSchoolFilterConfig。getFilterName(),loginUser,request,tenantId);exportTask。setExportTaskEndTime(newDate());exportTaskRecordService。updateById(exportTask);}catch(Exceptione){log。error(订单数据分页导出失败,e);}}默认线程池的弊端
在线程池应用中,参考阿里巴巴Java开发规范:线程池不允许使用Executors去创建,不允许使用系统默认的线程池,推荐通过ThreadPoolExecutor的方式,这样的处理方式让开发的工程师更加明确线程池的运行规则,规避资源耗尽的风险。Executors各个方法的弊端:newFixedThreadPool和newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。newCachedThreadPool和newScheduledThreadPool:要问题是线程数最大数是Integer。MAXVALUE,可能会创建数量非常多的线程,甚至OOM。
Async默认异步配置使用的是SimpleAsyncTaskExecutor,该线程池默认来一个任务创建一个线程,若系统中不断的创建线程,最终会导致系统占用内存过高,引发OutOfMemoryError错误。针对线程创建问题,SimpleAsyncTaskExecutor提供了限流机制,通过concurrencyLimit属性来控制开关,当concurrencyLimit0时开启限流机制,默认关闭限流机制即concurrencyLimit1,当关闭情况下,会不断创建新的线程来处理任务。基于默认配置,SimpleAsyncTaskExecutor并不是严格意义的线程池,达不到线程复用的功能。Async应用自定义线程池
自定义线程池,可对系统中线程池更加细粒度的控制,方便调整线程池大小配置,线程执行异常控制和处理。在设置系统自定义线程池代替默认线程池时,虽可通过多种模式设置,但替换默认线程池最终产生的线程池有且只能设置一个(不能设置多个类继承AsyncConfigurer)。自定义线程池有如下方式:重新实现接口AsyncConfigurer;继承AsyncConfigurerSupport;配置由自定义的TaskExecutor替代内置的任务执行器。
通过查看Spring源码关于Async的默认调用规则,会优先查询源码中实现AsyncConfigurer这个接口的类,实现这个接口的类为AsyncConfigurerSupport。但默认配置的线程池和异步处理方法均为空,所以,无论是继承或者重新实现接口,都需指定一个线程池。且重新实现publicExecutorgetAsyncExecutor()方法。实现接口AsyncConfigurerConfigurationpublicclassAsyncConfigurationimplementsAsyncConfigurer{Bean(taskExecutor)publicThreadPoolTaskExecutorexecutor(){ThreadPoolTaskExecutorexecutornewThreadPoolTaskExecutor();intcorePoolSize10;executor。setCorePoolSize(corePoolSize);intmaxPoolSize50;executor。setMaxPoolSize(maxPoolSize);intqueueCapacity10;executor。setQueueCapacity(queueCapacity);executor。setRejectedExecutionHandler(newThreadPoolExecutor。CallerRunsPolicy());executor。setThreadNamePrefix(asyncServiceExecutor);executor。setWaitForTasksToCompleteOnShutdown(true);executor。setAwaitTerminationSeconds(awaitTerminationSeconds);executor。initialize();returnexecutor;}OverridepublicExecutorgetAsyncExecutor(){returnexecutor();}}继承AsyncConfigurerSupportConfigurationEnableAsyncclassSpringAsyncConfigurerextendsAsyncConfigurerSupport{BeanpublicThreadPoolTaskExecutorasyncExecutor(){ThreadPoolTaskExecutorthreadPoolnewThreadPoolTaskExecutor();threadPool。setCorePoolSize(3);threadPool。setMaxPoolSize(3);threadPool。setWaitForTasksToCompleteOnShutdown(true);threadPool。setAwaitTerminationSeconds(6015);returnthreadPool;}OverridepublicExecutorgetAsyncExecutor(){returnasyncExecutor;}}配置自定义的TaskExecutor(建议采用方式)线程池参数配置,多个线程池实现线程池隔离,Async注解,默认使用系统自定义线程池,可在项目中设置多个线程池,在异步调用的时候,指明需要调用的线程池名称,比如:Async(taskName)author:jacklinsince:202151811:44EnableAsyncConfigurationpublicclassTaskPoolConfig{异步导出author:jacklinsince:2022111617:41Bean(taskExecutor)publicExecutortaskExecutor(){返回可用处理器的Java虚拟机的数量12intiRuntime。getRuntime()。availableProcessors();System。out。println(系统最大线程数:i);ThreadPoolTaskExecutorexecutornewThreadPoolTaskExecutor();核心线程池大小executor。setCorePoolSize(16);最大线程数executor。setMaxPoolSize(20);配置队列容量,默认值为Integer。MAXVALUEexecutor。setQueueCapacity(99999);活跃时间executor。setKeepAliveSeconds(60);线程名字前缀executor。setThreadNamePrefix(asyncServiceExecutor);设置此执行程序应该在关闭时阻止的最大秒数,以便在容器的其余部分继续关闭之前等待剩余的任务完成他们的执行executor。setAwaitTerminationSeconds(60);等待所有的任务结束后再关闭线程池executor。setWaitForTasksToCompleteOnShutdown(true);returnexecutor;}}多个线程池(线程池隔离)
Async注解,使用系统默认或者自定义的线程池(代替默认线程池)。可在项目中设置多个线程池,在异步调用时,指明需要调用的线程池名称,如Async(newtaskName)。四、解决Java行业常见业务开发数值计算丢失精度问题
一直以来我都会负责公司有关订单模块的项目开发,时常会面对各种金额的计算,在开发的过程中需要注意防止计算精度丢失的问题,今天我说说数值计算的精度、舍入和溢出问题,出于总结,也希望可以为一些读者闭坑。危险的Double
我们先从简单的反直觉的四则运算看起。对几个简单的浮点数进行加减乘除运算:System。out。println(0。10。2);System。out。println(1。00。8);System。out。println(4。015100);System。out。println(123。3100);doubleamount12。15;doubleamount21。10;if(amount1amount21。05)System。out。println(OK);
结果输出如下:0。300000000000000040。19999999999999996401。499999999999941。2329999999999999
可以看到,输出结果和我们预期的很不一样。比如,0。10。2输出的不是0。3而是0。30000000000000004;再比如,对2。151。10和1。05判等,结果判等不成立,出现这种问题的主要原因是,计算机是以二进制存储数值的,浮点数也不例外,对于计算机而言,0。1无法精确表达,这是浮点数计算造成精度损失的根源。
很多人可能会说,以0。1为例,其十进制和二进制间转换后相差非常小,不会对计算产生什么影响。但,所谓积土成山,如果大量使用double来作大量的金钱计算,最终损失的精度就是大量的资金出入。比如,每天有一百万次交易,每次交易都差一分钱,一个月下来就差30万。这就不是小事儿了。那,如何解决这个问题呢?BigDecimal类型
我们大都听说过BigDecimal类型,浮点数精确表达和运算的场景,一定要使用这个类型。不过,在使用BigDecimal时有几个坑需要避开。我们用BigDecimal把之前的四则运算改一下:System。out。println(newBigDecimal(0。1)。add(newBigDecimal(0。2)));System。out。println(newBigDecimal(1。0)。subtract(newBigDecimal(0。8)));System。out。println(newBigDecimal(4。015)。multiply(newBigDecimal(100)));System。out。println(newBigDecimal(123。3)。pide(newBigDecimal(100)));
输出如下:0。30000000000000001665334536937734810635447502136230468750。1999999999999999555910790149937383830547332763671875401。499999999999968025576890795491635799407958984375001。232999999999999971578290569595992565155029296875
可以看到,运算结果还是不精确,只不过是精度高了而已。这里给出浮点数运算避坑第一原则:使用BigDecimal表示和计算浮点数,且务必使用字符串的构造方法来初始化BigDecimal:System。out。println(newBigDecimal(0。1)。add(newBigDecimal(0。2)));System。out。println(newBigDecimal(1。0)。subtract(newBigDecimal(0。8)));System。out。println(newBigDecimal(4。015)。multiply(newBigDecimal(100)));System。out。println(newBigDecimal(123。3)。pide(newBigDecimal(100)));
改进后,就得到我们想要的输出结果了:0。30。2401。5001。233数值判断
现在我们知道了,应该使用BigDecimal来进行浮点数的表示、计算、格式化。Java中的原则:包装类的比较要通过equals进行,而不能使用。那么,使用equals方法对两个BigDecimal判等,一定能得到我们想要的结果吗?比如:System。out。println(newBigDecimal(1。0)。equals(newBigDecimal(1)));
答案是:false,为什么呢?BigDecimal的equals方法的注释中说明了原因,equals比较的是BigDecimal的value和scale,1。0的scale是1,1的scale是0,所以结果一定是false。
如果我们希望只比较BigDecimal的value,可以使用compareTo方法,修改代码如下:System。out。println(newBigDecimal(1。0)。compareTo(newBigDecimal(1))0);
输出结果是:true解决方案,自定义ArithmeticUtils工具类,用于高精度处理常用的数学运算packageio。halo。payment。utils;importjava。math。BigDecimal;importjava。math。RoundingMode;用于高精度处理常用的数学运算author:austinsince:2022122022:54publicclassArithmeticUtils{默认除法运算精度privatestaticfinalintDIVSCALE10;加法运算paramvar1被加数paramvar2加数publicstaticdoubleadd(doublevar1,doublevar2){BigDecimalb1newBigDecimal(Double。toString(var1));BigDecimalb2newBigDecimal(Double。toString(var2));returnb1。add(b2)。doubleValue();}加法运算paramvar1被加数paramvar2加数publicstaticBigDecimaladd(Stringvar1,Stringvar2){BigDecimalb1newBigDecimal(var1);BigDecimalb2newBigDecimal(var2);returnb1。add(b2);}加法运算paramvar1被加数paramvar2加数paramscale保留scale位小数publicstaticStringadd(Stringvar1,Stringvar2,intscale){if(scale0){thrownewIllegalArgumentException(Thescalemustbeapositiveintegerorzero);}BigDecimalb1newBigDecimal(var1);BigDecimalb2newBigDecimal(var2);returnb1。add(b2)。setScale(scale,RoundingMode。HALFUP)。toString();}减法运算paramvar1被减数paramvar2减数publicstaticdoublesub(doublevar1,doublevar2){BigDecimalb1newBigDecimal(Double。toString(var1));BigDecimalb2newBigDecimal(Double。toString(var2));returnb1。subtract(b2)。doubleValue();}减法运算paramvar1被减数paramvar2减数publicstaticBigDecimalsub(Stringvar1,Stringvar2){BigDecimalb1newBigDecimal(var1);BigDecimalb2newBigDecimal(var2);returnb1。subtract(b2);}减法运算paramvar1被减数paramvar2减数paramscale保留scale位小数publicstaticStringsub(Stringvar1,Stringvar2,intscale){if(scale0){thrownewIllegalArgumentException(Thescalemustbeapositiveintegerorzero);}BigDecimalb1newBigDecimal(var1);BigDecimalb2newBigDecimal(var2);returnb1。subtract(b2)。setScale(scale,RoundingMode。HALFUP)。toString();}乘法运算paramvar1被乘数paramvar2乘数publicstaticdoublemul(doublevar1,doublevar2){BigDecimalb1newBigDecimal(Double。toString(var1));BigDecimalb2newBigDecimal(Double。toString(var2));returnb1。multiply(b2)。doubleValue();}乘法运算paramvar1被乘数paramvar2乘数publicstaticBigDecimalmul(Stringvar1,Stringvar2){BigDecimalb1newBigDecimal(var1);BigDecimalb2newBigDecimal(var2);returnb1。multiply(b2);}乘法运算paramvar1被乘数paramvar2乘数paramscale保留scale位小数publicstaticdoublemul(doublevar1,doublevar2,intscale){BigDecimalb1newBigDecimal(Double。toString(var1));BigDecimalb2newBigDecimal(Double。toString(var2));returnround(b1。multiply(b2)。doubleValue(),scale);}乘法运算paramvar1被乘数paramvar2乘数paramscale保留scale位小数publicstaticStringmul(Stringvar1,Stringvar2,intscale){if(scale0){thrownewIllegalArgumentException(Thescalemustbeapositiveintegerorzero);}BigDecimalb1newBigDecimal(var1);BigDecimalb2newBigDecimal(var2);returnb1。multiply(b2)。setScale(scale,RoundingMode。HALFUP)。toString();}提供(相对)精确的除法运算,当发生除不尽的情况时,精确到小数点以后10位,以后的数字四舍五入paramvar1被除数paramvar2除数publicstaticdoublep(doublevar1,doublevar2){returnp(var1,var2,DIVSCALE);}提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指定精度,以后的数字四舍五入paramvar1被除数paramvar2除数paramscale表示表示需要精确到小数点以后几位。publicstaticdoublep(doublevar1,doublevar2,intscale){if(scale0){thrownewIllegalArgumentException(Thescalemustbeapositiveintegerorzero);}BigDecimalb1newBigDecimal(Double。toString(var1));BigDecimalb2newBigDecimal(Double。toString(var2));returnb1。pide(b2,scale,RoundingMode。HALFUP)。doubleValue();}提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指定精度,以后的数字四舍五入paramvar1被除数paramvar2除数paramscale表示需要精确到小数点以后几位publicstaticStringp(Stringvar1,Stringvar2,intscale){if(scale0){thrownewIllegalArgumentException(Thescalemustbeapositiveintegerorzero);}BigDecimalb1newBigDecimal(var1);BigDecimalb2newBigDecimal(var2);returnb1。pide(b2,scale,RoundingMode。HALFUP)。toString();}提供精确的小数位四舍五入处理paramvar需要四舍五入的数字paramscale小数点后保留几位publicstaticdoubleround(doublevar,intscale){if(scale0){thrownewIllegalArgumentException(Thescalemustbeapositiveintegerorzero);}BigDecimalbnewBigDecimal(Double。toString(var));returnb。setScale(scale,RoundingMode。HALFUP)。doubleValue();}提供精确的小数位四舍五入处理paramvar需要四舍五入的数字paramscale小数点后保留几位publicstaticStringround(Stringvar,intscale){if(scale0){thrownewIllegalArgumentException(Thescalemustbeapositiveintegerorzero);}BigDecimalbnewBigDecimal(var);returnb。setScale(scale,RoundingMode。HALFUP)。toString();}比较大小paramvar1被比较数paramvar2比较数return如果v1大于v2则返回true否则falsepublicstaticbooleancompare(Stringvar1,Stringvar2){BigDecimalb1newBigDecimal(var1);BigDecimalb2newBigDecimal(var2);intresultb1。compareTo(b2);returnresult0?true:false;}}五、HutoolTreeUtil快速构造返回树形结构
项目中经常会遇到各种需要以树形结构展示的功能,如菜单树、分类树、部门树,Hutool的TreeUtil主要是用来快速构造树形结构,以及获取所有叶子节点等操作。步骤:
1引入hutool最新pom包。
2获取构造树的分类数据。
3TreeNodeConfig信息配置,配置节点名称、孩子节点key信息、排序等等。
4调用TreeUtil。build()构造树。pom依赖dependencygroupIdcn。hutoolgroupIdhutoolallartifactIdversion5。7。22versiondependency资料分类Service接口层构造班型资料分类树方法author:jacklindate:202242016:44ListTreeStringconstructTree();实现层OverridepublicListTreeStringconstructTree(){1。获取所有资料分类ListSdSchoolClassTypeDataCategorydataListthis。lambdaQuery()。getBaseMapper()。selectList(Wrappers。lambdaQuery(SdSchoolClassTypeDataCategory。class)。eq(SdSchoolClassTypeDataCategory::getStatus,SchoolConstant。ENABLESTATUS)。eq(SdSchoolClassTypeDataCategory::getDeleted,SchoolConstant。DELETESTATUSNORMAL));2。配置TreeNodeConfigconfignewTreeNodeConfig();config。setIdKey(id);默认id,可以不设置config。setParentIdKey(pid);父idconfig。setNameKey(dataCategoryName);分类名称config。setDeep(3);最大递归深度config。setChildrenKey(childrenList);孩子节点config。setWeightKey(sort);排序字段3。转树ListTreeStringtreeListTreeUtil。build(dataList,0,config,((object,treeNode){treeNode。putExtra(id,object。getId());treeNode。putExtra(pid,object。getPid());treeNode。putExtra(dataCategoryName,object。getDataCategoryName());treeNode。putExtra(level,object。getLevel());treeNode。putExtra(sort,object。getSort());扩展属性。。。}));returntreeList;}
通过TreeNodeConfig我们可以自定义节点的名称、关系节点id名称,这样就可以和不同的数据库做对应。Controller层获取构造树author:jacklindate:202242017:18ApiOperation(value获取构造树,notes获取构造树)GetMapping(valuegetConstructTree)publicResultlt;?getConstructTree(){ListTreeStringtreeListsdSchoolClassTypeDataCategoryService。constructTree();returnResult。OK(treeList);}响应内容{success:true,message:操作成功!,code:200,result:〔{id:1447031605584797698,pid:0,dataCategoryName:开发测试资料一级分类,level:1,sort:1,childrenList:〔{id:1447031722601684993,pid:1447031605584797698,dataCategoryName:开发测试资料二级分类,level:2,sort:1,childrenList:〔{id:1516684508672299010,pid:1447031722601684993,dataCategoryName:开发测试资料三级分类,level:3,sort:1}〕}〕},{id:1447849327826636801,pid:0,dataCategoryName:测试资料分类,level:1,sort:1,childrenList:〔{id:1447849471787732993,pid:1447849327826636801,dataCategoryName:测试资料分类21,level:2,sort:1},{id:1447849472085528577,pid:1447849327826636801,dataCategoryName:测试资料分类22,level:2,sort:1},{id:1447849472219746305,pid:1447849327826636801,dataCategoryName:测试资料分类23,level:2,sort:1}〕}〕}Hutool树结构工具TreeUtil六、事务Transactional的失效场景
6。1失效场景集一:代理不生效
Spring中注解解析的尿性都是基于代理来实现的,如果目标方法无法被Spring代理到,那么它将无法被Spring进行事务管理。
Spring生成代理的方式有两种:基于接口的JDK动态代理,要求目标代理类需要实现一个接口才能被代理基于实现目标类子类的CGLIB代理
以下情况会因为代理不生效导致事务管控失败:
(1)将注解标注在接口方法上
Transactional是支持标注在方法与类上的。一旦标注在接口上,对应接口实现类的代理方式如果是CGLIB,将通过生成子类的方式生成目标类的代理,将无法解析到Transactional,从而事务失效。
这种错误我们还是犯得比较少的,基本上我们都会将注解标注在接口的实现类方法上,官方也不推荐这种。
(2)被final、static关键字修饰的类或方法
CGLIB是通过生成目标类子类的方式生成代理类的,被final、static修饰后,无法继承父类与父类的方法。
(3)类方法内部调用
事务的管理是通过代理执行的方式生效的,如果是方法内部调用,将不会走代理逻辑,也就调用不到了。
(4)当前类没有被Spring管理
这个没什么好说的,都没有被Spring管理成为IOC容器中的一个bean,更别说被事务切面代理到了。6。2失效场景集二:框架或底层不支持的功能
这类失效场景主要聚焦在框架本身在解析Transactional时的内部支持。如果使用的场景本身就是框架不支持的,那事务也是无法生效的。
(1)非public修饰的方法
不支持非public修饰的方法进行事务管理。
(2)多线程调用
事务信息是跟线程绑定的。因此在多线程环境下,事务的信息都是独立的,将会导致Spring在接管事务上出现差异。
(3)数据库本身不支持事务
比如MySQL的Myisam存储引擎是不支持事务的,只有innodb存储引擎才支持。
这个问题出现的概率极其小,因为MySQL5之后默认情况下是使用innodb存储引擎了。
但如果配置错误或者是历史项目,发现事务怎么配都不生效的时候,记得看看存储引擎本身是否支持事务。
(4)未开启事务
这个也是一个比较麻烦的问题,在SpringBoot项目中已经不存在了,已经有DataSourceTransactionManagerAutoConfiguration默认开启了事务管理。
但是在MVC项目中还需要在applicationContext。xml文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。6。3失效场景集撒三:错误的使用Trasactional
日常开发我们最常犯的错误的可能因为配置不正确,导致方法上的事务没生效,回滚失败!
(1)错误的传播机制
Spring支持了7种传播机制,分别为:
事务行为
说明
REQUIRED(Spring默认的事务传播类型)
如果当前没有事务,则自己新建一个事务,如果当前存在事务则加入这个事务
SUPPORTS
当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
MANDATORY
当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常
REQUIRESNEW
创建一个新事务,如果存在当前事务,则挂起该事务
NOTSUPPORTED
以非事务方式执行,如果当前存在事务,则挂起当前事务
NEVER
如果当前没有事务存在,就以非事务方式执行;如果有,就抛出异常。就是B从不以事务方式运行A中不能有事务,如果没有,B就以非事务方式执行,如果A存在事务,那么直接抛异常
NESTED(嵌套的)
如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)如果A中没有事务,那么B创建一个事务执行,如果A中也有事务,那么B会会把事务嵌套在里面
上面不支持事务的传播机制为:SUPPORTS,NOTSUPPORTED,NEVER。
如果配置了这三种传播方式的话,在发生异常的时候,事务是不会回滚的。
(2)rollbackFor属性设置错误
默认情况下事务仅回滚运行时异常和Error,不回滚受检异常(例如IOException)。
因此如果方法中抛出了IO异常,默认情况下事务也会回滚失败。
我们可以通过指定Transactional(rollbackForException。class)的方式进行全异常捕获。
(3)异常被程序内部catch
如果需要对特定的异常进行捕获处理,记得再次将异常抛出,让最外层的事务感知到。
(4)嵌套事务七、SpringEvent实现异步,业务解耦神器
实际业务开发过程中,业务逻辑可能非常复杂,核心业务N个子业务。如果都放到一块儿去做,代码可能会很长,耦合度不断攀升,维护起来也麻烦,甚至头疼。还有一些业务场景不需要在一次请求中同步完成,比如邮件发送、短信发送等。
MQ确实可以解决这个问题,但MQ相对来说比较重,非必要不提升架构复杂度。针对这些问题,我们了解一下SpringEvent。7。1自定义事件
定义事件,继承ApplicationEvent的类成为一个事件类:publicclassAsyncSendEmailEventextendsApplicationEvent{邮箱privateStringemail;主题privateStringsubject;内容privateStringcontent;接收者privateStringtargetUserId;}7。2定义事件监听器Slf4jComponentpublicclassAsyncSendEmailEventListenerimplementsApplicationListener{AutowiredprivateIMessageHandlermesageHandler;Async(taskExecutor)OverridepublicvoidonApplicationEvent(AsyncSendEmailEventevent){if(eventnull){return;}Stringemailevent。getEmail();Stringsubjectevent。getSubject();Stringcontentevent。getContent();StringtargetUserIdevent。getTargetUserId();mesageHandler。sendsendEmailSms(email,subject,content,targerUserId);}}7。3开启异步启动类增加EnableAsync注解Listener类需要开启异步的方法增加Async注解
另外,可能有些时候采用ApplicationEvent实现异步的使用,当程序出现异常错误的时候,需要考虑补偿机制,那么这时候可以结合SpringRetry重试来帮助我们避免这种异常造成数据不一致问题。八、业务开发中通用的策略模式模板
在策略模式(StrategyPattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。业务背景
商场搞活动,根据客户购买商品的金额,收费时给与不同的打折,比如,购买金额2000的打八折(0。8),金额5001000的,打九折(0。9),购买金额0500的九五折(0。95),根据不同的金额走不同计算策略逻辑。首先定义一个Strategy接口来表示一个策略:publicinterfaceStrategy{采用策略Stringstrategy();计算方法逻辑voidalgorithm();}
其中strategy方法返回当前策略的唯一标识,algorithm则是该策略的具体执行的计算逻辑。
下面是Strategy接口的两个实现类:publicclassConcreteStrategyAimplementsStrategy{OverridepublicStringstrategy(){returnStrategySelector。strategyA。getStrategy();}Overridepublicvoidalgorithm(){System。out。println(processwithstrategyA。。。);}}publicclassConcreteStrategyBimplementsStrategy{OverridepublicStringstrategy(){returnStrategySelector。strategyB。getStrategy();}Overridepublicvoidalgorithm(){System。out。println(processwithstrategyB。。。);}}publicclassConcreteStrategyCimplementsStrategy{OverridepublicStringstrategy(){returnStrategySelector。strategyC。getStrategy();}Overridepublicvoidalgorithm(){System。out。println(processwithstrategyC。。。);}}自定义策略选择枚举StrategySelector:GetterpublicenumStrategySelector{strategyA(1,strategyA),strategyB(2,strategyB),strategyC(3,strategyC);privateIntegercode;privateStringstrategy;StrategySelector(Integercode,Stringstrategy){this。codecode;this。strategystrategy;}}
然后定义一个StrategyRunner接口用来表示策略的调度器:publicinterfaceStrategyRunner{voidexecute(Stringstrategy);}
execute方法内部通过判断strategy的值来决定具体执行哪一个策略。publicclassStrategyRunnerImplimplementsStrategyRunner{privatestaticfinalListStrategySTRATEGIESArrays。asList(newConcreteStrategyA(),newConcreteStrategyB(),newConcreteStrategyC());privatestaticMapString,StrategySTRATEGYMAPMaps。newHashMap();static{STRATEGYMAPSTRATEGIES。stream()。collect(Collectors。toMap(Strategy::strategy,ss));}Overridepublicvoidexecute(Stringstrategy){STRATEGYMAP。get(strategy)。algorithm();}}
在StrategyRunnerImpl内部,定义了一个STRATEGIES列表来保存所有Strategy实现类的实例,以及一个叫做STRATEGYMAP的Map来保存strategy和Strategy实例之间的对应关系,static块中的代码用于从STRATEGIES列表构造STRATEGYMAP。这样,在execute方法中就可以很方便地获取到指定strategy的Strategy实例。SpringBoot项目中实现并运用策略模式ComponentpublicclassConcreteStrategyAimplementsStrategy{OverridepublicStringstrategy(){returnStrategySelector。strategyA。getStrategy();}Overridepublicvoidalgorithm(){System。out。println(processwithstrategyA。。。);}}ComponentpublicclassConcreteStrategyBimplementsStrategy{OverridepublicStringstrategy(){returnStrategySelector。strategyB。getStrategy();}Overridepublicvoidalgorithm(){System。out。println(processwithstrategyB。。。);}}ComponentpublicclassConcreteStrategyCimplementsStrategy{OverridepublicStringstrategy(){returnStrategySelector。strategyC。getStrategy();}Overridepublicvoidalgorithm(){System。out。println(processwithstrategyC。。。);}}复制代码
然后,定义一个StrategyConfig配置类,用于向容器注入一个StrategyRunner:ConfigurationpublicclassStrategyConfig{BeanpublicStrategyRunnerrunner(ListStrategystrategies){MapString,StrategystrategyMapstrategies。stream()。collect(Collectors。toMap(Strategy::strategy,ss));returnflagstrategyMap。get(flag)。algorithm();}}
不难发现,strategyRunner方法的实现,其中的逻辑与之前的StrategyRunnerImpl几乎完全相同,也是根据一个List来构造一个MapString,Strategy。只不过,这里的strategies列表不是我们自己构造的,而是通过方法参数传进来的。由于strategyRunner标注了Bean注解,因此参数上的List实际上是在SpringBoot初始化过程中从容器获取的,所以我们之前向容器中注册的那两个实现类会在这里被注入。
这样,我们再也无需操心系统中一共有多少个Strategy实现类,因为SpringBoot的自动配置会帮我们自动发现所有实现类。我们只需编写自己的Strategy实现类,然后将它注册进容器,并在任何需要的地方注入StrategyRunner:AutowiredprivateStrategyRunnerstrategyRunner;
然后直接使用strategyRunner就行了:RestControllerRequestMapping(valuedesignPatterns)publicclassDesignPatternController{AutowiredprivateStrategyRunnerstrategyRunner;GetMapping(valuealgorithm)publicvoidalgorithm(RequestParam(strategy)Stringstrategy){strategyRunner。execute(strategy);}}
访问:http:localhost:10069designPatternsalgorithm控制台输出如下:processwithstrategyA。。。
类似的业务场景,完全可以结合业务通过方面的代码来进行改造实现,非常实用九、使用ip2region获取用户地址信息
ip2regionv2。0是一个离线IP地址定位库和IP定位数据管理框架。
现在很多软件比如:微博、抖音、小红书、头条、快手、腾讯等各大平台陆续都上线了网络用户IP地址显示功能,境外用户显示的是国家,国内的用户显示的省份。
以往,Java中获取IP属性的,主要分为以下几步:通过HttpServletRequest对象,获取用户的IP地址通过IP地址,获取对应的省份、城市
首先需要写一个IP获取的工具类,因为每一次用户的Request请求,都会携带上请求的IP地址放到请求头中,下面这段代码你肯定不陌生:获取IP地址使用Nginx等反向代理软件,则不能通过request。getRemoteAddr()获取IP地址如果使用了多级反向代理的话,XForwardedFor的值并不止一个,而是一串IP地址,XForwardedFor中第一个非unknown的有效IP字符串,则为真实IP地址publicstaticStringgetIpAddr(HttpServletRequestrequest){Stringipnull;try{iprequest。getHeader(xforwardedfor);if(StringUtils。isEmpty(ip)unknown。equalsIgnoreCase(ip)){iprequest。getHeader(ProxyClientIP);}if(StringUtils。isEmpty(ip)ip。length()0unknown。equalsIgnoreCase(ip)){iprequest。getHeader(WLProxyClientIP);}if(StringUtils。isEmpty(ip)unknown。equalsIgnoreCase(ip)){iprequest。getHeader(HTTPCLIENTIP);}if(StringUtils。isEmpty(ip)unknown。equalsIgnoreCase(ip)){iprequest。getHeader(HTTPXFORWARDEDFOR);}if(StringUtils。isEmpty(ip)unknown。equalsIgnoreCase(ip)){iprequest。getHeader(XREALIP);}if(StringUtils。isEmpty(ip)unknown。equalsIgnoreCase(ip)){iprequest。getRemoteAddr();}}catch(Exceptione){logger。error(FailedtogettheIPaddressinformation,e);}return0:0:0:0:0:0:0:1。equals(ip)?127。0。0。1:ip;}
通过此方法,从请求Header中获取到用户的IP地址。
还有之前的的项目获取IP地址归属省份、城市的需求,比较常用的是淘宝ip库,地址:
ip。taobao。com
输入本地IP地址可以查询到对应的省市信息:
模拟根据ip从淘宝IP库获取当前位置信息,源码如下:publicstaticJSONObjectgetAddressByIp(Stringip,RestTemplaterestTemplate){logger。info(淘宝IP库获取用户IP地址信息。。。);ResponseEntityStringforEntityrestTemplate。getForEntity(https:ip。taobao。comoutGetIpInfo?ipip,String。class);JSONObjectresultJSONObject。parseObject(forEntity。getBody());logger。info(获取到淘宝IP库响应信息:{},result);if(result。getIntValue(code)0){logger。info(requestsuccessful!);}else{logger。info(requestfailed,原因:{},result。getString(msg));}returngetAddressByIp(ip,restTemplate);}publicstaticvoidmain(String〔〕args){getAddressByIp(119。129。116。64,newRestTemplate());}
响应:11:14:53。266〔main〕INFOorg。universal。common。util。IPUtils淘宝IP库获取用户IP地址信息。。。11:14:55。063〔main〕DEBUGorg。springframework。web。client。RestTemplateHTTPGEThttps:ip。taobao。comoutGetIpInfo?ip119。129。116。6411:14:55。107〔main〕DEBUGorg。springframework。web。client。RestTemplateAccept〔textplain,applicationjson,applicationjson,〕11:14:57。416〔main〕DEBUGorg。springframework。web。client。RestTemplateResponse200OK11:14:57。418〔main〕DEBUGorg。springframework。web。client。RestTemplateReadingto〔java。lang。String〕asapplicationjson;charsetUTF811:14:58。273〔main〕INFOorg。universal。common。util。IPUtils获取到淘宝IP库响应信息:{msg:therequestovermaxqpsforuser,theaccessKeypublic,code:4}11:14:58。522〔main〕INFOorg。universal。common。util。IPUtilsrequestfailed,原因:therequestovermaxqpsforuser,theaccessKeypublic11:14:58。522〔main〕INFOorg。universal。common。util。IPUtils淘宝IP库获取用户IP地址信息。。。11:14:58。522〔main〕DEBUGorg。springframework。web。client。RestTemplateHTTPGEThttps:ip。taobao。comoutGetIpInfo?ip119。129。116。6411:14:58。523〔main〕DEBUGorg。springframework。web。client。RestTemplateAccept〔textplain,applicationjson,applicationjson,〕11:14:58。657〔main〕DEBUGorg。springframework。web。client。RestTemplateResponse200OK11:14:58。657〔main〕DEBUGorg。springframework。web。client。RestTemplateReadingto〔java。lang。String〕asapplicationjson;charsetUTF8成功获取到ip地址信息(中国广东广州)START11:14:58。658〔main〕INFOorg。universal。common。util。IPUtils获取到淘宝IP库响应信息:{msg:querysuccess,code:0,data:{area:,country:中国,ispid:100017,queryIp:119。129。116。64,city:广州,ip:119。129。116。64,isp:电信,county:,regionid:440000,areaid:,region:广东,countryid:CN,cityid:440100}}11:14:58。658〔main〕INFOorg。universal。common。util。IPUtilsrequestsuccessful!11:14:58。658〔main〕INFOorg。universal。common。util。IPUtils淘宝IP库获取用户IP地址信息。。。11:14:58。681〔main〕DEBUGorg。springframework。web。client。RestTemplateHTTPGEThttps:ip。taobao。comoutGetIpInfo?ip119。129。116。6411:14:58。682〔main〕DEBUGorg。springframework。web。client。RestTemplateAccept〔textplain,applicationjson,applicationjson,〕11:14:58。802〔main〕DEBUGorg。springframework。web。client。RestTemplateResponse200OK11:14:58。803〔main〕DEBUGorg。springframework。web。client。RestTemplateReadingto〔java。lang。String〕asapplicationjson;charsetUTF8成功获取到ip地址信息END11:14:58。805〔main〕INFOorg。universal。common。util。IPUtils获取到淘宝IP库响应信息:{msg:therequestovermaxqpsforuser,theaccessKeypublic,code:4}11:14:58。805〔main〕INFOorg。universal。common。util。IPUtilsrequestfailed,原因:therequestovermaxqpsforuser,theaccessKeypublic11:14:58。805〔main〕INFOorg。universal。common。util。IPUtils淘宝IP库获取用户IP地址信息。。。11:14:58。806〔main〕DEBUGorg。springframework。web。client。RestTemplateHTTPGEThttps:ip。taobao。comoutGetIpInfo?ip119。129。116。6411:14:58。806〔main〕DEBUGorg。springframework。web。client。RestTemplateAccept〔textplain,applicationjson,applicationjson,〕11:14:58。947〔main〕DEBUGorg。springframework。web。client。RestTemplateResponse200OK11:14:58。976〔main〕DEBUGorg。springframework。web。client。RestTemplateReadingto〔java。lang。String〕asapplicationjson;charsetUTF811:14:58。981〔main〕INFOorg。universal。common。util。IPUtils获取到淘宝IP库响应信息:{msg:therequestovermaxqpsforuser,theaccessKeypublic,code:4}11:14:58。981〔main〕INFOorg。universal。common。util。IPUtilsrequestfailed,原因:therequestovermaxqpsforuser,theaccessKeypublic11:14:59。092〔main〕INFOorg。universal。common。util。IPUtils淘宝IP库获取用户IP地址信息。。。11:14:59。092〔main〕DEBUGorg。springframework。web。client。RestTemplateHTTPGEThttps:ip。taobao。comoutGetIpInfo?ip119。129。116。6411:14:59。092〔main〕DEBUGorg。springframework。web。client。RestTemplateAccept〔textplain,applicationjson,applicationjson,〕11:14:59。223〔main〕DEBUGorg。springframework。web。client。RestTemplateResponse200OK11:14:59。223〔main〕DEBUGorg。springframework。web。client。RestTemplateReadingto〔java。lang。String〕asapplicationjson;charsetUTF811:14:59。223〔main〕INFOorg。universal。common。util。IPUtils获取到淘宝IP库响应信息:{msg:therequestovermaxqpsforuser,theaccessKeypublic,code:4}11:14:59。223〔main〕INFOorg。universal。common。util。IPUtilsrequestfailed,原因:therequestovermaxqpsforuser,theaccessKeypublic11:14:59。320〔main〕INFOorg。universal。common。util。IPUtils淘宝IP库获取用户IP地址信息。。。11:14:59。321〔main〕DEBUGorg。springframework。web。client。RestTemplateHTTPGEThttps:ip。taobao。comoutGetIpInfo?ip119。129。116。6411:14:59。321〔main〕DEBUGorg。springframework。web。client。RestTemplateAccept〔textplain,applicationjson,applicationjson,〕11:14:59。470〔main〕DEBUGorg。springframework。web。client。RestTemplateResponse200OK11:14:59。471〔main〕DEBUGorg。springframework。web。client。RestTemplateReadingto〔java。lang。String〕asapplicationjson;charsetUTF811:14:59。471〔main〕INFOorg。universal。common。util。IPUtils获取到淘宝IP库响应信息:{msg:therequestovermaxqpsforuser,theaccessKeypublic,code:4}11:14:59。471〔main〕INFOorg。universal。common。util。IPUtilsrequestfailed,原因:therequestovermaxqpsforuser,theaccessKeypublic11:14:59。471〔main〕INFOorg。universal。common。util。IPUtils淘宝IP库获取用户IP地址信息。。。11:14:59。472〔main〕DEBUGorg。springframework。web。client。RestTemplateHTTPGEThttps:ip。taobao。comoutGetIpInfo?ip119。129。116。6411:14:59。472〔main〕DEBUGorg。springframework。web。client。RestTemplateAccept〔textplain,applicationjson,applicationjson,〕11:14:59。598〔main〕DEBUGorg。springframework。web。client。RestTemplateResponse200OK11:14:59。598〔main〕DEBUGorg。springframework。web。client。RestTemplateReadingto〔java。lang。String〕asapplicationjson;charsetUTF811:14:59。598〔main〕INFOorg。universal。common。util。IPUtils获取到淘宝IP库响应信息:{msg:therequestovermaxqpsforuser,theaccessKeypublic,code:4}11:14:59。599〔main〕INFOorg。universal。common。util。IPUtilsrequestfailed,原因:therequestovermaxqpsforuser,theaccessKeypublic
可以看到控制台输出的日志文件中,大量的请求返回失败,原因:therequestovermaxqpsforuser,theaccessKeypublic,主要是由于接口淘宝对接口进行QPS限流。
而随着ip2region项目的开源和更新迭代,可以帮助我们解决IP地址定位解析的业务场景开发需求问题,Gitee地址:ip2region99。9准确率:
数据聚合了一些知名ip到地名查询提供商的数据,这些是他们官方的的准确率,经测试着实比经典的纯真IP定位准确一些。ip2region的数据聚合自以下服务商的开放API或者数据(升级程序每秒请求次数2到4次)。p2regionV2。0特性
1、标准化的数据格式
每个ip数据段的region信息都固定了格式:国家区域省份城市ISP,只有中国的数据绝大部分精确到了城市,其他国家部分数据只能定位到国家,后前的选项全部是0。
2、数据去重和压缩
xdb格式生成程序会自动去重和压缩部分数据,默认的全部IP数据,生成的ip2region。xdb数据库是11MiB,随着数据的详细度增加数据库的大小也慢慢增大。
3、极速查询响应
即使是完全基于xdb文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:vIndex索引缓存:使用固定的512KiB的内存空间缓存vectorindex数据,减少一次IO磁盘操作,保持平均查询效率稳定在1020微秒之间。xdb整个文件缓存:将整个xdb文件全部加载到内存,内存占用等同于xdb文件大小,无磁盘IO操作,保持微秒级别的查询效率。
4、极速查询响应
v2。0格式的xdb支持亿级别的IP数据段行数,region信息也可以完全自定义,例如:你可以在region中追加特定业务需求的数据,例如:GPS信息国际统一地域信息编码邮编等。也就是你完全可以使用ip2region来管理你自己的IP定位数据。ip2regionxdbjavaIP地址信息解析客户端实现
pom依赖dependencygroupIdorg。lionsoulgroupIdip2regionartifactIdversion2。6。4versiondependency
完全基于文件的查询importorg。lionsoul。ip2region。xdb。Searcher;importjava。io。;importjava。util。concurrent。TimeUnit;publicclassSearcherTest{publicstaticvoidmain(String〔〕args){1、创建searcher对象StringdbPathip2region。xdbfilepath;Searchersearchernull;try{searcherSearcher。newWithFileOnly(dbPath);}catch(IOExceptione){System。out。printf(failedtocreatesearcherwiths:s,dbPath,e);return;}2、查询try{Stringip1。2。3。4;longsTimeSystem。nanoTime();Stringregionsearcher。search(ip);longcostTimeUnit。NANOSECONDS。toMicros((long)(System。nanoTime()sTime));System。out。printf({region:s,ioCount:d,took:ds},region,searcher。getIOCount(),cost);}catch(Exceptione){System。out。printf(failedtosearch(s):s,ip,e);}3、关闭资源searcher。close();备注:并发使用,每个线程需要创建一个独立的searcher对象单独使用。}}
IDEA代码实现,测试获取当前IP地址信息:publicclassSearchTest{publicstaticvoidmain(String〔〕args)throwsIOException{1、创建searcher对象StringdbPathD:Sourcetreeworkplacegit优秀开源项目ip2regiondataip2region。xdb;Searchersearchernull;try{searcherSearcher。newWithFileOnly(dbPath);}catch(IOExceptione){System。out。printf(failedtocreatesearcherwiths:s,dbPath,e);return;}本地IP地址Stringip119。129。116。64;2、查询try{longsTimeSystem。nanoTime();Stringregionsearcher。search(ip);longcostTimeUnit。NANOSECONDS。toMicros((long)(System。nanoTime()sTime));System。out。printf({region:s,ioCount:d,took:ds},region,searcher。getIOCount(),cost);}catch(Exceptione){System。out。printf(failedtosearch(s):s,ip,e);}3、关闭资源searcher。close();备注:并发使用,每个线程需要创建一个独立的searcher对象单独使用。}}
完全基于文件的查询
IP属地国内的话,会展示省份,国外的话,只会展示国家。可以通过如下图这个方法进行进一步封装,得到获取IP属地的信息,查询结果如下:
十、利用好Java现有优秀的开发库
俗话说:工欲善其事,必先利其器,好的工具可以达到事半功倍的效果。
一名优秀的技术开发者,往往都能利用现有的资源,利用好市面上优秀的工具包来协助开发,基本上,每个项目里都有一个包,叫做utils。这个包专门承载我们自己项目的工具类,比如常见的DateUtils、HttpUtils、Collections
所谓Utils就是:这个东西我们用得很多,但是原API不够好用,于是我们给它封装为一个比较通用的方法。10。1JAVA常用工具包推荐
工具包
介绍
ApacheCommons
地址
Guava
地址
Hutool
地址
最新maven仓库!apache。commonslang3dependencygroupIdorg。apache。commonsgroupIdcommonslang3artifactIdversion3。12。0versiondependency!google。guavadependencygroupIdcom。google。guavagroupIdguavaartifactIdversion31。1jreversiondependency!hutoolalldependencygroupIdcn。hutoolgroupIdhutoolallartifactIdversion5。8。10versiondependency10。2Http请求远程调用库推荐
HTTP调用是非常常见的,很多公司对外的接口几乎都会提供HTTP调用。比如我们调用百度UNIT智能对话API实现与机器人对话服务,调用各个渠道商发送短信等等等。JDK自带的HttpURLConnection标准库ApacheHTTPComponentsHttpClientOkHttpRetrofitForest10。2。1HttpURLConnection
使用HttpURLConnection发起HTTP请求最大的优点是不需要引入额外的依赖,但是使用起来非常繁琐,也缺乏连接池管理、域名机械控制等特性支持。
使用标准库的最大好处就是不需要引入额外的依赖,但使用起来比较繁琐,就像直接使用JDBC连接数据库那样,需要很多模板代码。来发起一个简单的HTTPPOST请求:publicclassHttpUrlConnectionDemo{publicstaticvoidmain(String〔〕args)throwsIOException{StringurlStringhttps:httpbin。orgpost;StringbodyStringpassword123;URLurlnewURL(urlString);HttpURLConnectionconn(HttpURLConnection)url。openConnection();conn。setRequestMethod(POST);conn。setDoOutput(true);OutputStreamosconn。getOutputStream();os。write(bodyString。getBytes(utf8));os。flush();os。close();if(conn。getResponseCode()HttpURLConnection。HTTPOK){InputStreamisconn。getInputStream();BufferedReaderreadernewBufferedReader(newInputStreamReader(is));StringBuildersbnewStringBuilder();Stringline;while((linereader。readLine())!null){sb。append(line);}System。out。println(响应内容:sb。toString());}else{System。out。println(响应码:conn。getResponseCode());}}}
HttpURLConnection发起的HTTP请求比较原始,基本上算是对网络传输层的一次浅层次的封装;有了HttpURLConnection对象后,就可以获取到输出流,然后把要发送的内容发送出去;再通过输入流读取到服务器端响应的内容;最后打印。
不过HttpURLConnection不支持HTTP2。0,为了解决这个问题,Java9的时候官方的标准库增加了一个更高级别的HttpClient,再发起POST请求就显得高大上多了,不仅支持异步,还支持顺滑的链式调用。publicclassHttpClientDemo{publicstaticvoidmain(String〔〕args)throwsURISyntaxException{HttpClientclientHttpClient。newHttpClient();HttpRequestrequestHttpRequest。newBuilder()。uri(newURI(https:postmanecho。compost))。headers(ContentType,textplain;charsetUTF8)。POST(HttpRequest。BodyPublishers。ofString(二哥牛逼))。build();client。sendAsync(request,HttpResponse。BodyHandlers。ofString())。thenApply(HttpResponse::body)。thenAccept(System。out::println)。join();}}10。2。2ApacheHttpComponentsHttpClient
ApacheHttpComponentsHttpClient支持的特性也非常丰富:基于标准、纯净的Java语言,实现了HTTP1。0和HTTP1。1;以可扩展的面向对象的结构实现了HTTP全部的方法;支持加密的HTTPS协议(HTTP通过SSL协议);Request的输出流可以避免流中内容体直接从socket缓冲到服务器;Response的输入流可以有效的从socket服务器直接读取相应内容。10。2。3OkHttp
OkHttp是一个执行效率比较高的HTTP客户端:支持HTTP2。0,当多个请求对应同一个Host地址时,可共用同一个Socket;连接池可减少请求延迟;支持GZIP压缩,减少网络传输的数据大小;支持Response数据缓存,避免重复网络请求;publicclassOkHttpPostDemo{publicstaticfinalMediaTypeJSONMediaType。get(applicationjson;charsetutf8);OkHttpClientclientnewOkHttpClient();Stringpost(Stringurl,Stringjson)throwsIOException{RequestBodybodyRequestBody。create(json,JSON);RequestrequestnewRequest。Builder()。url(url)。post(body)。build();try(Responseresponseclient。newCall(request)。execute()){returnresponse。body()。string();}}publicstaticvoidmain(String〔〕args)throwsIOException{OkHttpPostDemoexamplenewOkHttpPostDemo();Stringjson{name:二哥};Stringresponseexample。post(https:httpbin。orgpost,json);System。out。println(response);}}10。2。4Forest
Forest是一个高层的、极简的声明式HTTP调用API框架。相比于直接使用Httpclient你不再用写一大堆重复的代码了,而是像调用本地方法一样去发送HTTP请求。
Forest就字面意思而言,就是森林的意思。但仔细看可以拆成For和Rest两个单词,也就是为了Rest(Rest为一种基于HTTP的架构风格)。而合起来就是森林,森林由很多树木花草组成(可以理解为各种不同的服务),它们表面上看独立,实则在地下根茎交错纵横、相互连接依存,这样看就有点现代分布式服务化的味道了。最后,这两个单词反过来读就像是Resultful。
Maven依赖dependencygroupIdcom。dtflys。forestgroupIdforestspringbootstarterartifactIdversion1。5。19versiondependency简单请求publicinterfaceMyClient{Request(http:localhost:8080hello)StringsimpleRequest();}
通过Request注解,将上面的MyClient接口中的simpleRequest()方法绑定了一个HTTP请求,其URL为http:localhost:8080hello,并默认使用GET方式,且将请求响应的数据以String的方式返回给调用者。稍微复杂点的请求,需要在请求头设置信息publicinterfaceMyClient{Request(urlhttp:localhost:8080hellouser,headersAccept:textplain)StringsendRequest(Query(uname)Stringusername);}
上面的sendRequest方法绑定的HTTP请求,定义了URL信息,以及把Accept:textplain加到了请求头中,方法的参数Stringusername绑定了注解Query(uname),它的作用是将调用者传入入参username时,自动将username的值加入到HTTP的请求参数uname中。
这段实际产生的HTTP请求如下:GEThttp:localhost:8080hellouser?unamefooHEADER:Accept:textplain请求方法,假设发起post请求,有3种写法:publicinterfaceMyClient{使用Post注解,可以去掉typePOST这行属性Post(http:localhost:8080hello)StringsimplePost1();通过Request注解的type参数指定HTTP请求的方式。Request(urlhttp:localhost:8080hello,typePOST)StringsimplePost2();使用PostRequest注解,和上面效果等价PostRequest(http:localhost:8080hello)StringsimplePost3();}
可以用GetRequest,PostRequest等注解代替Request注解,这样就可以省去写type属性的麻烦了。请求体
在POST和PUT等请求方法中,通常使用HTTP请求体进行传输数据。在Forest中有多种方式设置请求体数据。表单格式
上面使用Body注解的例子用的是普通的表单格式,也就是contentType属性为applicationxwwwformurlencoded的格式,即contentType不做配置时的默认值。
表单格式的请求体以字符串key1value1key2value2。。。key{n}value{n}的形式进行传输数据,其中value都是已经过URLEncode编码过的字符串。contentType属性设置为applicationxwwwformurlencoded即为表单格式,当然不设置的时候默认值也为applicationxwwwformurlencoded,也同样是表单格式。在Body注解的value属性中设置的名称为表单项的key名,而注解所修饰的参数值即为表单项的值,它可以为任何类型,不过最终都会转换为字符串进行传输。Post(urlhttp:localhost:8080user,contentTypeapplicationxwwwformurlencoded,headers{Accept:textplain})StringsendPost(Body(key1)Stringvalue1,Body(key2)Integervalue2,Body(key3)Longvalue3);
调用后产生的结果可能如下:POSThttp:localhost:8080hellouserHEADER:ContentType:applicationxwwwformurlencodedBODY:key1xxxkey21000key39999
当Body注解修饰的参数为一个对象,并注解的value属性不设置任何名称的时候,会将注解所修饰参数值对象视为一整个表单,其对象中的所有属性将按属性名1属性值1属性名2属性值2。。。属性名{n}属性值{n}的形式通过请求体进行传输数据。contentType属性不设置默认为applicationxwwwformurlencoded要以对象作为表达传输项时,其Body注解的value名称不能设置Post(urlhttp:localhost:8080hellouser,headers{Accept:textplain})Stringsend(BodyUseruser);
调用产生的结果如下:POSThttp:localhost:8080hellouserHEADER:ContentType:applicationxwwwformurlencodedBODY:username。。。