SSM整合
介绍
SSM(Spring+SpringMVC+MyBatis)框架集由Spring、MyBatis两个开源框架整合而成(SpringMVC是Spring中的部分内容),常作为数据源较简单的web项目的框架。
步骤
- SSM整合主要是将各个框架的核心组件都交给spring ioc容器管理
- 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:嵌套,如果没有事务就自己创建新的事务来执行,如果有则在当前的事务中创建一个子事务嵌套执行。嵌套的子事务有独立的回滚点,可以自己进行回滚,但是如果父事务回滚那么子事务也会相应回滚。