hellolvs

hellolvs 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 www.hellolvs.cn 编辑
编辑

个人动态

hellolvs 发布了文章 · 2020-01-20

基于反射、POI和OSS的异步导出工具(含同步)

一行代码搞定各种excel导出需求的精简导出组件。

前言

平时我们的项目中,经常会遇到各种各样的导出需求,不管是导出何种类型的DO,同步导出还是异步导出,小数据量导出亦或是大数据量的导出,有没有一个通用的工具类,只需要ExcelHelper.export()就搞定了,而不需要自己去为各类需求编码各种各样的导出方法。

本篇就是分享这样一种精简的导出工具。

  • 同步导出

    ExcelHelper.export(String fileName, List<T> list, HttpServletResponse response);

    fileName随便定义,list直接传入数据集即可。(数据DO类导出字段需要加@HeaderColumn注解,下述)

  • 异步导出

    excelHelper.exportAsync(DataFetcher<T> dataFetcher);

    dataFetcher传入一个Lambda表达式,自定义取数查询逻辑,分页查询和数据量上限可以自行定义。

简述

  1. Apache POI

    POI提供了很多对Microsoft Office的功能,这里只涉及POI的Excel导出功能。

    POI提供了三种Excel导出的API。

    HSSF——Excel '97(-2007)格式的导出,即.xls,最大行数65535,列数256

    XSSF—— Excel 2007 OOXML格式的导出,即.xlsx,最大行数1048576,列数16384

    SXSSF——poi3.8-beta3版本加入,基于XSSF针对大数据量的导出做了优化。HSSF和XSSF会将所有Row放到内存中,不但容易导致OOM,而且频繁GC性能较低。而SXSSF提供了一种流式API,会在内存中维护一个滑动窗口,不断将数据刷到磁盘中,滑动窗口默认大小为100,内存消耗和性能都得到了提升。

    本文当然使用第三种API。

  2. 反射ReflectASM

    实现各种各样的数据DO的导出通用性,反射是必不可少的。

    不过我们知道反射的性能开销是很大的,对于大数据量导出,如果频繁用反射获取属性值或方法调用,性能是非常低下的。

    这里引入了高效的反射工具ReflectASM,通过字节码生成技术使得其性能几乎跟代码直接调用一样,原理请自行查阅。不过生成字节码MethodAccess、FeildAccess这一步是比较耗时的,这里使用了本地缓存来缓存字节码,这样字节码生成在每个导出任务中至多执行一次。

  3. 对象存储OSS

    异步导出的话,需要将导出的excel存储起来,提供给用户下载。阿里云上有很方便的对象存储平台OSS。非阿里云用户可以考虑其他存储方式,原理一样。

    文档:https://help.aliyun.com/docum...

    控制台:https://oss.console.aliyun.co...

  4. 异步导出

    异步导出的交互形式:

    第一次请求异步导出接口:xx/xxxExportAsync
    返回:
    {

    "success": true,
    "data": {
        "token": "xxxxxxx"
    },
    "msg": ""

    }

    之后用拿到的token轮询请求: xxxx/getExport?token=xxxxxx
    成功返回:
    {

    "success": true,
    "data": {
        "status": "SUCCESS",
      "url": "http://xxxxxxxxx",
    
        "msg":""
     
    },
    "msg": ""

    }
    失败:status为FAILURE,msg为失败信息
    处理中:status为PROCESSING

    任务完成后,用户直接用返回的url下载excel。

工具包引入

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>3.10-FINAL</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>3.10-FINAL</version>
</dependency>
<dependency>
    <groupId>com.esotericsoftware.reflectasm</groupId>
    <artifactId>reflectasm</artifactId>
    <version>1.09</version>
</dependency>
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>2.8.3</version>
</dependency>
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>20.0</version>
</dependency>

其他还需要spring和servlet,一般工程都有就不列了,其中还有jdk8的语法,用低版本jdk的可以自行替换掉。

实现

  1. 列头注解

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface HeaderColumn {
    
        String value() default "";
    
        String sortIndex() default "";
    
        boolean visible() default true;
    
        boolean sortable() default false;
    
        boolean editable() default false;
    }

    这里只用value属性就可以了,表示列名,如下

    @HeaderColumn("商品名称")
    private String itemTitle;
  2. 反射缓存

 * 缓存ReflectASM生成的字节码
 */
private static LoadingCache<Class<?>, MethodAccess> methodCache = CacheBuilder.newBuilder()
    .maximumSize(1000)
    .build(new CacheLoader<Class<?>, MethodAccess>() {
        @Override
        public MethodAccess load(Class<?> clazz) {
            return MethodAccess.get(clazz);
        }
    });

/**
 * 类与属性映射缓存
 */
private static LoadingCache<Class<?>, Field[]> declaredFieldsCache = CacheBuilder.newBuilder()
    .maximumSize(1000)
    .build(new CacheLoader<Class<?>, Field[]>() {
        @Override
        public Field[] load(Class<?> clazz) {
            Field[] result = clazz.getDeclaredFields();
            return result.length == 0 ? NO_FIELDS : result;
        }
    });
    

