Bootstrap

SSM整合及使用

SSM整合

介绍

SSM(Spring+SpringMVC+MyBatis)框架集由Spring、MyBatis两个开源框架整合而成(SpringMVC是Spring中的部分内容),常作为数据源较简单的web项目的框架。

步骤

  1. SSM整合主要是将各个框架的核心组件都交给spring ioc容器管理
  2. SSM整合的位置在web.xml中体现
    2.1 spring-mvc.xml
    a. 视图解析器
    b. mvc注解驱动(消息转换器:字符串类型的消息转换器,JSON格式的消息转换器)
    c. 组件扫描(主要扫描控制器在哪里)
    2.2 spring-mybatis.xml
    a. 数据源(DruidDataSource, HikariDataSource)
    b. SqlSessionFactory配置
    c. Mapper接口扫描器配置
    d. 数据源事务管理器配置
    e. 事务的注解驱动
    f. 组件扫描(主要扫描业务层在哪里)

pom.xml配置

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.qf.SSM</groupId>
<artifactId>Day55_SSM</artifactId>
<packaging>war</packaging>
<version>1.0</version>
<name>Day55_SSM Maven Webapp</name>
<url>http://maven.apache.org</url>
<properties>
 <maven.compiler.source>8</maven.compiler.source>
 <maven.compiler.target>8</maven.compiler.target>
 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 <spring.version>5.3.10</spring.version>
</properties>
<dependencies>
 <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context-support</artifactId>
   <version>${spring.version}</version>
 </dependency>
 <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-jdbc</artifactId>
   <version>${spring.version}</version>
 </dependency>
 <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-webmvc</artifactId>
   <version>${spring.version}</version>
 </dependency>
 <dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>8.0.28</version>
 </dependency>
 <dependency>
   <groupId>org.mybatis</groupId>
   <artifactId>mybatis</artifactId>
   <version>3.5.13</version>
 </dependency>
 <dependency>
   <groupId>org.mybatis</groupId>
   <artifactId>mybatis-spring</artifactId>
   <version>2.0.6</version>
 </dependency>
 <dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <version>1.18.26</version>
 </dependency>
 <dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>fastjson</artifactId>
   <version>2.0.42</version>
 </dependency>
 <dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid</artifactId>
   <version>1.2.6</version>
 </dependency>
 <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.13</version>
   <scope>test</scope>
 </dependency>
 <dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>javax.servlet-api</artifactId>
   <version>4.0.1</version>
   <scope>provided</scope>
 </dependency>
</dependencies>
<build>
 <finalName>Day55_SSM</finalName>
</build>
</project>

spring-mvc.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">


 <!--配置视图解析器-->
 <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/" p:suffix=".jsp"/>
 <!--配置消息转换器-->
 <bean class="org.springframework.http.converter.StringHttpMessageConverter" id="stringHttpMessageConverter"/>
 <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter" id="fastJsonHttpMessageConverter">
     <property name="supportedMediaTypes">
         <list>
             <value>text/html;charset=UTF-8</value>
             <value>application/json;charset=UTF-8</value>
         </list>
     </property>
 </bean>
 <!--开启注解驱动-->
 <mvc:annotation-driven>
     <mvc:message-converters>
         <ref bean="stringHttpMessageConverter"/>
         <ref bean="fastJsonHttpMessageConverter"/>
     </mvc:message-converters>
 </mvc:annotation-driven>

 <!--扫描-->
 <context:component-scan base-package="com.qf.controller"/>
</beans>

spring-mtbatis.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd">


 <!--配置数据源-->
 <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
     <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
     <property name="url" value="jdbc:mysql://localhost:3306/lesson?serverTimezone=Asia/Shanghai"/>
     <property name="username" value="root"/>
     <property name="password" value="123456"/>
 </bean>
 <!--日志打印-->
 <bean id="configuration" class="org.apache.ibatis.session.Configuration">
     <property name="logImpl" value="org.apache.ibatis.logging.stdout.StdOutImpl"/>
 </bean>

 <!--配置sqlSessionFactory-->
 <bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
     <property name="dataSource" ref="dataSource"/>
     <property name="mapperLocations" value="classpath:mapper/*Mapper.xml"/>
     <property name="configuration" ref="configuration"/>
     <property name="typeAliasesPackage" value="com.qf.pojo"/>
 </bean>
 <!--配置Mapper接口文件-->
 <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
     <property name="basePackage" value="com.qf.mapper"/>
     <property name="sqlSessionFactoryBeanName" value="sessionFactory"/>
 </bean>
 <!--配置事务管理-->
 <bean id="tm" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
     <property name="dataSource" ref="dataSource"/>
 </bean>
 <!--开启注解驱动-->
 <tx:annotation-driven transaction-manager="tm"/>
 <!--扫描-->
 <context:component-scan base-package="com.qf.service"/>
