15、SpringBoot实现Excel的导入导出


前言

需求:正如标题所言,需求有数据的导入、导出

导入:给用户提供给一个导入数据的模板,用户填写数据后上传,实现文件的批量导入。

导出:将数据列表直接导进excel,用户通过浏览器下载。

首先考虑用经典的apache poi实现这一功能,但是发现不是很好用,后面有换了阿里的 easy excel,效率比较高。

如果需求不高,只是简单的导入导出,不涉及复杂对象,可以直接使用第一版的代码。

excel基本构成

虽然只写个导入导出并不要求我们对excel有多熟悉,但是最起码得知道excel有哪些构成。

整个文件:student.xlsx,对应与poi中的Workbook

sheet:一张表

 

 cell:单元格,每一小格

apache poi

依赖

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.poi/poi 03版的excel -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>4.1.2</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml 新版的excel -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>4.1.2</version>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

实体类

@Data
@AllArgsConstructor
public class Student {
    private String name;

    private Integer age;

    private String hobby;

    private Float score;

    private Date birth;
}

读取

HSSF开头是老版本的excel,拓展名为xls

我们这里用的是新版本得,拓展名xlsx

public class TestRead {

    public static void main(String[] args) throws IOException {
        XSSFWorkbook workbook = new XSSFWorkbook("E:\\personcode\\office-learn\\src\\main\\resources\\excel\\test.xlsx");

        XSSFSheet sheet = workbook.getSheetAt(0);
        for (Row row : sheet) {
            for (Cell cell : row) {
                System.out.print(cell.toString() +"\t");
            }
            System.out.println();
        }

        workbook.cloneSheet(0);
    }
}

写入

public class TestWrite {

    public static List<Student> getStudentList() {
        return Arrays.asList(
                new Student("学生1", 18, "学习", 59.5F, new Date()),
                new Student("学生2", 19, "游泳", 68F, new Date()),
                new Student("学生3", 19, "游泳", 90F, new Date()),
                new Student("学生4", 19, "游泳", 100F, new Date())
        );
    }