反射工具多与缓存结合使用,可以提升性能。

  1. OSS接入与异步线程池

    private static final String END_POINT = "http://oss-xxxx.com";
    private static final String ACCESS_KEY_ID = "********";
    private static final String ACCESS_KEY_SECRET = "********";
    private static final String BUCKET_NAME = "********";
    private static final String XLSX_SUFFIX = ".xlsx";
    private static OSSClient ossClient;
    
    private static final int DEFAULT_CORE_POOL_SIZE = 10;
    private static final int DEFAULT_MAX_POOL_SIZE = 720;
    private static final int DEFAULT_KEEP_ALIVE_TIME = 10;
    private static final String DEFAULT_THREAD_NAME_PREFIX = "ExcelHelper-Thread-";
    private static ExecutorService executor;
    
    @PostConstruct
        void init() {
            ossClient = new OSSClient(END_POINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
            SetBucketLifecycleRequest request = new SetBucketLifecycleRequest(BUCKET_NAME);
            // 距最后修改时间1天后过期。
            request.AddLifecycleRule(new LifecycleRule("rule0", "", LifecycleRule.RuleStatus.Enabled, 1));
            ossClient.setBucketLifecycle(request);
    
            executor = new ThreadPoolExecutor(DEFAULT_CORE_POOL_SIZE, DEFAULT_MAX_POOL_SIZE, DEFAULT_KEEP_ALIVE_TIME,
                TimeUnit.MINUTES, new SynchronousQueue<>(), new ThreadFactory() {
    
                private int counter = 0;
    
                @Override
                public Thread newThread(Runnable run) {
                    Thread t = new Thread(run, DEFAULT_THREAD_NAME_PREFIX + counter);
                    counter++;
                    return t;
                }
            }, (r, e) -> {
                throw new RejectedExecutionException(
                    "ExcelHelper thread pool is full, max pool size : " + DEFAULT_MAX_POOL_SIZE);
            });
        }
        
        @PreDestroy
        void destroy() {
            if (null != ossClient) {
                ossClient.shutdown();
            }
            if (null != executor) {
                executor.shutdown();
            }
        }

    注意替换oss接入相关常量,导出文件不需要在oss持久存储,所以设置了1天自动删除节省空间。

  2. 创建表头

   /**
    * 获取表头各列属性描述
    *
    * @param clazz 数据类型
    * @return 表头属性描述
    * @throws ExecutionException e
    */
   private static LinkedHashMap<String, String> createHeaders(Class clazz) throws ExecutionException {
   
       LinkedHashMap<String, String> headers = new LinkedHashMap<>();
       Class<?> searchType = clazz;
       while (Object.class != searchType && searchType != null) {
           Field[] fields = declaredFieldsCache.get(searchType);
           for (Field field : fields) {
               HeaderColumn annotation = field.getAnnotation(HeaderColumn.class);
               if (annotation != null) {
                   headers.put(field.getName(), annotation.value());
               }
           }
           searchType = searchType.getSuperclass();
       }
   
       return headers;
   }

LinkedHashMap保证列头的顺序性,有些数据DO是有继承父类的,所以要加上循环输出父类注解属性。

  1. 写入表数据

       /**
        * 创建Excel
        *
        * @param list  数据列表
        * @param sheet excel中的sheet
        * @param <T>   泛型T
        * @throws ExecutionException e
        */
       private static <T> void createExcel(List<T> list, Sheet sheet) throws ExecutionException {
   
           if (list == null || list.isEmpty()) {
               return;
           }
           Class clazz = list.get(0).getClass();
   
           /* 表头 */
           LinkedHashMap<String, String> headers = createHeaders(clazz);
           Row header = sheet.createRow(0);
           Iterator<Map.Entry<String, String>> headTitle = headers.entrySet().iterator();
           for (int i = 0; headTitle.hasNext(); i++) {
               Cell cell = header.createCell(i);
               cell.setCellValue(headTitle.next().getValue());
           }
   
           MethodAccess access = methodCache.get(clazz);
   
           /* 表数据 */
           for (int i = 0; i < list.size(); i++) {
               Object obj = list.get(i);
               Row row = sheet.createRow(i + 1);
               Iterator<Map.Entry<String, String>> headTitle2 = headers.entrySet().iterator();
               for (int j = 0; headTitle2.hasNext(); j++) {
                   Cell cell = row.createCell(j);
                   String dataIndex = headTitle2.next().getKey();
                   //反射获取属性值
                   Object result;
                   try {
                       result = access.invoke(obj, createGetMethod(dataIndex));
                   } catch (Exception e) {
                       result = access.invoke(obj, createIsMethod(dataIndex));
                   }
                   if (result instanceof String) {
                       cell.setCellValue((String)result);
                   } else if (result instanceof Date) {
                       Date date = (Date)result;
                       cell.setCellValue(DEFAULT_DATE_TIME_FORMATTER.format(date.toInstant()));
                   } else if (result instanceof Integer) {
                       cell.setCellValue((Integer)result);
                   } else if (result instanceof Double) {
                       cell.setCellValue((Double)result);
                   } else if (result instanceof Boolean) {
                       cell.setCellValue((Boolean)result);
                   } else if (result instanceof Float) {
                       cell.setCellValue((Float)result);
                   } else if (result instanceof Short) {
                       cell.setCellValue((Short)result);
                   } else if (result instanceof Byte) {
                       cell.setCellValue((Byte)result);
                   } else if (result instanceof Long) {
                       cell.setCellValue((Long)result);
                   } else if (result instanceof BigDecimal) {
                       cell.setCellValue(((BigDecimal)result).doubleValue());
                   } else if (result instanceof Character) {
                       cell.setCellValue((Character)result);
                   } else {
                       cell.setCellValue(result == null ? "" : result.toString());
                   }
               }
           }
       }

这里使用了reflectASM来取属性数据 ,方法拼凑如下,注意布尔型属性的get方法可能is开头。

/**
 * 通过属性名称拼凑getter方法
 *
 * @param fieldName 属性名称
 * @return getter方法名
 */
private static String createGetMethod(String fieldName) {
    if (fieldName == null || fieldName.length() == 0) {
        return null;
    }
    return "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
}

/**
 * 通过属性名称拼凑is方法
 *
 * @param fieldName 属性名称
 * @return getter方法名
 */
private static String createIsMethod(String fieldName) {
    if (fieldName == null || fieldName.length() == 0) {
        return null;
    }
    return "is" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
}
  1. 同步导出

   /**
    * 同步导出excel
    *
    * @param fileName 文件名
    * @param list     数据列表
    * @param response http响应
    * @param <T>      元素类型
    * @throws Exception e
    */
   public static <T> void export(String fileName, List<T> list, HttpServletResponse response)
       throws Exception {
       Preconditions.checkNotNull(fileName);
       Preconditions.checkNotNull(list);
   
       SXSSFWorkbook wb = new SXSSFWorkbook();
       Sheet sheet = wb.createSheet();
   
       createExcel(list, sheet);
       output(fileName, wb, response);
   }
   
   /**
        * 输出excel到response
        *
        * @param fileName 文件名
        * @param wb       SXSSFWorkbook对象
        * @param response response
        */
       private static void output(String fileName, SXSSFWorkbook wb, HttpServletResponse response) throws IOException {
           OutputStream out = null;
           try {
               response.setCharacterEncoding("utf-8");
               response.addHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
               response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
               out = response.getOutputStream();
               wb.write(out);
           } finally {
               if (out != null) {
                   out.flush();
                   out.close();
               }
               wb.dispose();
           }
       }

同步导出适合数据量小的任务,将excel直接以附件形式放到response里提供下载。

同步导出直接在controller层调用下面即可。

ExcelHelper.export(String fileName, List<T> list, HttpServletResponse response);
  1. 异步导出

       /**
        * 异步导出excel
        *
        * @param dataFetcher 数据获取接口
        * @param <T>         元素类型
        * @return 导出任务token
        */
       public <T> Map<String, String> exportAsync(DataFetcher<T> dataFetcher) {
   
           //生成任务查询token
           String token = UUID.randomUUID().toString();
   
           RiskAsyncExportDO riskAsyncExportDO = new RiskAsyncExportDO();
           riskAsyncExportDO.setGmtCreate(new Date());
           riskAsyncExportDO.setGmtModified(new Date());
           riskAsyncExportDO.setToken(token);
           riskAsyncExportDO.setStatus(PROCESSING);
           riskAsyncExportDO.setUrl("");
           riskAsyncExportDO.setMsg("");
           riskAsyncExportRepository.save(riskAsyncExportDO);
   
           //异步导出任务
           executor.execute(new ThreadPoolTask<>(token, dataFetcher));
   
           Map<String, String> result = Maps.newHashMap();
           result.put("token", token);
           return result;
       }
   
   /**
    * 异步导出线程
    *
    * @param <T> 泛型T
    */
   private class ThreadPoolTask<T> implements Runnable, Serializable {
   
       private final String token;
       private final DataFetcher<T> dataFetcher;
   
       ThreadPoolTask(String token, DataFetcher<T> dataFetcher) {
           this.token = token;
           this.dataFetcher = dataFetcher;
       }
   
       @Override
       public void run() {
           try {
               List<T> list = dataFetcher.fetchData();
   
               SXSSFWorkbook wb = new SXSSFWorkbook();
               Sheet sheet = wb.createSheet();
   
               createExcel(list, sheet);
               outputAsync(token + XLSX_SUFFIX, wb);
   
               /*oss生成含签名的资源url*/
               GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(BUCKET_NAME, token + XLSX_SUFFIX,
                   HttpMethod.GET);
               //设置url一天过期
               request.setExpiration(Date.from(LocalDateTime.now().plusDays(1).atZone(ZoneId.systemDefault())
                   .toInstant()));
               URL signedUrl = ossClient.generatePresignedUrl(request);
   
               //更新导出任务状态
               riskAsyncExportRepository.updateBytoken(token, SUCCESS, signedUrl.toString(), "");
           } catch (Exception e) {
               //任务失败
               riskAsyncExportRepository.updateBytoken(token, FAILURE, "",
                       e.getMessage() == null ? "null" : e.getMessage());
           }
       }
   }
   
       /**
        * 上传excel到oss
        *
        * @param key oss的key
        * @param wb  SXSSFWorkbook对象
        * @throws Exception e
        */
       private static void outputAsync(String key, SXSSFWorkbook wb) throws Exception {
           try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
               wb.write(out);
               ossClient.putObject(BUCKET_NAME, key, new ByteArrayInputStream(out.toByteArray()));
           } finally {
               wb.dispose();
           }
       }
   
       /**
        * 函数式数据获取接口
        *
        * @param <T> 泛型T
        */
       @FunctionalInterface
       public interface DataFetcher<T> {
           /**
            * 数据获取方法,由业务层实现该方法
            *
            * @return 数据列表
            */
           List<T> fetchData();
       }
   
       /**
        * 获取导出任务结果
        *
        * @param token 导出任务token
        * @return 导出任务结果
        */
       public Map<String, String> getExport(String token) {
           RiskAsyncExportDO riskAsyncExportDO = riskAsyncExportRepository.findByToken(token);
           if (riskAsyncExportDO == null) {
               return null;
           }
           Map<String, String> result = Maps.newHashMap();
           result.put("status", riskAsyncExportDO.getStatus());
           result.put("url", riskAsyncExportDO.getUrl());
           result.put("msg", riskAsyncExportDO.getMsg());
           return result;
       }
代码逻辑

exportAsync会返回导出任务token,同时将任务信息插入到任务表中,并开一个线程去做查询导出。

异步线程中查询接口DataFetcher作为参数由具体业务传入执行,之后生成excel并上传到oss,返回含签名信息的url(1天有效期),完成后更新任务表的任务status和导出url。

使用说明

在具体页面controller中注入excelHelper。然后在异步导出接口中调用excelHelper.exportAsync(DataFetcher<T> dataFetcher); 该接口返回本次任务token。