</beans>

web.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
      version="4.0">
 <display-name>Archetype Created Web Application</display-name>

 <context-param>
     <param-name>contextConfigLocation</param-name>
     <param-value>classpath:spring-mybatis.xml</param-value>
 </context-param>
 <listener>
     <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 </listener>
 <servlet>
     <servlet-name>dispatcherServlet</servlet-name>
     <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
     <init-param>
         <param-name>contextConfigLocation</param-name>
         <param-value>classpath:spring-mvc.xml</param-value>
     </init-param>
     <load-on-startup>1</load-on-startup>
 </servlet>
 <servlet-mapping>
     <servlet-name>dispatcherServlet</servlet-name>
     <url-pattern>/</url-pattern>
 </servlet-mapping>
 <filter>
     <filter-name>encodingFilter</filter-name>
     <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
     <init-param>
         <param-name>encoding</param-name>
         <param-value>UTF-8</param-value>
     </init-param>
     <init-param>
         <param-name>forceEncoding</param-name>
         <param-value>true</param-value>
     </init-param>
 </filter>
 <filter-mapping>
     <filter-name>encodingFilter</filter-name>
     <url-pattern>/*</url-pattern>
 </filter-mapping>
</web-app>

至此,整合完毕,关键在于web.xml文件中,利用 Jsp工作原理进行整合,在 Jsp 中,ServletContextListener 能够监听上下文参数配置,并对该配置做出相应的处理。这个监听器可以用来配置 Mybatis 的相关信息,完成 Mybatis 的整合。 Servlet 的运作需要在 web.xml 中进行配置,在配置 Servlet 时,可以配置该 Servlet 的初始化参数。因此可以用来完成 Spring MVC 的整合。所以,SSM 框架整合的场所就是在web.xml 中。

测试

从数据库中查询user表的数据

pojo中user类:

package com.qf.pojo;

import lombok.Data;

@Data
public class User {
 private String username;
 private String password;
 private String name;
}

mapper层:

public interface UserMapper {
 List<User> getUsers();
}

service层:

public interface UserService {

 List<User> getUsers();
}

//---------------------------------------------------------------


@Service
public class UserServiceImpl implements UserService {
 @Autowired
 private UserMapper userMapper;

 @Transactional(readOnly = true,isolation = Isolation.READ_COMMITTED)
 @Override
 public List<User> getUsers() {
     return userMapper.getUsers();
 }
}

注:@Service//这个注解会创建一个bean对象放入ioc容器;

@Autowired//这个注解能够从ioc容器中找到类型匹配的实例注入值,但是前提必须ioc容器中有实例,所以必须有@Service注解;

@Transactional(readOnly = true,isolation = Isolation.READ_COMMITTED)这个注解表示事务的,将事务设置为只读,能够提升查询效率,同时设置了隔离级别为读已提交,避免脏读。为什么在service层设置事务而不是mapper层?-因为service层设置可以根据业务逻辑将事务中包含多个操作,符合事务的一致性

controller层:

@RestController
@RequestMapping("/user")
public class UserController {
 @Autowired
 private UserService userService;


 @GetMapping
 public List<User> searchUsers(){
     return userService.getUsers();
 }

}

userMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qf.mapper.UserMapper">
 <select id="getUsers" resultType="User">
     select * from user
 </select>
</mapper>

===========================================================================

拦截器

自定义拦截器需要实现HandlerInterceptor接口,此接口底层如下:

public interface HandlerInterceptor {
 //前置拦截,在控制器执行之前做事
 default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
     return true;
 }
	//后置拦截,在控制器执行之后做事
 default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
 }
	//在视图被渲染后做事
 default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
 }
}

应用-登录超时

当从请求中获取的username不存在时,无法登录,即登录超时,这个功能在控制器执行之前完成

package com.qf.com.qf;


import com.alibaba.fastjson.JSONObject;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;

public class TimeoutInterceptor implements HandlerInterceptor {

 @Override
 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
     HttpSession session = request.getSession();
     Object username = session.getAttribute("username");
     if (username == null) {
         //响应内容设置,APPLICATION_JSON_VALUE = application/json
         response.setContentType(MediaType.APPLICATION_JSON_VALUE);
         PrintWriter writer = response.getWriter();
         JSONObject json = new JSONObject();
         json.put("msg", "登录超时");
         json.put("status", 400);
         writer.write(json.toString());
         writer.flush();
         writer.close();
         return false;
     }
     return true;
 }
}

spring-mvc.xml文件中配置拦截器使其生效:

<mvc:interceptors>
 <mvc:interceptor>
     <!--拦截的URL地址-->
     <mvc:mapping path="/**"/>
     <!--不拦截的URL地址-->
     <mvc:exclude-mapping path="/user/login"/>
     <!--使用的拦截器-->
     <bean class="com.qf.interceptor.TimeoutInterceptor"/>
 </mvc:interceptor>
</mvc:interceptors>

测试:从前端传入JSON格式的数据登录

mapper层:

User getUserByUsername(@Param("username")String username);

userMapper.xml:

<select id="getUserByUsername" resultType="user">
     select * from user where username=#{username}
 </select>

service层:

int getUser(User user, HttpSession session);
//------------------------------------------
@Transactional(readOnly = true,isolation = Isolation.READ_COMMITTED)
@Override
public int getUser(User user, HttpSession session) {
 User dbUser = userMapper.getUserByUsername(user.getUsername());
 if(dbUser==null){
     return -1;
 }
 if(dbUser.getPassword().equals(user.getPassword())){
     session.setAttribute("username",user.getUsername());
     return 1;
 }
 return 0;
}

controller层:

@PostMapping("/login")
public int login(@RequestBody User user, HttpSession session){
 return userService.getUser(user,session);
}

当还没有进行登录时,访问user页面会报错,报错信息为拦截器设置的登录超时,状态码为400,当使用postman发送post请求,传入正确的json数据后,如{

“username”:“zs”,

“password”:“123123”

},再次访问user页面就可以正常访问了。

应用-权限检测

根据RBAC权限模型在数据库中查询用户拥有哪些权限,如果请求地址和权限匹配则放行,不匹配则返回false。此功能在控制器执行之前完成。

package com.qf.com.qf.interceptor;

import com.alibaba.fastjson.JSONObject;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class PermissionInterceptor implements HandlerInterceptor {
 @Override
 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
     String requestURI = request.getRequestURI();
     String contextPath = request.getContextPath();
     String requestUrl = requestURI.replace(contextPath, "");
     //从数据库中查询用户的权限,返回权限的集合,这里手动创建权限集合
     List<String> accessUrls = Arrays.asList("/user/register");
     if(accessUrls.contains(requestUrl)){
         return true;
     }else {
         response.setContentType(MediaType.APPLICATION_JSON_VALUE);
         PrintWriter writer = response.getWriter();
         JSONObject json = new JSONObject();
         json.put("msg","没有该权限");
         json.put("status",400);
         writer.write(json.toString());
         writer.flush();
         writer.close();
         return false;
     }
 }
}

spring-mvc.xml配置使其生效:

<mvc:interceptors>
     <!--...其他拦截器...-->

     <mvc:interceptor>
         <mvc:mapping path="/**"/>
         <mvc:exclude-mapping path="/user/login"/>
         <bean class="com.qf.interceptor.PermissionInterceptor"/>
     </mvc:interceptor>
 </mvc:interceptors>

测试:

这时访问user界面就会报错,信息为没有访问权限,因为在拦截器中只设置了"/user/register"的访问权限。

拦截器执行流程

在这里插入图片描述

===========================================================================

文件的上传和下载

文件上传

导入依赖:

<dependency>
   <groupId>commons-fileupload</groupId>
   <artifactId>commons-fileupload</artifactId>
   <version>1.4</version>
 </dependency>

spring-mvc.xml中配置支撑

<!--文件上传下载的配置,这个bean的id值必须是multipartResolver-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
 <!--设置默认编码-->
 <property name="defaultEncoding" value="UTF-8"/>
 <!--内存中可用大小:3 * 1024 * 1024 = 3145728 = 3M-->
 <property name="maxInMemorySize" value="3145728"/>
 <!--设置临时存储目录,当上传文件大小超过内存中可使用大小时将产生临时文件-->
 <property name="uploadTempDir" value="/upload"/>
 <!--50 * 1024 * 1024 = 52428800 = 50M -->
 <!--最大上传大小-->
 <property name="maxUploadSize" value="52428800"/>
 <!--单个文件大小-->
 <property name="maxUploadSizePerFile" value="5242880"/>
</bean>

测试:保存用户信息,其中包括头像。

数据库中user表新增head_icon字段

修改pojo包中的user类:

@Data
public class User {
 private String username;
 private String password;
 private String name;
 private String headIcon;
}

mapper接口:

int saveUser(@Param("user") User user);
//--------------------------------------------------------------

	
//保存文件位置
private static final String SAVE_DIR = "e:/users/icon";
//rollbackFor表示一旦这个方法出现了异常,就需要进行数据的回滚操作
@Transactional(rollbackFor = Exception.class,isolation = Isolation.REPEATABLE_READ)
@Override
public int addUser(User user, MultipartFile file) {
 User dbUser = userMapper.getUserByUsername(user.getUsername());
 if(dbUser!=null) return -1;
 //获取文件的原始名称
 String originalFilename = file.getOriginalFilename();
 int index = originalFilename.lastIndexOf('.');
 String extension = "";//文件的扩展名
 if(index>0){
     extension = originalFilename.substring(index);
 }
 String saveFileName = user.getUsername() + extension;
 File folder = new File(SAVE_DIR);
 if(!folder.exists()){
     folder.mkdirs();
 }
 try {
     //将文件传输至给定的位置保存
     file.transferTo(new File(folder,saveFileName));
     user.setHeadIcon(saveFileName);
     return userMapper.saveUser(user);
 } catch (IOException e) {
     throw new RuntimeException(e);
 }
}

注:这里使用file类的transferTo方法进行文件的输入,相较于原来使用的文件流的传入传出更加简便。

controller层:

@PostMapping("/register")
 public int addUser(User user, @RequestPart("icon")MultipartFile file){return userService.addUser(user,file);}

userMapper.xml:

<insert id="saveUser" >
     INSERT INTO user(username, password, name, head_icon)
     VALUES(#{user.username}, #{user.password}, #{user.name}, #{user.headIcon})
 </insert>

文件下载

配置如上

测试:

controller层:

//ResponseEntity表示响应的实体,这个实体只是包含响应的数据、状态码、状态信息、响应头等
@GetMapping("/icon/{filename}")
public ResponseEntity<byte[]> download(@PathVariable("filename")String fileName){
 return userService.download(fileName);
}

service层:响应头需要添加两次参数,一次为响应文件格式,一次为响应文件的下载形式和名字

ResponseEntity<byte[]> download(String fileName);
//-------------------------------------------------------------
@Override
public ResponseEntity<byte[]> download(String fileName) {
 File file = new File(SAVE_DIR,fileName);

 try {
     //将文件保存至一个字节数组中
     byte[] fileData = FileCopyUtils.copyToByteArray(file);
     //构建HTTP响应头
     HttpHeaders headers = new HttpHeaders();
     //响应数据的类型是可执行文件
     headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
     //设置响应文件的形式是以附件的形式存在,附件的名字如果是中文,那么必须要进行转码处理。
     //因为浏览器默认的编码格式是ISO-8859-1,而中文支持的格式是UTF-8,两种编码格式不一致,因此乱码
     //将中文字符串在当前编码下转换为字节数据,因为字节数据不存在乱码情况
     byte[] data = fileName.getBytes(StandardCharsets.UTF_8);
     //再将没有乱码的字节数据重新制定编码格式进行转换
     String s = new String(data, StandardCharsets.ISO_8859_1);
     headers.add(HttpHeaders.CONTENT_DISPOSITION,"attachment;filename="+s);
     return new ResponseEntity<>(fileData,headers, HttpStatus.OK);
 } catch (IOException e) {
     throw new RuntimeException(e);
 }
}

由于没有操作数据库,所以不写mapper层以及mapper映射文件

注:这里要把权限打开或者拦截器配置放行。

<!--放行-->
<mvc:interceptor>
 <mvc:mapping path="/**"/>
 <mvc:exclude-mapping path="/user/login"/>
 <mvc:exclude-mapping path="/user/icon/**"/>
 <bean class="com.qf.interceptor.PermissionInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>

===========================================================================

定时任务

在spring-mvc中配置:

<!--开启定时任务的注解驱动-->
<task:annotation-driven />

<!--控制器组件扫描-->
<context:component-scan base-package="com.qf.controller,com.qf.task" />

注:在扫描中需要扫描这个定时任务包

/**
 * 定时任务需要在配置文件中配置才会生效
*/
@Component
public class ClearTask {

