范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

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

  前沿
  突然回首,博主算上大学到工作已经从事后端已经有好几年时间了,借助本篇文章,旨在对自己在公司、个人项目的实战经验总结,包括JAVA常遇到的业务场景技术栈、第三方库以及可复用的代码编写,希望能给大家带来帮助。
  目前,我总结了10条常见的业务开发经验,毫无保留的分享给大家..., 主要涵盖内容如下:悟耕开源Easypoi Excel导入导出最佳实践Alibaba Excel导出时自定义格式转换优雅实现不建议直接使用@Async实现异步,需自定义线程池解决Java行业常见业务开发数值计算丢失精度问题Hutool TreeUtil快速构造返回树形结构事务@Transactional的失效场景Spring Event实现异步业务开发中通用的策略模式模板使用ip2region获取用户地址位置信息利用好Java现有优秀的开发库一、悟耕开源easypoi - Excel导入导出最佳实践
  Excel导入导出几乎在很多中后台项目都会用到,特别是一些CRM、OA、商城、企业应用等系统都十分常见,在开发的过程中我也遇到过很多Excel大数据导入导出的功能,一直以来,使用easypoi做了不少导入导出的需求,导入导出数据量从10万级到现在百万级(Excel峰值103万数据量),整理了一下easypoi的导入导出的基础和高级用法。大数据导出数据转换以及数据加密Excel导入数据校验,并提供错误日志下载1.1 注解说明
  常见的5个注解类分别是:@Excel :作用到filed上面,是对Excel列的一个描述;@ExcelCollection:表示一个集合,主要针对一对多的导出,比如一个老师对应多个科目,科目就可以用集合表示;@ExcelEntity:表示一个继续深入导出的实体,但他没有太多的实际意义,只是告诉系统这个对象里面同样有导出的字段;@ExcelIgnore:和名字一样表示这个字段被忽略跳过这个导导出;@ExcelTarget:这个是作用于最外层的对象,描述这个对象的id,以便支持一个对象可以针对不同导出做出不同处理。1.2 定义easypoi实体类import cn.afterturn.easypoi.excel.annotation.Excel; import cn.afterturn.easypoi.handler.inter.IExcelDataModel; import cn.afterturn.easypoi.handler.inter.IExcelModel; import lombok.Data;  import javax.validation.constraints.NotBlank; import java.io.Serializable;  @Data public class SdSchoolSysUserVerify implements IExcelModel, IExcelDataModel, Serializable {      private static final long serialVersionUID = 1L;      @Excel(name = "行号")     private Integer rowNum;      @Excel(name = "错误信息")     private String errorMsg;      /**      * 真实姓名      */     @Excel(name = "姓名(必填)", width = 25)     @NotBlank(message = "姓名不能为空")     private String realname;      /**      * 部门编码,需要和用户导入模板名称对应      */     @Excel(name = "部门编码(必填)", width = 30)     @NotBlank(message = "部门编码不能为空")     private String deptOrgCode;      /**      * 角色编码      */     @Excel(name = "角色编码(必填)", width = 15)     @NotBlank(message = "角色编码不能为空")     private String roleCode;      /**      * 手机号码      */     @Excel(name = "手机号码(选填)", width = 15)     private String phone;      /**      * 电子邮件      */     @Excel(name = "电子邮件(选填)", width = 15)     private String email;      /**      * 性别(1:男 2:女)      */     @Excel(name = "性别(选填)", width = 15)     private String sexName;      /**      * 工号(选填)      */     @Excel(name = "工号(选填)", width = 15)     private String workNo;          /**      * 商户ID      **/     private Integer tenantId; } 复制代码1.3 基础的导入导出逻辑(数据校验)
  easyPoi导入校验使用起来也很简单,以导入系统优化为例:
  第一步,定义一个检验类SdSchoolSysUserVerify,通过实现IExcelModel、IExcelDataModel,当我们需要输出导入校验错误信息的时候,它们两个就显的很重要了,IExcelModel负责设置错误信息,IExcelDataModel负责设置行号。package cn.afterturn.easypoi.handler.inter;  /**  * Excel 本身数据文件  */ public interface IExcelDataModel {      /**      * 获取行号      */     public Integer getRowNum();      /**      *  设置行号      */     public void setRowNum(Integer rowNum);  } 复制代码
  第二步,定义完实体之后,那么如何实现我们的校验逻辑呢,接着自定义一个系统用户导入校验处理器SdSchoolSysUserVerifyHandler,通过实现IExcelVerifyHandler,处理器里编写我们的校验逻辑:/**  * 系统用户批量导入校验处理器  *  * @author: jacklin  * @since: 2021/3/31 11:47  **/ @Component public class SdSchoolSysUserVerifyHandler implements IExcelVerifyHandler {      private static final String PREFIX = "【";     private static final String SUFFIX = "】";      @Autowired     private ISysBaseAPI sysBaseAPI;      @Override     public ExcelVerifyHandlerResult verifyHandler(SdSchoolSysUserVerify userVerify) {         LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();         userVerify.setTenantId(Integer.valueOf(loginUser.getRelTenantIds()));          StringJoiner joiner = new StringJoiner(", ", PREFIX, SUFFIX);         if (StringUtils.isBlank(userVerify.getRealname())) {             joiner.add("用户姓名不能为空");         }         //根据用户姓名和商户ID查询用户记录,大于0则提示该姓名用户已存在         int realNameCount = sysBaseAPI.countByRealName(userVerify.getRealname(), userVerify.getTenantId());         if (realNameCount > 0) {             joiner.add("该姓名用户已存在,如需添加该用户请在页面添加");         }         if (StringUtils.isBlank(userVerify.getDeptOrgCode())) {             joiner.add("部门编码不能为空");         } else {             //查询系统是否存在该部门编码             int deptOrgCodeCount = sysBaseAPI.queryDepartCountByDepartSysCodeTenantId(userVerify.getDeptOrgCode(), userVerify.getTenantId());             if (deptOrgCodeCount == 0) {                 joiner.add("部门编码不存在");             }         }         if (oConvertUtils.isEmpty(userVerify.getRoleCode())) {             joiner.add("用户角色编码不能为空");         } else {             //查询系统是否存在该角色             int count = sysBaseAPI.queryRoleCountByRoleCodeTenantId(userVerify.getRoleCode(), userVerify.getTenantId());             if (count == 0) {                 joiner.add("该用户角色编码不存在");             } else {                 //查询配置是否用户支持导入该角色                 int supportUserImportCount = sysBaseAPI.queryIsSupportUserImportByRoleCode(userVerify.getRoleCode(), userVerify.getTenantId());                 if (supportUserImportCount == 0) {                     joiner.add("该用户角色编码不支持导入");                 }             }         }         if (oConvertUtils.isNotEmpty(userVerify.getPhone())) {             boolean isPhone = Validator.isMobile(userVerify.getPhone());             if (!isPhone) {                 joiner.add("手机号填写格式不正确");             }         }         if (oConvertUtils.isNotEmpty(userVerify.getEmail())) {             boolean isEmail = Validator.isEmail(userVerify.getEmail());             if (!isEmail) {                 joiner.add("邮箱填写格式不正确");             }         }         if (!"【】".equals(joiner.toString())) {             return new ExcelVerifyHandlerResult(false, joiner.toString());         }         return new ExcelVerifyHandlerResult(true);     } }  复制代码
  第三步,在完成第一、二步之后,我们只需要在导入的时候通过 params.setVerifyHandler(userVerifyHandler)、params.setNeedVerfiy(true)即可以实现导入校验了。1.4 不同类型数据的导入和导出(Map/Object)
  在某些复杂的场景,我们导入的时候不想直接构造一个bean然后标记注解,但是中间需要处理一些字段逻辑没办法直接导入到数据库,这是用可以用map的形式导入,下面我以一个客户导入的需求演示一下如何通过map的方式导入数据:核心方法://Map数据格式导入 ExcelImportResult> importResult = ExcelImportUtil.importExcelMore(inputStream, Map.class, params); 复制代码 复制代码
  // 获取导入检验通过的数据
  List> rightMapList = importResult.getList();
  // 获取导入检验失败的数据
  List> failMapList = importResult.getFailList();
  最后可以将校验失败的数据,通过excel错误日志输出,非常的方便。1.5 基于多线程ForkJoin实现导入优化
  在4.0后的版本,easypoi导入支持了fork/join的多线程支持,使用方法很简单 ImportParams 新加了两个参数,设置为true就可以了,多线程导入处理可以提高了导入的处理效率,比如:params.setConcurrentTask(true);            //4.1版本都支持基于fork/join的线程 复制代码1.6 自定义导入数据处理
  这里列举说明一下easypoi的几个比较重要的接口和类:IExcelDataHandler:当存在一下比较特殊的需求场景,easypoi基础服务无法满足客户的需求时,可以通过实现IExcelDataHandler去自定义数据处理,比如数值转换器处理。IExcelVerifyHandler:一般都是通过实现IExcelVerifyHandler接口实现自己的校验逻辑。IExcelModel:自定义实体校验类,主要用于输出错误日志,IExcelModel负责错误信息。IExcelDataModel:自定义实体校验类,主要用于输出错误日志,IExcelDataModel负责设置行号。IExcelDataHandler/**  * Excel 导入导出 数据处理接口  *   */ public interface IExcelDataHandler {      /**      * 导出处理方法      *       * @param obj   当前对象      * @param name  前字段名称          * @param value 当前值        * @return      */     public Object exportHandler(T obj, String name, Object value);    } 复制代码1.7 导入组内数据重复校验实现
  可以通过ThreadLocal来实现组内校验,可以定位输出每一个错误数据的具体是哪一行,方便我们做导入排错:/**  * IM 批量推送用户导入校验处理器  *  * @author: jacklin  * @since: 2022/1/18 10:45  **/ @Slf4j @Component public class SdSchoolBatchPushCustomerVerifyHandler implements IExcelVerifyHandler {     @Autowired     private ISdSchoolCustomerService sdSchoolCustomerService;          private final ThreadLocal> threadLocal = new ThreadLocal<>();      private static final String PREFIX = "【";     private static final String SUFFIX = "】";       /**      * 最新采用ThreadLocal线程本地内存变量方式实现组内校验,效果可以      *      * @author: jacklin      * @since: 2022/2/11 16:26      **/     @Override     public ExcelVerifyHandlerResult verifyHandler(SdSchoolBatchPushCustomerVerify customerVerify) {          StringJoiner joiner = new StringJoiner(", ", PREFIX, SUFFIX);         String registerUserPhone = customerVerify.getRegisterUserPhone();         if (StringUtils.isBlank(registerUserPhone)) {             joiner.add("注册手机号不能为空");         } else {             //手机号格式校验             boolean mobile = Validator.isMobile(registerUserPhone);             if (!mobile) {                 joiner.add("手机号格式不正确");             }         }          List threadLocalValue = threadLocal.get();         if (threadLocalValue == null) {             threadLocalValue = new ArrayList<>();         }          threadLocalValue.forEach(e -> {             if (e.getRegisterUserPhone().equals(customerVerify.getRegisterUserPhone())) {                 int lineNumber = e.getRowNum() + 1;                 joiner.add("数据与第" + lineNumber + "行重复");             }         });         //添加本行数据对象到ThreadLocal中         threadLocalValue.add(customerVerify);         threadLocal.set(threadLocalValue);          if (!"【】".equals(joiner.toString())) {             return new ExcelVerifyHandlerResult(false, joiner.toString());         }         return new ExcelVerifyHandlerResult(true);     }      public ThreadLocal> getThreadLocal() {         return threadLocal;     } }  复制代码
  核心代码:threadLocalValue.forEach(e -> {     if (e.getRegisterUserPhone().equals(customerVerify.getRegisterUserPhone())) {         int lineNumber = e.getRowNum() + 1;         joiner.add("数据与第" + lineNumber + "行重复");     } }); //添加本行数据对象到ThreadLocal中 threadLocalValue.add(customerVerify); threadLocal.set(threadLocalValue); 复制代码二、Alibaba excel导出时自定义格式转换优雅实现
  EasyExcel 是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。
  Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。EasyExcel 是 alibaba 出的一个基于 java poi得Excel通用处理类库,它的优势在于内存消耗。对比easypoi方案,EasyExcel在内存消耗、知名度上更出众些。
  博主在使用过程中发现导出Excel,官网对自定义格式字段提供了 converter 接口,当我们的Excel导入需要将是/否文字转成数据库1/0的时候,这时候就需要自定义转换器WhetherConverter实现了:import com.alibaba.excel.converters.Converter; import com.alibaba.excel.enums.CellDataTypeEnum; import com.alibaba.excel.metadata.GlobalConfiguration; import com.alibaba.excel.metadata.data.ReadCellData; import com.alibaba.excel.metadata.data.WriteCellData; import com.alibaba.excel.metadata.property.ExcelContentProperty; import com.dragonpass.global.modules.agent.enumreate.Whether;  import java.util.Objects;  /**  * 自定义Excel导入导出转换器  *  * @author Linbz  * @since 2022/11/24 9:55  */ public class WhetherConverter implements Converter {      @Override     public Class<?> supportJavaTypeKey() {         return Integer.class;     }      @Override     public CellDataTypeEnum supportExcelTypeKey() {         return CellDataTypeEnum.STRING;     }      /**      * 导入,文字转数字,是/否 -> 1/0      */     @Override     public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {         Integer result = Whether.NO.getCode();         result = Whether.YES.getDesc().equals(cellData.getStringValue()) ? Whether.YES.getCode() : Whether.NO.getCode();         return result;     }      /**      * 导出,数字转文字,1/0 -> 是/否      */     @Override     public WriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {         return new WriteCellData(Objects.equals(value, Whether.YES.getCode()) ? Whether.YES.getDesc() : Whether.NO.getDesc());     } } 复制代码导入导出实体类
  在导出 ExcelProperty 中添加 WhetherConverter ,就优雅得实现了自定义格式得需求:public static class ExportTemplate {      @Data     public static class ExcelInput {          private String agentId;     }      @Data     public static class ExcelOutput {          @ExcelProperty(value = "类型名称")         @ColumnWidth(12)         private String typeName;          @ExcelProperty(value = "权益扣取后额外扣费(是/否)", converter = WhetherConverter.class)         @ColumnWidth(24)         private Integer needPay;          @ExcelProperty(value = "扣费金额")         @ColumnWidth(12)         private BigDecimal price;          @ExcelProperty(value = "是否为默认项(是/否)", converter = WhetherConverter.class)         @ColumnWidth(24)         private Integer isDefault;          @ExcelProperty(value = "Ncode")         @ColumnWidth(12)         private String loungeCode;     }      @Data     @NoArgsConstructor     public static class DataCheckResult {         @ExcelProperty(value = "结果")         private Boolean checkResult = Boolean.TRUE;          @ExcelProperty(value = "备注")         private String remark;          public DataCheckResult(Boolean checkResult, String remark) {             this.checkResult = checkResult;             this.remark = remark;         }     } } 复制代码ExcelUtil.importByEasyExcel导入@Override @Transactional(rollbackFor = Exception.class) public AgtAgentDiffPriceRuleDTO.ImportExcelDataDTO.Output importExcelData(AgtAgentDiffPriceRuleDTO.ImportExcelDataDTO.Input input) {     // ExcelUtil.importByEasyExcel     List dataList = ExcelUtil.importByEasyExcel(input.getFile().getInputStream(), AgtAgentDiffPriceRuleDTO.ExportTemplate.ExcelOutput.class, Integer.MAX_VALUE, true);     // 导入数据校验     AgtAgentDiffPriceRuleDTO.ExportTemplate.DataCheckResult dataCheckResult = dataCheckForResult(dataList, input);      if (dataCheckResult.getCheckResult()) {         //TODO 校验成功,插入数据...     } }  复制代码三、不建议直接使用@Async实现异步,需自定义线程池@Async 应用默认线程池
  Spring应用默认的线程池,指在@Async注解在使用时,不指定线程池的名称。查看源码,@Async的默认线程池为SimpleAsyncTaskExecutor。无返回值的异步调用@Override @Async("taskExecutor") public void pageExportOrderBigExcel(HttpServletResponse response, JSONObject queryConditionDataJson, SdSchoolFilterConfig sdSchoolFilterConfig, LoginUser loginUser, SdSchoolDataExportTaskRecord exportTask, HttpServletRequest request, String tenantId) {     try {          Thread.sleep(1000);         exportTask.setExportTaskStartTime(new Date());         sdSchoolOrderService.exportOrderBigExcelPage(response, queryConditionDataJson, exportTask, sdSchoolFilterConfig.getFilterName(), loginUser, request, tenantId);         exportTask.setExportTaskEndTime(new Date());         exportTaskRecordService.updateById(exportTask);      } catch (Exception e) {         log.error("订单数据分页导出失败", e);    } } 复制代码默认线程池的弊端
  在线程池应用中,参考阿里巴巴Java开发规范:线程池不允许使用Executors去创建,不允许使用系统默认的线程池,推荐通过ThreadPoolExecutor的方式,这样的处理方式让开发的工程师更加明确线程池的运行规则,规避资源耗尽的风险。Executors各个方法的弊端:newFixedThreadPool和newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。newCachedThreadPool和newScheduledThreadPool:要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
  @Async默认异步配置使用的是SimpleAsyncTaskExecutor,该线程池默认来一个任务创建一个线程,若系统中不断的创建线程,最终会导致系统占用内存过高,引发OutOfMemoryError错误。针对线程创建问题,SimpleAsyncTaskExecutor提供了限流机制,通过concurrencyLimit属性来控制开关,当concurrencyLimit>=0时开启限流机制,默认关闭限流机制即concurrencyLimit=-1,当关闭情况下,会不断创建新的线程来处理任务。基于默认配置,SimpleAsyncTaskExecutor并不是严格意义的线程池,达不到线程复用的功能。@Async应用自定义线程池
  自定义线程池,可对系统中线程池更加细粒度的控制,方便调整线程池大小配置,线程执行异常控制和处理。在设置系统自定义线程池代替默认线程池时,虽可通过多种模式设置,但替换默认线程池最终产生的线程池有且只能设置一个(不能设置多个类继承 AsyncConfigurer)。自定义线程池有如下方式:重新实现接口AsyncConfigurer;继承AsyncConfigurerSupport;配置由自定义的TaskExecutor替代内置的任务执行器。
  通过查看Spring源码关于@Async的默认调用规则,会优先查询源码中实现AsyncConfigurer这个接口的类,实现这个接口的类为AsyncConfigurerSupport。但默认配置的线程池和异步处理方法均为空,所以,无论是继承或者重新实现接口,都需指定一个线程池。且重新实现 public Executor getAsyncExecutor () 方法。实现接口AsyncConfigurer@Configuration  public class AsyncConfiguration implements AsyncConfigurer {       @Bean("taskExecutor")      public ThreadPoolTaskExecutor executor() {          ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();          int corePoolSize = 10;          executor.setCorePoolSize(corePoolSize);          int maxPoolSize = 50;          executor.setMaxPoolSize(maxPoolSize);          int queueCapacity = 10;          executor.setQueueCapacity(queueCapacity);          executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());          executor.setThreadNamePrefix( "asyncServiceExecutor-");          executor.setWaitForTasksToCompleteOnShutdown(true);          executor.setAwaitTerminationSeconds(awaitTerminationSeconds);          executor.initialize();          return executor;      }        @Override      public Executor getAsyncExecutor() {          return executor();      }  } 复制代码继承AsyncConfigurerSupportConfiguration   @EnableAsync   class SpringAsyncConfigurer extends AsyncConfigurerSupport {          @Bean       public ThreadPoolTaskExecutor asyncExecutor() {           ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();           threadPool.setCorePoolSize(3);           threadPool.setMaxPoolSize(3);           threadPool.setWaitForTasksToCompleteOnShutdown(true);           threadPool.setAwaitTerminationSeconds(60 * 15);           return threadPool;       }          @Override       public Executor getAsyncExecutor() {           return asyncExecutor;     }   } 复制代码配置自定义的TaskExecutor (建议采用方式)/**  * 线程池参数配置,多个线程池实现线程池隔离,@Async注解,默认使用系统自定义线程池,可在项目中设置多个线程池,在异步调用的时候,指明需要调用的线程池名称,比如:@Async("taskName")  *  * @author: jacklin  * @since: 2021/5/18 11:44  **/ @EnableAsync @Configuration public class TaskPoolConfig {      /**      * 异步导出      *      * @author: jacklin      * @since: 2022/11/16 17:41      **/     @Bean("taskExecutor")     public Executor taskExecutor() {         //返回可用处理器的Java虚拟机的数量 12         int i = Runtime.getRuntime().availableProcessors();         System.out.println("系统最大线程数  : " + i);         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();         //核心线程池大小         executor.setCorePoolSize(16);         //最大线程数         executor.setMaxPoolSize(20);         //配置队列容量,默认值为Integer.MAX_VALUE         executor.setQueueCapacity(99999);         //活跃时间         executor.setKeepAliveSeconds(60);         //线程名字前缀         executor.setThreadNamePrefix("asyncServiceExecutor -");         //设置此执行程序应该在关闭时阻止的最大秒数,以便在容器的其余部分继续关闭之前等待剩余的任务完成他们的执行         executor.setAwaitTerminationSeconds(60);         //等待所有的任务结束后再关闭线程池         executor.setWaitForTasksToCompleteOnShutdown(true);         return executor;     } } 复制代码多个线程池(线程池隔离)
  @Async注解,使用系统默认或者自定义的线程池(代替默认线程池)。可在项目中设置多个线程池,在异步调用时,指明需要调用的线程池名称,如@Async("new_taskName")。四、解决Java行业常见业务开发数值计算丢失精度问题
  一直以来我都会负责公司有关订单模块的项目开发,时常会面对各种金额的计算,在开发的过程中需要注意防止计算精度丢失的问题,今天我说说数值计算的精度、舍入和溢出问题,出于总结,也希望可以为一些读者"闭坑"。"危险"的 Double
  我们先从简单的反直觉的四则运算看起。对几个简单的浮点数进行加减乘除运算:System.out.println(0.1+0.2); System.out.println(1.0-0.8); System.out.println(4.015*100); System.out.println(123.3/100); double amount1 = 2.15; double amount2 = 1.10; if (amount1 - amount2 == 1.05) System.out.println("OK"); 复制代码
  结果输出如下:0.30000000000000004 0.19999999999999996 401.49999999999994 1.2329999999999999 复制代码
  可以看到,输出结果和我们预期的很不一样。比如,0.1+0.2 输出的不是 0.3 而是0.30000000000000004;再比如,对 2.15-1.10 和 1.05 判等,结果判等不成立,出现这种问题的主要原因是,计算机是以二进制存储数值的,浮点数也不例外,对于计算机而言,0.1 无法精确表达,这是浮点数计算造成精度损失的根源。
  很多人可能会说,以 0.1 为例,其十进制和二进制间转换后相差非常小,不会对计算产生什么影响。但,所谓积土成山,如果大量使用double来作大量的金钱计算,最终损失的精度就是大量的资金出入。比如,每天有一百万次交易,每次交易都差一分钱,一个月下来就差30 万。这就不是小事儿了。那,如何解决这个问题呢?BigDecimal 类型
  我们大都听说过BigDecimal类型,浮点数精确表达和运算的场景,一定要使用这个类型。不过,在使用 BigDecimal时有几个坑需要避开。我们用BigDecimal把之前的四则运算改一下:System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2))); System.out.println(new BigDecimal(1.0).subtract(new BigDecimal(0.8))); System.out.println(new BigDecimal(4.015).multiply(new BigDecimal(100))); System.out.println(new BigDecimal(123.3).pide(new BigDecimal(100))); 复制代码
  输出如下:0.3000000000000000166533453693773481063544750213623046875 0.1999999999999999555910790149937383830547332763671875 401.49999999999996802557689079549163579940795898437500 1.232999999999999971578290569595992565155029296875 复制代码
  可以看到,运算结果还是不精确,只不过是精度高了而已。这里给出浮点数运算避坑第一原则:使用 BigDecimal 表示和计算浮点数,且务必使用字符串的构造方法来初始化BigDecimal:System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2"))); System.out.println(new BigDecimal("1.0").subtract(new BigDecimal("0.8"))); System.out.println(new BigDecimal("4.015").multiply(new BigDecimal("100"))); System.out.println(new BigDecimal("123.3").pide(new BigDecimal("100"))); 复制代码
  改进后,就得到我们想要的输出结果了:0.3 0.2 401.500 1.233 复制代码数值判断
  现在我们知道了,应该使用BigDecimal来进行浮点数的表示、计算、格式化。Java中的原则:包装类的比较要通过equals进行,而不能使用 ==。那么,使用equals方法对两个BigDecimal判等,一定能得到我们想要的结果吗?比如:System.out.println(new BigDecimal("1.0").equals(new BigDecimal("1"))); 复制代码
  答案是:false,为什么呢?BigDecimal的equals方法的注释中说明了原因,equals比较的是 BigDecimal的value和scale,1.0的scale是 1,1的scale是0,所以结果一定是false。
  如果我们希望只比较 BigDecimal 的 value,可以使用 compareTo 方法,修改代码如下:System.out.println(new BigDecimal("1.0").compareTo(new BigDecimal("1"))==0); 复制代码
  输出结果是:true解决方案,自定义ArithmeticUtils工具类,用于高精度处理常用的数学运算package io.halo.payment.utils;  import java.math.BigDecimal; import java.math.RoundingMode;  /**  * 用于高精度处理常用的数学运算  *  * @author: austin  * @since: 2022/12/20 22:54  */ public class ArithmeticUtils {      /**      * 默认除法运算精度      */     private static final int DIV_SCALE = 10;      /**      * 加法运算      *      * @param var1 被加数      * @param var2 加数      */      public static double add(double var1, double var2) {         BigDecimal b1 = new BigDecimal(Double.toString(var1));         BigDecimal b2 = new BigDecimal(Double.toString(var2));         return b1.add(b2).doubleValue();     }      /**      * 加法运算      *      * @param var1 被加数      * @param var2 加数      */     public static BigDecimal add(String var1, String var2) {         BigDecimal b1 = new BigDecimal(var1);         BigDecimal b2 = new BigDecimal(var2);         return b1.add(b2);     }      /**      * 加法运算      *      * @param var1  被加数      * @param var2  加数      * @param scale 保留scale位小数      */     public static String add(String var1, String var2, int scale) {         if (scale < 0) {             throw new IllegalArgumentException("The scale must be a positive integer or zero");         }         BigDecimal b1 = new BigDecimal(var1);         BigDecimal b2 = new BigDecimal(var2);         return b1.add(b2).setScale(scale, RoundingMode.HALF_UP).toString();     }      /**      * 减法运算      *      * @param var1 被减数      * @param var2 减数      */     public static double sub(double var1, double var2) {         BigDecimal b1 = new BigDecimal(Double.toString(var1));         BigDecimal b2 = new BigDecimal(Double.toString(var2));         return b1.subtract(b2).doubleValue();     }      /**      * 减法运算      *      * @param var1 被减数      * @param var2 减数      */     public static BigDecimal sub(String var1, String var2) {         BigDecimal b1 = new BigDecimal(var1);         BigDecimal b2 = new BigDecimal(var2);         return b1.subtract(b2);     }      /**      * 减法运算      *      * @param var1  被减数      * @param var2  减数      * @param scale 保留scale 位小数      */     public static String sub(String var1, String var2, int scale) {         if (scale < 0) {             throw new IllegalArgumentException("The scale must be a positive integer or zero");         }         BigDecimal b1 = new BigDecimal(var1);         BigDecimal b2 = new BigDecimal(var2);         return b1.subtract(b2).setScale(scale, RoundingMode.HALF_UP).toString();     }      /**      * 乘法运算      *      * @param var1 被乘数      * @param var2 乘数      */     public static double mul(double var1, double var2) {         BigDecimal b1 = new BigDecimal(Double.toString(var1));         BigDecimal b2 = new BigDecimal(Double.toString(var2));         return b1.multiply(b2).doubleValue();     }      /**      * 乘法运算      *      * @param var1 被乘数      * @param var2 乘数      */     public static BigDecimal mul(String var1, String var2) {         BigDecimal b1 = new BigDecimal(var1);         BigDecimal b2 = new BigDecimal(var2);         return b1.multiply(b2);     }      /**      * 乘法运算      *      * @param var1  被乘数      * @param var2  乘数      * @param scale 保留scale 位小数      */     public static double mul(double var1, double var2, int scale) {         BigDecimal b1 = new BigDecimal(Double.toString(var1));         BigDecimal b2 = new BigDecimal(Double.toString(var2));         return round(b1.multiply(b2).doubleValue(), scale);     }      /**      * 乘法运算      *      * @param var1  被乘数      * @param var2  乘数      * @param scale 保留scale 位小数      */     public static String mul(String var1, String var2, int scale) {         if (scale < 0) {             throw new IllegalArgumentException("The scale must be a positive integer or zero");         }         BigDecimal b1 = new BigDecimal(var1);         BigDecimal b2 = new BigDecimal(var2);         return b1.multiply(b2).setScale(scale, RoundingMode.HALF_UP).toString();     }      /**      * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到小数点以后10位,以后的数字四舍五入      *      * @param var1 被除数      * @param var2 除数      */      public static double p(double var1, double var2) {         return p(var1, var2, DIV_SCALE);     }      /**      * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指定精度,以后的数字四舍五入      *      * @param var1  被除数      * @param var2  除数      * @param scale 表示表示需要精确到小数点以后几位。      */     public static double p(double var1, double var2, int scale) {         if (scale < 0) {             throw new IllegalArgumentException("The scale must be a positive integer or zero");         }         BigDecimal b1 = new BigDecimal(Double.toString(var1));         BigDecimal b2 = new BigDecimal(Double.toString(var2));         return b1.pide(b2, scale, RoundingMode.HALF_UP).doubleValue();     }      /**      * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指      * 定精度,以后的数字四舍五入      *      * @param var1  被除数      * @param var2  除数      * @param scale 表示需要精确到小数点以后几位      */     public static String p(String var1, String var2, int scale) {         if (scale < 0) {             throw new IllegalArgumentException("The scale must be a positive integer or zero");         }         BigDecimal b1 = new BigDecimal(var1);         BigDecimal b2 = new BigDecimal(var2);         return b1.pide(b2, scale, RoundingMode.HALF_UP).toString();     }      /**      * 提供精确的小数位四舍五入处理      *      * @param var   需要四舍五入的数字      * @param scale 小数点后保留几位      */     public static double round(double var, int scale) {         if (scale < 0) {             throw new IllegalArgumentException("The scale must be a positive integer or zero");         }         BigDecimal b = new BigDecimal(Double.toString(var));         return b.setScale(scale, RoundingMode.HALF_UP).doubleValue();     }      /**      * 提供精确的小数位四舍五入处理      *      * @param var   需要四舍五入的数字      * @param scale 小数点后保留几位      */     public static String round(String var, int scale) {         if (scale < 0) {             throw new IllegalArgumentException("The scale must be a positive integer or zero");         }         BigDecimal b = new BigDecimal(var);         return b.setScale(scale, RoundingMode.HALF_UP).toString();     }          /**      * 比较大小      *      * @param var1 被比较数      * @param var2 比较数      * @return 如果v1大于v2 则返回true 否则false      */     public static boolean compare(String var1, String var2) {         BigDecimal b1 = new BigDecimal(var1);         BigDecimal b2 = new BigDecimal(var2);         int result = b1.compareTo(b2);         return result > 0 ? true : false;     } } 复制代码五、Hutool TreeUtil快速构造返回树形结构
  项目中经常会遇到各种需要以树形结构展示的功能,如菜单树、分类树、部门树,Hutool的TreeUtil主要是用来快速构造树形结构,以及获取所有叶子节点等操作。步骤:
  1  引入hutool最新pom包。
  2  获取构造树的分类数据。
  3  TreeNodeConfig信息配置,配置节点名称、孩子节点key信息、排序等等。
  4  调用TreeUtil.build()构造树。pom依赖     cn.hutool     hutool-all     5.7.22  复制代码资料分类Service接口层/**  * 构造班型资料分类树方法  *  * @author: jacklin  * @date: 2022/4/20 16:44  **/ List> constructTree(); 复制代码实现层@Override public List> constructTree() {     //1.获取所有资料分类     List dataList = this.lambdaQuery().getBaseMapper().selectList(Wrappers.lambdaQuery(SdSchoolClassTypeDataCategory.class)             .eq(SdSchoolClassTypeDataCategory::getStatus, SchoolConstant.ENABLE_STATUS)             .eq(SdSchoolClassTypeDataCategory::getDeleted, SchoolConstant.DELETE_STATUS_NORMAL));      //2.配置     TreeNodeConfig config = new TreeNodeConfig();     config.setIdKey("id");                              //默认id,可以不设置     config.setParentIdKey("pid");                       //父id     config.setNameKey("dataCategoryName");              //分类名称     config.setDeep(3);                                  //最大递归深度     config.setChildrenKey("childrenList");              //孩子节点     config.setWeightKey("sort");                        //排序字段      //3.转树     List> treeList = TreeUtil.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());         //扩展属性...     }));      return treeList; } 复制代码
  通过TreeNodeConfig我们可以自定义节点的名称、关系节点id名称,这样就可以和不同的数据库做对应。Controller层/**  * 获取构造树  *  * @author: jacklin  * @date: 2022/4/20 17:18  **/ @ApiOperation(value = "获取构造树", notes = "获取构造树") @GetMapping(value = "/getConstructTree") public Result<?> getConstructTree() {     List> treeList = sdSchoolClassTypeDataCategoryService.constructTree();     return Result.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":"测试资料分类2-1",                     "level":2,                     "sort":1                 },                 {                     "id":"1447849472085528577",                     "pid":"1447849327826636801",                     "dataCategoryName":"测试资料分类2-2",                     "level":2,                     "sort":1                 },                 {                     "id":"1447849472219746305",                     "pid":"1447849327826636801",                     "dataCategoryName":"测试资料分类2-3",                     "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)未开启事务
  这个也是一个比较麻烦的问题,在Spring Boot项目中已经不存在了,已经有DataSourceTransactionManagerAutoConfiguration默认开启了事务管理。
  但是在MVC项目中还需要在applicationContext.xml文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。6.3 失效场景集撒三:错误的使用@Trasactional
  日常开发我们最常犯的错误的可能因为配置不正确,导致方法上的事务没生效,回滚失败!
  (1)错误的传播机制
  Spring支持了7种传播机制,分别为:
  事务行为
  说明
  REQUIRED(Spring默认的事务传播类型)
  如果当前没有事务,则自己新建一个事务,如果当前存在事务则加入这个事务
  SUPPORTS
  当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
  MANDATORY
  当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常
  REQUIRES_NEW
  创建一个新事务,如果存在当前事务,则挂起该事务
  NOT_SUPPORTED
  以非事务方式执行,如果当前存在事务,则挂起当前事务
  NEVER
  如果当前没有事务存在,就以非事务方式执行;如果有,就抛出异常。就是B从不以事务方式运行A中不能有事务,如果没有,B就以非事务方式执行,如果A存在事务,那么直接抛异常
  NESTED(嵌套的)
  如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务) 如果A中没有事务,那么B创建一个事务执行,如果A中也有事务,那么B会会把事务嵌套在里面
  上面不支持事务的传播机制为:SUPPORTS,NOT_SUPPORTED,NEVER。
  如果配置了这三种传播方式的话,在发生异常的时候,事务是不会回滚的。
  (2)rollbackFor属性设置错误
  默认情况下事务仅回滚运行时异常和Error,不回滚受检异常(例如IOException)。
  因此如果方法中抛出了IO异常,默认情况下事务也会回滚失败。
  我们可以通过指定@Transactional(rollbackFor = Exception.class)的方式进行全异常捕获。
  (3)异常被程序内部catch
  如果需要对特定的异常进行捕获处理,记得再次将异常抛出,让最外层的事务感知到。
  (4)嵌套事务七、Spring Event实现异步,业务解耦神器
  实际业务开发过程中,业务逻辑可能非常复杂,核心业务 + N 个子业务。如果都放到一块儿去做,代码可能会很长,耦合度不断攀升,维护起来也麻烦,甚至头疼。还有一些业务场景不需要在一次请求中同步完成,比如邮件发送、短信发送等。
  MQ确实可以解决这个问题,但MQ相对来说比较重,非必要不提升架构复杂度。针对这些问题,我们了解一下Spring Event。7.1 自定义事件
  定义事件,继承ApplicationEvent的类成为一个事件类:public class AsyncSendEmailEvent extends ApplicationEvent {      /**      * 邮箱      **/     private String email;     /**      * 主题      **/     private String subject;      /**      * 内容      **/     private String content;        /**      * 接收者      **/     private String targetUserId;  } 复制代码7.2 定义事件监听器@Slf4j @Component public class AsyncSendEmailEventListener implements ApplicationListener {      @Autowired     private IMessageHandler mesageHandler;          @Async("taskExecutor")     @Override     public void onApplicationEvent(AsyncSendEmailEvent event) {         if (event == null) {             return;         }          String email = event.getEmail();         String subject = event.getSubject();         String content = event.getContent();         String targetUserId = event.getTargetUserId();         mesageHandler.sendsendEmailSms(email, subject, content, targerUserId);       } } 复制代码7.3 开启异步启动类增加@EnableAsync注解Listener类需要开启异步的方法增加@Async注解
  另外,可能有些时候采用ApplicationEvent实现异步的使用,当程序出现异常错误的时候,需要考虑补偿机制,那么这时候可以结合Spring Retry重试来帮助我们避免这种异常造成数据不一致问题。八、业务开发中通用的策略模式模板
  在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。业务背景
  商场搞活动,根据客户购买商品的金额,收费时给与不同的打折,比如,购买 金额>=2000 的打八折(0.8),金额 500 ~ 1000 的,打九折(0.9),购买金额 0 ~ 500 的九五折(0.95),根据不同的金额走不同计算策略逻辑。首先定义一个Strategy接口来表示一个策略:public interface Strategy {      /**      * 采用策略      */     String strategy();      /**      * 计算方法逻辑      */     void algorithm(); } 复制代码
  其中strategy方法返回当前策略的唯一标识,algorithm则是该策略的具体执行的计算逻辑。
  下面是Strategy接口的两个实现类:public class ConcreteStrategyA implements Strategy {          @Override     public String strategy() {         return StrategySelector.strategyA.getStrategy();     }      @Override     public void algorithm() {         System.out.println("process with strategyA...");     } }  public class ConcreteStrategyB implements Strategy {      @Override     public String strategy() {         return StrategySelector.strategyB.getStrategy();     }      @Override     public void algorithm() {         System.out.println("process with strategyB...");     } }  public class ConcreteStrategyC implements Strategy {      @Override     public String strategy() {         return StrategySelector.strategyC.getStrategy();     }      @Override     public void algorithm() {         System.out.println("process with strategyC...");     } } 复制代码自定义策略选择枚举StrategySelector:@Getter public enum StrategySelector {      strategyA(1,"strategyA"),     strategyB(2,"strategyB"),     strategyC(3,"strategyC");          private Integer code;     private String strategy;      StrategySelector(Integer code, String strategy) {         this.code = code;         this.strategy = strategy;     } } 复制代码
  然后定义一个StrategyRunner接口用来表示策略的调度器:public interface StrategyRunner {     void execute(String strategy); } 复制代码
  execute方法内部通过判断strategy的值来决定具体执行哪一个策略。public class StrategyRunnerImpl implements StrategyRunner {      private static final List STRATEGIES = Arrays.asList(new ConcreteStrategyA(), new ConcreteStrategyB(), new ConcreteStrategyC());     private static Map STRATEGY_MAP = Maps.newHashMap();      static {         STRATEGY_MAP = STRATEGIES.stream().collect(Collectors.toMap(Strategy::strategy, s -> s));     }      @Override     public void execute(String strategy) {         STRATEGY_MAP.get(strategy).algorithm();     } } 复制代码
  在StrategyRunnerImpl内部,定义了一个STRATEGIES列表来保存所有Strategy实现类的实例,以及一个叫做STRATEGY_MAP的Map来保存strategy和Strategy实例之间的对应关系,static块中的代码用于从STRATEGIES列表构造STRATEGY_MAP。这样,在execute方法中就可以很方便地获取到指定strategy的Strategy实例。SpringBoot项目中实现并运用策略模式@Component public class ConcreteStrategyA implements Strategy {          @Override     public String strategy() {         return StrategySelector.strategyA.getStrategy();     }      @Override     public void algorithm() {         System.out.println("process with strategyA...");     } }  @Component public class ConcreteStrategyB implements Strategy {      @Override     public String strategy() {         return StrategySelector.strategyB.getStrategy();     }      @Override     public void algorithm() {         System.out.println("process with strategyB...");     } }  @Component public class ConcreteStrategyC implements Strategy {      @Override     public String strategy() {         return StrategySelector.strategyC.getStrategy();     }      @Override     public void algorithm() {         System.out.println("process with strategyC...");     } } 复制代码
  然后,定义一个StrategyConfig配置类,用于向容器注入一个StrategyRunner:@Configuration public class StrategyConfig {      @Bean     public StrategyRunner runner(List strategies) {         Map strategyMap = strategies.stream().collect(Collectors.toMap(Strategy::strategy, s -> s));         return flag -> strategyMap.get(flag).algorithm();     } } 复制代码
  不难发现,strategyRunner方法的实现,其中的逻辑与之前的StrategyRunnerImpl几乎完全相同,也是根据一个List来构造一个Map。只不过,这里的strategies列表不是我们自己构造的,而是通过方法参数传进来的。由于strategyRunner标注了Bean注解,因此参数上的List实际上是在Spring Boot初始化过程中从容器获取的,所以我们之前向容器中注册的那两个实现类会在这里被注入。
  这样,我们再也无需操心系统中一共有多少个Strategy实现类,因为Spring Boot的自动配置会帮我们自动发现所有实现类。我们只需编写自己的Strategy实现类,然后将它注册进容器,并在任何需要的地方注入StrategyRunner:@Autowired private StrategyRunner strategyRunner; 复制代码
  然后直接使用strategyRunner就行了:@RestController @RequestMapping(value = "/designPatterns") public class DesignPatternController {      @Autowired     private StrategyRunner strategyRunner;      @GetMapping(value = "/algorithm")     public void algorithm(@RequestParam("strategy") String strategy) {         strategyRunner.execute(strategy);     } } 复制代码
  访问:http://localhost:10069/designPatterns/algorithm 控制台输出如下:process with strategyA... 复制代码
  类似的业务场景,完全可以结合业务通过方面的代码来进行改造实现,非常实用~九、使用ip2region获取用户地址信息
  ip2region v2.0 - 是一个离线IP地址定位库和IP定位数据管理框架。
  现在很多软件比如:微博、抖音、小红书、头条、快手、腾讯等各大平台陆续都上线了 网络用户IP地址显示功能,境外用户显示的是国家,国内的用户显示的省份。
  以往,Java中获取IP属性的,主要分为以下几步:通过HttpServletRequest对象,获取用户的IP地址通过IP地址,获取对应的省份、城市
  首先需要写一个IP获取的工具类,因为每一次用户的Request请求,都会携带上请求的IP地址放到请求头中,下面这段代码你肯定不陌生: /**      * 获取IP地址      *       * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址      * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址      */     public static String getIpAddr(HttpServletRequest request) {         String ip = null;         try {             ip = request.getHeader("x-forwarded-for");             if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {                 ip = request.getHeader("Proxy-Client-IP");             }             if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {                 ip = request.getHeader("WL-Proxy-Client-IP");             }             if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {                 ip = request.getHeader("HTTP_CLIENT_IP");             }             if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {                 ip = request.getHeader("HTTP_X_FORWARDED_FOR");             }             if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {                 ip = request.getHeader("X-REAL-IP");             }             if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {                 ip = request.getRemoteAddr();             }         } catch (Exception e) {             logger.error("Failed to get the IP address information", e);         }          return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;     } 复制代码
  通过此方法,从请求Header中获取到用户的IP地址。
  还有之前的的项目获取IP地址归属省份、城市的需求,比较常用的是淘宝ip库,地址:
  ip.taobao.com/
  输入本地IP地址可以查询到对应的省市信息:
  模拟根据ip从淘宝IP库获取当前位置信息,源码如下:public static JSONObject getAddressByIp(String ip, RestTemplate restTemplate) {     logger.info("淘宝IP库获取用户IP地址信息...");     ResponseEntity forEntity = restTemplate.getForEntity("https://ip.taobao.com/outGetIpInfo?ip=" + ip, String.class);     JSONObject result = JSONObject.parseObject(forEntity.getBody());     logger.info("获取到淘宝IP库响应信息: {}", result);     if (result.getIntValue("code") == 0) {         logger.info("request successful!");     } else {         logger.info("request failed, 原因:{}", result.getString("msg"));     }     return getAddressByIp(ip, restTemplate); }  public static void main(String[] args) {     getAddressByIp("119.129.116.64", new RestTemplate()); } 复制代码
  响应:11:14:53.266 [main] INFO org.universal.common.util.IPUtils - 淘宝IP库获取用户IP地址信息... 11:14:55.063 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET https://ip.taobao.com/outGetIpInfo?ip=119.129.116.64 11:14:55.107 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[text/plain, application/json, application/*+json, */*] 11:14:57.416 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK 11:14:57.418 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "application/json;charset=UTF-8" 11:14:58.273 [main] INFO org.universal.common.util.IPUtils - 获取到淘宝IP库响应信息: {"msg":"the request over max qps for user ,the accessKey=public","code":4} 11:14:58.522 [main] INFO org.universal.common.util.IPUtils - request failed, 原因:the request over max qps for user ,the accessKey=public 11:14:58.522 [main] INFO org.universal.common.util.IPUtils - 淘宝IP库获取用户IP地址信息... 11:14:58.522 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET https://ip.taobao.com/outGetIpInfo?ip=119.129.116.64 11:14:58.523 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[text/plain, application/json, application/*+json, */*] 11:14:58.657 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK 11:14:58.657 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "application/json;charset=UTF-8"  // ---------------- 成功获取到ip地址信息(中国/广东/广州) START ---------------- 11:14:58.658 [main] INFO org.universal.common.util.IPUtils - 获取到淘宝IP库响应信息: {"msg":"query success","code":0,"data":{"area":"","country":"中国","isp_id":"100017","queryIp":"119.129.116.64","city":"广州","ip":"119.129.116.64","isp":"电信","county":"","region_id":"440000","area_id":"","region":"广东","country_id":"CN","city_id":"440100"}} 11:14:58.658 [main] INFO org.universal.common.util.IPUtils - request successful! 11:14:58.658 [main] INFO org.universal.common.util.IPUtils - 淘宝IP库获取用户IP地址信息... 11:14:58.681 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET https://ip.taobao.com/outGetIpInfo?ip=119.129.116.64 11:14:58.682 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[text/plain, application/json, application/*+json, */*] 11:14:58.802 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK 11:14:58.803 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "application/json;charset=UTF-8" // ------------------------- 成功获取到ip地址信息 END -------------------------  11:14:58.805 [main] INFO org.universal.common.util.IPUtils - 获取到淘宝IP库响应信息: {"msg":"the request over max qps for user ,the accessKey=public","code":4} 11:14:58.805 [main] INFO org.universal.common.util.IPUtils - request failed, 原因:the request over max qps for user ,the accessKey=public 11:14:58.805 [main] INFO org.universal.common.util.IPUtils - 淘宝IP库获取用户IP地址信息... 11:14:58.806 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET https://ip.taobao.com/outGetIpInfo?ip=119.129.116.64 11:14:58.806 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[text/plain, application/json, application/*+json, */*] 11:14:58.947 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK 11:14:58.976 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "application/json;charset=UTF-8" 11:14:58.981 [main] INFO org.universal.common.util.IPUtils - 获取到淘宝IP库响应信息: {"msg":"the request over max qps for user ,the accessKey=public","code":4} 11:14:58.981 [main] INFO org.universal.common.util.IPUtils - request failed, 原因:the request over max qps for user ,the accessKey=public 11:14:59.092 [main] INFO org.universal.common.util.IPUtils - 淘宝IP库获取用户IP地址信息... 11:14:59.092 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET https://ip.taobao.com/outGetIpInfo?ip=119.129.116.64 11:14:59.092 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[text/plain, application/json, application/*+json, */*] 11:14:59.223 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK 11:14:59.223 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "application/json;charset=UTF-8" 11:14:59.223 [main] INFO org.universal.common.util.IPUtils - 获取到淘宝IP库响应信息: {"msg":"the request over max qps for user ,the accessKey=public","code":4} 11:14:59.223 [main] INFO org.universal.common.util.IPUtils - request failed, 原因:the request over max qps for user ,the accessKey=public 11:14:59.320 [main] INFO org.universal.common.util.IPUtils - 淘宝IP库获取用户IP地址信息... 11:14:59.321 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET https://ip.taobao.com/outGetIpInfo?ip=119.129.116.64 11:14:59.321 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[text/plain, application/json, application/*+json, */*] 11:14:59.470 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK 11:14:59.471 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "application/json;charset=UTF-8" 11:14:59.471 [main] INFO org.universal.common.util.IPUtils - 获取到淘宝IP库响应信息: {"msg":"the request over max qps for user ,the accessKey=public","code":4} 11:14:59.471 [main] INFO org.universal.common.util.IPUtils - request failed, 原因:the request over max qps for user ,the accessKey=public 11:14:59.471 [main] INFO org.universal.common.util.IPUtils - 淘宝IP库获取用户IP地址信息... 11:14:59.472 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET https://ip.taobao.com/outGetIpInfo?ip=119.129.116.64 11:14:59.472 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[text/plain, application/json, application/*+json, */*] 11:14:59.598 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK 11:14:59.598 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "application/json;charset=UTF-8" 11:14:59.598 [main] INFO org.universal.common.util.IPUtils - 获取到淘宝IP库响应信息: {"msg":"the request over max qps for user ,the accessKey=public","code":4} 11:14:59.599 [main] INFO org.universal.common.util.IPUtils - request failed, 原因:the request over max qps for user ,the accessKey=public 复制代码
  可以看到控制台输出的日志文件中,大量的请求返回失败,原因:the request over max qps for user ,the accessKey=public,主要是由于接口淘宝对接口进行QPS限流。
  而随着ip2region项目的开源和更新迭代,可以帮助我们解决IP地址定位解析的业务场景开发需求问题,Gitee地址: ip2region99.9%准确率:
  数据聚合了一些知名ip到地名查询提供商的数据,这些是他们官方的的准确率,经测试着实比经典的纯真IP定位准确一些。 ip2region的数据聚合自以下服务商的开放API或者数据(升级程序每秒请求次数2到4次)。p2region V2.0 特性
  1、标准化的数据格式
  每个 ip 数据段的 region 信息都固定了格式:国家|区域|省份|城市|ISP,只有中国的数据绝大部分精确到了城市,其他国家部分数据只能定位到国家,后前的选项全部是0。
  2、数据去重和压缩
  xdb 格式生成程序会自动去重和压缩部分数据,默认的全部 IP 数据,生成的 ip2region.xdb 数据库是 11MiB,随着数据的详细度增加数据库的大小也慢慢增大。
  3、极速查询响应
  即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:vIndex 索引缓存 :使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。xdb 整个文件缓存:将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。
  4、极速查询响应
  v2.0 格式的 xdb 支持亿级别的 IP 数据段行数,region信息也可以完全自定义,例如:你可以在region中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用ip2region来管理你自己的 IP 定位数据。ip2region xdb java IP地址信息解析客户端实现
  pom依赖     org.lionsoul     ip2region     2.6.4  复制代码
  完全基于文件的查询import org.lionsoul.ip2region.xdb.Searcher; import java.io.*; import java.util.concurrent.TimeUnit;  public class SearcherTest {     public static void main(String[] args) {         // 1、创建 searcher 对象         String dbPath = "ip2region.xdb file path";         Searcher searcher = null;         try {             searcher = Searcher.newWithFileOnly(dbPath);         } catch (IOException e) {             System.out.printf("failed to create searcher with `%s`: %s ", dbPath, e);             return;         }          // 2、查询         try {             String ip = "1.2.3.4";             long sTime = System.nanoTime();             String region = searcher.search(ip);             long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));             System.out.printf("{region: %s, ioCount: %d, took: %d μs} ", region, searcher.getIOCount(), cost);         } catch (Exception e) {             System.out.printf("failed to search(%s): %s ", ip, e);         }          // 3、关闭资源         searcher.close();                  // 备注:并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。     } } 复制代码
  IDEA代码实现,测试获取当前IP地址信息:public class SearchTest {     public static void main(String[] args) throws IOException {         // 1、创建 searcher 对象         String dbPath = "D:Sourcetree_workplacegit-优秀开源项目ip2regiondataip2region.xdb";         Searcher searcher = null;         try {             searcher = Searcher.newWithFileOnly(dbPath);         } catch (IOException e) {             System.out.printf("failed to create searcher with `%s`: %s ", dbPath, e);             return;         }          // 本地IP地址         String ip = "119.129.116.64";          // 2、查询         try {             long sTime = System.nanoTime();             String region = searcher.search(ip);             long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));             System.out.printf("{region: %s, ioCount: %d, took: %d μs} ", region, searcher.getIOCount(), cost);         } catch (Exception e) {             System.out.printf("failed to search(%s): %s ", ip, e);         }          // 3、关闭资源         searcher.close();          // 备注:并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。     } } 复制代码
  完全基于文件的查询
  IP属地国内的话,会展示省份,国外的话,只会展示国家。可以通过如下图这个方法进行进一步封装,得到获取IP属地的信息,查询结果如下:
  十、利用好Java现有优秀的开发库
  俗话说:工欲善其事,必先利其器,好的工具可以达到事半功倍的效果。
  一名优秀的技术开发者,往往都能利用现有的资源,利用好市面上优秀的工具包来协助开发,基本上,每个项目里都有一个包,叫做utils。这个包专门承载我们自己项目的工具类,比如常见的DateUtils、HttpUtils、Collections
  所谓Utils就是:这个东西我们用得很多,但是原API不够好用,于是我们给它封装为一个比较通用的方法。10.1 JAVA常用工具包推荐
  工具包
  介绍
  Apache Commons
  地址
  Guava
  地址
  Hutool
  地址
  最新maven仓库      org.apache.commons     commons-lang3     3.12.0         com.google.guava     guava     31.1-jre         cn.hutool     hutool-all     5.8.10  复制代码10.2 Http请求远程调用库推荐
  HTTP调用是非常常见的,很多公司对外的接口几乎都会提供HTTP调用。比如我们调用百度UNIT智能对话API实现与机器人对话服务,调用各个渠道商发送短信等等等。JDK自带的HttpURLConnection标准库Apache HTTPComponents HttpClientOkHttpRetrofitForest10.2.1 HttpURLConnection
  使用HttpURLConnection发起HTTP请求最大的优点是不需要引入额外的依赖,但是使用起来非常繁琐,也缺乏连接池管理、域名机械控制等特性支持。
  使用标准库的最大好处就是不需要引入额外的依赖,但使用起来比较繁琐,就像直接使用JDBC连接数据库那样,需要很多模板代码。来发起一个简单的HTTP POST请求:public class HttpUrlConnectionDemo {     public static void main(String[] args) throws IOException {         String urlString = "https://httpbin.org/post";         String bodyString = "password=123";          URL url = new URL(urlString);         HttpURLConnection conn = (HttpURLConnection) url.openConnection();         conn.setRequestMethod("POST");         conn.setDoOutput(true);          OutputStream os = conn.getOutputStream();         os.write(bodyString.getBytes("utf-8"));         os.flush();         os.close();          if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {             InputStream is = conn.getInputStream();             BufferedReader reader = new BufferedReader(new InputStreamReader(is));             StringBuilder sb = new StringBuilder();             String line;             while ((line = reader.readLine()) != null) {                 sb.append(line);             }             System.out.println("响应内容:" + sb.toString());         } else {             System.out.println("响应码:" + conn.getResponseCode());         }     } }  复制代码
  HttpURLConnection发起的HTTP请求比较原始,基本上算是对网络传输层的一次浅层次的封装;有了 HttpURLConnection对象后,就可以获取到输出流,然后把要发送的内容发送出去;再通过输入流读取到服务器端响应的内容;最后打印。
  不过HttpURLConnection不支持HTTP/2.0,为了解决这个问题,Java 9的时候官方的标准库增加了一个更高级别的HttpClient,再发起POST请求就显得高大上多了,不仅支持异步,还支持顺滑的链式调用。public class HttpClientDemo {     public static void main(String[] args) throws URISyntaxException {         HttpClient client = HttpClient.newHttpClient();         HttpRequest request = HttpRequest.newBuilder()                 .uri(new URI("https://postman-echo.com/post"))                 .headers("Content-Type", "text/plain;charset=UTF-8")                 .POST(HttpRequest.BodyPublishers.ofString("二哥牛逼"))                 .build();         client.sendAsync(request, HttpResponse.BodyHandlers.ofString())                 .thenApply(HttpResponse::body)                 .thenAccept(System.out::println)                 .join();     } }  复制代码10.2.2 Apache HttpComponents HttpClient
  Apache HttpComponents HttpClient支持的特性也非常丰富:基于标准、纯净的Java语言,实现了HTTP1.0和HTTP1.1;以可扩展的面向对象的结构实现了HTTP全部的方法;支持加密的HTTPS协议(HTTP通过SSL协议);Request的输出流可以避免流中内容体直接从socket缓冲到服务器;Response的输入流可以有效的从socket服务器直接读取相应内容。10.2.3 OkHttp
  OkHttp是一个执行效率比较高的HTTP客户端:支持HTTP/2.0,当多个请求对应同一个Host地址时,可共用同一个Socket;连接池可减少请求延迟;支持GZIP压缩,减少网络传输的数据大小;支持Response数据缓存,避免重复网络请求;public class OkHttpPostDemo {     public static final MediaType JSON = MediaType.get("application/json; charset=utf-8");      OkHttpClient client = new OkHttpClient();      String post(String url, String json) throws IOException {         RequestBody body = RequestBody.create(json, JSON);         Request request = new Request.Builder()                 .url(url)                 .post(body)                 .build();         try (Response response = client.newCall(request).execute()) {             return response.body().string();         }     }      public static void main(String[] args) throws IOException {         OkHttpPostDemo example = new OkHttpPostDemo();         String json = "{"name":"二哥"}";         String response = example.post("https://httpbin.org/post", json);         System.out.println(response);     } }  复制代码10.2.4 Forest
  Forest是一个高层的、极简的声明式HTTP调用API框架。相比于直接使用Httpclient你不再用写一大堆重复的代码了,而是像调用本地方法一样去发送HTTP请求。
  Forest就字面意思而言,就是森林的意思。但仔细看可以拆成For和Rest两个单词,也就是为了Rest(Rest为一种基于HTTP的架构风格)。 而合起来就是森林,森林由很多树木花草组成(可以理解为各种不同的服务),它们表面上看独立,实则在地下根茎交错纵横、相互连接依存,这样看就有点现代分布式服务化的味道了。 最后,这两个单词反过来读就像是Resultful。
  Maven依赖     com.dtflys.forest     forest-spring-boot-starter     1.5.19  复制代码简单请求public interface MyClient {      @Request("http://localhost:8080/hello")     String simpleRequest();  } 复制代码
  通过@Request注解,将上面的MyClient接口中的simpleRequest()方法绑定了一个 HTTP 请求, 其 URL 为http://localhost:8080/hello ,并默认使用GET方式,且将请求响应的数据以String的方式返回给调用者。稍微复杂点的请求,需要在请求头设置信息public interface MyClient {      @Request(             url = "http://localhost:8080/hello/user",             headers = "Accept: text/plain"     )     String sendRequest(@Query("uname") String username); } 复制代码
  上面的sendRequest方法绑定的HTTP请求,定义了URL信息,以及把Accept:text/plain加到了请求头中, 方法的参数String username绑定了注解@Query("uname"),它的作用是将调用者传入入参username时,自动将username的值加入到 HTTP 的请求参数uname中。
  这段实际产生的HTTP请求如下:GET http://localhost:8080/hello/user?uname=foo HEADER:     Accept: text/plain 复制代码请求方法,假设发起post请求,有3种写法:public interface MyClient {      /**      * 使用 @Post 注解,可以去掉 type = "POST" 这行属性      */     @Post("http://localhost:8080/hello")     String simplePost1();       /**      * 通过 @Request 注解的 type 参数指定 HTTP 请求的方式。      */     @Request(             url = "http://localhost:8080/hello",             type = "POST"     )     String simplePost2();      /**      * 使用 @PostRequest 注解,和上面效果等价      */     @PostRequest("http://localhost:8080/hello")     String simplePost3();  } 复制代码
  可以用@GetRequest, @PostRequest等注解代替@Request注解,这样就可以省去写type属性的麻烦了。请求体
  在POST和PUT等请求方法中,通常使用 HTTP 请求体进行传输数据。在 Forest 中有多种方式设置请求体数据。表单格式
  上面使用@Body注解的例子用的是普通的表单格式,也就是contentType属性为application/x-www-form-urlencoded的格式,即contentType不做配置时的默认值。
  表单格式的请求体以字符串 key1=value1&key2=value2&...&key{n}=value{n} 的形式进行传输数据,其中value都是已经过URL Encode编码过的字符串。/**  * contentType属性设置为 application/x-www-form-urlencoded 即为表单格式,  * 当然不设置的时候默认值也为 application/x-www-form-urlencoded, 也同样是表单格式。  * 在 @Body 注解的 value 属性中设置的名称为表单项的 key 名,  * 而注解所修饰的参数值即为表单项的值,它可以为任何类型,不过最终都会转换为字符串进行传输。  */ @Post(     url = "http://localhost:8080/user",     contentType = "application/x-www-form-urlencoded",     headers = {"Accept:text/plain"} ) String sendPost(@Body("key1") String value1,  @Body("key2") Integer value2, @Body("key3") Long value3); 复制代码
  调用后产生的结果可能如下:POST http://localhost:8080/hello/user HEADER:     Content-Type: application/x-www-form-urlencoded BODY: ...

天文学家利用射电望远镜侦测到木卫一大气受到表面火山喷发影响天文学家利用阿塔卡马大型毫米波次毫米波阵列(AtacamaLargeMillimetersubmillimeterArray)射电电望远镜的新射电图像首次显示木卫一(Io)上的火山2022。5。29iOS今日限免推荐周日不偷懒,继续给大家带来今天最新限免,共五款,快来看看吧一我的足迹骑行远足慢跑有内购介绍足迹记录和分析工具,无论走路跑步骑车,都能帮你记录运动路线速度距离和海拔高度等数据。无需登谷歌,再次推迟发布折叠屏手机RossYoung从今年第四季度推迟到明年春天谷歌正在开发的折叠屏智能手机再次推迟上市。美国IT媒体TheVerge25日(当地时间)援引市场调查企业显示器供应链顾问(DSCC)创梦中情机三星GalaxyS22系列,打造手机摄影新体验对于全民Vlogger的Z世代用户来说,使用影像记录和分享生活,是如呼吸和喝水般的日常操作。三星GalaxyS22系列凭借突破性的摄像头和前沿的AI技术,将手机摄影提升机全新的高度荣耀发力PC领域,入局者能否成为破局者?南都讯记者严兆鑫荣耀日前发布笔记本电脑新品MagicBook14。值得注意的是,荣耀此次还发出进军PC领域的新战略基于软硬件协同和底层创新,对现有架构及体系实现升级和跃迁。从华为独实际上人类还无法解释飞机为什么能飞无法解释的升力在严格的数学层面上,工程师知道如何设计能够保持飞行的飞机。但是方程式本身并不能解释升力产生的原理。有两种主流的升力解释理论。可惜,两者都是不完整的解释。空气动力学家最你的下一款iPhone将产自越南?看到越南产苹果耳机投诉量,不担心了本文来源时代周报作者林渺越南制造业步入了发展快车道。近日,越南交出一份亮眼成绩3月出口额347。1亿美元,环比增长48。2,超过同期深圳出口总额。一时间,越南的重要经济引擎加工制造假如通胀来临,老百姓该如何减少自身的损失?前言对于通货膨胀,可能很多人都会为之而感到害怕。我们都知道如果流通的货币超过了实际的需求量,就会引起货币贬值物价持续而普遍地上涨,也就是社会总需求大于社会总供给。估计很多人对于曾经央妈大幅降息,你不买房,还提前还贷,几个意思?最近,阿志在和几个做按揭的朋友聊天时,对方都和我吐槽市场好差啊,我感觉坟头草已经萌芽了。央妈不断地降息,却没能带来预期中的楼市复苏,很多朋友,甚至提前还贷了。可以说,降息是一味猛药内幕交易大案曝光券商员工操纵24个账户,配资9亿炒股巨亏5200万华夏时报(www。chinatimes。net。cn)记者陈锋见习记者邱利北京报道一顿操作猛如虎,一看收益还为负。昔日打工皇帝唐骏当年进军资本市场的往事时隔5年再度浮现,这次还牵涉刘云天透露曹云金境况,暗示后悔离开德云社,被喊话归还云字大家好,我是相声演员刘云天,这句话刘云天已经很久没有说了,毕竟谁也不会想到,曾经在相声界有着非常高人气的刘云天,竟然会靠直播为生。刘云天透露曹云金境况刘云天在直播中透露,自己跟曹云
央视曝光国内青训猫腻费用高到离谱,培养哈兰德都没这么费钱头条创作挑战赛众所周知,近期男足球员比较接近留洋的是广州队年轻前锋谭凯元,然而在塞尔维亚试训通过并且训练了半年时间后,谭凯元依旧没能完成转会,其留洋失败的原因是广州队索要6万欧元青因嫖娼身败名裂的8位艺人,有人死鸭子嘴硬,有人气焰嚣张古剑奇谭的男主角因嫖娼被抓,据警方通报还是多次。消息传来,石破天惊,神州大地,风雷激荡。一个帅气逼人光环加身的大明星,居然嫖娼!实在让人不敢相信。实际上演艺圈爆嫖娼丑闻的明星大有人门德斯喉舌切尔西新老板已和坎波斯接触,想让他当新总监根据为泰晤士报供稿的门德斯喉舌DuncanCastles的最新报道,切尔西有意聘请路易斯坎波斯作为他们的体育总监,与新任命的主教练格雷厄姆波特一起工作。58岁的葡萄牙人被巴黎方面称对有童秃的新生儿,应如何注意保护头发?才能促进毛发生长有的新生儿出生后。头皮光秃秃的。稀稀拉拉长了几根又黄又软的头发。这是正常现象。婴儿出生时,头发的多少本来是有差别的。胎儿在母亲的子宫里。发育到五到六个月时。全身就有浓密的胎毛。以后影响小儿生长发育的因素很多,为了孩子的健康成长,你必须了解在小儿成长的过程中,体格生长智能及心理的发展一直受到内外因素的影响。(1)遗传父母的高矮对孩子身高影响较大。近亲结婚子女智能迟缓发生率高。父母的性格可以遗传给下一代。(2)营养小儿吉利下场微型纯电车市场,新车几何M2会定价多少?如果我们翻开近两年的新能源车销量排行榜,榜单前十的车型,基本上都是以五菱宏光MINIEV为首的微型纯电车,凭借着亲民的价格以及不俗的城市代步属性,该细分市场的基本盘也越做越大。时至苹果有意采用中国芯片,体现它正认识到中国市场非常重要苹果有意采用中国的存储芯片引发了各方的关注,此举其实体现了苹果认识到中国市场如今对它非常重要,印度越南等市场无法与中国市场相比,因此苹果此举可能是为了示好中国消费者。在苹果传出可能机皇逐渐务实定价,其他品牌还不反思?高价市场已经不吃香看文章听音乐是种享受,想听什么留言告知(都是付费无损包)如果细看近两年手机市场的定价,我们可以看到,一些传统老牌手机品牌和一些新兴手机品牌在定价方面都逐渐开始变得比较接地气了,这个新外观能取胜?机构iPhone14有望巩固苹果高端手机市场优势9月11日消息,IDC最新发布的数据显示,2022年上半年中国600美元以上高端手机市场份额达到13。3,相比2021和2020年分别增长1。0和3。8个百分点。IDC指出,高端手补钙,别光喝牛奶,提醒中老年人这5种也不差,还不用乱花钱老年人随年龄增加,骨矿物质不断丢失,骨密度逐渐下降,尤其是女性朋友们在绝经后由于激素水平变化骨质丢失更为严重。另一方面老年人胃肠功能下降,钙吸收不足,如果饮食再不多加以注意,就更容身体乏力,五脏亏虚?五种中成药,补足心肝脾肺肾之气大家好,我是贾医生,如果你总是觉得自己身体很乏力,做什么都提不起精神,没有力气,那么你就要注意了,千万不要忽视,这种情况多见是由于我们的五脏亏虚所致,今天贾医生就来给大家分享五种中