DataFetcher为业务自定义数据查询接口,dk8可使用lamdba表达式,低版本重写接口方法亦可,该接口主要是业务查询逻辑,注意自行分页。

之后在一个通用controller中写一个查询导出任务结果的方法供前端轮询,该方法中调用getExport(String token);

页面导出接口使用示例

Map<String, String> result = excelHelper.exportAsync(() -> {
            List<AscpLogSupplierVO> list = new ArrayList<>();
            JsonResult<CaiyunIndexTableResult<AscpLogSupplierVO>> jsonResult;
            int i = 1;
            while (true) {
                logQueryVO.setPageIndex(i);
                logQueryVO.setPageSize(1000);
                jsonResult = getSupplier(logQueryVO);
                if (jsonResult.getData() != null && jsonResult.getData().getList() != null
                    && jsonResult.getData().getList().size() > 0 && list.size() < 100000) {
                    list.addAll(jsonResult.getData().getList());
                } else {
                    break;
                }
                i++;
            }
            return list;
        });

通用轮询接口示例

@GetMapping("/getExport")
public JsonResult getExport(String token) {
    try {
        Map<String, String> map = excelHelper.getExport(token);
        return JsonResult.succ(map);
    } catch (Exception e) {
        return JsonResult.fail(e.getMessage());
    }
}

导出任务表结构:

CREATE TABLE `async_export` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `gmt_create` datetime NOT NULL COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL COMMENT '修改时间',
  `token` varchar(255) NOT NULL DEFAULT '' COMMENT 'oss导出token',
  `status` varchar(64) NOT NULL DEFAULT '' COMMENT '导出任务状态',
  `url` varchar(1024) NOT NULL DEFAULT '' COMMENT '下载链接',
  `msg` varchar(1024) NOT NULL DEFAULT '' COMMENT '失败信息',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='异步导出任务表';

DataFetcher中不要使用类似SessionUtil含有ThreadLocal属性的类,因为DataFetcher是在新线程工作,ThreadLocal属性会丢失。可以将session信息获取放到外层,传入到DataFetcher。

附完整代码

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HeaderColumn {

    String value() default "";

    String sortIndex() default "";

    boolean visible() default true;

    boolean sortable() default false;

    boolean editable() default false;
}
@Component
public class ExcelHelper {

    private static final Field[] NO_FIELDS = {};

    private static final String SUCCESS = "SUCCESS";
    private static final String FAILURE = "FAILURE";
    private static final String PROCESSING = "PROCESSING";

    private static final String END_POINT = "http://oss-xxxx.com";
    private static final String ACCESS_KEY_ID = "********";
    private static final String ACCESS_KEY_SECRET = "********";
    private static final String BUCKET_NAME = "********";
    private static final String XLSX_SUFFIX = ".xlsx";
    private static OSSClient ossClient;

    private static final int DEFAULT_CORE_POOL_SIZE = 10;
    private static final int DEFAULT_MAX_POOL_SIZE = 720;
    private static final int DEFAULT_KEEP_ALIVE_TIME = 10;
    private static final String DEFAULT_THREAD_NAME_PREFIX = "ExcelHelper-Thread-";
    private static ExecutorService executor;

    private static final DateTimeFormatter DEFAULT_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(
        "yyyy-MM-dd HH:mm:ss").withLocale(Locale.CHINA).withZone(ZoneId.systemDefault());

    /**
     * 缓存ReflectASM生成的字节码
     */
    private static LoadingCache<Class<?>, MethodAccess> methodCache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .build(new CacheLoader<Class<?>, MethodAccess>() {
            @Override
            public MethodAccess load(Class<?> clazz) {
                return MethodAccess.get(clazz);
            }
        });

    /**
     * 类与属性映射缓存
     */
    private static LoadingCache<Class<?>, Field[]> declaredFieldsCache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .build(new CacheLoader<Class<?>, Field[]>() {
            @Override
            public Field[] load(Class<?> clazz) {
                Field[] result = clazz.getDeclaredFields();
                return result.length == 0 ? NO_FIELDS : result;
            }
        });

    private final RiskAsyncExportRepository riskAsyncExportRepository;

    @Autowired
    public ExcelHelper(RiskAsyncExportRepository riskAsyncExportRepository) {
        this.riskAsyncExportRepository = riskAsyncExportRepository;
    }