 /**
     * cron表达式就是用来指定定时任务执行的时间
     * 涉及的单位顺序:秒 分 时 日 月 周 年
     *  * 表示所有值. 例如:在分的字段上设置 "*",表示每一分钟都会触发。
     *  ? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。
     *  - 表示区间。例如 在小时上设置 "10-12",表示 10,11,12点都会触发。
     *  , 表示指定多个值,例如在周字段上设置 "MON,WED,FRI" 表示周一,周三和周五触发
     *  / 用于递增触发。如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)
  */
 @Scheduled(cron = "0/5 48 11,12 * * ?")
 public void work(){
     System.out.println("定时任务执行");
 }
}

定时任务可能存在的问题:

Spring 默认配置下,将会使用具有单线程的 ScheduledExecutorService,单线程执行定时任务,如果某一个定时任务执行时间较长,将会影响其他定时任务执行。如果存在多个定时任务,为了保证定时任务执行时间的准确性,可以修改默认配置,使其使用多线程执行定时任务。

<!--开启定时任务的注解驱动-->
<task:annotation-driven />

<bean class="java.util.concurrent.ScheduledThreadPoolExecutor">
 <constructor-arg index="0" value="5"/><!--最多有5个线程同时执行任务-->
</bean>

<!--控制器组件扫描-->
<context:component-scan base-package="com.qfedu.ssm.controller,com.qfedu.ssm.task" />