    public static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) throws IllegalAccessException {
        //创建一个表格
        XSSFWorkbook xssfWorkbook = new XSSFWorkbook();
        List<Student> studentList = getStudentList();
        //创建一个sheet
        XSSFSheet sheet = xssfWorkbook.createSheet("学生成绩");

        //单元格格式
        XSSFCellStyle cellStyle = xssfWorkbook.createCellStyle();
        cellStyle.setFillBackgroundColor(IndexedColors.PINK.getIndex());


        //字体样式
        XSSFFont font = xssfWorkbook.createFont();
        font.setFontName("黑体");
        font.setColor(IndexedColors.BLUE.getIndex());
        cellStyle.setFont(font);


        //设置第一行
        XSSFRow row = sheet.createRow(0);
        row.createCell(0).setCellValue("姓名");
        row.createCell(1).setCellValue("年龄");
        row.createCell(2).setCellValue("兴趣");
        row.createCell(3).setCellValue("分数");
        row.createCell(4).setCellValue("日期");

        for (int i = 1; i < studentList.size(); i++) {
            Student student = studentList.get(i);
            XSSFRow xrow = sheet.createRow(i);
            //如果不设置格式
//            xrow.createCell(0).setCellValue(student.getName());
//            xrow.createCell(1).setCellValue(student.getAge());
//            xrow.createCell(2).setCellValue(student.getHobby());
//            xrow.createCell(3).setCellValue(student.getScore());
//            xrow.createCell(4).setCellValue(student.getBirth());
            XSSFCell cell1 = xrow.createCell(0);
            cell1.setCellValue(student.getName());
            cell1.setCellStyle(cellStyle);

            XSSFCell cell2 = xrow.createCell(1);
            cell2.setCellValue(student.getAge());
            cell2.setCellStyle(cellStyle);

            XSSFCell cell3 = xrow.createCell(2);
            cell3.setCellValue(student.getHobby());
            cell3.setCellStyle(cellStyle);

            XSSFCell cell4 = xrow.createCell(3);
            cell4.setCellValue(student.getScore());
            cell4.setCellStyle(cellStyle);

            XSSFCell cell5 = xrow.createCell(4);
            cell5.setCellValue(format.format(student.getBirth()));
            cell5.setCellStyle(cellStyle);



        }

        //获取样式
//        XSSFCellStyle cellStyle = xssfWorkbook.createCellStyle();
//        XSSFDataFormat format = xssfWorkbook.createDataFormat();
//        cellStyle.setDataFormat(format.getFormat("yyyy年m月d日"));

        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream("E:\\personcode\\office-learn\\src\\main\\resources\\excel\\student.xlsx");
            xssfWorkbook.write(fileOutputStream);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                xssfWorkbook.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

 Easy Excel

我们先用简单的数据结构测试一下,能跑起来才是王道,然后再考虑集成进SpringBoot,以及复杂数据。

依赖

spring之类的依赖就不赘述了

        <!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.2.6</version>
        </dependency>

实体类

@Data
@AllArgsConstructor
@NoArgsConstructor //如果加上面一个注解,这个必须加上,否则easy excel会报无法实例化
public class Student {

    @ExcelProperty("姓名")
    private String name;

    @ExcelProperty("年龄")
    private Integer age;

    @ExcelProperty("爱好")
    private String hobby;

    @ExcelProperty("分数")
    private Float score;

    @ExcelProperty("生日")
    private Date birth;
}

简单写入

public class TestWrite {

    public static List<Student> getStudentList() {
        return Arrays.asList(
                new Student("学生1", 18, "学习", 59.5F, new Date()),
                new Student("学生2", 19, "游泳", 68F, new Date()),
                new Student("学生3", 19, "游泳", 90F, new Date()),
                new Student("学生4", 19, "游泳", 100F, new Date())
        );
    }

    public static void main(String[] args) {
        String fileName = "E:\\personcode\\office-learn\\src\\main\\resources\\excel\\student.xlsx";
        // 这里 需要指定写用哪个class去读,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        // 如果这里想使用03 则 传入excelType参数即可
        EasyExcel.write(fileName, Student.class).sheet("学生信息").doWrite(getStudentList());
    }
}

执行结果

 

 

 

对比POI,有一种从SSM换到了SpringBoot的感觉。

简单读取

public class TestRead {

    public static void main(String[] args) {
        String fileName = "E:\\personcode\\office-learn\\src\\main\\resources\\excel\\student.xlsx";
        // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
        EasyExcel.read(fileName, Student.class, new DemoDataListener()).sheet().doRead();
    }
}

简单读取需要配置一个监听类,在这个监听类里处理数据,可以边读取边处理。

下面监听类的作用是,打印每一行数据

public class DemoDataListener extends AnalysisEventListener<Student> {

    public DemoDataListener() {}

    //解析每条数据的时候会调用这个方法
    @Override
    public void invoke(Student student, AnalysisContext analysisContext) {
        System.out.println(student);
    }

    //解析完成后调用这个方法
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {

    }
}

控制台输出

Student(name=学生1, age=18, hobby=学习, score=59.5, birth=Wed Nov 11 20:32:20 CST 2020)
Student(name=学生2, age=19, hobby=游泳, score=68.0, birth=Wed Nov 11 20:32:20 CST 2020)
Student(name=学生3, age=19, hobby=游泳, score=90.0, birth=Wed Nov 11 20:32:20 CST 2020)
Student(name=学生4, age=19, hobby=游泳, score=100.0, birth=Wed Nov 11 20:32:20 CST 2020)

集成进SpringBoot

在完成基本的导入导出功能后,我们就可以考虑将代码集成进SpringBoot了。

第一版

仅仅针对上面的代码,做初步集成,暂不考虑其他需求,例如多个sheet、日期转换等

既然要继承进Spring环境,我主张完全交给Spring来管理,不要再new对象了。

依赖

模拟真实开发环境,需要加入数据库相关的依赖,这里我还是用的mybatis plus:

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.2.6</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

配置文件

server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://ip:3306/test?useUnicode=true&characterEncoding=UTF-8
    username: root
    password: root


mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  type-enums-package: com.dayrain.nums
  global-config:
    db-config:
      logic-not-delete-value: 1
      logic-delete-value: 0

  mapper-locations: classpath*:/mapper/**/*.xml

实体类

与上面相比,添加了一个id作为主键,

其中@Excelgnore表示导出的时候忽略该字段。

@Data
@AllArgsConstructor
@NoArgsConstructor //如果加上面一个注解,这个必须加上,否则easy excel会报无法实例化
public class Student {

    @TableId(type = IdType.AUTO)
    @ExcelIgnore
    private Integer id;

    @ExcelProperty("姓名")
    private String name;

    @ExcelProperty("年龄")
    private Integer age;

    @ExcelProperty("爱好")
    private String hobby;

    @ExcelProperty("分数")
    private Float score;

    @ExcelProperty("生日")
    private Date birth;
}

DAO

@Mapper
public interface StudentMapper extends BaseMapper<Student> {
}

service

@Service
public class StudentServiceImpl implements StudentService {

    @Autowired
    StudentMapper studentMapper;

    @Override
    public void insertStudent(Student student) {
        studentMapper.insert(student);
    }

    @Override
    public void insertStudents(List<Student> students) {
        students.forEach(student -> studentMapper.insert(student));
    }

    @Override
    public List<Student> selectStudents() {
        return studentMapper.selectList(null);
    }
}
public interface StudentService {
    void insertStudent(Student student);

    void insertStudents(List<Student>students);

    List<Student> selectStudents();
}

excel

excel相关的类我也写成了service,便于拓展和使用

public interface ExcelService {
    void simpleDownload(HttpServletResponse response) throws IOException;

    void simpleUpload(MultipartFile file) throws IOException;
}

 

@Service
public class ExcelServiceImpl implements ExcelService {
    @Autowired
    StudentServiceImpl studentService;

    @Autowired
    StudentDataListener studentDataListener;

    @Override
    public void simpleDownload(HttpServletResponse response) throws IOException {
        // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        // 这里URLEncoder.encode可以防止中文乱码
        String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
        EasyExcel.write(response.getOutputStream(), Student.class).sheet("学生信息").doWrite(studentService.selectStudents());
    }

    @Override
    public void simpleUpload(MultipartFile file) throws IOException {
        EasyExcel.read(file.getInputStream(), Student.class, studentDataListener).sheet().doRead();
    }
}

 

@Component
public class StudentDataListener extends AnalysisEventListener<Student> {


    //因为相关的类没有交给spring管理,所以不能直接通过注解注入
    @Autowired
   private StudentService studentService;


    //解析每条数据的时候会调用这个方法
    @Override
    public void invoke(Student student, AnalysisContext analysisContext) {
        studentService.insertStudent(student);
    }

    //解析完成后调用这个方法
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {

    }
}

controller

@RestController
public class ExcelController {
    @Autowired
    ExcelService excelService;

    //导出
    @GetMapping("/download")
    public void download(HttpServletResponse response) throws IOException {
        excelService.simpleDownload(response);
    }

    //导入
    @PostMapping("/upload")
    public String upload(MultipartFile file) throws IOException {
        excelService.simpleUpload(file);
        return "success";
    }
}

目录结构

 

 

 分析

1、对比

web版的excel导入导出,与之前简单demo相比,不需要指定文件的存放路径。

导入的时候,web端直接分析流文件,将每一行都插进数据库。

导出的时候,web端将查到数据,直接写入response,无需在服务器端生成临时文件。

2、问题

一个项目里会有很多个导入导出,并不只是Student类的导出。

但是不管是何种数据的导出,api都是相同的,区别就在于handler类不一样(因为我觉得与netty中的handler很像,故如此取名)。

例如我们现在需要导出学校信息,我们可以这么做:

第二版

新增一个实体类;

@Data
public class School {

    @TableId(type = IdType.AUTO)
    private Integer id;

    private String name;

    private Student year;
}

写好对应的service:

@Service
public class SchoolServiceImpl implements SchoolService {

    @Autowired
    SchoolMapper schoolMapper;

    @Override
    public void insert(School school) {
        schoolMapper.insert(school);
    }
}

新增对应的处理类

@Component
public class SchoolHandler extends AnalysisEventListener<School> {//因为相关的类没有交给spring管理,所以不能直接通过注解注入
    @Autowired
    private SchoolService schoolService;


    //解析每条数据的时候会调用这个方法
    @Override
    public void invoke(School school, AnalysisContext analysisContext) {
        schoolService.insert(school);
    }

    //解析完成后调用这个方法
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {

    }
}

修改excel相关类,将接口设计成可以动态添加 Listen 类

public interface ExcelService {
    void simpleDownload(HttpServletResponse response) throws IOException;

    void simpleUpload(MultipartFile file, AnalysisEventListener listener) throws IOException;
}

 

@Service
public class ExcelServiceImpl implements ExcelService {
    @Autowired
    StudentServiceImpl studentService;

    @Autowired
    StudentDataListener studentDataListener;

    @Override
    public void simpleDownload(HttpServletResponse response) throws IOException {
        // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        // 这里URLEncoder.encode可以防止中文乱码
        String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
        EasyExcel.write(response.getOutputStream(), Student.class).sheet("学生信息").doWrite(studentService.selectStudents());
    }

    @Override
    public void simpleUpload(MultipartFile file, AnalysisEventListener listener) throws IOException {
        EasyExcel.read(file.getInputStream(), Student.class, listener).sheet().doRead();
    }
}

controller层调用

 

@RestController
public class ExcelController {
    @Autowired
    ExcelService excelService;

    @Autowired
    SchoolHandler schoolHandler;

    @Autowired
    StudentDataListener studentDataListener;

    //导出
    @GetMapping("/download")
    public void download(HttpServletResponse response) throws IOException {
        excelService.simpleDownload(response);
    }

    //导入
    @PostMapping("/student/upload")
    public String upload(MultipartFile file) throws IOException {
        excelService.simpleUpload(file, studentDataListener);
        return "success";
    }

    @PostMapping("/school/upload")
    public String upload2(MultipartFile file) throws IOException {
        excelService.simpleUpload(file, schoolHandler);
        return "success";
    }


}

其他代码不变。

补充

大体框架基本拉出来了,我们需要考虑一下细节问题。

导出所有sheet

ExcelService添加一个类

注意是doReadAll()

    @Override
    public void allSheetUpload(MultipartFile file, AnalysisEventListener listener) throws IOException {
        EasyExcel.read(file.getInputStream(), Student.class, listener).doReadAll();
    }

导出部分sheet

这里写死了只有两个sheet。

一般用户使用的模板都是系统提供的,所以当每个sheet存放不同类型的内容时,我们可以提前感知,在后台进行相应的映射。

@Override
    public void PartSheetUpload(MultipartFile file, List<AnalysisEventListener>listeners) {
        // 读取部分sheet

        ExcelReader excelReader = null;
        try {
            excelReader = EasyExcel.read(file.getInputStream()).build();

            // 这里为了简单 所以注册了 同样的head 和Listener 自己使用功能必须不同的Listener
            ReadSheet readSheet1 =
                    EasyExcel.readSheet(0).head(Student.class).registerReadListener(listeners.get(0)).build();
            ReadSheet readSheet2 =
                    EasyExcel.readSheet(1).head(School.class).registerReadListener(listeners.get(1)).build();
            // 这里注意 一定要把sheet1 sheet2 一起传进去,不然有个问题就是03版的excel 会读取多次,浪费性能
            excelReader.read(readSheet1, readSheet2);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (excelReader != null) {
                // 这里千万别忘记关闭,读的时候会创建临时文件,到时磁盘会崩的
                excelReader.finish();
            }
        }
    }

 格式转换

可以在对象上直接加注解,也可以自己写一个转化类。

@Data
@AllArgsConstructor
@NoArgsConstructor //如果加上面一个注解,这个必须加上,否则easy excel会报无法实例化
//@ColumnWidth(25)设置行高
public class Student {

    @TableId(type = IdType.AUTO)
    @ExcelIgnore
    private Integer id;

    @ExcelProperty("姓名")
//    @ColumnWidth(50)设置行高,覆盖上面的设置
    private String name;

    @ExcelProperty("年龄")
    private Integer age;

    @ExcelProperty("爱好")
    private String hobby;

    @ExcelProperty("分数")
    private Float score;

    @ExcelProperty("生日")
    @DateTimeFormat("yyyy年MM月dd日")
    private Date birth;
}

如果业务比较复杂,需要自己创建转换器

public class CustomStringStringConverter implements Converter<String> {
    @Override
    public Class supportJavaTypeKey() {
        return null;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return null;
    }

    @Override
    public String convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        return null;
    }

    @Override
    public CellData convertToExcelData(String s, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        return null;
    }
}