    @PostConstruct
    void init() {
        ossClient = new OSSClient(END_POINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
        SetBucketLifecycleRequest request = new SetBucketLifecycleRequest(BUCKET_NAME);
        // 距最后修改时间1天后过期。
        request.AddLifecycleRule(new LifecycleRule("rule0", "", LifecycleRule.RuleStatus.Enabled, 1));
        ossClient.setBucketLifecycle(request);

        executor = new ThreadPoolExecutor(DEFAULT_CORE_POOL_SIZE, DEFAULT_MAX_POOL_SIZE, DEFAULT_KEEP_ALIVE_TIME,
            TimeUnit.MINUTES, new SynchronousQueue<>(), new ThreadFactory() {

            private int counter = 0;

            @Override
            public Thread newThread(Runnable run) {
                Thread t = new Thread(run, DEFAULT_THREAD_NAME_PREFIX + counter);
                counter++;
                return t;
            }
        }, (r, e) -> {
            throw new RejectedExecutionException(
                "ExcelHelper thread pool is full, max pool size : " + DEFAULT_MAX_POOL_SIZE);
        });
    }

    /**
     * 同步导出excel
     *
     * @param fileName 文件名
     * @param list     数据列表
     * @param response http响应
     * @param <T>      元素类型
     * @throws Exception e
     */
    public static <T> void export(String fileName, List<T> list, HttpServletResponse response)
        throws Exception {
        Preconditions.checkNotNull(fileName);
        Preconditions.checkNotNull(list);

        SXSSFWorkbook wb = new SXSSFWorkbook();
        Sheet sheet = wb.createSheet();

        createExcel(list, sheet);
        output(fileName, wb, response);
    }

    /**
     * 异步导出excel
     *
     * @param dataFetcher 数据获取接口
     * @param <T>         元素类型
     * @return 导出任务token
     */
    public <T> Map<String, String> exportAsync(DataFetcher<T> dataFetcher) {

        //生成任务查询token
        String token = UUID.randomUUID().toString();

        RiskAsyncExportDO riskAsyncExportDO = new RiskAsyncExportDO();
        riskAsyncExportDO.setGmtCreate(new Date());
        riskAsyncExportDO.setGmtModified(new Date());
        riskAsyncExportDO.setToken(token);
        riskAsyncExportDO.setStatus(PROCESSING);
        riskAsyncExportDO.setUrl("");
        riskAsyncExportDO.setMsg("");
        riskAsyncExportRepository.save(riskAsyncExportDO);

        //异步导出任务
        executor.execute(new ThreadPoolTask<>(token, dataFetcher));

        Map<String, String> result = Maps.newHashMap();
        result.put("token", token);
        return result;
    }

    /**
     * 获取导出任务结果
     *
     * @param token 导出任务token
     * @return 导出任务结果
     */
    public Map<String, String> getExport(String token) {
        RiskAsyncExportDO riskAsyncExportDO = riskAsyncExportRepository.findByToken(token);
        if (riskAsyncExportDO == null) {
            return null;
        }
        Map<String, String> result = Maps.newHashMap();
        result.put("status", riskAsyncExportDO.getStatus());
        result.put("url", riskAsyncExportDO.getUrl());
        result.put("msg", riskAsyncExportDO.getMsg());
        return result;
    }

    /**
     * 异步导出线程
     *
     * @param <T> 泛型T
     */
    private class ThreadPoolTask<T> implements Runnable, Serializable {

        private final String token;
        private final DataFetcher<T> dataFetcher;

        ThreadPoolTask(String token, DataFetcher<T> dataFetcher) {
            this.token = token;
            this.dataFetcher = dataFetcher;
        }

        @Override
        public void run() {
            try {
                List<T> list = dataFetcher.fetchData();

                SXSSFWorkbook wb = new SXSSFWorkbook();
                Sheet sheet = wb.createSheet();

                createExcel(list, sheet);
                outputAsync(token + XLSX_SUFFIX, wb);

                /*oss生成含签名的资源url*/
                GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(BUCKET_NAME, token + XLSX_SUFFIX,
                    HttpMethod.GET);
                //设置url一天过期
                request.setExpiration(Date.from(LocalDateTime.now().plusDays(1).atZone(ZoneId.systemDefault())
                    .toInstant()));
                URL signedUrl = ossClient.generatePresignedUrl(request);

                //更新导出任务状态
                riskAsyncExportRepository.updateBytoken(token, SUCCESS, signedUrl.toString(), "");
            } catch (Exception e) {
                //任务失败
                riskAsyncExportRepository.updateBytoken(token, FAILURE, "",
                    e.getMessage() == null ? "null" : e.getMessage());
            }
        }
    }

    /**
     * 创建Excel
     *
     * @param list  数据列表
     * @param sheet excel中的sheet
     * @param <T>   泛型T
     * @throws ExecutionException e
     */
    private static <T> void createExcel(List<T> list, Sheet sheet) throws ExecutionException {

        if (list == null || list.isEmpty()) {
            return;
        }
        Class clazz = list.get(0).getClass();

        /* 表头 */
        LinkedHashMap<String, String> headers = createHeaders(clazz);
        Row header = sheet.createRow(0);
        Iterator<Map.Entry<String, String>> headTitle = headers.entrySet().iterator();
        for (int i = 0; headTitle.hasNext(); i++) {
            Cell cell = header.createCell(i);
            cell.setCellValue(headTitle.next().getValue());
        }

        MethodAccess access = methodCache.get(clazz);

        /* 表数据 */
        for (int i = 0; i < list.size(); i++) {
            Object obj = list.get(i);
            Row row = sheet.createRow(i + 1);
            Iterator<Map.Entry<String, String>> headTitle2 = headers.entrySet().iterator();
            for (int j = 0; headTitle2.hasNext(); j++) {
                Cell cell = row.createCell(j);
                String dataIndex = headTitle2.next().getKey();
                //反射获取属性值
                Object result;
                try {
                    result = access.invoke(obj, createGetMethod(dataIndex));
                } catch (Exception e) {
                    result = access.invoke(obj, createIsMethod(dataIndex));
                }
                if (result instanceof String) {
                    cell.setCellValue((String)result);
                } else if (result instanceof Date) {
                    Date date = (Date)result;
                    cell.setCellValue(DEFAULT_DATE_TIME_FORMATTER.format(date.toInstant()));
                } else if (result instanceof Integer) {
                    cell.setCellValue((Integer)result);
                } else if (result instanceof Double) {
                    cell.setCellValue((Double)result);
                } else if (result instanceof Boolean) {
                    cell.setCellValue((Boolean)result);
                } else if (result instanceof Float) {
                    cell.setCellValue((Float)result);
                } else if (result instanceof Short) {
                    cell.setCellValue((Short)result);
                } else if (result instanceof Character) {
                    cell.setCellValue((Character)result);
                }
            }
        }
    }

    /**
     * 获取表头各列属性描述
     *
     * @param clazz 数据类型
     * @return 表头属性描述
     * @throws ExecutionException e
     */
    private static LinkedHashMap<String, String> createHeaders(Class clazz) throws ExecutionException {

        LinkedHashMap<String, String> headers = new LinkedHashMap<>();
        Class<?> searchType = clazz;
        while (Object.class != searchType && searchType != null) {
            Field[] fields = declaredFieldsCache.get(searchType);
            for (Field field : fields) {
                HeaderColumn annotation = field.getAnnotation(HeaderColumn.class);
                if (annotation != null) {
                    headers.put(field.getName(), annotation.value());
                }
            }
            searchType = searchType.getSuperclass();
        }

        return headers;
    }

    /**
     * 输出excel到response
     *
     * @param fileName 文件名
     * @param wb       SXSSFWorkbook对象
     * @param response response
     */
    private static void output(String fileName, SXSSFWorkbook wb, HttpServletResponse response) throws IOException {
        OutputStream out = null;
        try {
            response.setCharacterEncoding("utf-8");
            response.addHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
            out = response.getOutputStream();
            wb.write(out);
        } finally {
            if (out != null) {
                out.flush();
                out.close();
            }
            wb.dispose();
        }
    }

    /**
     * 上传excel到oss
     *
     * @param key oss的key
     * @param wb  SXSSFWorkbook对象
     * @throws Exception e
     */
    private static void outputAsync(String key, SXSSFWorkbook wb) throws Exception {
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            wb.write(out);
            ossClient.putObject(BUCKET_NAME, key, new ByteArrayInputStream(out.toByteArray()));
        } finally {
            wb.dispose();
        }
    }

    /**
     * 通过属性名称拼凑getter方法
     *
     * @param fieldName 属性名称
     * @return getter方法名
     */
    private static String createGetMethod(String fieldName) {
        if (fieldName == null || fieldName.length() == 0) {
            return null;
        }
        return "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
    }

    /**
     * 通过属性名称拼凑is方法
     *
     * @param fieldName 属性名称
     * @return getter方法名
     */
    private static String createIsMethod(String fieldName) {
        if (fieldName == null || fieldName.length() == 0) {
            return null;
        }
        return "is" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
    }

    @PreDestroy
    void destroy() {
        if (null != ossClient) {
            ossClient.shutdown();
        }
        if (null != executor) {
            executor.shutdown();
        }
    }

    /**
     * 函数式数据获取接口
     *
     * @param <T> 泛型T
     */
    @FunctionalInterface
    public interface DataFetcher<T> {
        /**
         * 数据获取方法,由业务层实现该方法
         *
         * @return 数据列表
         */
        List<T> fetchData();
    }
}

导出任务表的DAO层就省略了。

个人博客:www.hellolvs.cn

查看原文

赞 1 收藏 0 评论 0

hellolvs 收藏了文章 · 2019-11-08

ThreadLocal弱引用与内存泄漏分析

本文对ThreadLocal弱引用进行一些解析,以及ThreadLocal使用注意事项。

ThreadLocal

首先,简单回顾一下,ThreadLocal是一个线程本地变量,每个线程维护自己的变量副本,多个线程互相不可见,因此多线程操作该变量不必加锁,适合不同线程使用不同变量值的场景。

其实现原理这里就不做详细阐述,其数据结构是每个线程Thread类都有个属性ThreadLocalMap,用来维护该线程的多个ThreadLocal变量,该Map是自定义实现的Entry<K,V>[]数组结构,并非继承自原生Map类,Entry其中Key即是ThreadLocal变量本身,Value则是具体该线程中的变量副本值。结构如图:

threadlocal.png

因此ThreadLocal其实只是个符号意义,本身不存储变量,仅仅是用来索引各个线程中的变量副本。

值得注意的是,Entry的Key即ThreadLocal对象是采用弱引用引入的,如源代码:

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

本文下面重点分析为何使用弱引用,以及可能存在的问题。

首先看下弱引用。

弱引用

java语言中为对象的引用分为了四个级别,分别为 强引用 、软引用、弱引用、虚引用。

其余三种具体可自行查阅相关资料。

弱引用具体指的是java.lang.ref.WeakReference<T>类。

对对象进行弱引用不会影响垃圾回收器回收该对象,即如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)。

再来说说内存泄漏,假如一个短生命周期的对象被一个长生命周期对象长期持有引用,将会导致该短生命周期对象使用完之后得不到释放,从而导致内存泄漏。

因此,弱引用的作用就体现出来了,可以使用弱引用来引用短生命周期对象,这样不会对垃圾回收器回收它造成影响,从而防止内存泄漏。

ThreadLocal中的弱引用

1.为什么ThreadLocalMap使用弱引用存储ThreadLocal?

假如使用强引用,当ThreadLocal不再使用需要回收时,发现某个线程中ThreadLocalMap存在该ThreadLocal的强引用,无法回收,造成内存泄漏。

因此,使用弱引用可以防止长期存在的线程(通常使用了线程池)导致ThreadLocal无法回收造成内存泄漏。

2.那通常说的ThreadLocal内存泄漏是如何引起的呢?

我们注意到Entry对象中,虽然Key(ThreadLocal)是通过弱引用引入的,但是value即变量值本身是通过强引用引入。

这就导致,假如不作任何处理,由于ThreadLocalMap和线程的生命周期是一致的,当线程资源长期不释放,即使ThreadLocal本身由于弱引用机制已经回收掉了,但value还是驻留在线程的ThreadLocalMap的Entry中。即存在key为null,但value却有值的无效Entry。导致内存泄漏。

但实际上,ThreadLocal内部已经为我们做了一定的防止内存泄漏的工作。

即如下方法:

/**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

上述方法的作用是擦除某个下标的Entry(置为null,可以回收),同时检测整个Entry[]表中对key为null的Entry一并擦除,重新调整索引。

该方法,在每次调用ThreadLocal的get、set、remove方法时都会执行,即ThreadLocal内部已经帮我们做了对key为null的Entry的清理工作。

但是该工作是有触发条件的,需要调用相应方法,假如我们使用完之后不做任何处理是不会触发的。

总结

  • (强制)在代码逻辑中使用完ThreadLocal,都要调用remove方法,及时清理。

目前我们使用多线程都是通过线程池管理的,对于核心线程数之内的线程都是长期驻留池内的。显式调用remove,一方面是防止内存泄漏,最为重要的是,不及时清除有可能导致严重的业务逻辑问题,产生线上故障(使用了上次未清除的值)。

最佳实践:在ThreadLocal使用前后都调用remove清理,同时对异常情况也要在finally中清理。

  • (非规范)对ThreadLocal是否使用全局static修饰的讨论。

在某些代码规范中遇到过这样一条要求:“尽量不要使用全局的ThreadLocal”。关于这点有两种解读。最初我的解读是,因为静态变量的生命周期和类的生命周期是一致的,而类的卸载时机可以说比较苛刻,这会导致静态ThreadLocal无法被垃圾回收,容易出现内存泄漏。另一个解读,我咨询了编写该规范的对方解释是,如果流程中改变了变量值,下次复用该流程可能导致获取到非预期的值。

但实际上,这两个解读都是不必要的,首先,静态ThreadLocal资源回收的问题,即使ThreadLocal本身无法回收,但线程中的Entry是可以通过remove清理掉的也就不会出现泄漏。第二种解读,多次复用值改变的问题,其实在调用remove后也不会出现。

而如果ThreadLocal不加static,则每次其所在类实例化时,都会有重复ThreadLocal创建。这样即使线程在访问时不出现错误也有资源浪费。

因此,ThreadLocal一般加static修饰,同时要遵循第一条及时清理

个人博客:www.hellolvs.cn

查看原文

hellolvs 发布了文章 · 2019-11-05

ThreadLocal弱引用与内存泄漏分析

本文对ThreadLocal弱引用进行一些解析,以及ThreadLocal使用注意事项。

ThreadLocal

首先,简单回顾一下,ThreadLocal是一个线程本地变量,每个线程维护自己的变量副本,多个线程互相不可见,因此多线程操作该变量不必加锁,适合不同线程使用不同变量值的场景。

其实现原理这里就不做详细阐述,其数据结构是每个线程Thread类都有个属性ThreadLocalMap,用来维护该线程的多个ThreadLocal变量,该Map是自定义实现的Entry<K,V>[]数组结构,并非继承自原生Map类,Entry其中Key即是ThreadLocal变量本身,Value则是具体该线程中的变量副本值。结构如图:

threadlocal.png

因此ThreadLocal其实只是个符号意义,本身不存储变量,仅仅是用来索引各个线程中的变量副本。

值得注意的是,Entry的Key即ThreadLocal对象是采用弱引用引入的,如源代码:

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

本文下面重点分析为何使用弱引用,以及可能存在的问题。

首先看下弱引用。

弱引用

java语言中为对象的引用分为了四个级别,分别为 强引用 、软引用、弱引用、虚引用。

其余三种具体可自行查阅相关资料。

弱引用具体指的是java.lang.ref.WeakReference<T>类。

对对象进行弱引用不会影响垃圾回收器回收该对象,即如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)。

再来说说内存泄漏,假如一个短生命周期的对象被一个长生命周期对象长期持有引用,将会导致该短生命周期对象使用完之后得不到释放,从而导致内存泄漏。

因此,弱引用的作用就体现出来了,可以使用弱引用来引用短生命周期对象,这样不会对垃圾回收器回收它造成影响,从而防止内存泄漏。

ThreadLocal中的弱引用

1.为什么ThreadLocalMap使用弱引用存储ThreadLocal?

假如使用强引用,当ThreadLocal不再使用需要回收时,发现某个线程中ThreadLocalMap存在该ThreadLocal的强引用,无法回收,造成内存泄漏。

因此,使用弱引用可以防止长期存在的线程(通常使用了线程池)导致ThreadLocal无法回收造成内存泄漏。

2.那通常说的ThreadLocal内存泄漏是如何引起的呢?

我们注意到Entry对象中,虽然Key(ThreadLocal)是通过弱引用引入的,但是value即变量值本身是通过强引用引入。

这就导致,假如不作任何处理,由于ThreadLocalMap和线程的生命周期是一致的,当线程资源长期不释放,即使ThreadLocal本身由于弱引用机制已经回收掉了,但value还是驻留在线程的ThreadLocalMap的Entry中。即存在key为null,但value却有值的无效Entry。导致内存泄漏。

但实际上,ThreadLocal内部已经为我们做了一定的防止内存泄漏的工作。

即如下方法:

/**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

上述方法的作用是擦除某个下标的Entry(置为null,可以回收),同时检测整个Entry[]表中对key为null的Entry一并擦除,重新调整索引。

该方法,在每次调用ThreadLocal的get、set、remove方法时都会执行,即ThreadLocal内部已经帮我们做了对key为null的Entry的清理工作。

但是该工作是有触发条件的,需要调用相应方法,假如我们使用完之后不做任何处理是不会触发的。

总结

  • (强制)在代码逻辑中使用完ThreadLocal,都要调用remove方法,及时清理。

目前我们使用多线程都是通过线程池管理的,对于核心线程数之内的线程都是长期驻留池内的。显式调用remove,一方面是防止内存泄漏,最为重要的是,不及时清除有可能导致严重的业务逻辑问题,产生线上故障(使用了上次未清除的值)。

最佳实践:在ThreadLocal使用前后都调用remove清理,同时对异常情况也要在finally中清理。

  • (非规范)对ThreadLocal是否使用全局static修饰的讨论。

在某些代码规范中遇到过这样一条要求:“尽量不要使用全局的ThreadLocal”。关于这点有两种解读。最初我的解读是,因为静态变量的生命周期和类的生命周期是一致的,而类的卸载时机可以说比较苛刻,这会导致静态ThreadLocal无法被垃圾回收,容易出现内存泄漏。另一个解读,我咨询了编写该规范的对方解释是,如果流程中改变了变量值,下次复用该流程可能导致获取到非预期的值。

但实际上,这两个解读都是不必要的,首先,静态ThreadLocal资源回收的问题,即使ThreadLocal本身无法回收,但线程中的Entry是可以通过remove清理掉的也就不会出现泄漏。第二种解读,多次复用值改变的问题,其实在调用remove后也不会出现。

而如果ThreadLocal不加static,则每次其所在类实例化时,都会有重复ThreadLocal创建。这样即使线程在访问时不出现错误也有资源浪费。

因此,ThreadLocal一般加static修饰,同时要遵循第一条及时清理

个人博客:www.hellolvs.cn

查看原文

赞 5 收藏 4 评论 7

hellolvs 关注了标签 · 2018-02-24

区块链

区块链(英语:Blockchain 或 Block chain)是一种分布式数据库,起源自比特币。区块链是一串使用密码学方法相关联产生的数据块,每一个数据块中包含了一次比特币网络交易的信息,用于验证其信息的有效性(防伪)和生成下一个区块。该概念在中本聪的白皮书中提出,中本聪创造第一个区块,即“创世区块”。

区块链在网络上是公开的,可以在每一个离线比特币钱包数据中查询。比特币钱包的功能依赖于与区块链的确认,一次有效检验称为一次确认。通常一次交易要获得数个确认才能进行。轻量级比特币钱包使用在线确认,即不会下载区块链数据到设备存储中。

比特币的众多竞争币也使用同样的设计,只是在工作量证明上和算法上略有不同。如,采用权益证明和 SCrypt 算法等等。

关注 51199

hellolvs 关注了标签 · 2018-02-24

关注 65925

hellolvs 关注了标签 · 2018-02-24

程序员

一种近几十年来出现的新物种,是工业革命的产物。英文(Programmer Monkey)是一种非常特殊的、可以从事程序开发、维护的动物。一般分为程序设计猿和程序编码猿,但两者的界限并不非常清楚,都可以进行开发、维护工作,特别是在中国,而且最重要的一点,二者都是一种非常悲剧的存在。

国外的程序员节

国外的程序员节,(英语:Programmer Day,俄语:День программи́ста)是一个俄罗斯官方节日,日期是每年的第 256(0x100) 天,也就是平年的 9 月 13 日和闰年的 9 月 12 日,选择 256 是因为它是 2 的 8 次方,比 365 少的 2 的最大幂。

1024程序员节,中国程序员节

1024是2的十次方,二进制计数的基本计量单位之一。程序员(英文Programmer)是从事程序开发、维护的专业人员。程序员就像是一个个1024,以最低调、踏实、核心的功能模块搭建起这个科技世界。1GB=1024M,而1GB与1级谐音,也有一级棒的意思。

从2012年,SegmentFault 创办开始我们就从网络上引导社区的开发者,发展成中国程序员的节日 :) 计划以后每年10月24日定义为程序员节。以一个节日的形式,向通过Coding 改变世界,也以实际行动在浮躁的世界里,固执地坚持自己对于知识、技术和创新追求的程序员们表示致敬。并于之后的最为临近的周末为程序员们举行了一个盛大的狂欢派对。

2015的10月24日,我们SegmentFault 也在5个城市同时举办黑客马拉松这个特殊的形式,聚集开发者开一个编程大爬梯。

特别推荐:

【SF 黑客马拉松】:http://segmentfault.com/hacka...
【1024程序员闯关秀】小游戏,欢迎来挑战 http://segmentfault.com/game/

  • SF 开发者交流群:206236214
  • 黑客马拉松交流群:280915731
  • 开源硬件交流群:372308136
  • Android 开发者交流群:207895295
  • iOS 开发者交流群:372279630
  • 前端开发者群:174851511

欢迎开发者加入~

交流群信息


程序员相关问题集锦:

  1. 《程序员如何选择自己的第二语言》
  2. 《如何成为一名专业的程序员?》
  3. 《如何用各种编程语言书写hello world》
  4. 《程序员们最常说的谎话是什么?》
  5. 《怎么加入一个开源项目?》
  6. 《是要精于单挑,还是要善于合作?》
  7. 《来秀一下你屎一般的代码...》
  8. 《如何区分 IT 青年的“普通/文艺/二逼”属性?》
  9. 程序员必读书籍有哪些?
  10. 你经常访问的技术社区或者技术博客(IT类)有哪些?
  11. 如何一行代码弄崩你的程序?我先来一发
  12. 编程基础指的是什么?
  13. 后端零起步:学哪一种比较好?
  14. 大家都用什么键盘写代码的?

爱因斯坦

程序猿崛起

关注 145559

hellolvs 关注了标签 · 2018-02-24

关注 88667

hellolvs 关注了标签 · 2018-02-24

segmentfault

SegmentFault (www.sf.gg) 是一个面向中文开发者的专业技术社区。社区采用良性、合理的机制来让开发者自由生长,希望通过最干净、简洁、优质的产品体验,来吸引国内优秀的开发者和技术人员,一起打造一个纯粹的技术交流社区。

我们希望为中文开发者提供一个纯粹、高质的技术交流平台,与开发者一起学习、交流与成长,创造属于开发者的时代!

网站产品

问答平台 专注高效地解决技术问题。确保内容质量的投票机制,合理区分的答案与回馈信息,用户参与改进的维基化内容,SegmentFault 帮你快捷地找到答案。

文章平台 简洁安静的技术经验分享。简约干净的界面,让你专注于内容的撰写;好用到爆的 Markdown 编辑器,和你的思维速度匹配无间。让你重新爱上写博客。

活动平台 在活动中找到志同道合的好基友。黑客马拉松、开发竞赛、线下沙龙、知识讲座、线上活动…… 总有一款适合你。

技术笔记 一个方便快捷的代码笔记本,使用 CodeMirror 编辑器,支持纯文本、Markdown、Java、CSS 等多种类型的文本渲染,还有神奇的笔记传送门。

程序员招聘平台 我们针对企业推出了面向开发者的专属招聘功能,企业组织可以展示自己的资料、团队成员、技术背景等内容。最重要的是,还能招募到契合的团队成员。

获得成就

  1. 3人创始团队创业初期利用1年时间独立开发底层框架,上线问答、博客、活动等社区平台,聚集十多万开发者。
  2. SegmentFault 团队将黑客马拉松活动引入中国,至今,已经在国内一线互联网城市以及台北、新加坡、硅谷等地区举办了超过 20 场黑客马拉松。SegmentFault 是目前中国最大的黑客马拉松组织方。
  3. SegmentFault 在 2013 中国新媒体创业大赛中获得全国决赛第二名,并入选微软创投加速器第 4 期,,并获得 IDG 资本数百万天使投资。
  4. SegmentFault 在 2014 年获得 IDG资本 数百万天使投资。
  5. SegmentFault 在 2015 年获得顶级 VC 赛富亚洲基金(软银赛富)领投、IDG资本 跟投的数千万 A 轮融资。

网站架构

SegmentFault 基于我们自己开发的 Typecho Framework 开源框架,这是一个简单、轻量、可扩展的 PHP 框架。其中引入了类似 JAVA 注入变量的概念,解决了 PHP 项目中模块的自由引用问题。存储采用 Redis、MySQL,搜索引擎选用 xunsearch,前端为响应式设计,使用了 Sass、Compass、jQuery 等技术。整个项目通过 GitHub、BaseCamp、Gmail 进行协作。

关注 53046

hellolvs 回答了问题 · 2017-12-28

怎么进行java数组逆序排序?

guava值得我们学习。Arrays.sort(nums, Ordering.natural().reverse());

关注 14 回答 11

hellolvs 赞了文章 · 2017-12-28

玩转京东支付(python)

说明

github地址

做了微信。支付宝和京东支付之后,发现,最扯蛋的支付,肯定是京东支付,要完整开发京东支付,必须要看完京东支付开发者文档的官网每一个角落,绝对不能凭你的任何经验去猜测有些流程,比如公私钥加解密(不看官网,保证你后悔)、发送请求的方式(form表单提交,看了官网你会发现好怪异),支付同步跳转(还是post,fk),支付成功后返回居然没有支付订单号(完全靠自己去维护,fk)

技术描点

首先要去看官网的:http://payapi.jd.com/。 项目使用的是pc网页支付

一. 统一下单的接口:https://wepay.jd.com/jdpay/sa...

参数说明:http://payapi.jd.com/docList....

一定要仔细的看这些参数的说明

特殊参数说明如下:

1) 在以上的请求参数中,商户号是在注册开通京东支付功能的时候,京东支付商户管理系统为用户分配的。
2) 用户账号是商户系统的用户账号。
3) 交易流水号是用来标识每次支付请求的号码,需要商户保证在每一次支付请求的时候交易流水号唯一,多次请求不能使用同一交易流水号,否则京东支付服务在处理后面的支付请求时,会把此交易当做重复支付处理。
4) 签名规则详见:“接口安全规范-签名算法”;
5) 为保证信息安全,表单中的各个字段除了merchant(商户号)、版本号(version)、签名(sign)以外,其余字段全部采用3DES进行加密。

二. 生成签名

签名过程分为两步,首先是将原始参数按照规则拼接成一个字符串S1,然后再将S1根据签名算法生成签名字符串sign。
参数原始字符串的拼接规则:

1) 对于POST表单提交的参数:所有参数按照参数名的ASCII码顺序从小到大排序(字典序),使用URL键值对的方式拼接成字符串S1,(如:k1=value1&k2=value2&k3=value3…)
2) 对于XML报文交互的参数:将XML报文的各行去掉空格后直接拼接成一行字符串作为S1。如果报文只有一行则直接作为S1,不需要再进行拼接。

生成签名的过程如下:

1) 对拼接的参数字符串S1通过SHA256算法计算摘要,得到字符串S2;
2) 对字符串S2使用私钥证书进行加密,并进行base64转码,得到签名字符串sign; 接收方收到报文后先进行base64解码,再使用公钥证书解密,然后验证签名的合法性。

注意事项:

1) 空参数不参与签名;
2) 参数列表中的sign字段不参与签名;
3) 为了简化处理,<xml>标签也参与签名;
4) 参数区分大小写;
5) RSA加密的规则为:由交易发起方进行私钥加密,接收方进行公钥解密;(可以使用RSA公私钥校验工具来校验商户RSA公私钥是否匹配)
6) 系统会对商户公钥证书的有效性进行校验。

签名代码:


def get_sign_str(params, is_compatible=False):
    """
    生成签名的字符串
    Args:
        params: 签名的字典数据
        is_compatible: 是否是兼容模式(对字典中value值为空的也签名)

    Returns:
        返回签名
    """

    raw = [(k, params[k]) for k in sorted(params.keys())]
    if is_compatible:
        order_str = "&".join("=".join(kv) for kv in raw)
    else:
        order_str = "&".join("=".join(kv) for kv in raw if kv[1])

    return order_str



def sign(self, prestr):
    """
    生成签名
    Args:
        prestr(str): 生成签名的原字符串

    Returns:
        返回生成好的签名
    """
    key = MRSA.load_key(self.MERCHANT_RSA_PRI_KEY)
    signature = key.private_encrypt(self.sha256(prestr), MRSA.pkcs1_padding)
    sign = base64.b64encode(signature)
    return sign

三. DES3对每个参数进行加密(merchant(商户号)、版本号(version)、签名(sign)除外)

为防止明文数据在post表单提交的时候暴露,所以京东做了DES3对字段进行加密(不用表单提交不就行了,还搞这么复杂,真该学学支付宝和微信)

京东DES加密说明如下:

   除特定说明外,商户和京东支付接口调用报文采用3DES加密,再通过base64转换为字符串。
   3DES加密算法标为DESede,工作模式为电子密码本模式ECB,不填充(DESede/ECB/NoPadding)。
注:服务端NoPadding 为不填充,所以加密的原文字节必须是8的整数倍(如果调用我们提供的加密接口API则不必处理原文字节,加密接口内部已处理)。如果自己实现加密,原文字节不够8的整数倍,则按如下规则转为8的整数倍。
    1.  把原文字符串转成字节数组。
    2.  根据字节数组长度判断是否需要补位。
        补位逻辑为:
        int x = (i+ 4) % 8;
        int y = (x == 0) ? 0 : (8 - x);
        i为字节数组的长度,y为需要补位的长度。
        补位值为0。
    3.  将有效数据长度byte[]添加到原始byte数组的头部。
        i为字节数组的长度。
        result[0] = (byte) ((i >> 24) & 0xFF);
        result[1] = (byte) ((i >> 16) & 0xFF);
        result[2] = (byte) ((i >> 8) & 0xFF);
        result[3] = (byte) (i & 0xFF);

    4.  原文字节数组前面加上第三步的4个字节,再加上需补位的值。
        例如:字符串”1”,转换成字节数组是[49],计算补位y=3, 计算有效数据长度为[0, 0, 0, 1],最后字节数组为[0, 0, 0, 1, 49, 0, 0, 0]。
Form表单接口的加密方式:
如果商户通过表单方式提交支付请求至收银台,为保证信息安全,表单中的各个字段除了merchant(商户号)、verion(版本号)、sign(签名)以外,其余字段全部采用3DES进行加密。

XML请求接口的加密方式:
通过XML接口方式和京东支付服务器交互的请求,应该对报文进行加密,加密方式为对整个报文整体进行3DES加密,再进行base64转码使其变为可读字符串,加密后的密文置于<encrypt></encrypt>标签中,同时再将报文中的<merchant>(商户号)、<version>(版本号)这两个字段单独置于<jdpay>标签下。

接收到京东支付加密报文后的处理方式:
接收到京东支付返回的加密报文后,先判断<jdpay>标签下的<result>标签的返回码,检查接口调用是否正常返回。然后再读取<encrypt>标签的密文内容进行base64解码,再进行3DES解密,解密后的报文即是原始报文。

示例代码:


def des_pad(data):
    e = len(data)
    x = (e + 4) % 8
    y = 0 if x == 0 else 8 - x
    sizeByte = struct.pack('>I', e)
    resultByte = range(len(sizeByte) + e + y)
    resultByte[0:4] = sizeByte
    resultByte[4:4 + e] = data
    for i in range(0, y):
        resultByte[e + 4 + i] = "\x00"
    resultstr = ''.join(resultByte)
    return resultstr


def encode_des(to_encode_str, des_key):
    """
    DES3加密数据
    Args:
        to_encode_str(str): 要被加密的原字符串,这里的字符串需要被des_pad一下
        des_key(str): 加密的key
    Returns:

    """

    key = base64.b64decode(des_key)
    des3 = DES3.new(key, DES3.MODE_ECB)
    return des3.encrypt(ToolsClass.des_pad(to_encode_str)).encode('hex_codec')

这样的话,签名和加密都已完成,往后就拼到页面里的form里

 <form method="post" action="https://wepay.jd.com/jdpay/saveOrder" id="batchForm">
        <input name="merchant" type="hidden" id="merchant" value="22294531" /><br/>
        <input name="notifyUrl" type="hidden" id="notifyUrl" value="da652ac3b881c4ddc2ac26793b20c37fba91a994f108bf8a0a42b5ead05111997bfe2a97eaf4aa49562de1b6d1d32cd7" /><br/>
         <input name="userId" type="hidden" id="userId" value="f23f2b73027cb0f8deb349af3086fdc50f6892f17c9f45b81b6d273d0cdb1cae8151f083427fc8f0" /><br/>
            <input name="sign" type="hidden" id="sign" value="SJ6qfS+9CmXkt6ghJcf9nIdHJDReTFNkRyjFh5XZAsTAtfHT4SdmKeD88t+2dMnaszJ7vVjBnSu64aJyt6SODW2FHJk0WXEvZNixmo2h8F7vHO5lTE2jEG/9uN7sqg2c7kH2Fnu5cFLCeaMfb8uZqZ8CKi+g7Aw4b6rywvoH/8M="
        /><br/>
        <input name="currency" type="hidden" id="currency" value="ac7132c57f10d3ce" /><br/>
         <input name="orderType" type="hidden" id="orderType" value="e00c693e6c5b8a60" /><br/>
         <input name="tradeNum" type="hidden" id="tradeNum" value="05439876d54534c7604c42eca17c14cdf8eece390982627a0799194a74809ee6c9d07d3cff8a7c60"
        /><br/>
         <input name="amount" type="hidden" id="amount" value="e5a6c3761ab9ddaf" /><br/>
        <input name="version" type="hidden" id="version" value="V2.0" /><br/>
        <input name="tradeTime" type="hidden" id="tradeTime" value="d9668085c69c2ecb33367c0710f42c4bc7432967ba39f140"
        /><br/> <input name="tradeName" type="hidden" id="tradeName" value="3e111657e2839e3a3ba10d54bb446817e5000daf14a2e3badbf9a93316ed6003" /><br/>
        <input name="callbackUrl" type="hidden" id="callbackUrl" value="51c916293675ac44c2ac26793b20c37fba91a994f108bf8a0a42b5ead05111997bfe2a97eaf4aa49229a23b8c688e767"
        /><br/><input type="submit" />
    </form>

怎么组织就自己去实现好了

四. 异步回调

提交之后请求之后,就会跳转到京东的支付页面,可登录账户支付,也可用京东app或者微信扫描支付。

当用户扫码支付之后,京东会主动跳转到你指定的一个网址(在提交支付请求的时候有这个字段),并且会异步post一个请求到指定的一个地址(在提交支付请求的时候有这个字段),同步跳转是在用户扫码支付之后,如果京东支付页面还在的话会跳转。而异步是无论如何都会发支付结果通知的。对于新手来说,一定要知道这个行业潜规则(微信,支付宝or其它都是)。而且一定要以这个异步通知的结果为准。

京东返回的是xml格式的字符串

返回格式如下(没有换行的,我这里演示换了行的):

<?xml version=\"1.0\" encoding=\"UTF-8\" ?>
<jdpay>
<version>V2.0</version>
<merchant>22294531</merchant>
<result> <code>000000</code> <desc>success</desc> </result>
<encrypt>MWYxMjBjMzViZjgwOWM5ZDhjNjc0YmY1ZWJlY2QyODU0YTc5NmQ3ZWQxMWU1NzE3MWQ0OTUwOGI5NzllYmE4ZjM1YzRiZjlmYWE1M2ZiYjVmYzBmYTgyMDYyM2Q0YjM0NGM1ODFkZDhlYTA2Mjk0ZDE5ZDBlZDk5NTc3MmE4Nzk4OTFlYjIwZDgzMTc4MDU3NGVkZTFjNDY0MDMzNzNjZjc2OWZiMDQ0YjVhZGNhYmRhMGZmYTkyNzRhZDNhM2IxOGY5ZjZhYjBmYjhmZmI3Yzg0OTA3YzM0OGJmZTYwZTIzNzM3YjVmYzMzNmNkYTE0MjM2OWIwZDM5MjI2YWM5YmY3ZmZjZDBkNWJmM2ZkYWY4YTU3OWU4MDE3ZjQ5YmQ0ZWIyMDA0NTFmODZkNmViMDBiMDE2YTU3NTNjMzJjNDIzNWI5ZDkyYzQ3OTU4OTc2ZGIyZmNiMGUxNGRjNTM2OGZjYjQ0NmE0YWY1ZWVjZDYzNWI5ZDkyYzQ3OTU4OTc2NmIwM2QyZTU1ODJlNDNjM2M1NjA2YmQ5ZDc3MTRkMmNjN2ZiMDM3Yzg5ZDk1ODFkMWJhZmVjYjUwMzJlNTdkMTFmN2QxMDAxNjgyMzJjNTZhMmQzNTcyZGE4OTUzYWFjNTU5MDY4YWYyODE5ZDcyNmY5NmE1YTBmYWFiZTRiZTQ2OGZhMmM4M2JjMGM5NmNiMDE3ZWQ4MDkxY2FjZThiNzg4MjY5OWY1ZTJlYzBjOTIxODBhOGExNjExNGY4NWQwM2NkZjI2MTFmM2VmODcxYWM3MjUxZjMxMzZlYjFmNzI1NWE0OWM4MjMxZGY1MzBmY2Y1Mjg2NGUzMWRlMjc0M2I5ZDM5NjQzN2ZmZWQ1Y2M5NDY4ZDcwNWM1YzVhZmRlYzYwZWU3MDVhNjE0N2I1MGVlM2UyMGE2MzExNTE4YTUxOGRjMzBmMmUxZjE2NzYzNGRiNDJlODFmMDczOGYzZjMxN2NkMjkzNmU4ODc3NzJjMjkzM2ZlODlmMjUyNDVmNDI2MDA0M2VkYmUwOTlkNGEyNjU3YTM5YTE4ODU2OTBmNGQyNDcwZDE0ZWRjMmQxYjgxMzhhNjA5M2ZlNDkxYTQyMzE5YzBlNTA0MTdkYTg2ZGQ2NDQwODBmMjM4ZGI2YzIzMjNhOTE0M2VmMjZiZjczN2M5NWQwODYxMWY2OGE5MDQ0ZDZmNzE0NmIxZjQwZDdmZDMxOTQ2ZDM3YjIwNDJiODUzZGM0NTk0MzM5YzJkN2M2NDdiNGM4MzQ4MTRjZTIxZTlmYTYzNDYxNGMxMjlhZTE3NjE0ZDIzM2Q2MTQ4YzJiNWE3ZWVjMDU5MjFmNzJkNGNjNTU1NWZkNzVhN2U5Y2I1MDU1NjhlMWRlNjVhNzkyOGUxMThlODQyMGJkNzE2NjdmMDc3YmEyYTFkNmQyOTFiOGNjZTU2ZGMyYmE3ODY5ZGZiNmMyMWViYjc2ODc0Y2I3YTc4NGQ5NWY2NjY2Y2E5NjI0N2I1MGE4MTliMDBkNGIzNmViZTJlY2JmYTcwODUzYTM5ZTcwMDVmYWEzNWY2MDFhMWM2MGQ1MzEyYmQxNDU3Zjg4ZWVhNzY2YjZhOGE4ZGMxMGY3NjYwOWEzNWY2MDFhMWM2MGQ1MzFhNzA4NTNhMzllNzAwNWZhYTYxMmJmNjJiMmFlMGY5ODMxMzQ0MzQ0NjMxZDc3MTUyY2FiMjZlMjcyYmJjYmQzODVmNDY4OTA5YTdjMjlmNTI5NWFlZjE3NTI4ZmE4MzVhNzA4NTNhMzllNzAwNWZhNDk5OTQ2ZGU0OGU0NGQ2ZTE4YmRiYTBjZjNhM2ZkNjY5ODJjNGVhZjQzMjIyYWFhMWM0ZmU1ODRiNTg5OWEwYzAwNjI2NTllMDZkYzhiYTVmMjI3ZjUyYmQ3MjcyODllZmEwYzhiNDIwODc4ZjUzODY1MzAzZDkyNDM5OTRkNDczMTBjZDBhMTc4ZjAwOTIyZmM2ODk5YjkyYTJiODcwNjU4MzkzMzJkZWYzNDY1MzJlYTNiYTFhNjM0MWIwNjM4NjBjNjlmMzg1NWZjZWM5YWExMDdjZWY1MjkwZTZjMzgzOGYxNTRiNzFlN2E1YTczYWFkNzJlOTRiOWI3MmI2YWYyMTJjMjQ5Y2UzMmUxMGI4YWE0N2YzYzFmNjNiOGY4NjJlZmU1ZDM5NjcwODA3MGNjY2JjYWFkYjM3NzBmMGQzYjIyMGFmZTE3YWNjZWU1N2RmZTQxMzAxYjA2MDdlMg==</encrypt>
</jdpay>

先要用DES3对encrypt节点里的串进行解密


def un_des_pad(data):
    resultByte = data[0:4]
    e = struct.unpack('>I', resultByte)[0]
    x = (e + 4) % 8
    y = 0 if x == 0 else 8 - x
    return data[4:] if y == 0 else data[4:-y]

def decode_des(to_decode_str, des_key):
    """
    解密数据
    Args:
        to_decode_str(str): 要解密的原字符串
        des_key(str): 解密的key
    Returns:

    """
    key = base64.b64decode(des_key)
    des3 = DES3.new(key, DES3.MODE_ECB)
    param = to_decode_str.decode("hex_codec") if to_decode_str is bytes else base64.b64decode(to_decode_str).decode(
        "hex_codec")
    param = des3.decrypt(param)
    return ToolsClass.un_des_pad(param)


sign_begin = xml_data.find('<encrypt>')
sign_end = xml_data.find('</encrypt>')
encrypt_str = xml_data[sign_begin + 9:sign_end]
xml_str = JdPay.decode_des(encrypt_str, deskey)

解密后的明文如下:

<?xml version="1.0" encoding="UTF-8" >
<jdpay>
  <version>V2.0</version>
  <merchant>110290193003</merchant>
<result>
  <code>000000</code>
  <desc>success</desc>
</result>
<device>6220</device>
<sign>SJ6qfS+9CmXkt6ghJcf9nIdHJDReTFNkRyjFh5XZAsTAtfHT4SdmKeD88t+2dMnaszJ7vVjBnSu64aJyt6SODW2FHJk0WXEvZNixmo2h8F7vHO5lTE2jEG/9uN7sqg2c7kH2Fnu5cFLCeaMfb8uZqZ8CKi+g7Aw4b6rywvoH/8M=</sign>
<tradeNum>201704250935156041484635</tradeNum>
<tradeType>0</tradeType>
<amount>3140</amount>
<status>2</status>
<payList>
  <pay>
    <payType>3</payType>
    <amount>1500</amount>
    <currency>CNY</currency>
    <tradeTime>20170425093516</tradeTime>
 </pay>
 <pay>
   <payType>1</payType>
   <amount>1640</amount>
   <currency>CNY</currency>
   <tradeTime>20170425093516</tradeTime>
   <detail>
     <cardHolderMobile>150****1596</cardHolderMobile>
   </detail>
  </pay>
</payList>
</jdpay>

解密之后就是验证签名是否正确,从上边的串中拿到签名和去除签名之后的字符串

def verify_mysign(cls, sign, xml_str, jd_public_key):
    """
    验证签名
    Args:
        sign: 签名
        xml_str: 去除签名后的xml字符串
        jd_public_key: 用于验证的key

    Returns:

    """
    xml_sha_str = SHA256.new(xml_str).hexdigest()
    key = MRSA.load_pub_key(jd_public_key)
    signature = key.public_decrypt(base64.standard_b64decode(sign),
                                   MRSA.pkcs1_padding)
    return signature == xml_sha_str

验证通过之后再返回去除sign的xml字符串,并提取出里边的内容(详情参数所代表的含义请看官方文档)

五. 同步跳转

同步跳转就没啥好说了,只是给个跳转地址,但是这里一定要注意,这个的是一个post请求(好像京东啥都喜欢post),而非微信或者支付宝或者other什么的get请求。所以不要设置错了

好了,到这里一个完整的在线支付就完成了。这里还要说明的是,涉及到加密和解密,就一定会有key,有DES3使用的对称加密key,还有签名使用的非对称公钥和私钥。所以一定要配置好。
这里我的源代码里用的都是京东提供的测试商户号,还有一大推京东设置好的key,具体要去下载京东的【京东支付PC&H5接口文档】,在文档的最底部有帐号信息。

demo里边还有申请退款,申请撤单的接口,其实写好一个接口的完成流程,别的流程都是直接套用就可以了。

博客地址

查看原文

赞 5 收藏 9 评论 2

认证与成就

  • 获得 12 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-10-09
个人主页被 524 人浏览