===========================================================================

Excel处理

Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。
easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便 。

Excel生成

即将数据库中的数据导出到Excel中

Excel生成的模板类:子类重写读取数据的方法,创建表,将数据写入表,使用的时候利用输出流。

/**
 * Excel导出工具模板
 */
public abstract class ExcelExportHandler<T> {

    private final int sheetDataCount;//一张表中需要存放多少条数据

    private final String sheetName;//表名

    private final Class<T> clazz;//表数据对应的类

    public ExcelExportHandler(int sheetDataCount, String sheetName, Class<T> clazz) {
        this.sheetDataCount = sheetDataCount;
        this.sheetName = sheetName;
        this.clazz = clazz;
    }

    /**
     * 获取数据总条数
     * @param params 查询条件
     * @return
     */
    abstract protected int getTotal(Map<String,Object> params);

    /**
     * 获取表中的数据
     * @param sheetNo 表的序号
     * @param sheetDataCount 表数据条数
     * @param params 查询条件
     * @return
     */
    abstract protected List<T> getSheetData(int sheetNo,int sheetDataCount,Map<String,Object> params);


    /**
     * 将数据写入excel中
     * @param out 写入的excel文件的输出流
     * @param params 查询条件
     */
    public void export(OutputStream out,Map<String,Object> params){
        try(ExcelWriter excelWriter = EasyExcel.write(out, clazz).build()) {
            int total = getTotal(params);
            int sheetCount = total / sheetDataCount;
            if(total % sheetDataCount>0)
                sheetCount++;
            for (int i = 1; i <= sheetCount; i++) {
                WriteSheet sheet = new WriteSheet();
                sheet.setSheetNo(i);
                sheet.setSheetName(sheetName);
                List<T> sheetData = getSheetData(i, sheetDataCount, params);
                excelWriter.write(sheetData,sheet);
            }
            excelWriter.finish();
        }
    }