上面根据自己的业务进行填写

比如要转化成LocalDateTime,就把String替换成 LocalDateTime

可以参考:https://blog.csdn.net/weixin_47098539/article/details/109385543?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~all~sobaiduend~default-2-109385543.nonecase&utm_term=easyexcel%20%E6%97%B6%E9%97%B4%E7%B1%BB%E5%9E%8B%E7%9A%84%E8%BD%AC%E6%8D%A2&spm=1000.2123.3001.4430

添加转换器

    @Override
    public void Convert(MultipartFile file) throws IOException {

        // 这里 需要指定读用哪个class去读,然后读取第一个sheet
        EasyExcel.read(file.getInputStream(), Student.class, studentDataListener)
                // 这里注意 我们也可以registerConverter来指定自定义转换器, 但是这个转换变成全局了, 所有java为string,excel为string的都会用这个转换器。
                // 如果就想单个字段使用请使用@ExcelProperty 指定converter
                 .registerConverter(new CustomStringStringConverter())
                // 读取sheet
                .sheet().doRead();
    }

如果工作中用到了其他功能,后面再来补充。 

 异常处理

消息转换

控制台报的一个warning,虽然不影响结果,但是看着很烦。

SpringMvc默认的消息转换 MessageConverters不支持我们设置的媒体类型。添加一个配置类即可解决问题。

