纠纷奇闻社交美文家庭
投稿投诉
家庭城市
爱好生活
创业男女
能力餐饮
美文职业
心理周易
母婴奇趣
两性技能
社交传统
新闻范文
工作个人
思考社会
作文职场
家居中考
兴趣安全
解密魅力
奇闻笑话
写作笔记
阅读企业
饮食时事
纠纷案例
初中历史
说说童话
乐趣治疗

分享十条Java后端开发实战经验,干货满满!

7月11日 血海塔投稿
  前沿
  借助本篇文章,旨在对自己在公司、个人项目的实战经验总结,包括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。Eimportcn。afterturn。easypoi。handler。inter。IExcelDataMimportcn。afterturn。easypoi。handler。inter。IExcelMimportlombok。Dimportjavax。validation。constraints。NotBimportjava。io。SDatapublicclassSdSchoolSysUserVerifyimplementsIExcelModel,IExcelDataModel,Serializable{privatestaticfinallongserialVersionUID1L;Excel(name行号)privateIntegerrowNExcel(name错误信息)privateStringerrorM真实姓名Excel(name姓名(必填),width25)NotBlank(message姓名不能为空)privateS部门编码,需要和用户导入模板名称对应Excel(name部门编码(必填),width30)NotBlank(message部门编码不能为空)privateStringdeptOrgC角色编码Excel(name角色编码(必填),width15)NotBlank(message角色编码不能为空)privateStringroleC手机号码Excel(name手机号码(选填),width15)privateS电子邮件Excel(name电子邮件(选填),width15)privateS性别(1:男2:女)Excel(name性别(选填),width15)privateStringsexN工号(选填)Excel(name工号(选填),width15)privateStringworkNo;商户IDprivateIntegertenantId;}1。3基础的导入导出逻辑(数据校验)
  easyPoi导入校验使用起来也很简单,以导入系统优化为例:
  第一步,定义一个检验类SdSchoolSysUserVerify,通过实现IExcelModel、IExcelDataModel,当我们需要输出导入校验错误信息的时候,它们两个就显的很重要了,IExcelModel负责设置错误信息,IExcelDataModel负责设置行号。packagecn。afterturn。easypoi。handler。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{AutowiredprivateISdSchoolCustomerServicesdSchoolCustomerSprivatefinalThreadLocalListSdSchoolBatchPushCustomerVerifythreadLocalnewThreadLocal();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(){returnthreadL}}
  核心代码: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。Cimportcom。alibaba。excel。enums。CellDataTypeEimportcom。alibaba。excel。metadata。GlobalCimportcom。alibaba。excel。metadata。data。ReadCellDimportcom。alibaba。excel。metadata。data。WriteCellDimportcom。alibaba。excel。metadata。property。ExcelContentPimportcom。dragonpass。global。modules。agent。enumreate。Wimportjava。util。O自定义Excel导入导出转换器authorLinbzsince202211249:55publicclassWhetherConverterimplementsConverterInteger{OverridepublicC?supportJavaTypeKey(){returnInteger。}OverridepublicCellDataTypeEnumsupportExcelTypeKey(){returnCellDataTypeEnum。STRING;}导入,文字转数字,是否10OverridepublicIntegerconvertToJavaData(ReadCellD?cellData,ExcelContentPropertycontentProperty,GlobalConfigurationglobalConfiguration)throwsException{IntegerresultWhether。NO。getCode();resultWhether。YES。getDesc()。equals(cellData。getStringValue())?Whether。YES。getCode():Whether。NO。getCode();}导出,数字转文字,10是否OverridepublicWriteCellD?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)privateStringtypeNExcelProperty(value权益扣取后额外扣费(是否),converterWhetherConverter。class)ColumnWidth(24)privateIntegerneedPExcelProperty(value扣费金额)ColumnWidth(12)privateBigDExcelProperty(value是否为默认项(是否),converterWhetherConverter。class)ColumnWidth(24)privateIntegerisDExcelProperty(valueNcode)ColumnWidth(12)privateStringloungeC}DataNoArgsConstructorpublicstaticclassDataCheckResult{ExcelProperty(value结果)privateBooleancheckResultBoolean。TRUE;ExcelProperty(value备注)privateSpublicDataCheckResult(BooleancheckResult,Stringremark){this。checkResultcheckRthis。}}}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)。自定义线程池有如下方式:重新实现接口AsyncC继承AsyncConfigurerS配置由自定义的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();}OverridepublicExecutorgetAsyncExecutor(){returnexecutor();}}继承AsyncConfigurerSupportConfigurationEnableAsyncclassSpringAsyncConfigurerextendsAsyncConfigurerSupport{BeanpublicThreadPoolTaskExecutorasyncExecutor(){ThreadPoolTaskExecutorthreadPoolnewThreadPoolTaskExecutor();threadPool。setCorePoolSize(3);threadPool。setMaxPoolSize(3);threadPool。setWaitForTasksToCompleteOnShutdown(true);threadPool。setAwaitTerminationSeconds(6015);returnthreadP}OverridepublicExecutorgetAsyncExecutor(){returnasyncE}}配置自定义的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);}}多个线程池(线程池隔离)
  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。importjava。math。BigDimportjava。math。RoundingM用于高精度处理常用的数学运算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:}}五、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());扩展属性。。。}));returntreeL}
  通过TreeNodeConfig我们可以自定义节点的名称、关系节点id名称,这样就可以和不同的数据库做对应。Controller层获取构造树author:jacklindate:202242017:18ApiOperation(value获取构造树,notes获取构造树)GetMapping(valuegetConstructTree)publicR?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{邮箱privateS主题privateS内容privateS接收者privateStringtargetUserId;}7。2定义事件监听器Slf4jComponentpublicclassAsyncSendEmailEventListenerimplementsApplicationListener{AutowiredprivateIMessageHandlermesageHAsync(taskExecutor)OverridepublicvoidonApplicationEvent(AsyncSendEmailEventevent){if(eventnull){}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);privateIprivateSStrategySelector(Integercode,Stringstrategy){this。this。}}
  然后定义一个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:AutowiredprivateStrategyRunnerstrategyR
  然后直接使用strategyRunner就行了:RestControllerRequestMapping(valuedesignPatterns)publicclassDesignPatternController{AutowiredprivateStrategyRunnerstrategyRGetMapping(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){Stry{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:}
  通过此方法,从请求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〕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〕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〕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〕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〕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〕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〕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。Simportjava。io。;importjava。util。concurrent。TimeUpublicclassSearcherTest{publicstaticvoidmain(String〔〕args){1、创建searcher对象StringdbPathip2region。Stry{searcherSearcher。newWithFileOnly(dbPath);}catch(IOExceptione){System。out。printf(failedtocreatesearcherwiths:s,dbPath,e);}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。Stry{searcherSearcher。newWithFileOnly(dbPath);}catch(IOExceptione){System。out。printf(failedtocreatesearcherwiths:s,dbPath,e);}本地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。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();Swhile((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,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地址时,可共用同一个S连接池可减少请求延迟;支持GZIP压缩,减少网络传输的数据大小;支持Response数据缓存,避免重复网络请求;publicclassOkHttpPostDemo{publicstaticfinalMediaTypeJSONMediaType。get(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:usernamefoopasswordbarJS。。。

羊了个羊前三季度吸金10亿元单人分红高达3亿元【CNMO新闻】此前不久,一款名为《羊了个羊》的游戏爆火,一夜之间几乎能够在任何地方看到被这款游戏折磨的玩家。当大家沉迷于这款游戏的时候,不少网友也是开始盘算,如此火热的一款游……理想汽车回应王兴减持4。2亿港元港美股占其个人持股很小一部分Tech星球4月4日消息,对于王兴近半个月减持约4。2亿港元理想汽车港美股,理想汽车回应表示,本次交易股票为个人行为,交易占他(王兴)总持股比例很小一部分,不涉及美团持股部分。……远泰丨民宿设计三大原则现在出门旅游大家都不喜欢住酒店了,反而会选择有当地特色的网红民宿。因此如今市面上,商家对于民宿设计的需求越来越多,甚至很多旅游景点的酒店都摘掉牌子,选择以这种更接地气的装修方式……水庆霞谈决赛对阵韩国对手进步很大,但我们会做好准备虎扑02月04日讯中国女足主教练水庆霞在接受《五星体育》采访时表示,决赛的老对手韩国近几年进步很大,球队会做最困难的准备,希望能赢得冠军。水庆霞采访原文:韩国队应该……CBA4消息!小丁复出时间敲定,CBA门口价格80起,联赛动2122赛季,CBA常规赛将于明日16日准点开战,下午16时30分第三场比赛山东高速将迎来北控紫禁勇士的挑战,山东高速的揭幕战丁彦雨航将迎来3年后的首度复出,根据媒体爆出的最新……挤走主唱,替身被吐槽后又后悔,糊掉的飞儿乐队真该学学凤凰传奇韩睿从替代飞儿(詹雯婷),成为飞儿乐队新主唱的那一刻开始,注定要饱受争议。在某档音乐类综艺节目上,丁太升毫不客气评价韩睿:你真的还挺差的。陶喆接过话筒后,对韩睿的评……郭艾伦拒绝和辽宁谈判!撕破脸最后都不让步,直接找体育局遭回绝北京时间8月31日,CBA截止日还剩下最后的几个小时,郭艾伦已经不可能和辽宁队按时达成一致了。因为就在今日,中国篮坛的第一名嘴苏群老师再次在社交媒体上曝光了内幕,实际上郭……0失误的欧洲裸色唇配色公式!欧洲裸色唇色料贴牌哪家好?2022年,欧洲唇技术进入爆发式增长阶段,从2月一直火到4月,整整3个月时间,客流与营收同步增长。绣芳院某合作品牌商,开设欧洲唇技术教学班及销售相关产品,从一个月收入10……季后赛场次最多,生涯却无一冠的十大球星(15名),现役一人上文书我们盘点了季后赛场次最多但无缘总冠军的第10到6名的球员,可以看到这些球员都是NBA历史上非常优秀的选手,只不过却始终都跟冠军失之交臂,其中的两人更是严重的乔丹受害者,总……大吉大利天天吃鸡,中华名鸡知多少?有你家乡的鸡吗冬日生活打卡季鸡肉味道鲜美、营养丰富,几千年以来深得人们的喜爱。对鸡肉的烹饪方式也是花样繁多,如烤、烧、炸、炖、煮等。人们喜欢吃鸡肉,但鲜有人知道平时吃的鸡有多少种类,不同的鸡……原神屈居亚军,2022年6月全球手游畅销榜简评时光匆匆,又到了每个月例行的介绍全球手游畅销榜的日子。根据以往的经验,畅销榜前三甲一般都是《王者荣耀》、《绝地求生手游》以及《原神》,那么在2022年6月份(本月出的是上个月的……几块钱一瓶的甲硝唑,对这5种疾病效果很好,不妨了解一下甲硝唑是一种比较常见的药物,在临床上的应用非常广泛,它是一种人工合成的抗厌氧细菌的药物,属于抗菌药,不属于抗生素。在日常生活中,甲硝唑的适用人群是比较多的,但是相信大部分……
通讯Plus5G全千兆来一起薅羊毛!和彩云网盘免费领2GB流视频加载中。。。看到精彩时刻就没网?明明10M却要下载半个小时?好不容易等了几个小时却跳出下载失败拳头揣在怀里硬是捏了又捏佛系青年都要被这神操作给……美国学者称中国在空间站种植水稻,标志着开始殖民太空一旦你在某个地方种植庄稼,你就正式‘殖民’了它。这是美国科幻电影《火星救援》中的一句台词,近日美国学者引用这句台词,提醒美国警惕中国的太空殖民计划。电影《火星救援》……林瑞阳前妻发文不会落井下石,被他抛弃的琼女郎,如今怎样了?林瑞阳前妻曾哲贞最新发文:不会落井下石。近日,知名艺人张庭夫妇公司因涉嫌传销被立案调查一事引发了网友的关注。这对昔日异常高调的夫妻现在也翻车,真是成也微商,败也微商……非洲债务危机怪中国?数据说话近日,英国研究机构债务公正发布报告显示,不少非洲国家政府欠西方银行、资产管理公司和石油交易商的债务,是来自中国贷款的3倍。这些西方企业向非洲国家收取的利率是中国同行的近2倍。报……请回答2019春日生活打卡季还记得疫情前的那个春天,你去了哪里,看了哪些风景吗?我在武大看樱花,故宫赏春雪。我在看祖国山河,世界精彩。那时候的我们不用戴口……看完这场发布会,我只想说华为的实力超出想象华为nova11系列的配置确实还不错,下放了很多旗舰的设计,其中包括十档物理可变光圈、昆仑玻璃、双向卫星通信等等,这些配置很让人意外。当然,有人说价格会高一点,但毕竟定位也是旗……MacBookAirM2简单体验2023年的1月29日,我拿到了MacBookAir这也是我第一次买Mac电脑,也是我第一次接触macOS,简单写一下我刚接触Mac的感受吧。MacBookAir上手的第……电池级碳酸锂跌破19万元,新能源汽车厂商马太效应加剧?【大河财立方记者张克瑶】今年以来,作为锂电池的重要原材料,碳酸锂价格整体呈现下跌走势,最新价格已跌破19万元吨。与此同时,新能源汽车龙头企业特斯拉新一轮降价已在路上。在此之前,……印尼中亚银行首席经济学家东盟加强货币使用自主权凸显去美元化趋新华社雅加达4月4日电(记者汪奥娜)印度尼西亚最大私营银行中亚银行首席经济学家戴维苏穆阿尔日前在雅加达接受新华社记者专访时表示,去美元化趋势在不少地区已逐渐显现。东盟计划加强本……上海男篮27分惨败辽宁,李春江是最大毒瘤,攻防一团糟,只会怒3月8号晚,辽宁男篮在主场首秀,迎战上海男篮,最终辽宁男篮105比78大胜上海,主场开门红。本场比赛其实辽宁男篮困难不少,赵继伟和郭艾伦赛前流感高烧,一个38度,一个39……长沙连续两周客流上演狂飙消费市场活力再现红网时刻新闻记者向婉杨淑华长沙报道324。65万!上周末,长沙地铁打破315。1万的单日客流记录再创新高,长沙,成为一座周末即可刷新地铁客流记录的城市,橘子洲、五一广场等……房山旅游(三)湖光山色的水和鬼斧神工的洞2、房山的水主要指一河一湖的水房山区域内河流众多,有拒马河、大清河、小清河、永定河、大石河、刺猬河等等,具有旅游资源的主要就是拒马河,其他的河流适合休闲度假,一家人搭个帐……
友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找美丽时装彩妆资讯历史明星乐活安卓数码常识驾车健康苹果问答网络发型电视车载室内电影游戏科学音乐整形