    /**
     * 用于导出的方法
     * @param path 导出文件路径
     * @param params 查询条件
     */
    public void export(String path,Map<String,Object> params){
        File file = new File(path);
        File parentFile = file.getParentFile();
        if(!parentFile.exists()){
            parentFile.mkdirs();
        }
        try {
            FileOutputStream out = new FileOutputStream(file);
            export(out,params);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }


}

Excel解析

即读取Excel数据并导入数据库中

Excel解析的模版类:实现ReadListener接口创建监听器,重写invoke方法,此方法用于循环获取每一批次的数据,如果用于装数据的readDataList满了就进行导入,导入的方法由子类重写。

注:1.外部类有泛型了内部类上不用再次声明泛型。

2.重新赋值而不是清理,因为save方法里面可能保存到线程中处理,如果清理则数据丢失。

3.导入的数据量并非是批处理的整数倍,所以添加一个processRestData方法进行保存。

4.save方法耗时较大,会影响性能,因此用线程执行此方法。线程池是静态的,因为抽象类的子类不能一直创建线程池,内存会崩,因此在导入的方法执行的时候需要通过静态的任务类来执行。

package com.qf.excel;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public abstract class ExcelImportHandler<T> {
    private final int batchCount;//批量处理的数据量
    private final Class<T> clazz;

    private static final ThreadPoolExecutor TASK_EXECUTOR = new ThreadPoolExecutor(5,10,1, TimeUnit.MINUTES,new LinkedBlockingDeque<>(2048));


    public ExcelImportHandler(int batchCount, Class<T> clazz) {
        this.batchCount = batchCount;
        this.clazz = clazz;
    }

    /**
     * 将数据保存到数据库中
     * @param dataList 读取的数据
     */
    protected abstract void save(List<T> dataList);

    /**
     * 创建监听器类,用于设定批量导入的时机
     */
    class ExcelDataReadListener implements ReadListener<T>{

        private List<T> readDataList = new ArrayList<>();
        
        @Override
        public void invoke(T data, AnalysisContext analysisContext) {
            readDataList.add(data);
            if(readDataList.size()==batchCount){
//                TASK_EXECUTOR.submit(()->{
//                    save(readDataList);
//                });
                TASK_EXECUTOR.submit(new ImportTask<>(readDataList,ExcelImportHandler.this));
                readDataList = new ArrayList<>();
            }

        }

        @Override
        public void doAfterAllAnalysed(AnalysisContext analysisContext) {

        }

        /**
         * 如果readDataList中存在没有写满的批次数据,就将剩下的数据进行导入
         */
        public void processRestData(){
            if(!readDataList.isEmpty()){
                TASK_EXECUTOR.submit(new ImportTask<>(readDataList,ExcelImportHandler.this));
            }
        }
    }

    static class ImportTask<E> implements Runnable{

        private final List<E> dataList;
        private final ExcelImportHandler<E> handler;

        public ImportTask(List<E> dataList, ExcelImportHandler<E> handler) {
            this.dataList = dataList;
            this.handler = handler;
        }

        @Override
        public void run() {
            handler.save(dataList);
        }
    }

    /**
     * 用于导入的方法
     * @param in 要读取的文件的输入流
     */
    public void importExcel(InputStream in){
        ExcelDataReadListener listener = new ExcelDataReadListener();
        EasyExcel.read(in,clazz,listener).doReadAll();
        listener.processRestData();
    }
    public void importExcel(String path){
        try {
            importExcel(new FileInputStream(path));
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

整合到SSM项目中

将ExcelExportHandler和ExcelImportHandler移入项目中,需要引入easyExcel依赖。

导入依赖:

<dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>easyexcel</artifactId>
 <version>3.3.2</version>
</dependency>

测试导入(Excel解析)

思路:前端上传文件,服务器读取文件数据将文件数据插入到数据库中

mapper层:

int batchSave(@Param("users") List<User> users);

Mapper.xml:

<insert id="batchSave">
    insert into user (username,password,name,head_icon) values
    <foreach collection="users" item="user" separator=",">
        (#{user.username},#{user.password},#{user.name},#{user.headIcon})
    </foreach>
</insert>

service层:(业务实现)

int batchSaveUser(List<User> users);
//-----------------------------------------------------------------
@Transactional(rollbackFor = Exception.class,isolation = Isolation.REPEATABLE_READ)
@Override
public int batchSaveUser(List<User> users) {
    return userMapper.batchSave(users);
}

子类模板继承(利用父类的构造方法填入参数):

public class UserImportHandler extends ExcelImportHandler{
    private UserService userService;

    public UserImportHandler(UserService userService) {
        super(100, User.class);
        this.userService = userService;
    }

    @Override
    protected void save(List dataList) {
        System.out.println("当前数据保存量"+dataList.size());
        userService.batchSaveUser(dataList);

    }
}

controller层:

@PostMapping("/import")
public String importUser(@RequestPart("user")MultipartFile file){
    UserImportHandler handler = new UserImportHandler(userService);
    try {
        handler.importExcel(file.getInputStream());
        return "文件导入成功";
    } catch (IOException e) {
        return "导入失败";
    }
}

测试导出(Excel生成)

思路:根据查询条件查询数据,将数据导出到Excel文件,把文件以字节流数组形式存放到响应头中供前端用户下载

UserDto:

@Data
public class UserDto {
    private String username;
    private String name;
}

mapper层:

int getTotal(@Param("params")Map<String,Object> params);

List<User> getUserData(@Param("params") Map<String,Object> params);

Mapper.xml:

<select id="getTotal" resultType="int">
    select count(0) from user
    <where>
        <if test="params.username!=null and params.username!=''">
            AND username = #{params.username}
        </if>
        <if test="params.name!=null and params.name!=''">
            AND name LIKE CONCAT('%',#{params.name},'%')
        </if>
    </where>
</select>

<select id="getUserData" resultType="user">
    select * from user
    <where>
        <if test="params.username!=null and params.username!=''">
            AND username = #{params.username}
        </if>
        <if test="params.name!=null and params.name!= ''">
            AND name LIKE CONCAT('%' ,#{params.name} ,'%')
        </if>
    </where>
    LIMIT #{params.offset},#{params.pageSize}
</select>

service层:(业务实现)

int getTotal(Map<String, Object> params);

List<User> getUserData(int sheetNo, int sheetDataCount, Map<String, Object> params);
//----------------------------------------------------------------------------------
@Override
public int getTotal(Map<String, Object> params) {
    return userMapper.getTotal(params);
}

@Override
public List<User> getUserData(int sheetNo, int sheetDataCount, Map<String, Object> params) {
    int offSet = sheetDataCount * (sheetNo - 1);
    params.put("offset",offSet);
    params.put("pageSize",sheetDataCount);
    return userMapper.getUserData(params);
}

子类模板继承:

public class UserExportHandler extends ExcelExportHandler<User>{

    private UserService userService;
    public UserExportHandler(UserService userService) {
        super(100,"用户信息表", User.class);
        this.userService = userService;
    }

    @Override
    protected int getTotal(Map<String,Object> params) {
        return userService.getTotal(params);
    }

    @Override
    protected List<User> getSheetData(int sheetNo, int sheetDataCount, Map<String,Object> params) {
        return userService.getUserData(sheetNo,sheetDataCount,params);

    }
}

controller层:

通过工具模板的子类获取子类对象,调用其handler方法实现导出,将数据导出到输出流中,这里需要把userDto转换为jsonObject对象(等同于Map)。将输入流转换为字节流数组,即Excel文件数据,然后将数据,响应头放到ResponseEntity中供用户下载。

@GetMapping("/export")
public ResponseEntity<byte[]> exportUser(UserDto userDto){
    UserExportHandler handler = new UserExportHandler(userService);
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    JSONObject json = (JSONObject) JSONObject.toJSON(userDto);
    handler.export(out,json);
    byte[] data = out.toByteArray();
    HttpHeaders headers = new HttpHeaders();
    headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
    String fileName = "用户信息表.xlsx";
    byte[] bytes = fileName.getBytes(StandardCharsets.UTF_8);
    fileName = new String(bytes, StandardCharsets.ISO_8859_1);
    headers.add(HttpHeaders.CONTENT_DISPOSITION,"attachment;filename=" + fileName);
    return new ResponseEntity<>(data,headers, HttpStatus.OK);
}

===========================================================================

Spring事务传播行为

REQUIRED:支持当前事务,如果不存在事务就自己创建一个事务。

SUPPORTS:支持当前事务,如果不存在事务自己也不创建事务,以非事务的形式执行。

MANDATORY:支持当前事务,且要求必须有事务,否则不执行并抛出异常。

REQUIRES_NEW:当前有事务的话就挂起来,开启新的事物执行完自身后再恢复之前的事务,如果没有事务就直接创建一个新的事物执行自身。

NOT_SUPPORTED:不支持当前事务,如果当前有事务则挂起,以非事务的形式执行自身之后再恢复事务。

NEVER:不支持事务的形式执行,如果有事务就抛异常。

NESTED:嵌套,如果没有事务就自己创建新的事务来执行,如果有则在当前的事务中创建一个子事务嵌套执行。嵌套的子事务有独立的回滚点,可以自己进行回滚,但是如果父事务回滚那么子事务也会相应回滚。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;