一.日志处理
日志框架诞生原因:没有日志处理框架时我们需要写大量的system…语句,当需要修改时非常的麻烦,慢慢的出现了日志框架,通过日志框架我们只需要配置好,将日志输出到统一的便于区分的文件夹或者直接写入到数据库当中,非常的方便。
目前市面上的日志框架以及他们的搭配:JUL、JCL、Jboss-logging、logback、log4j、log4j2、slf4j…
左边选一个门面(抽象层)、右边来选一个实现;
日志门面:SLF4J;
日志实现:Logback;
SpringBoot:底层是Spring框架,Spring框架默认是用JCL,SpringBoot选用 SLF4j和logback
SLF4j使用:如何在系统中使用SLF4j
:https://www.slf4j.org">https://www.slf4j.org 开发的时候,日志记录方法的调用,不直接调用日志的实现类,而是调用日志抽象层里面的方法;
给系统里面导入slf4j的jar和 logback的实现jar
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorld {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(HelloWorld.class);
logger.info("Hello World");
}
}
图示:
注意:每一个日志的实现框架都有自己的配置文件。使用slf4j以后,配置文件还是做成日志实现框架自己本身的配置文件;遗留问题:springboot(slf4j+logback): Spring(commons-logging)、Hibernate(jboss-logging)用的日志框架不一样,容易出问题,我们应该统日志框架使用。
如何让系统中所有的日志都统一到slf4j
:
1、将系统中其他日志框架先排除出去;
2、用中间包来替换原有的日志框架;
3、我们导入slf4j其他的实现
SpringBoot日志关系:springboot的日志关系都在这里
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
底层依赖关系
:
总结
:
1
.SpringBoot底层也是使用slf4j+logback的方式进行日志记录
2
.SpringBoot也把其他的日志都替换成了slf4j;
3
.中间替换包?
4
.如果我们要引入其他框架?一定要把这个框架的默认日志依赖移除掉?
Spring框架用的是commons-logging;
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
注意:SpringBoot能自动适配所有的日志,而且底层使用slf4j+logback的方式记录日志,引入其他框架的时候,只需要把这个框架依赖的日志框架排除掉即可;
日志使用:默认配置
:SpringBoot默认帮我们配置好了日志
//记录器
Logger logger = LoggerFactory.getLogger(getClass());
@Test
public void contextLoads() {
//System.out.println();
//日志的级别;
//由低到高 trace<debug<info<warn<error
//可以调整输出的日志级别;日志就只会在这个级别以以后的高级别生效
logger.trace("这是trace日志...");
logger.debug("这是debug日志...");
//SpringBoot默认给我们使用的是info级别的,没有指定级别的就用SpringBoot默认规定的级别;root级别
logger.info("这是info日志...");
logger.warn("这是warn日志...");
logger.error("这是error日志...");
}
日志输出格式:
%d表示日期时间,
%thread表示线程名,
%-5level:级别从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。
%msg:日志消息,
%n是换行符
-->
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
SpringBoot修改日志的默认配置:
logging.level.com.atguigu=trace
#logging.path=
# 不指定路径在当前项目下生成springboot.log日志
# 可以指定完整的路径;
#logging.file=G:/springboot.log
# 在当前磁盘的根路径下创建spring文件夹和里面的log文件夹;使用 spring.log 作为默认文件
logging.path=/spring/log
# 在控制台输出的日志的格式
logging.pattern.console=%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n
# 指定文件中日志输出的格式
logging.pattern.file=%d{yyyy-MM-dd} === [%thread] === %-5level === %logger{50} ==== %msg%n
指定配置
:给类路径下放上每个日志框架自己的配置文件即可;SpringBoot就不使用他默认配置的了
logback.xml:直接就被日志框架识别了;
logback-spring.xml:日志框架就不直接加载日志的配置项,由SpringBoot解析日志配置,可以使用SpringBoot的高级Profile功能
<springProfile name="staging">
<!-- configuration to be enabled when the "staging" profile is active -->
<!--可以指定某段配置只在某个环境下生效-->
</springProfile>
如:
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<!--
日志输出格式:
%d表示日期时间,
%thread表示线程名,
%-5level:级别从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。
%msg:日志消息,
%n是换行符
-->
<layout class="ch.qos.logback.classic.PatternLayout">
<springProfile name="dev">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ----> [%thread] ---> %-5level %logger{50} - %msg%n</pattern>
</springProfile>
<springProfile name="!dev">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ==== [%thread] ==== %-5level %logger{50} - %msg%n</pattern>
</springProfile>
</layout>
</appender>
如果使用logback.xml作为日志配置文件,还要使用profile功能,会有以下错误:no applicable action for [springProfile]
切换日志框架:slf4j+log4j的方式
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
<exclusion>
<artifactId>log4j-over-slf4j</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
切换为log4j2
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
日志框架结合AOP:直接把数据写入到数据库
:这里只贴出了重要的部分,其他的可以自己写
1.创建切面切入到我们要增强的地方:
@Component
@Aspect
public class LogAop {
@Autowired
private HttpServletRequest request;
@Autowired
private LogService logService;
private Date visitTime; //开始时间
//前置通知 主要是获取开始时间,执行的类是哪一个,执行的是哪一个方法
@Before("execution(* com.zgw.controller.*.*(..))")
public void doBefore(JoinPoint jp) throws NoSuchMethodException {
visitTime = new Date();//当前时间就是开始访问的时间
}
//后置通知
@After("execution(* com.zgw.controller.*.*(..))")
public void doAfter(JoinPoint jp) throws Exception {
//保存日志
Log log = new Log();
//从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) jp.getSignature();
//获取切入点所在的方法
Method method = signature.getMethod();
//获取操作
MyLog myLog = method.getAnnotation(MyLog.class);
if (myLog != null) {
String value = myLog.value();
log.setCaozuo(value);//保存获取的操作
}
//url
log.setUrl(request.getRequestURI());
//访问时间
log.setCreatetime(visitTime);
//获取用户ip地址
log.setIp( request.getRemoteAddr());
//保存log信息
logService.save(log);
}
}
2.自定义注解:切入的地方加了自定义注解,可以直接得到用户的操作
/**
* 自定义注解类
*/
@Target(ElementType.METHOD) //注解放置的目标位置,METHOD是可注解在方法级别上
@Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行
@Documented //生成文档
public @interface MyLog {
String value() default "";
}
3.使用:
@MyLog(value = "跳转到登录")
@ApiOperation("跳转到登录")
@RequestMapping("/toLogin")
public String toLogin(){
return "pages/login";
}
把数据写入到指定的文件夹中
:
1.创建切面切入到我们要增强的地方:
@Aspect
@Component
public class LogAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Pointcut("execution(* com.zgw.controller.*.*(..))")
public void log() {}
@Before("log()")
public void doBefore(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String url = request.getRequestURL().toString();
String ip = request.getRemoteAddr();
String classMethod = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
RequestLog requestLog = new RequestLog(url, ip, classMethod, args);
logger.info("Request : {}", requestLog);
}
@After("log()")
public void doAfter() {
// logger.info("--------doAfter--------");
}
/* @AfterReturning(returning = "result",pointcut = "log()")
public void doAfterRuturn(Object result) {
logger.info("Result : {}", result);
}*/
private class RequestLog {
private String url;
private String ip;
private String classMethod;
private Object[] args;
public RequestLog(String url, String ip, String classMethod, Object[] args) {
this.url = url;
this.ip = ip;
this.classMethod = classMethod;
this.args = args;
}
@Override
public String toString() {
return "{" +
"url='" + url + '\'' +
", ip='" + ip + '\'' +
", classMethod='" + classMethod + '\'' +
", args=" + Arrays.toString(args) +
'}';
}
}
}
2.配置文件中指定日志写入的位置以及日志级别:
logging:
level:
root: info
com.zgw: debug
file:
name: log/my.log
3.当访问会增强的类会记录日志,我们也可以自己定义一些特殊的业务,当它出错时我们把它用我们熟悉的格式写入到日志文件中,有利于我们的排错。
二.拦截器
原理
:
1、根据当前请求,找到HandlerExecutionChain【可以处理请求的handler以及handler的所有 拦截器】
2、先来顺序执行 所有拦截器的 preHandle方法
• 1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
• 2、如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion;
3、如果任何一个拦截器返回false。直接跳出不执行目标方法
4、所有拦截器都返回True。执行目标方法
5、倒序执行所有拦截器的postHandle方法。
6、前面的步骤有任何异常都会直接倒序触发 afterCompletion
7、页面成功渲染完成以后,也会倒序触发 afterCompletion
实战使用
:
1.配置拦截器:
/**
* 1、编写一个拦截器实现HandlerInterceptor接口
* 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
* 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
*/
@Configuration
public class HandleConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()) //加载自定义的拦截器
.addPathPatterns("/**") //拦截所有路径
.excludePathPatterns("/","/gotologin","/css/**","/fonts/**","/images/**","/js/**"); //排除静态资源
}
}
2.实现HandlerInterceptor 接口:
/**
* 登录检查
* 1、配置好拦截器要拦截哪些请求
* 2、把这些配置放在容器中
*/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
StringBuffer requestURL = request.getRequestURL();
log.info("登陆者的url是{}" + requestURL);
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if (loginUser != null){
return true;
}
//返回到登录页面
request.setAttribute("msg","你还没有登录,请登录");
request.getRequestDispatcher("/gotologin").forward(request,response);
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行了{}");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行了{}");
}
}
3.写一个控制层和相应的跳转也买你
@Controller
public class LoginController {
@RequestMapping("/login")
public String gotos(HttpSession session){
return "/success";
}
@RequestMapping("/gotologin")
public String gotoLogin(HttpSession session){
session.setAttribute("loginUser","zheng");
return "/login";
}
}
三.异常处理
异常处理流程
:
1、执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束;并且用dispatchException
2、进入视图解析流程(页面渲染?)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
3、mv = processHandlerException;处理handler发生的异常,处理完成返回ModelAndView;
1.遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】
2.系统默认的 异常解析器;
• 1、DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null;
• 2、默认没有任何人能处理异常,所以异常会被抛出
1.如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理
2.解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。
3、默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html
4、模板引擎最终响应这个页面 error/500.html
异常处理自动配置原理
:原理
错误处理
:
1.默认情况:Spring Boot提供/error处理所有错误的映射对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据.
要对其进行自定义,添加View解析为error要完全替换默认行为,可以实现 ErrorController 并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。error/下的4xx,5xx页面会被自动解析;
2.定制:定制错误跳转页面,在controller中写出对应的跳转
@Component
public class ErrorPageConfig implements ErrorPageRegistrar {
@Override
public void registerErrorPages(ErrorPageRegistry registry) {
ErrorPage error400Page = new ErrorPage(HttpStatus.BAD_REQUEST, "/error/404");
ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/error/404");
ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500");
registry.addErrorPages(error400Page,error404Page,error500Page);
}
}
全局异常处理
:
1.自定义一个异常类:
public class MyException extends RuntimeException{
public MyException(String message) {
super(message);
}
}
2.创建一个全局异常抓取器:
@ControllerAdvice("com.zgw")//返回视图,com.zgw包下出了异常都会被知道
//@RestControllerAdvice//返回json字符串
public class GlobalExceptionHandle {
//处理自定义异常
@ExceptionHandler({MyException.class,NullPointerException.class}) //处理空指针异常和自定义异常
public ModelAndView myExceptionHandle(Exception ex){
ModelAndView mv = new ModelAndView();
String message = ex.getMessage();
if (!StringUtils.isEmpty(message)){
mv.addObject("msg",message +"mye");
}
mv.setViewName("/exception");//出现异常,跳转到异常处理页面
return mv;
}
//处理所有异常
@ExceptionHandler
public ModelAndView allExceptionHandle(Exception ex){
ModelAndView mv = new ModelAndView();
String message = ex.getMessage();
if (!StringUtils.isEmpty(message)){
mv.addObject("msg",message + "alle");
}
mv.setViewName("/exception");//出现异常,跳转到异常处理页面
return mv;
}
}
3.定义相应的异常页面
四.全局常量枚举
全局枚举
:一般放在公共模块中
public enum PurchaseEnum {
//新建-0,已分配-1,已领取-2,已完成-3,未分配-4
CREATE(0,"新建"),
ASSIGNED(1,"已分配"),
RECEIVED(2,"已领取"),
COMPLETED(3,"已完成"),
NOASSIGNED(4,"未分配");
private int code;
private String msg;
PurchaseEnum(int code, String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
用法:ParchaseDetailsEnum.ASSIGNED.getCode();
全局常量
:一般放在公共模块中
public class Constant {
public static final String USER_NAME = "人才1";
public static final String ADDRESS = "人才2";
public static final int AGE = 20;
}
用法:System.out.println(Constant.ADDRESS);
五.单元测试
JUnit4:实战使用
:
1.导入依赖:JUnit5兼容JUnit4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--JUnit5兼容JUnit4-->
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
2.使用:
import org.junit.*;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
//就是用JUnit加载Spring的配置文件以完成Context的初始化,然后从Context中取出Bean并完成测试:
/**
* 一、JUnit4常用的注解
* (1)@Test:将一个方法标记为测试方法;
* (2)@Before:每一个测试方法调用前必执行的方法;
* (3)@After:每一个测试方法调用后必执行的方法;
* (4)@BeforeClass:所有测试方法调用前执行一次,在测试类没有实例化之前就已被加载,需用static修饰;
* (5)@AfterClass:所有测试方法调用后执行一次,在测试类没有实例化之前就已被加载,需用static修饰;
* (6)@Ignore:暂不执行该方法;
* 二、一个JUnit4的单元测试用例执行顺序为:
* @BeforeClass -> @Before -> @Test -> @After -> @AfterClass
* 每一个测试方法的调用顺序为:
* @Before -> @Test -> @After;
*/
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
//@ContextConfiguration(locations = { "classpath:applicationContext.xml" })
//就是用JUnit加载Spring的配置文件以完成Context的初始化,然后从Context中取出Bean并完成测试:
public class SpringbootTestApplicationTests {
@Test
public void hello(){
System.out.println("Test");
}
@Before
public void hello1(){
System.out.println("Before");
}
@After
public void hello2(){
System.out.println("After");
}
@BeforeClass
public static void hello3(){
System.out.println("BeforeClass");
}
@AfterClass
public static void hello4(){
System.out.println("AfterClass");
}
@Ignore
public void hello5(){
System.out.println("Ignore");
}
}
注意:JUnit4的类必须式public
JUnit5:
1.JUnit5 的变化
2.JUnit5常用注解
3.断言(assertions)
简单断言
:
用来对单个值进行简单的验证。如:
@Test
@DisplayName("simple assertion")
public void simple() {
assertEquals(3, 1 + 2, "simple math");
assertNotEquals(3, 1 + 1);
assertNotSame(new Object(), new Object());
Object obj = new Object();
assertSame(obj, obj);
assertFalse(1 > 2);
assertTrue(1 < 2);
assertNull(null);
assertNotNull(new Object());
}
数组断言
:
通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等
@Test
@DisplayName("array assertion")
public void array() {
assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}
组合断言
:
assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言
@Test
@DisplayName("assert all")
public void all() {
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0)
);
}
异常断言
:
在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows() ,配合函数式编程就可以进行使用。
@Test
@DisplayName("异常测试")
public void exceptionTest() {
ArithmeticException exception = Assertions.assertThrows(
//扔出断言异常
ArithmeticException.class, () -> System.out.println(1 % 0));
}
超时断言
:
Junit5还提供了Assertions.assertTimeout() 为测试方法设置了超时时间
@Test
@DisplayName("超时测试")
public void timeoutTest() {
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}
快速失败
:
通过 fail 方法直接使得测试失败
@Test
@DisplayName("fail")
public void shouldFail() {
fail("This should fail");
}
4.前置条件(assumptions)
JUnit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。
@DisplayName("前置条件")
public class AssumptionsTest {
private final String environment = "DEV";
@Test
@DisplayName("simple")
public void simpleAssume() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "PROD"));
}
@Test
@DisplayName("assume then do")
public void assumeThenDo() {
assumingThat(
Objects.equals(this.environment, "DEV"),
() -> System.out.println("In DEV")
);
}
}
assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。
5.嵌套测试
JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
6、参数化测试
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
利用@ValueSource等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource: 表示为参数化测试提供一个null的入参
@EnumSource: 表示为参数化测试提供一个枚举入参
@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试1")
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}
@ParameterizedTest
@MethodSource("method") //指定方法名
@DisplayName("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}
static Stream<String> method() {
return Stream.of("apple", "banana");
}
7、迁移指南
在进行迁移的时候需要注意如下的变化:
• 注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,
前置条件在 org.junit.jupiter.api.Assumptions 类中。
• 把@Before 和@After 替换成@BeforeEach 和@AfterEach。
• 把@BeforeClass 和@AfterClass 替换成@BeforeAll 和@AfterAll。
• 把@Ignore 替换成@Disabled。
• 把@Category 替换成@Tag。
• 把@RunWith、@Rule 和@ClassRule 替换成@ExtendWith。
六.文件上传
原理
:
文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties:自动配置好了 StandardServletMultipartResolver
【文件上传解析器】
原理步骤:
• 1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回 MultipartHttpServletRequest)
文件上传请求
• 2、参数解析器来解析请求中的文件内容封装成MultipartFile
• 3、将request中文件信息封装为一个Map;MultiValueMap<String, MultipartFile>
FileCopyUtils。实现文件流的拷贝
实战使用
:
1.页面表单
<!--单文件-->
<form method="post" action="/upload" enctype="multipart/form-data">
<input type="file" name="file"><br>
<input type="submit" value="提交">
</form>
<!多文件>
<form method="post" action="/upload" enctype="multipart/form-data">
<input type="file" name="photos" multiple><br>
<input type="submit" value="提交">
</form>
2.文件上传代码
@RequestMapping("/upload")
public String fileUpload(//@RequestParam("onephoto")MultipartFile onePhoto,
@RequestParam("photos") MultipartFile[] photos,
HttpServletRequest request) throws IOException {
request.getRequestURL();
//单文件上传
/*if (onePhoto != null){
//获取文件名 : file.getOriginalFilename();
String filename = onePhoto.getOriginalFilename();
String fileDirPath = new String("D:\\javaIDEAcx4-zerotoall\\mavendemos\\springboot-test\\src\\main\\resources\\static\\img\\" + filename);
//如果路径不存在,创建一个
File realPath = new File(fileDirPath);
if (!realPath.exists()){
realPath.mkdir();
}
onePhoto.transferTo(realPath);
}*/
//多文件上传
if (photos.length > 0){
for (MultipartFile photo : photos){
if (!photo.isEmpty()){
//获取文件名 : file.getOriginalFilename();
String filename = photo.getOriginalFilename();
String fileDirPath = new String("D:\\javaIDEAcx4-zerotoall\\mavendemos\\springboot-test\\src\\main\\resources\\static\\img\\" + filename);
//如果路径不存在,创建一个
File realPath = new File(fileDirPath);
if (!realPath.exists()){
realPath.mkdir();
}
photo.transferTo(realPath);
}
}
}
return "uploadsuccess";
}
注意几点
:
1.设置上传文件大小:
方式一:在application.properties中配置
// 设置单个文件大小
spring.servlet.multipart.max-file-size= 50MB
// 设置单次请求文件的总大小
spring.servlet.multipart.max-request-size= 50MB
方式二:自定义配置 -->在类上需要加上@Configuration声明式配置类
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
//允许上传的文件最大值
factory.setMaxFileSize("50MB"); //KB,MB
/// 设置总上传数据总大小
factory.setMaxRequestSize("50MB");
return factory.createMultipartConfig();
}
2.@RequestParam(“photos”)与name="photos"需要对应
七.头像上传
八.跨域的处理
什么是跨域
:
在前后端分离的模式下,前后端的域名是不一致的,此时就会发生跨域访问问题。在请求的过程中我们要想回去数据一般都是post/get请求,所以跨域问题出现
跨域问题来源于JavaScript的同源策略,即只有 协议+主机名+端口号(如存在)相同,则允许相互访问。也就是说JavaScript只能访问和操作自己域下的资源,不能访问和操作其他域下的资源。跨域问题是针对JS和ajax的,html本身没有跨域问题,比如a标签、script标签、甚至form标签(可以直接跨域发送数据并接收数据)等
引用 --> 链接:https://www.jianshu.com/p/9b6b6a135432解决
:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)//设置优先加载,因为有加载顺序,可能会不生效
class GulimallCorsConfiguration {
@Bean
public CorsWebFilter crosWebFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
//设置跨域
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
九.JSR303校验
常用注解
:
1 空检查
2 @Null 验证对象是否为null
3 @NotNull 验证对象是否不为null, 无法查检长度为0的字符串
4 @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
5 @NotEmpty 检查约束元素是否为NULL或者是EMPTY.
6
7 Booelan检查
8 @AssertTrue 验证 Boolean 对象是否为 true
9 @AssertFalse 验证 Boolean 对象是否为 false
10
11 长度检查
12 @Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
13 @Length(min=, max=) Validates that the annotated string is between min and max included.
14
15 日期检查
16 @Past 验证 Date 和 Calendar 对象是否在当前时间之前
17 @Future 验证 Date 和 Calendar 对象是否在当前时间之后
18 @Pattern 验证 String 对象是否符合正则表达式的规则
19
20 数值检查,建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为"",Integer为null
21 @Min 验证 Number 和 String 对象是否大等于指定的值
22 @Max 验证 Number 和 String 对象是否小等于指定的值
23 @DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
24 @DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
25 @Digits 验证 Number 和 String 的构成是否合法
26 @Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
27
28 @Range(min=, max=) 检查数字是否介于min和max之间.
29 @Range(min=10000,max=50000,message="range.bean.wage")
30 private BigDecimal wage;
31
32 @Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)
33 @CreditCardNumber信用卡验证
34 @Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。
35 @ScriptAssert(lang= ,script=, alias=)
36 @URL(protocol=,host=, port=,regexp=, flags=)
简单校验
:
1.导入依赖:
<!--JSR303注解-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.17.Final</version>
</dependency>
2.在实体bean上加上注解:
@Data
public class BrandEntity implements Serializable {
/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名javax.validation.constraints.NotNull
*/
@NotNull(message = "用户名不能为空")
private String name;
/**
* 品牌logo地址
*/
@URL(message = "logo必须合法")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@ListValue(vals = {0,1})
private Integer showStatus;
/**
* 检索首字母 使用一个正则表达式,只能是a-zA-Z
*/
@NotEmpty
//@Pattern(regexp = "/^[a-zA-Z]$/",message = "检索首字母必须是一个字母",groups = {AddBrand.class, UpdateBrand.class})
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value = 0,message = "排序必须大于等于0")
private Integer sort;
}
3.在对应的controller加上@Valid开启注解校验
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult bindResult){
Map<String,String> map = new HashMap<>();
if (bindResult.hasErrors()){
List<FieldError> fieldErrors = bindResult.getFieldErrors();
for (FieldError fieldError : fieldErrors){
String defaultMessage = fieldError.getDefaultMessage();//获取错误的信息
String field = fieldError.getField();//获取错误的字段
map.put(field,defaultMessage);
}
return MyR.error(400,"提交的数据不合法").put("msg",map);
}
brandService.save(brand);
return R.ok();
}
分组校验
:
1.创建两个接口作为分组:这两个接口只用来做分组,没有其他用途
//增加品牌
public interface AddBrand {}
//更新品牌
public interface UpdateBrand {}
2.在实体bean上加上注解:
@Data
public class BrandEntity implements Serializable {
/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名javax.validation.constraints.NotNull
*/
@NotNull(message = "用户名不能为空",groups = {AddBrand.class, UpdateBrand.class})
private String name;
/**
* 品牌logo地址
*/
@URL(message = "logo必须合法",groups = {UpdateBrand.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@ListValue(vals = {0,1},groups = {AddBrand.class, UpdateBrand.class})
private Integer showStatus;
/**
* 检索首字母 使用一个正则表达式,只能是a-zA-Z
*/
@NotEmpty(groups = {AddBrand.class, UpdateBrand.class})
//@Pattern(regexp = "/^[a-zA-Z]$/",message = "检索首字母必须是一个字母",groups = {AddBrand.class, UpdateBrand.class})
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value = 0,message = "排序必须大于等于0",groups = {AddBrand.class})
private Integer sort;
}
3.在对应的controller加上@Validated开启注解校验,并且指定分组
@RequestMapping("/save")
public R save(@Validated(AddBrand.class) @RequestBody BrandEntity brand, BindingResult bindResult){
Map<String,String> map = new HashMap<>();
if (bindResult.hasErrors()){
List<FieldError> fieldErrors = bindResult.getFieldErrors();
for (FieldError fieldError : fieldErrors){
String defaultMessage = fieldError.getDefaultMessage();//获取错误的信息
String field = fieldError.getField();//获取错误的字段
map.put(field,defaultMessage);
}
return MyR.error(400,"提交的数据不合法").put("msg",map);
}
brandService.save(brand);
return R.ok();
}
定义一个全局表单校验异常处理器
:有了这个BindingResult bindResult就不用了
@RestControllerAdvice(basePackages = "com.zgw.controller")//抓取controller下发生的异常,并返回json
public class GlobelExceptionHandle {
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R shujuException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();//拿到发生的异常的具体信息,然后返回
Map<String, String> map = new HashMap<>();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
String defaultMessage = fieldError.getDefaultMessage();//获取错误的信息
String field = fieldError.getField();//获取错误的字段
map.put(field, defaultMessage);
}
return R.error(BizCodeEunm.VALID_EXCEPTION.getCode(),BizCodeEunm.VALID_EXCEPTION.getMsg()).put("data", map);
}
@ExceptionHandler
public R otherException(Throwable e) {
String message = e.getMessage();
return R.error(BizCodeEunm.UNKNOW_EXCEPTION.getCode(), BizCodeEunm.UNKNOW_EXCEPTION.getMsg()).put("data", message);
}
}
自定义校验
:
1.编写一个自定义的校验注解@ListValue(vals={0,1}),创建一个annotion
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { ListValueConstrainValidator.class })
public @interface ListValue {
String message() default "{com.zgw.common.valid.ListValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int[] vals() default{ };
}
2.编写一个自定义的校验器
public class ListValueConstrainValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set = new HashSet<>();
//初始化方法
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for (int val : vals){
set.add(val);
}
}
//判断是否检验成功
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
3.使用:
/**
* 显示状态[0-不显示;1-显示]
*/
@ListValue(vals = {0,1},groups = {AddBrand.class, UpdateBrand.class})
private Integer showStatus;
十. 集成文档编辑器
1.下载:下载完成后导入有用的部分,如下图
富文本编辑器有很多,我用的是Editor.md:https://pandao.github.io/editor.md/">https://pandao.github.io/editor.md/
2.基础工程的搭建:
建表
:后面工程建好编写相应的实体类
CREATE TABLE `article` (
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'int文章的唯一ID',
`author` varchar(50) NOT NULL COMMENT '作者',
`title` varchar(100) NOT NULL COMMENT '标题',
`content` longtext NOT NULL COMMENT '文章的内容',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
导入pom
:
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
导入editormd
:上面讲过零碎的自己完成
:如几个层次,xml,配置文件等
mapper接口:
@Mapper
@Repository
public interface ArticleMapper {
//查询所有的文章
List<Article> queryArticles();
//新增一个文章
int addArticle(Article article);
//根据文章id查询文章
Article getArticleById(int id);
//根据文章id删除文章
int deleteArticleById(int id);
}
xxxmapper.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.kuang.mapper.ArticleMapper">
<select id="queryArticles" resultType="Article">
select * from article
</select>
<select id="getArticleById" resultType="Article">
select * from article where id = #{id}
</select>
<insert id="addArticle" parameterType="Article">
insert into article (author,title,content) values (#{author},#{title},#{content});
</insert>
<delete id="deleteArticleById" parameterType="int">
delete from article where id = #{id}
</delete>
</mapper>
3.文章编辑整合(重点):编辑文章页面 editor.html、需要引入 jQuery
:
<!DOCTYPE html>
<html class="x-admin-sm" lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>富文本编辑器</title>
<meta name="renderer" content="webkit">
<!-- <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width,user-scalable=yes, minimum-scale=0.4, initial-scale=0.8,target-densitydpi=low-dpi" />-->
<!--Editor.md-->
<link rel="stylesheet" th:href="@{/editormd/css/editormd.css}"/>
<link rel="shortcut icon" href="https://pandao.github.io/editor.md/favicon.ico" type="image/x-icon" />
</head>
<body>
<div class="layui-fluid">
<div class="layui-row layui-col-space15">
<div class="layui-col-md12">
<!--博客表单-->
<form name="mdEditorForm" action="/addeditor" method="post">
<div>
标题:<input type="text" name="title">
</div>
<div>
作者:<input type="text" name="author">
</div>
<div id="article-content">
<textarea name="content" id="content" style="display:none;"> </textarea>
</div>
<input type="submit" value="提交"/>
</form>
</div>
</div>
</div>
</body>
<!--editormd-->
<script th:src="@{/editormd/lib/jquery.min.js}"></script>
<script th:src="@{/editormd/editormd.js}"></script>
<script type="text/javascript">
var testEditor;
//window.onload = function(){ }
$(function() {
testEditor = editormd("article-content", {
width : "95%",
height : 400,
syncScrolling : "single",
path : "../editormd/lib/",
saveHTMLToTextarea : true, // 保存 HTML 到 Textarea
emoji: true,//开启表情包
theme: "default ",//工具栏主题
previewTheme: "default ",//预览主题
editorTheme: "pastel-on-default ",//编辑主题
tex : true, // 开启科学公式TeX语言支持,默认关闭
flowChart : true, // 开启流程图支持,默认关闭
sequenceDiagram : true, // 开启时序/序列图支持,默认关闭,
//图片上传
imageUpload : true,//本地上传
imageFormats : ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
imageUploadURL : "/article/file/upload",
onload : function() {
console.log('onload', this);
},
/*指定需要显示的功能按钮*/
toolbarIcons : function() {
return ["undo","redo","|",
"bold","del","italic","quote","ucwords","uppercase","lowercase","|",
"h1","h2","h3","h4","h5","h6","|",
"list-ul","list-ol","hr","|",
"link","reference-link","image","code","preformatted-text",
"code-block","table","datetime","emoji","html-entities","pagebreak","|",
"goto-line","watch","preview","fullscreen","clear","search","|",
"help","info","releaseIcon", "index"]
},
/*自定义功能按钮,下面我自定义了2个,一个是发布,一个是返回首页*/
/* toolbarIconTexts : {
releaseIcon : "<span bgcolor=\"gray\">发布</span>",
index : "<span bgcolor=\"red\">返回首页</span>",
},*/
/*给自定义按钮指定回调函数*/
/* toolbarHandlers:{
releaseIcon : function(cm, icon, cursor, selection) {
//表单提交
mdEditorForm.method = "post";
mdEditorForm.action = "/article/addArticle";//提交至服务器的路径
mdEditorForm.submit();
},
index : function(){
window.location.href = '/';
},
}*/
});
});
</script>
controller跳转到编辑页面
:
@Autowired
ArticleService articleService;
@GetMapping("/toEditor")
public String toEditor(){
return "editor";
}
@PostMapping("/addeditor")
public String addArticle(Article article){
articleService.addArticle(article);
return "editor";
}
图片上传
js代码中开启本地图片上传
:
//图片上传
imageUpload : true,//本地上传
imageFormats : ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
imageUploadURL : "/article/file/upload",
后台接收图片
:
//博客图片上传问题
@RequestMapping("/file/upload")
@ResponseBody
public JSONObject fileUpload(@RequestParam(value = "editormd-image-file", required = true) MultipartFile file, HttpServletRequest request) throws IOException {
//上传路径保存设置
//获得SpringBoot当前项目的路径:System.getProperty("user.dir")
String path = System.getProperty("user.dir")+"/src/main/resources/static/upload/images";
//不存在当前目录则创建
File realPath = new File(path);
if (!realPath.exists()){
realPath.mkdir();
}
//上传文件地址
System.out.println("上传文件保存地址:"+realPath);
//拿到文件名称
String filename = file.getOriginalFilename();
//通过CommonsMultipartFile的方法直接写文件(注意这个时候)
file.transferTo(new File(realPath +"/"+ filename));
//给editormd进行回调
JSONObject res = new JSONObject();
res.put("url","http://localhost:8000/upload/images"+"/"+ filename);//注意这里:可以使用动态的
res.put("success", 1);
res.put("message", "upload success!");
return res;
}
4.文章展示:Controller 根据id拿到文章
:
@GetMapping("/{id}")
public String show(@PathVariable("id") int id, Model model){
Article article = articleService.getArticleById(id);
model.addAttribute("article",article);
return "article";
}
article.html展示
:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title th:text="${article.title}"></title>
</head>
<body>
<div>
<!--文章头部信息:标题,作者,最后更新日期,导航-->
<h2 style="margin: auto 0" th:text="${article.title}"></h2>
作者:<span style="float: left" th:text="${article.author}"></span>
<!--文章主体内容-->
<div id="doc-content">
<textarea style="display:none;" placeholder="markdown" th:text="${article.content}"></textarea>
</div>
</div>
<link rel="stylesheet" th:href="@{/editormd/css/editormd.preview.css}" />
<script th:src="@{/editormd/lib/jquery.min.js}"></script>
<script th:src="@{/editormd/lib/marked.min.js}"></script>
<script th:src="@{/editormd/lib/prettify.min.js}"></script>
<script th:src="@{/editormd/lib/raphael.min.js}"></script>
<script th:src="@{/editormd/lib/underscore.min.js}"></script>
<script th:src="@{/editormd/lib/sequence-diagram.min.js}"></script>
<script th:src="@{/editormd/lib/flowchart.min.js}"></script>
<script th:src="@{/editormd/lib/jquery.flowchart.min.js}"></script>
<script th:src="@{/editormd/editormd.js}"></script>
<script type="text/javascript">
var testEditor;
$(function () {
testEditor = editormd.markdownToHTML("doc-content", {//注意:这里是上面DIV的id
htmlDecode: "style,script,iframe",
emoji: true,
taskList: true,
tocm: true,
tex: true, // 默认不解析
flowChart: true, // 默认不解析
sequenceDiagram: true, // 默认不解析
codeFold: true
});});
</script>
</body>
</html>
十一.集成EasyExcel:抽取于项目,知道大概步骤
一.测试demo
导入依赖
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyExcel</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
创建实体类
@Data
public class UserDate {
@ExcelProperty("用户编号")
private int id;
@ExcelProperty("用户名称")
private String name;
}
EasyExcel写操作
public class WriteTest {
public static void main(String[] args) {
List<UserDate> lists = new ArrayList<>();
for (int i = 0;i<10;i++){
UserDate userDate = new UserDate();
userDate.setId(i);
userDate.setName("excel"+i);
lists.add(userDate);
}
String fileName = "E:\\excel\\01.xlsx";
EasyExcel.write(fileName,UserDate.class).sheet("用户信息").doWrite(lists);
}
}
EasyExcel读操作
1.配置类
public class ExcelConfig extends AnalysisEventListener<UserDate> {
//一行一行读取excel文件中的数据,除了第一行
@Override
public void invoke(UserDate userDate, AnalysisContext analysisContext) {
System.out.println(userDate);
}
//读取第一行数据
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
System.out.println(headMap);
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println(analysisContext);
}
}
2.测试
public class ReadTest {
public static void main(String[] args) {
String fileName = "E:\\excel\\01.xlsx";
EasyExcel.read(fileName,UserDate.class,new ExcelConfig()).sheet().doRead();
}
}
二.实战使用
导入依赖
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyExcel</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
创建实体类
@Data
public class DictEeVo {
@ExcelProperty(value = "id" ,index = 0)
private Long id;
@ExcelProperty(value = "上级id" ,index = 1)
private Long parentId;
@ExcelProperty(value = "名称" ,index = 2)
private String name;
@ExcelProperty(value = "值" ,index = 3)
private String value;
@ExcelProperty(value = "编码" ,index = 4)
private String dictCode;
}
EasyExcel写操作
@Override
public void exportDictData(HttpServletResponse response) {
try {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("数据字典", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename="+ fileName + ".xlsx");
//先从数据库当中拿到所有的字典数据
List<Dict> dictList = baseMapper.selectList(null);
List<DictEeVo> dictEeVos = new ArrayList<>();
for (Dict dict : dictList){
DictEeVo dictEeVo = new DictEeVo();
//把数据拷贝到vo
BeanUtils.copyProperties(dict, dictEeVo);
dictEeVos.add(dictEeVo);
}
//把数据写入到excel中
EasyExcel.write(response.getOutputStream(),DictEeVo.class).sheet().doWrite(dictEeVos);
}catch (IOException e){
e.printStackTrace();
}
}
EasyExcel读操作
1.创建监听器
public class ExcelListener extends AnalysisEventListener<DictEeVo> {
private DictMapper dictMapper;
public ExcelListener(DictMapper dictMapper) {
this.dictMapper = dictMapper;
}
//该方法主要使用来一行一行写入,从第二行开始
@Override
public void invoke(DictEeVo dictEeVo, AnalysisContext analysisContext) {
Dict dict = new Dict();
BeanUtils.copyProperties(dictEeVo,dict);
dictMapper.insert(dict);
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
2.service层
@Override
public void importData(MultipartFile file) {
try {
EasyExcel.read(file.getInputStream(),DictEeVo.class,new ExcelListener(baseMapper)).sheet().doRead();
} catch (IOException e) {
e.printStackTrace();
}
}