这里用的是默认的jackson,也可以用Gson或者fastjson

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedOrigins("*")
                .allowedHeaders("*")
                .allowedMethods("*")
                .allowCredentials(true)
                .maxAge(30*1000);
    }

@Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setSupportedMediaTypes(getSupportedMediaTypes()); converters.add(converter); } public List<MediaType> getSupportedMediaTypes() { //创建fastJson消息转换器 List<MediaType> supportedMediaTypes = new ArrayList<>(); supportedMediaTypes.add(MediaType.APPLICATION_JSON); supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML); supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED); supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM); supportedMediaTypes.add(MediaType.APPLICATION_PDF); supportedMediaTypes.add(MediaType.APPLICATION_RSS_XML); supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML); supportedMediaTypes.add(MediaType.APPLICATION_XML); supportedMediaTypes.add(MediaType.IMAGE_GIF); supportedMediaTypes.add(MediaType.IMAGE_JPEG); supportedMediaTypes.add(MediaType.IMAGE_PNG); supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM); supportedMediaTypes.add(MediaType.TEXT_HTML); supportedMediaTypes.add(MediaType.TEXT_MARKDOWN); supportedMediaTypes.add(MediaType.TEXT_PLAIN); supportedMediaTypes.add(MediaType.TEXT_XML); supportedMediaTypes.add(MediaType.ALL); return supportedMediaTypes; } }

 

 

如有错误,欢迎批评指正~


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM