AOP应用 AOP日志记录 引入依赖 1 2 3 4 5 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-aop</artifactId > <version > x.x.x</version > </dependency >
自定义注解 自定义注解,注解中定义参数用于指定日志的描述信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Inherited @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Log { String businessName () default "" ; String businessType () default "" ; }
定义切面类 定义切面类,使用@Aspect注解标识 在切面类中,使用自定义的注解作为切点,用于拦截注解标识的方法 在切面类中,编写环绕通知方法,在方法执行前后分别打印日志 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 @Slf4j @Aspect @Component public class LogAspect { @Autowired private OperationLogMapper operationLogMapper; @Pointcut("@annotation(com.wen.aop.log.Log)") public void pt () { } @Around("pt()") public Object printLot (ProceedingJoinPoint pjp) { Object proceed = null ; try { log.info("<==========Start==========>" ); proceed = pjp.proceed(); ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); recordOperationLog(proceed, pjp, request); saveOperationLog(proceed, pjp, request); } catch (Throwable e) { e.printStackTrace(); log.error("[抛出异常] - {}" , e.getMessage()); } finally { log.info("<==========End==========>" + System.lineSeparator()); } return proceed; } private void recordOperationLog (Object proceed, ProceedingJoinPoint pjp, HttpServletRequest request) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); log.info("User ID: : {}" , loginUser.getUser() != null ? loginUser.getUser().getId() : -1L ); log.info("NickName: : {}" , loginUser.getUser() != null ? loginUser.getUser().getNickName() : "用户未登录" ); log.info("BusinessName : {}" , getLogAnnotation(pjp).businessName()); log.info("BusinessType : {}" , getLogAnnotation(pjp).businessType()); log.info("Module: : {}" , getApiAnnotation(pjp).tags()[0 ]); log.info("Class : {}" , pjp.getSignature().getDeclaringTypeName()); log.info("Method : {}" , ((MethodSignature) pjp.getSignature()).getName()); log.info("Description: : {}" , getApiOperation(pjp).value()); log.info("IP Address: : {}" , IpUtils.getIpAddress(request)); log.info("Ip Location: : {}" , IpUtils.getIpLocation(IpUtils.getIpAddress(request))); log.info("DeviceName: : {}" , IpUtils.getDeviceName(request)); log.info("Request URL: : {}" , request.getRequestURI()); log.info("Request Method: : {}" , request.getMethod()); log.info("Request Param: : {}" , JSON.toJSONString(pjp.getArgs())); log.info("Response Data: : {}" , JSON.toJSONStringWithDateFormat(proceed, "yyyy-MM-dd HH:mm:ss" , SerializerFeature.WriteDateUseDateFormat)); } private void saveOperationLog (Object proceed, ProceedingJoinPoint pjp, HttpServletRequest request) { OperationLog operationLog = new OperationLog (); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); operationLog.setUserId(loginUser.getUser() != null ? loginUser.getUser().getId() : -1L ); operationLog.setNickName(loginUser.getUser() != null ? loginUser.getUser().getNickName() : "用户未登录" ); operationLog.setBusinessName(getLogAnnotation(pjp).businessName()); operationLog.setBusinessType(getLogAnnotation(pjp).businessType()); operationLog.setModule(getApiAnnotation(pjp).tags()[0 ]); operationLog.setClassName(pjp.getTarget().getClass().getName()); operationLog.setMethodName(pjp.getSignature().getName()); operationLog.setDescription(getApiOperation(pjp).value()); operationLog.setIpAddress(IpUtils.getIpAddress(request)); operationLog.setIpLocation(IpUtils.getIpLocation(IpUtils.getIpAddress(request))); operationLog.setDeviceName(IpUtils.getDeviceName(request)); operationLog.setRequestUrl(request.getRequestURI()); operationLog.setRequestMethod(request.getMethod()); operationLog.setRequestParam(JSON.toJSONString(pjp.getArgs())); operationLog.setResponseData(JSON.toJSONStringWithDateFormat(proceed, "yyyy-MM-dd HH:mm:ss" , SerializerFeature.WriteDateUseDateFormat)); operationLogMapper.insert(operationLog); } private Log getLogAnnotation (ProceedingJoinPoint pjp) { MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); Method method = methodSignature.getMethod(); return method.getAnnotation(Log.class); } private Api getApiAnnotation (ProceedingJoinPoint pjp) { Class<?> targetClass = pjp.getTarget().getClass(); return targetClass.getAnnotation(Api.class); } private ApiOperation getApiOperation (ProceedingJoinPoint pjp) { MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); Method method = methodSignature.getMethod(); return method.getAnnotation(ApiOperation.class); } }
使用方式 在需要打印日志的表现层接口中加上自定义的注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @Log("查询用户列表") @Log(businessName = "getUserListLog", businessType = "SELECT") @GetMapping("/") public List<User> getUserList () { return userService.getUserList(); } }
AOP缓存优化 引入依赖 1 2 3 4 5 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-aop</artifactId > <version > x.x.x</version > </dependency >
自定义注解 自定义注解,注解中定义参数用于指定日志的描述信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Inherited @Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Cache { String name () default "" ; long expire () default 60 * 1000 ; }
定义切面类 定义切面类,使用@Aspect注解标识 在切面类中,使用自定义的注解作为切点,用于拦截注解标识的方法 在切面类中,编写环绕通知方法,在方法执行前后分别打印日志 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 @Slf4j @Aspect @Component public class CacheAspect { @Autowired private RedisTemplate<String, String> redisTemplate; @Pointcut("@annotation(com.wen.aop.cache.Cache)") public void pt () { } @Around("pt()") public Object around (ProceedingJoinPoint pjp) { log.info("<==========Start==========>" ); try { String redisKey = getRedisKey(pjp); String redisValue = redisTemplate.opsForValue().get(redisKey); if (redisValue != null && !redisValue.isEmpty()) { log.info("命中缓存,走缓存~~~ redisKey:{}" , redisKey); return JSON.parseObject(redisValue, ResponseResult.class); } Object proceed = pjp.proceed(); MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); Method method = methodSignature.getMethod(); String jsonString = JSON.toJSONStringWithDateFormat(proceed, "yyyy-MM-dd HH:mm:ss" , SerializerFeature.WriteDateUseDateFormat); Duration duration = Duration.ofMillis(method.getAnnotation(Cache.class).expire()); redisTemplate.opsForValue().set(redisKey, jsonString, duration); log.info("未命中缓存,已存入缓存~~~" ); return proceed; } catch (Throwable e) { log.error("[抛出异常] - {}" , e.getMessage()); throw new RuntimeException (e.getMessage()); } finally { log.info("<==========End==========>" + System.lineSeparator()); } } private String getRedisKey (ProceedingJoinPoint pjp) { MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); Method method = methodSignature.getMethod(); Class<?> targetClass = pjp.getTarget().getClass(); Object[] args = pjp.getArgs(); Class<?>[] parameterTypes = method.getParameterTypes(); StringBuilder params = new StringBuilder (); for (int i = 0 ; i < args.length; i++) { if (args[i] != null ) { params.append(JSON.toJSONString(args[i])); parameterTypes[i] = args[i].getClass(); } else { parameterTypes[i] = null ; } } String name = method.getAnnotation(Cache.class).name(); String className = targetClass.getSimpleName(); String methodName = method.getName(); String paramStr = DigestUtils.md5Hex(params.toString()); return String.format("%s::%s::%s::%s" , name, className, methodName, paramStr); } }
使用方式 在需要打印日志的表现层接口中加上自定义的注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @Cache(name = "getUserListCache", expire = 60 * 1000) @GetMapping("/") public List<User> getUserList () { return userService.getUserList(); } }
AOP接口限流 引入依赖 1 2 3 4 5 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-aop</artifactId > <version > x.x.x</version > </dependency >
自定义注解 自定义注解,注解中定义参数用于指定日志的描述信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface AccessLimit { int seconds () default 60 ; int maxCount () default 100 ; }
定义切面类 定义切面类,使用@Aspect注解标识 在切面类中,使用自定义的注解作为切点,用于拦截注解标识的方法 在切面类中,编写环绕通知方法,在方法执行前后分别打印日志 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 @Slf4j @Aspect @Component public class AccessLimitAspect { @Autowired private RedisService redisService; @Pointcut("@annotation(com.wen.aop.limit.AccessLimit)") public void accessLimitPointCut () { } @Around("accessLimitPointCut()") public Object around (ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); AccessLimit accessLimit = method.getAnnotation(AccessLimit.class); int seconds = accessLimit.seconds(); int maxCount = accessLimit.maxCount(); log.info("已经启用接口限流:{}秒只能请求{}次" , seconds, maxCount); String key = generateKey(joinPoint); long count = redisService.incrExpire(key, seconds); if (count > maxCount) { log.error("访问频率超过设置的最大值" ); throw new RuntimeException ("请求过于频繁,请稍候再试" ); } return joinPoint.proceed(); } private String generateKey (ProceedingJoinPoint joinPoint) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String ip = IpUtils.getIpAddress(request); String methodName = joinPoint.getSignature().getName(); return ip + methodName; } }
使用方式 在需要打印日志的表现层接口中加上自定义的注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @AccessLimit(seconds = 60, maxCount = 1) @GetMapping("/") public List<User> getUserList () { return userService.getUserList(); } }
预加载 预加载是指在应用程序启动时,提前加载一些资源或执行某些初始化操作,以提高系统的响应速度和用户体验
实现CommandLineRunner接口 这是Spring Boot提供的一种预加载方案。通过实现CommandLineRunner接口,可以在Spring Boot应用程序启动后立即执行一些初始化操作。具体而言,实现该接口的类可以重写run方法,在该方法中编写需要在应用程序启动时执行的逻辑代码
1 2 3 4 5 6 7 8 @Component public class MyCommandLineRunner implements CommandLineRunner { @Override public void run (String... args) { System.out.println("实现CommandLineRunner接口方案:执行初始化操作..." ); } }
实现ApplicationRunner 接口 ApplicationRunner接口也是Spring Boot提供的一种预加载方案。通过实现ApplicationRunner接口,同样可以在Spring Boot应用程序启动后执行一些初始化操作。不同的是,实现该接口的类需要重写run方法,并且该方法传入了一个ApplicationArguments对象,可以用于获取应用程序启动时的命令行参数
1 2 3 4 5 6 7 8 @Component public class MyApplicationRunner implements ApplicationRunner { @Override public void run (ApplicationArguments args) throws Exception { System.out.println("实现ApplicationRunner 接口方案:执行初始化操作..." ); } }
实现InitializingBean接口 这是Spring Framework提供的一种预加载方案。通过实现InitializingBean接口,可以在Spring Bean创建完成后进行初始化操作。具体而言,实现该接口的类需要重写afterPropertiesSet方法,在该方法中编写需要在Bean创建完成后执行的逻辑代码。
1 2 3 4 5 6 7 8 @Component public class MyInitializingBean implements InitializingBean { @Override public void afterPropertiesSet () { System.out.println("实现InitializingBean接口方案:执行初始化操作..." ); } }
实现ApplicationListener接口 这是Spring Framework提供的另一种预加载方案。通过实现ApplicationListener接口,并监听ContextRefreshedEvent事件,可以在Spring容器刷新完成后执行一些初始化操作。具体而言,实现该接口的类需要重写onApplicationEvent方法,并在该方法中编写需要在容器刷新完成后执行的逻辑代码。
1 2 3 4 5 6 7 8 @Component public class MyApplicationListener implements ApplicationListener <ContextRefreshedEvent> { @Override public void onApplicationEvent (ContextRefreshedEvent event) { System.out.println("实现ApplicationListener<ContextRefreshedEvent>接口方案:执行初始化操作..." ); } }
使用@PostConstruct注解 这是Java EE提供的一种预加载方案。通过在方法上添加@PostConstruct注解,可以在Bean创建完成后进行初始化操作。具体而言,被@PostConstruct注解修饰的方法会在Bean创建完成后自动执行,可以在该方法中编写需要进行的初始化逻辑代码
1 2 3 4 5 6 7 8 @Component public class MyPostConstruct { @PostConstruct public void init () { System.out.println("使用@PostConstruct注解方案:执行初始化操作..." ); } }
使用@EventListener注解 这是Spring Framework提供的另一种预加载方案。通过在方法上添加@EventListener注解,并指定相关的事件类型,可以在特定事件触发时执行初始化操作。具体而言,被@EventListener注解修饰的方法会在对应的事件触发时执行,可以通过该方法编写需要进行的初始化逻辑代码。
1 2 3 4 5 6 7 8 @Component public class MyEventListener { @EventListener public void handleContextRefreshedEvent (ContextRefreshedEvent event) { System.out.println("使用@EventListener注解方案:执行初始化操作..." ); } }
任务调度 任务调度(定时任务)简介 任务调度是指系统为了自动完成特定任务,在约定的特定时刻去执行任务的过程。通过任务调度,可以实现自动执行重复性、定时性或周期性的任务,从而提高效率和减少人力成本。
常见 举例 时间驱动 在指定的时间点执行特定的任务。例如:定时发送优惠券、定时发送短信通知等 批量处理数据 定时执行批量任务来处理大量的数据,例如:每天定时统计上个月的账单、每周定时统计销售数据 固定频率 按固定的时间间隔执行任务。例如:每隔5分钟执行一次数据同步、每小时执行一次数据备份等。
任务调度常见实现方式 多线程方式 :手动开启一个线程,通过Thread.sleep()方法,实现任务的延时和定时执行JDK提供的java.util.Timer :使用TimerTask对象表示要执行的任务,并通过schedule()方法设置任务的执行时间和频率JDK提供的ScheduledExecutorService接口 :提供了一个线程池,并且能够在指定的时间间隔或特定时间执行任务Quartz调度器 :Quartz是一个功能强大的开源任务调度框架,提供了复杂的任务调度和作业管理功能Spring提供的注解 :Spring框架提供了简化任务调度的注解@EnableScheduling和@Scheduled多线程实现任务调度 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class TaskScheduler { public static void main (String[] args) { Timer timer = new Timer (); timer.schedule(new TimerTask () { @Override public void run () { System.out.println("Task executed at: " + System.currentTimeMillis()); } }, 1000 , 5000 ); } }
java.util.Timer实现任务调度 1 2 3 4 5 6 7 8 9 10 11 public class TaskScheduler { public static void main (String[] args) { ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1 ); scheduler.scheduleAtFixedRate(() -> { System.out.println("Task executed at: " + System.currentTimeMillis()); }, 1 , 5 , TimeUnit.SECONDS); } }
ScheduledExecutorService接口实现任务调度 1 2 3 4 5 6 7 8 9 10 11 public class TaskScheduler { public static void main (String[] args) { ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1 ); scheduler.scheduleAtFixedRate(() -> { System.out.println("Task executed at: " + System.currentTimeMillis()); }, 1 , 5 , TimeUnit.SECONDS); } }
第三方库Quartz实现任务调度 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class TaskScheduler { public static void main (String[] args) throws SchedulerException { SchedulerFactory schedulerFactory = new StdSchedulerFactory (); Scheduler scheduler = schedulerFactory.getScheduler(); JobDetail job = JobBuilder.newJob(MyJob.class) .withIdentity("myJob" , "group1" ) .build(); Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("myTrigger" , "group1" ) .startNow() .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(5 ) .repeatForever()) .build(); scheduler.scheduleJob(job, trigger); scheduler.start(); } public static class MyJob implements Job { @Override public void execute (JobExecutionContext context) throws JobExecutionException { System.out.println("Task executed at: " + System.currentTimeMillis()); } } }
Spring提供的注解实现任务调度 Spring定时任务实现步骤 Spring Boot提供了@EnableScheduling注解,用来开启定时任务功能,一般我们会在主启动类开启定时任务功能
1 2 3 4 5 6 7 @EnableScheduling @SpringBootApplication public class Application { public static void main (String[] args) { SpringApplication.run(Application.class, args); } }
当开启定时任务功能后,只需创建要执行的定时任务类,并在需要定时执行的方法上添加@Scheduled注解即可使用
1 2 3 4 5 6 7 @Component public class MyScheduledTask { @Scheduled(cron = "0 0 * * * ?") public void myTask () { } }
@Scheduled注解说明 @Scheduled注解是Spring框架提供的用于配置定时任务的注解,用于指定方法的执行时间和执行频率
常用属性 简介 fixedDelay 固定延迟时间,表示在上一次任务执行完成后,等待指定的时间间隔后再执行下一次任务。时间间隔单位为毫秒 fixedRate 固定执行速率,表示以固定的速率执行任务,不论上一次任务是否完成。时间间隔单位为毫秒 initialDelay 初始延迟时间,表示首次执行任务前的延迟时间。时间间隔单位为毫秒 cron 使用cron表达式配置任务执行时间。cron表达式可以精确到秒级,更灵活地定义任务的执行时间 zone 指定cron表达式的时区,默认为服务器所在时区
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @Component public class MyScheduledTask { @Scheduled(fixedDelay = 5000) public void myTask1 () { System.out.println("延迟5秒后执行=====>>>>>" + LocalDateTime.now()); } @Scheduled(fixedRate = 5000) public void myTask2 () { System.out.println("每隔5秒执行=====>>>>>" + LocalDateTime.now()); } @Scheduled(initialDelay = 3000, fixedRate = 5000) public void myTask3 () { System.out.println("初始延迟3秒,然后每隔5秒执行=====>>>>>" + LocalDateTime.now()); } @Scheduled(cron = "0 0 * * * ?") public void myTask4 () { System.out.println("每小时触发一次=====>>>>>" + LocalDateTime.now()); } @Scheduled(cron = "0 0 * * * ?", zone = "Asia/Shanghai") public void myTask5 () { System.out.println("使用上海时区触发每小时一次=====>>>>>" + LocalDateTime.now()); } }
Cron表达式 Cron表达式是一种用于定义定时任务执行时间的字符串格式。
Cron表达式由6或7个字段组成,表示秒、分钟、小时、天(月份中的某一天)、月份和星期几,语法如下
1 @Scheduled(cron = " {秒数} {分钟} {小时} {日期} {月份} {星期} {年份(可为空)} ")
字段 简介 Seconds(秒) 可以用数字0-59 表示,不允许为空值 Minutes(分) 可以用数字0-59 表示,不允许为空值 Hours(时) 可以用数字0-23表示,不允许为空值 Day-of-Month(天) 可以用数字1-31表示,但要注意一些特别的月份,不允许为空值 Month(月) 可以用0-11表示,还可以使用字符串 “JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and DEC” 表示,不允许为空值 Day-of-Week(每周) 可以用数字1-7(1 = 星期日)表示,还可以使用字符串“SUN, MON, TUE, WED, THU, FRI and SAT”表示,不允许为空值 Year(年) 1970~2099 ,允许为空
Cron表达式支持以下特殊符号和通配符
符号 含义 *该字段的每个可能值。例如,*在分钟字段表示每分钟触发。 /用于定义增量步长。例如,0/15在秒字段表示从0秒开始,每隔15秒触发一次。 ,用于指定多个值。例如,1,3,5在小时字段表示1点、3点和5点触发。 -用于指定范围。例如,10-15在分钟字段表示从第10分钟到第15分钟触发。 ?用于天和星期字段,表示不指定具体值。例如,?在天字段表示不关心天数,只关心星期几。 L用于天和星期字段,表示最后一天或最后一个星期几。例如,L在天字段表示每个月的最后一天触发。
Cron表达式一般不需要自己写,可以使用在线Cron表达式生成器 生成,一些示例Cron表达式如下
表达式 简介 */5 * * * * ? 每隔5秒执行一次 0 0 5-15 * * ? 每三分钟触发一次 0 0 12 ? * WED 每个星期三中午12点触发一次
Async异步任务 当定时任务增多时,如果一个任务卡死,可能会导致其他任务也无法执行,从而影响系统的正常运行。为了解决这个问题,可以使用配置类添加异步任务的配置,将每个任务的执行逻辑放在独立的线程中进行,这样即使某个任务出现问题,也不会影响其他任务的正常执行
(1)创建一个异步任务执行的线程池配置类,并使用@EnableAsync 注解开启异步方法执行的支持
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @EnableAsync @Configuration public class AsyncConfig { private int corePoolSize = 10 ; private int maxPoolSize = 200 ; private int queueCapacity = Integer.MAX_VALUE; private int keepAliveSeconds = 60 ; @Bean public Executor taskExecutor () { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor (); executor.setCorePoolSize(corePoolSize); executor.setMaxPoolSize(maxPoolSize); executor.setQueueCapacity(queueCapacity); executor.setKeepAliveSeconds(keepAliveSeconds); executor.setThreadNamePrefix("Async-Executor-" ); executor.setWaitForTasksToCompleteOnShutdown(true ); executor.setRejectedExecutionHandler(new ThreadPoolExecutor .CallerRunsPolicy()); executor.initialize(); return executor; } }
(2)配置异步任务后,可以使用@Async注解来标识需要异步执行的方法。通过在方法上添加@Async注解,Spring框架会将该方法放入线程池中执行,而不会阻塞当前线程。
1 2 3 4 5 6 7 @Component public class AsyncTask { @Async public void asyncMethod () { } }
跨域问题 跨域问题简介 跨域指的是不同服务器之间,不能相互访问各自的资源或者数据
不同协议的网站不能相互访问:例如https://baidu.com不能访问http://baidu.com,因为它们一个是HTTPS协议,一个是HTTP协议 不同IP的网站不能相互访问:例如http://10.111.115.31不能访问http://12.889.64.31,因为它们是不同的 IP 地址,属于不同的网络 域名与对应IP的网址不能相互访问:虽然http://baidu.com和http://39.156.66.14/都指向百度的资源,但由于域名和 IP 地址不一致,因此视为跨域访问。 不同子域名下的网站不能相互访问:例如https://www.example.com不能访问https://api.example.com 不同顶级域名下的网站不能相互访问:例如https://example.com不能访问https://example.net 同一台服务器不同端口互相访问:比如http://localhost:80不能访问http://localhost:8080 跨域出现的原因 跨域(Cross-Origin)出现的原因主要是为了保护用户的信息安全和防止恶意攻击。
浏览器所实施的跨域限制是基于同源策略(Same-Origin Policy),它规定了不同源之间的访问权限。
协议相同 :两个网页的协议必须相同,如都是使用 HTTP 或者 HTTPS。域名相同 :两个网页的域名必须相同,包括子域名也必须相同。端口相同 :两个网页的端口号必须相同,如果没有显式指定端口,默认为80。解决跨域问题的方案 JSONP(JSON with Padding):在前端使用动态创建 <script> 标签来加载跨域的 JavaScript 资源,通过回调函数方式获取数据。 CORS(Cross-Origin Resource Sharing):在服务器端设置响应头,允许特定域名的跨域请求。 代理服务器:将跨域请求发送到自己的服务器,再由服务器转发请求并返回结果。 WebSocket:WebSocket 协议不受同源策略限制,可以实现跨域通信。 使用服务器端中间件进行跨域设置。 Java解决跨域的方式 使用@CrossOrigin注解 在控制器方法上使用@CrossOrigin注解可以指定允许跨域的域名、请求头等信息
1 2 3 4 5 @CrossOrigin(origins = "http://example.com") @GetMapping("/api/data") public String getData () { }
通过实现WebMvcConfigurer接口并重写addCorsMappings方法,配置跨域规则
1 2 3 4 5 6 7 8 9 10 11 @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings (CorsRegistry registry) { registry.addMapping("/api/**" ) .allowedOrigins("http://example.com" ) .allowedMethods("GET" , "POST" ) .allowedHeaders("header1" , "header2" ) .allowCredentials(true ); } }
自定义拦截器 通过自定义拦截器,在请求过程中处理跨域相关逻辑
1 2 3 4 5 6 7 8 9 10 public class CorsInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { response.setHeader("Access-Control-Allow-Origin" , "http://example.com" ); response.setHeader("Access-Control-Allow-Methods" , "GET, POST" ); response.setHeader("Access-Control-Allow-Headers" , "header1, header2" ); response.setHeader("Access-Control-Allow-Credentials" , "true" ); return true ; } }
自定义过滤器 通过自定义 Filter,在请求过程中添加跨域相关的响应头信息
1 2 3 4 5 6 7 8 9 10 11 public class CorsFilter implements Filter { @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.addHeader("Access-Control-Allow-Origin" , "http://example.com" ); httpResponse.addHeader("Access-Control-Allow-Methods" , "GET, POST" ); httpResponse.addHeader("Access-Control-Allow-Headers" , "header1, header2" ); httpResponse.addHeader("Access-Control-Allow-Credentials" , "true" ); chain.doFilter(request, response); } }
使用Spring Web的CorsFilter过滤器 在 Spring Web 配置中添加CorsFilter过滤器,并配置跨域规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration public class WebConfig { @Bean public CorsFilter corsFilter () { CorsConfiguration config = new CorsConfiguration (); config.addAllowedOrigin("http://example.com" ); config.addAllowedMethod("GET" ); config.addAllowedHeader("header1" ); config.setAllowCredentials(true ); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource (); source.registerCorsConfiguration("/api/**" , config); return new CorsFilter (source); } }
使用Spring Cloud Gateway跨域配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 spring: cloud: gateway: globalcors: corsConfigurations: '[/**]' : allowedOrigins: - http://example.com allowedMethods: - GET - POST allowedHeaders: - header1 - header2 allowCredentials: true
使用Nginx配置 1 2 3 4 5 6 location /api/ { add_header Access-Control-Allow-Origin http://example.com; add_header Access-Control-Allow-Methods GET, POST; add_header Access-Control-Allow-Headers header1, header2; add_header Access-Control-Allow-Credentials true ; }
日期格式问题 问题说明 问题一 :后端从数据库获取日期时间传到前端进行展示的时候,发现有时候日期格式发生了变化。从数据库获取出来日期为:2022-01-01T00:00:00.000+00:00,但有时候传递到前端后日期格式变为了时间戳:1640995200000,而一般需要的日期格式为:yyyy-MM-dd或yyyy-MM-dd HH:mm:ss问题二 :前端传入日期时间给后端时,日期时间格式也可能会有格式转换问题问题一解决方案 (1)如果项目中使用的日期时间的字段类型只有Date类型,可以使用Springboot的配置文件application.yml配置日期时间的格式
1 2 3 4 spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8
(2)如果项目中用到的时间和日期API 比较混乱, java.util.Date、 java.util.Calendar 和 java.time LocalDateTime 都存在,可以使用@JsonFormat注解解决
1 2 3 4 5 public class Example { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date datetime; }
问题二解决方案 可以使用@RequestParam注解和@DateTimeFormat注解来指定日期时间的格式
1 2 3 4 @PostMapping("/example") public void example (@RequestParam("datetime") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date datetime) {}
自定义JSON序列化器或反序列化器 使用@JsonFormat注解和@DateTimeFormat注解只能单一针对一个日期时间属性生效,每个日期时间属性都配置的话有些麻烦,这时可以考虑使用统一配置,如果项目中存在复杂的日期时间转换需求,可以考虑自定义JSON序列化器(Serializer)和反序列化器(Deserializer)来处理。这样可以更加灵活地控制日期时间的格式转换。
@JsonComponent注解:注册自定义的JSON序列化器(Serializer)或反序列化器(Deserializer) @Configuration注解:利用配置类统一配置时间类型装换 使用@JsonComponent注解自定义的序列化器或反序列化器 @JsonComponent注解是Spring框架提供的注解,用于注册自定义的JSON序列化器(Serializer)或反序列化器(Deserializer)作为全局组件。当进行对象的JSON序列化或反序列化时,Spring会自动识别被@JsonComponent注解标记的类,并在对象转换过程中自动调用这些组件。这样就实现了自定义序列化器或反序列化器的全局生效,而无需显式地在每个需要进行对象转换的地方进行配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @JsonComponent public class GlobalDateTimeSerializer { private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat ("yyyy-MM-dd" ); private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); private static final DateTimeFormatter LOCAL_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ); public static class DateSerializer extends JsonSerializer <Date> { @Override public void serialize (Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { jsonGenerator.writeString(DATE_TIME_FORMAT.format(date)); } } public static class LocalDateTimeSerializer extends JsonSerializer <LocalDateTime> { @Override public void serialize (LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { jsonGenerator.writeString(LOCAL_DATE_TIME_FORMATTER.format(localDateTime)); } } public static class CalendarSerializer extends JsonSerializer <Calendar> { @Override public void serialize (Calendar calendar, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { jsonGenerator.writeString(DATE_FORMAT.format(calendar.getTime())); } } }
使用@Configuration注解统一配置时间类型装换 配置后所有的日期字段都会格式化成:yyyy-MM-dd HH:mm:ss 格式,如果需要:yyyy-MM-dd 格式,可配合注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 @Configuration public class DateHandlerConfig { private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss" ; private static final String DATE_FORMAT = "yyyy-MM-dd" ; private static final String TIME_FORMAT = "HH:mm:ss" ; @Bean public Converter<String, Date> dateConverter () { return new Converter <String, Date>() { @Override public Date convert (String source) { return DateUtil.parse(source.trim()); } }; } @Bean public Converter<String, LocalDate> localDateConverter () { Converter<String, LocalDate> converter = new Converter <String, LocalDate>() { @Override public LocalDate convert (String source) { return LocalDate.parse(source, DateTimeFormatter.ofPattern(DATE_FORMAT)); } }; return converter; } @Bean public Converter<String, LocalTime> localTimeConverter () { return new Converter <String, LocalTime>() { @Override public LocalTime convert (String source) { return LocalTime.parse(source, DateTimeFormatter.ofPattern(TIME_FORMAT)); } }; } @Bean public Converter<String, LocalDateTime> localDateTimeConverter () { return new Converter <String, LocalDateTime>() { @Override public LocalDateTime convert (String source) { return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)); } }; } @Bean public ObjectMapper objectMapper () { ObjectMapper objectMapper = new ObjectMapper (); JavaTimeModule javaTimeModule = new JavaTimeModule (); javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer (DateTimeFormatter.ofPattern(DATE_FORMAT))); javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer (DateTimeFormatter.ofPattern(TIME_FORMAT))); javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer (DateTimeFormatter.ofPattern(DATE_TIME_FORMAT))); javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer (DateTimeFormatter.ofPattern(DATE_FORMAT))); javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer (DateTimeFormatter.ofPattern(TIME_FORMAT))); javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer (DateTimeFormatter.ofPattern(DATE_TIME_FORMAT))); objectMapper.registerModule(new ParameterNamesModule ()) .registerModule(new Jdk8Module ()) .registerModule(javaTimeModule); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false ); objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false ); objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true ); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8:00" )); objectMapper.setDateFormat(new SimpleDateFormat (DATE_TIME_FORMAT)); return objectMapper; } }
异常处理 自定义异常处理类 自定义系统异常处理类 SystemException,用于处理系统中特定的异常情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Getter @NoArgsConstructor @AllArgsConstructor public class SystemException extends RuntimeException { private int code; private String msg; public SystemException (AppHttpCodeEnum httpCodeEnum) { super (httpCodeEnum.getMsg()); this .code = httpCodeEnum.getCode(); this .msg = httpCodeEnum.getMsg(); } }
自定义系统异常处理类 SystemException使用方式
1 2 3 4 throw new SystemException (500 , "服务器内部错误" );throw new SystemException (AppHttpCodeEnum.INTERNAL_SERVER_ERROR);
全局异常处理类 程序在运行的过程中,不可避免的会产生各种各样的错误,当程序抛异常时,为了日志的可读性,排查 Bug简单,以及更好的用户体验性,要对全局异常进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ResponseResult exceptionHandler (Exception e) { log.error("出现了异常! {}" , e.getMessage()); return ResponseResult.errorResult(500 , e.getMessage()); } }
统一请求响应模型 统一响应模型(Result) 在开发SpringBoot后端服务时,一般需要给前端统一响应格式,方便前端调试及配置错误提示等等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 @Getter @Setter @JsonInclude(JsonInclude.Include.NON_NULL) public class Result <T> implements Serializable { private static final long serialVersionUID = 1L ; private Integer code; private String msg; private T data; public Result () { } public Result (Integer code, String msg) { this .code = code; this .msg = msg; } public Result (Integer code, T data) { this .code = code; this .data = data; } public Result (Integer code, String msg, T data) { this .code = code; this .msg = msg; this .data = data; } public static <T> Result<T> success () { return new Result <>(200 , "操作成功" ); } public static <T> Result<T> success (T data) { return new Result <>(200 , "操作成功" , data); } public static <T> Result<T> success (int code, String msg) { return new Result <>(code, msg); } public static <T> Result<T> error () { return new Result <>(500 , "出现错误" ); } public static <T> Result<T> error (String msg) { return new Result <>(500 , msg); } public static <T> Result<T> error (int code, String msg) { return new Result <>(code, msg); } }
1 2 3 4 5 6 7 8 9 10 @Data @AllArgsConstructor @NoArgsConstructor public class PageResult <T> implements Serializable { private static final long serialVersionUID = 1L ; private long total; private long pageNum; private long pageSize; private List<T> records; }
分页请求模型(PageParams) 1 2 3 4 5 6 7 8 9 10 11 12 13 @Data @NoArgsConstructor @AllArgsConstructor public class PageParams implements Serializable { private static final long serialVersionUID = 1L ; @ApiModelProperty(value = "当前页码", example = "1") private long pageNum = 1 ; @ApiModelProperty(value = "每页记录数", example = "10") private long pageSize = 10 ; }
非空判断和为空判断 常见的 isBlank() 和 isEmpty() 方法 不同的工具类中可能实现细节有些区别,但功能一般都一样。
isBlank()方法 :判断字符串是否为空或者只包含空白字符(例如空格、制表符、换行符等)。如果字符串为 null,或者长度为 0,或者只包含空白字符,则返回 true,否则返回 false。isEmpty()方法 :判断字符串是否为空。如果字符串为 null,或者长度为 0,则返回 true,否则返回 false。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Main { public static void main (String[] args) { String str1 = "" ; String str2 = " " ; String str3 = null ; System.out.println(StrUtil.isBlank(str1)); System.out.println(StrUtil.isBlank(str2)); System.out.println(StrUtil.isBlank(str3)); System.out.println(StrUtil.isEmpty(str1)); System.out.println(StrUtil.isEmpty(str2)); System.out.println(StrUtil.isEmpty(str3)); } }
字符串 字符串非空判断 方式一:使用原生判断
1 2 3 4 5 6 7 8 9 10 String str = "example" ;if (Objects.nonNull(str) && !str.trim().isEmpty()) {} if (Objects.nonNull(str) && !str.isEmpty()) {} if (str != null && !str.trim().isEmpty()){}if (str != null && !str.isEmpty()){}if (str != null && str.trim().length() > 0 ){}if (str != null && str.length() > 0 ){}if (str != null && !str.trim().equals("" )){}if (str != null && !str.equals("" )){}
方式二:使用工具类判断
1 2 3 4 5 6 String str = "example" ;if (StrUtil.isNotEmpty(str)){}if (!Strings.isNullOrEmpty(str)){}if (!StringUtils.isEmpty(str)){}if (StringUtils.isNotBlank(str)){}if (StringUtils.isNotBlank(str)){}
字符串为空判断 方式一:使用原生判断
1 2 3 4 5 6 7 8 String str = "example" ;if (str == null || str.trim().isEmpty()){}if (str == null || str.isEmpty()){}if (str == null || str.trim().length() <= 0 ){}if (str == null || str.length() <= 0 ){}if (str == null || str.trim().equals("" )){}if (str == null || str.equals("" )){}if (str == null || str == " " ){}
方式二:使用工具类判断
1 2 3 4 5 6 String str = "example" ;if (StrUtil.isEmpty()){}if (Strings.isNullOrEmpty(str)){}if (StringUtils.isEmpty(str)){}if (StringUtils.isBlank(str)){}if (StringUtils.isBlank(str)){}
List集合 非空判断 方式一:使用原生判断
1 2 3 ArrayList<Object> list = new ArrayList <>(); if (null != list && list.size() > 0 ){}if (null != list && !list.isEmpty()){}
方式二:使用工具类判断
1 2 3 4 5 ArrayList<Object> list = new ArrayList <>(); if (CollUtil.isNotEmpty(list)){}if (!CollectionUtils.isEmpty(list)){}if (CollectionUtils.isNotEmpty(list)){}if (CollectionUtils.isNotEmpty(list)){}
为空判断 方式一:使用原生判断
1 2 3 ArrayList<Object> list = new ArrayList <>(); if (null == list || list.size() == 0 ){}if (null == list || list.isEmpty()){}
方式二:使用工具类判断
1 2 3 4 5 ArrayList<Object> list = new ArrayList <>(); if (CollUtil.isEmpty(list)){}if (CollectionUtils.isEmpty(list)){}if (CollectionUtils.isEmpty(list)){}if (CollectionUtils.isEmpty(list)){}
对象之间的拷贝 对象拷贝概念 在Java开发中,对象拷贝常见于各个表现层数据的转换(如PO、DTO、DO、VO之间的转换)以及系统交互过程中的序列化和反序列化。
对象拷贝常见方案 常见的Bean拷贝方案有:Apache的BeanUtils、Spring的BeanUtils、CGLib的BeanCopier、对象映射器Orika、对象映射器MapStruct、
Apache的BeanUtils :Apache的BeanUtils提供了一组工具方法,可以在不同的JavaBean之间进行属性拷贝。它使用反射机制来实现属性的读取和赋值,可以进行浅拷贝。Spring的BeanUtils :Spring的BeanUtils是基于Apache的BeanUtils进行了进一步封装,提供了更多的功能和扩展性。它也使用反射机制进行属性拷贝,并支持浅拷贝。CGLib的BeanCopier :CGLib的BeanCopier是一个高性能的Bean拷贝工具,使用字节码生成技术实现。它通过生成字节码来直接拷贝对象属性,避免了反射的性能开销。它也只支持浅拷贝。对象映射器的Dozer :Dozer是一个功能丰富的Java对象映射器,可以处理复杂的对象映射关系。它支持深拷贝和浅拷贝,并提供了灵活的转换选项。Dozer使用配置文件或注解来定义映射规则。对象映射器ModelMapper :ModelMapper是一个流行的Java对象映射库,旨在简化不同类型的Java对象之间的属性拷贝和转换。它提供了简洁的API和灵活的配置选项,使开发者能够轻松地进行对象映射操作。ModelMapper可以自动匹配相同字段名的属性,并支持自定义转换器来处理复杂的类型转换需求。对象映射器Orika :Orika是一个流行的Java对象映射器,它使用代码生成技术来实现高性能的对象映射。它支持基于注解或XML配置的映射规则,并具有高度的灵活性。Orika可以处理复杂的映射关系,包括嵌套对象和集合类型,并支持深拷贝和浅拷贝。对象映射器JMapper :JMapper是一个快速和简单的Java对象映射器,支持深拷贝和浅拷贝。它通过将源对象的属性复制到目标对象来实现映射,可以基于注解或者XML文件进行配置。对象映射器Selma :Selma也是一个Java对象映射库,专注于性能和易用性。它提供了简单而直观的注解驱动方式,用于指定对象之间的映射关系。Selma生成高效的字节码,以实现快速的对象映射,同时支持自定义转换器和高级映射功能。对象映射器MapStruct :MapStruct是一个注解驱动的Java对象映射器,它在编译时生成映射代码,以提供更高的性能。它使用接口声明映射方法,自动生成实现类,并提供了易于使用的API。MapStruct支持深拷贝和浅拷贝,并支持自定义转换器来处理复杂的映射场景。对象拷贝常见方案性能对比 CGLib的BeanCopier和MapStruct通常在性能上表现较好,适合大规模数据拷贝。而Apache的BeanUtils、Spring的BeanUtils、Dozer、Orika、ModelMapper、JMapper和Selma等方案在不同场景下也可以根据需求选择使用。
方案 性能(1,000,000次拷贝耗时) 说明 Apache的BeanUtils 约15秒 使用反射机制,性能较低 Spring的BeanUtils 约12秒 基于Apache的BeanUtils封装,性能相对较低 CGLib的BeanCopier 约2.5秒 使用字节码生成技术,性能较高,只支持浅拷贝 对象映射器Dozer 约3.5秒 功能强大,支持深拷贝和浅拷贝,性能较高 对象映射器ModelMapper 约1.8秒 简洁易用,性能一般,支持深拷贝和浅拷贝 对象映射器Orika 约1.5秒 使用代码生成技术,性能优秀,支持深拷贝和浅拷贝 对象映射器JMapper 约1.1秒 简单快捷,性能较好,支持深拷贝和浅拷贝 对象映射器Selma 约1.1秒 注解驱动,性能较好,支持深拷贝和浅拷贝 对象映射器MapStruct 约0.9秒 注解驱动,编译时生成映射代码,性能较高,支持深拷贝和浅拷贝
对象拷贝常见方案使用案例 Apache的BeanUtils Maven依赖添加:
1 2 3 4 5 6 <dependency > <groupId > commons-beanutils</groupId > <artifactId > commons-beanutils</artifactId > <version > 1.9.4</version > </dependency >
使用案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Main { public static void main (String[] args) throws InvocationTargetException, IllegalAccessException { User user = new User ("小明" , 20 ); UserVO userVO = new UserVO (); org.apache.commons.beanutils.BeanUtils.copyProperties(userVO, user); System.out.println("源对象:" + user); System.out.println("目标对象:" + userVO); } }
Spring的BeanUtils Maven依赖添加:
1 2 3 4 5 6 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-beans</artifactId > <version > 5.3.9</version > </dependency >
使用案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Main { public static void main (String[] args) { User user = new User ("小明" , 20 ); UserVO userVO = new UserVO (); org.springframework.beans.BeanUtils.copyProperties(user, userVO); System.out.println("源对象:" + user); System.out.println("目标对象:" + userVO); } }
CGLib的BeanCopier Maven依赖添加:
1 2 3 4 5 6 <dependency > <groupId > cglib</groupId > <artifactId > cglib</artifactId > <version > 3.3.0</version > </dependency >
使用案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Main { public static void main (String[] args) { User user = new User ("小明" , 20 ); UserVO userVO = new UserVO (); BeanCopier copier = org.springframework.cglib.beans.BeanCopier.create(User.class, UserVO.class, false ); copier.copy(user, userVO, null ); System.out.println("源对象:" + user); System.out.println("目标对象:" + userVO); } }
对象映射器Dozer Maven依赖添加:
1 2 3 4 5 6 <dependency > <groupId > com.github.dozermapper</groupId > <artifactId > dozer-core</artifactId > <version > 6.5.0</version > </dependency >
使用案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Main { public static void main (String[] args) { User user = new User ("小明" , 20 ); UserVO userVO = new UserVO (); Mapper mapper = DozerBeanMapperBuilder.buildDefault(); mapper.map(user, userVO); System.out.println("源对象:" + user); System.out.println("目标对象:" + userVO); } }
对象映射器ModelMapper Maven依赖添加:
1 2 3 4 5 6 <dependency > <groupId > org.modelmapper</groupId > <artifactId > modelmapper</artifactId > <version > 2.4.3</version > </dependency >
使用案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Main { public static void main (String[] args) { User user = new User ("小明" , 20 ); UserVO userVO = new UserVO (); ModelMapper modelMapper = new ModelMapper (); modelMapper.map(user, userVO); System.out.println("源对象:" + user); System.out.println("目标对象:" + userVO); } }
对象映射器Orika Maven依赖添加:
1 2 3 4 5 6 <dependency > <groupId > ma.glasnost.orika</groupId > <artifactId > orika-core</artifactId > <version > 1.5.2</version > </dependency >
使用案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Main { public static void main (String[] args) { User user = new User ("小明" , 20 ); UserVO userVO = new UserVO (); MapperFactory mapperFactory = new DefaultMapperFactory .Builder().build(); MapperFacade mapperFacade = mapperFactory.getMapperFacade(); mapperFacade.map(user, userVO); System.out.println("源对象:" + user); System.out.println("目标对象:" + userVO); } }
对象映射器JMapper Maven依赖添加:
1 2 3 4 5 6 <dependency > <groupId > com.googlecode.jmapper-framework</groupId > <artifactId > jmapper-core</artifactId > <version > 1.6.0</version > </dependency >
使用案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Main { public static void main (String[] args) { User user = new User ("小明" , 20 ); UserVO userVO = new UserVO (); JMapperAPI api = new JMapperAPI (); api.add(JMapperAPI.mappedClass(User.class).add(JMapperAPI.attribute("name" ))); api.add(JMapperAPI.mappedClass(User.class).add(JMapperAPI.attribute("age" ))); JMapper<UserVO, User> mapper = new JMapper <>(UserVO.class, User.class, api); userVO = mapper.getDestination(user); System.out.println("源对象:" + user); System.out.println("目标对象:" + userVO); } }
对象映射器Selma Maven依赖添加:
1 2 3 4 5 6 7 8 9 10 11 12 13 <dependency > <groupId > fr.xebia.extras</groupId > <artifactId > selma-processor</artifactId > <version > 1.0</version > <scope > provided</scope > </dependency > <dependency > <groupId > fr.xebia.extras</groupId > <artifactId > selma</artifactId > <version > 1.0</version > </dependency >
使用案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Main { public static void main (String[] args) { User user = new User ("小明" , 20 ); Mapper mapper = Selma.builder(Mapper.class).build(); UserVO userVO = mapper.asUserVO(user); System.out.println("源对象:" + user); System.out.println("目标对象:" + userVO); } } @fr .xebia.extras.selma.Mapperinterface Mapper { UserVO asUserVO (User user) ; }
对象映射器MapStruct Maven依赖添加:
1 2 3 4 5 6 <dependency > <groupId > org.mapstruct</groupId > <artifactId > mapstruct</artifactId > <version > 1.5.5.Final</version > </dependency >
使用案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Main { public static void main (String[] args) { User user = new User ("小明" , 20 ); UserVO userVO = UserMapper.INSTANCE.userToUserVO(user); System.out.println("源对象:" + user); System.out.println("目标对象:" + userVO); } } @Mapper interface UserMapper { UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); @Mapping(source = "name", target = "name") @Mapping(source = "age", target = "age") UserVO userToUserVO (User user) ; }
常用Excel操作相关库 Excel Excel简介 Excel 是一种电子表格软件,由 Microsoft 公司开发。它是最常用的办公软件之一,广泛用于各种任务,包括数据分析、数据处理、建立图表、制作报告和计算等。
Excel相关概念 在 Excel 中,工作簿(Workbook)是一个文件,它可以包含一个或多个工作表(Worksheet)。每个工作表都由行(Row)和列(Column)组成,行和列的交叉点形成了单元格(Cell)。
工作簿(Workbook) :在 Excel 中,工作簿是一个文件,用于存储和管理数据。一个工作簿可以包含一个或多个工作表,通常以.xlsx或.xls等文件格式保存。工作表(Worksheet) :工作表是工作簿中的一个分页,用于组织和展示数据。每个工作表由行和列组成,并且每个工作表都有一个唯一的名称,例如Sheet1、Sheet2等。行(Row) :行是水平方向上的排列单元格的序列。行由数字表示,从1开始逐渐递增,用于标识不同的行号。列(Column) :列是垂直方向上的排列单元格的序列。列由字母表示,从A开始逐渐递增,用于标识不同的列标。单元格(Cell) :单元格是行和列的交叉点,它是 Excel 表格中最小的单位。每个单元格可以存储文本、数字、日期、公式等数据,并且可以应用各种格式和样式。每个单元格都有一个唯一的地址,由列标和行号组成,例如A1、B2等。Java操作Excel常见方式 在Java中,实现导入导出Excel有几种方法,分别是使用JExcelApi、Apache POI、EasyPOI、EasyExcel
JExcelApi(JXL) :开源 Excel 处理库,主要用于读取、写入和操作 Excel 文件,支持处理 XLS 格式的文件,已经停止维护Apache POI :开源的Java API,可以用于读取、写入和操作Microsoft Office格式的文档,包括Excel、Word和PowerPoint等。虽然Apache POI功能强大,但是代码书写冗余繁杂,学习使用成本较高,在读写大文件耗费内存较大。EasyPOI :基于Apache POI封装的一个Java开发工具包,提供了简化的API和更友好的操作方式,使得处理Excel文件变得更加容易。EasyExcel :EasyExcel 是一个基于 Apache POI 的 Java 处理 Excel 文件的开源库,减少内存占用。Apache POI Apache POI简介 Apache POI是一个开源的Java类库,用于操作Microsoft Office文档格式,包括Excel、Word和PowerPoint等。它提供了许多API,可用于读取、写入和操作这些文档的内容、格式和样式。Apache POI是Apache软件基金会的顶级项目之一,已经成为Java处理Office文档的行业标准。Apache POI主要用途包括如下几种:
自动化处理Excel、Word等Office文档 读取、分析和转换Office文件中的数据 根据模板自动生成报表、表格等 批量生成和操作Office文档 创建和编辑复杂的Office文档 Apache POI相关依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <dependencies > <dependency > <groupId > org.apache.poi</groupId > <artifactId > poi</artifactId > <version > 4.1.2</version > </dependency > <dependency > <groupId > org.apache.poi</groupId > <artifactId > poi-ooxml</artifactId > <version > 4.1.2</version > </dependency > </dependencies >
Apache POI相关类库 Apache POI 针对不同 Microsoft Office 格式文件的读写功能,提供了相应的类来支持不同格式的文件操作。根据需要选择适当的类,可以实现对 Excel、Word、PowerPoint 等不同类型文件的读写操作,主要有以下几种:
HSSF (Horrible Spreadsheet Format):用于读写 Microsoft Excel 97-2003 格式的 .xls 文件。由于 .xls 格式的限制,HSSF 最多只支持 65536 行和 256 列数据。XSSF (XML Spreadsheet Format):用于读写 Microsoft Excel 2007 及以上版本的 .xlsx 文件。XSSF支持大批量数据导出,最大可以有 1048576 行和 16384 列数据。但是数据会先写入内存再进行导出,对于大规模数据集,可能会导致内存占用过高,进而引发内存溢出的问题。SXSSF :SXSSF方式是XSSF方式的一种延伸,也用于读写 Microsoft Excel 2007 及以上版本的 .xlsx 文件。数据会先写入到磁盘缓存中,然后再进行导出,从而避免了内存溢出的问题,但是涉及磁盘写入操作,运行速度可能会较慢。HWPF (Horrible Word Processor Format):用于读写 Microsoft Word 97-2003 格式的 .doc 文件。XWPF (XML Word Processor Format):用于读写 Microsoft Word 2007 及以上版本的 .docx 文件。HSLF (Horrible Slide Layout Format):用于读写 Microsoft PowerPoint 格式的 .ppt 文件。HDGF (Horrible DiaGram Format):用于读取 Microsoft Visio 格式的 .vsd 文件。HPBF (Horrible Publisher Format):用于读取 Microsoft Publisher 格式的 .pub 文件。HSMF (Horrible Stupid Mail Format):用于读取 Microsoft Outlook 格式的 .msg 文件。Apache POI操作Excel常用的类 Workbook :Workbook 是 Excel 文件的顶级对象,代表整个工作簿。它可以用于创建新的工作簿、读取现有的工作簿以及对工作簿进行保存。常见的 Workbook 子类包括 HSSFWorkbook(操作 .xls 格式文件)和 XSSFWorkbook(操作 .xlsx 格式文件)。Sheet :Sheet 代表工作簿中的一个工作表。可以使用 Workbook 对象的 getSheet() 或 createSheet() 方法来获取或创建 Sheet 对象。Row :Row 代表工作表中的一行数据。可以使用 Sheet 对象的 getRow() 或 createRow() 方法来获取或创建 Row 对象。Cell :Cell 代表工作表中的一个单元格。可以使用 Row 对象的 getCell() 或 createCell() 方法来获取或创建 Cell 对象。CellStyle :CellStyle 代表单元格的样式,如字体、背景色、边框等。可以使用 Workbook 对象的 createCellStyle() 方法创建 CellStyle 对象,并将其应用于 Cell 对象。DataFormat :DataFormat 用于处理单元格中的数据格式,如日期、数字等。可以使用 Workbook 对象的 createDataFormat() 方法创建 DataFormat 对象,并将其应用于 CellStyle 对象。FormulaEvaluator :用于计算 Excel 单元格中的公式的类。CreationHelper :用于创建各种对象,如绘图对象、注释等。Apache POI导入Excel 方式一:HSSF方式导入Excel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class ExcelRead2003Test { public static void main (String[] args) throws Exception { try { FileInputStream fis = new FileInputStream ("POI/src/Excel/HSSF方式-用户信息表.xls" ); HSSFWorkbook workbook = new HSSFWorkbook (fis); int numberOfSheets = workbook.getNumberOfSheets(); for (int i = 0 ; i < numberOfSheets; i++) { HSSFSheet sheet = workbook.getSheetAt(i); int numberOfRows = sheet.getPhysicalNumberOfRows(); for (int j = 0 ; j < numberOfRows; j++) { HSSFRow row = sheet.getRow(j); int numberOfColumns = row.getPhysicalNumberOfCells(); for (int k = 0 ; k < numberOfColumns; k++) { HSSFCell cell = row.getCell(k); System.out.print(cell.toString() + "\t" ); } System.out.println(); } } workbook.close(); fis.close(); } catch (IOException e) { e.printStackTrace(); } } }
方式二:XSSF方式导入Excel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class ExcelRead2007Test { public static void main (String[] args) throws Exception { try { FileInputStream fis = new FileInputStream ("POI/src/Excel/XSSF方式-用户信息表.xlsx" ); XSSFWorkbook workbook = new XSSFWorkbook (fis); int numberOfSheets = workbook.getNumberOfSheets(); for (int i = 0 ; i < numberOfSheets; i++) { XSSFSheet sheet = workbook.getSheetAt(i); int numberOfRows = sheet.getPhysicalNumberOfRows(); for (int j = 0 ; j < numberOfRows; j++) { XSSFRow row = sheet.getRow(j); int numberOfColumns = row.getPhysicalNumberOfCells(); for (int k = 0 ; k < numberOfColumns; k++) { XSSFCell cell = row.getCell(k); System.out.print(cell.toString() + "\t" ); } System.out.println(); } } workbook.close(); fis.close(); } catch (IOException e) { e.printStackTrace(); } } }
方式三:SXSSF方式导入Excel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class ExcelReadSXSSFTest { public static void main (String[] args) throws Exception { OPCPackage opcPackage = OPCPackage.open("POI/src/Excel/SXSSF方式-用户信息表.xlsx" ); FileInputStream fis = new FileInputStream ("POI/src/Excel/XSSF方式-用户信息表.xlsx" ); XSSFWorkbook workbook = new XSSFWorkbook (fis); int numberOfSheets = workbook.getNumberOfSheets(); for (int i = 0 ; i < numberOfSheets; i++) { XSSFSheet sheet = workbook.getSheetAt(i); int lastRowNum = sheet.getLastRowNum(); XSSFRow firstRow = sheet.getRow(0 ); int numberOfColumns = firstRow.getPhysicalNumberOfCells(); for (int j = 1 ; j <= lastRowNum; j++) { XSSFRow row = sheet.getRow(j); for (int k = 0 ; k < numberOfColumns; k++) { XSSFCell cell = row.getCell(k); System.out.print(cell.toString() + "\t" ); } System.out.println(); } } } }
Apache POI导出Excel 方式一:HSSF方式导出Excel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class ExcelWrite2003Test { public static void main (String[] args) throws Exception { long begin = System.currentTimeMillis(); HSSFWorkbook workbook = new HSSFWorkbook (); HSSFSheet sheet = workbook.createSheet("Sheet1" ); Object[][] data = { {"姓名" , "年龄" , "性别" }, {"张三" , 25 , "男" }, {"李四" , 30 , "女" }, {"王五" , 28 , "男" } }; for (int i = 0 ; i < data.length; i++) { HSSFRow row = sheet.createRow(i); for (int j = 0 ; j < data[i].length; j++) { HSSFCell cell = row.createCell(j); cell.setCellValue(String.valueOf(data[i][j])); } } FileOutputStream fos = new FileOutputStream ("POI/src/Excel/HSSF方式-用户信息表.xls" ); workbook.write(fos); workbook.close(); fos.close(); long end = System.currentTimeMillis(); System.out.println("写入完成,共耗时:" + (double ) (end - begin) / 1000 + "秒" ); } }
方式二:XSSF方式导出Excel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class ExcelWrite2007Test { public static void main (String[] args) throws Exception { long begin = System.currentTimeMillis(); XSSFWorkbook workbook = new XSSFWorkbook (); XSSFSheet sheet = workbook.createSheet("Sheet1" ); Object[][] data = { {"姓名" , "年龄" , "性别" }, {"张三" , 25 , "男" }, {"李四" , 30 , "女" }, {"王五" , 28 , "男" } }; for (int i = 0 ; i < data.length; i++) { XSSFRow row = sheet.createRow(i); for (int j = 0 ; j < data[i].length; j++) { XSSFCell cell = row.createCell(j); cell.setCellValue(String.valueOf(data[i][j])); } } FileOutputStream fos = new FileOutputStream ("POI/src/Excel/XSSF方式-用户信息表.xlsx" ); workbook.write(fos); workbook.close(); fos.close(); long end = System.currentTimeMillis(); System.out.println("写入完成,共耗时:" + (double ) (end - begin) / 1000 + "秒" ); } }
方式三:SXSSF方式导出Excel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class ExcelWriteSXSSFTest { public static void main (String[] args) throws Exception { long begin = System.currentTimeMillis(); SXSSFWorkbook workbook = new SXSSFWorkbook (); SXSSFSheet sheet = workbook.createSheet("Sheet1" ); Object[][] data = { {"姓名" , "年龄" , "性别" }, {"张三" , 25 , "男" }, {"李四" , 30 , "女" }, {"王五" , 28 , "男" } }; for (int i = 0 ; i < data.length; i++) { SXSSFRow row = sheet.createRow(i); for (int j = 0 ; j < data[i].length; j++) { SXSSFCell cell = row.createCell(j); cell.setCellValue(String.valueOf(data[i][j])); } } FileOutputStream fos = new FileOutputStream ("POI/src/Excel/SXSSF方式-用户信息表.xlsx" ); workbook.write(fos); workbook.close(); fos.close(); long end = System.currentTimeMillis(); System.out.println("写入完成,共耗时:" + (double ) (end - begin) / 1000 + "秒" ); } }
EasyPOI EasyPOI简介 EasyPOI是基于Apache POI封装的一个Java开发工具包,提供了简化的API和更友好的操作方式,使得处理Excel文件变得更加容易。
EasyPOI相关依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <dependencies > <dependency > <groupId > cn.afterturn</groupId > <artifactId > easypoi-base</artifactId > <version > 4.1.0</version > </dependency > <dependency > <groupId > cn.afterturn</groupId > <artifactId > easypoi-web</artifactId > <version > 4.1.0</version > </dependency > <dependency > <groupId > cn.afterturn</groupId > <artifactId > easypoi-annotation</artifactId > <version > 4.1.0</version > </dependency > </dependencies >
EasyPOI常用注解 @Excel: 用于将字段标记为导出或导入的 Excel 列。
name: Excel 列的标题名称。 orderNum: Excel 列的顺序,默认按照从小到大排序。 width: Excel 列的宽度,默认为 10。 format: Excel 列的数据格式,如日期格式、数字格式等。 @ExcelCollection: 用于标记子对象集合,表示该字段需要作为子表格导出或导入。
name: 子表格的表名。 type: 子表格的类型,支持 List、Set 和 Map。 width: 子表格的宽度,默认为 5000。 orderNum: 子表格的顺序,默认按照从小到大排序。 @ExcelEntity: 用于标记子对象,表示该字段需要作为子表格中的一行导出或导入。
name: 子表格中的每行的标记名称。 orderNum: 子表格中的每行的顺序,默认按照从小到大排序。 @ExcelIgnore: 用于标记忽略不导出或导入的字段。
@ExcelTarget: 用于标记实体类,表示该类可进行 Excel 导出或导入操作。
EasyPOI导入Excel 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class ExcelReadTest { public static void main (String[] args) throws Exception { File file = new File ("EasyPOI/src/Excel/用户信息表.xlsx" ); ImportParams params = new ImportParams (); params.setTitleRows(0 ); params.setHeadRows(1 ); List<User> userList = ExcelImportUtil.importExcel(file, User.class, params); for (User user : userList) { System.out.println(user); } } }
EasyPOI导出Excel 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class ExcelWriteTest { public static void main (String[] args) throws Exception { long begin = System.currentTimeMillis(); List<User> userList = User.getUserList(); ExportParams exportParams = new ExportParams (); exportParams.setType(ExcelType.XSSF); Workbook workbook = ExcelExportUtil.exportExcel(exportParams, User.class, userList); FileOutputStream fos = new FileOutputStream ("EasyPOI/src/Excel/用户信息表.xlsx" ); workbook.write(fos); fos.close(); long end = System.currentTimeMillis(); System.out.println("写入完成,共耗时:" + (double ) (end - begin) / 1000 + "秒" ); } }
EasyExcel EasyExcel简介 EasyExcel相关依赖 EasyExcel 2.x 版本:Java 7 & Java 6 版本支持
EasyExcel 3.x 版本:Java 8 版本支持
1 2 3 4 5 6 <dependency > <groupId > com.alibaba</groupId > <artifactId > easyexcel</artifactId > <version > 3.2.1</version > </dependency >
EasyExcel常用注解 @ExcelProperty 注解:用于定义实体类字段与 Excel 文件中列的映射关系。
value属性 :指定 Excel 列的索引或者列名,支持跨行跨列合并。index属性 :指定 Excel 列的索引,从0开始计数。format属性 :指定数据的格式,例如日期格式、数值格式等。converter属性 :指定字段值转换器,用于在读写 Excel 时进行特定值的转换。@ExcelIgnore 注解:用于忽略某个字段,不参与 Excel 文件的读写操作。
@ExcelIgnoreUnannotated 注解:用于标记实体类,表示在处理 Excel 时忽略未标记 @ExcelProperty 注解的字段。
@ExcelSheet 注解:用于指定读取或写入的 sheet 名称。
@ExcelHeadRowNumber 注解:用于标记实体类,指定作为表头行的行号,默认从 0 开始。
@DateTimeFormat 注解:用于标记日期类型的字段,指定读写日期时的格式。例如,@DateTimeFormat("yyyy-MM-dd")。
@NumberFormat 注解:用于标记数字类型的字段,指定读写数字时的格式。例如,@NumberFormat("#,###.00")。
@HeadRowHeight 注解:指定表头行的高度。
@ContentRowHeight 注解:指定内容行的高度。
@Table 注解:用于设置全局表格样式,包括表格名称、表格头部样式、表格内容样式等。
EasyExcel监听器 EasyExcel提供了两个监听器接口,分别是ReadListener和AnalysisEventListener。都可以用于方便地监听Excel文件的读取过程,并对读取的数据进行处理。一般来说,推荐使用AnalysisEventListener,因为它提供了更多的事件回调方法,能够更灵活地处理Excel文件的读取过程。
ReadListener :基础的文件解析监听器接口,可以监听到Excel文件的读取过程,并对读取的数据进行处理。AnalysisEventListener :ReadListener的子接口,在ReadListener的基础上增加了对读取完成后的处理,包括表头解析完成和全部数据解析完成等事件。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 public class ExcelReadListener <T> extends AnalysisEventListener <T> { private final List<T> dataList = new ArrayList <>(); @Override public void invoke (T data, AnalysisContext context) { dataList.add(data); } @Override public void doAfterAllAnalysed (AnalysisContext context) { } @Override public void invokeHead (Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) { super .invokeHead(headMap, context); } @Override public void invokeHeadMap (Map<Integer, String> headMap, AnalysisContext context) { } @Override public void onException (Exception exception, AnalysisContext context) { throw new RuntimeException ("Excel解析发生异常" , exception); } public List<T> getSheetDataList () { return dataList; } }
EasyExcel导入Excel 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class ExcelWriteTest { public static void main (String[] args) { long begin = System.currentTimeMillis(); List<User> userList = User.getUserList(); String fileName = "EasyExcel/src/Excel/用户信息表.xlsx" ; String sheetName = "Sheet1" ; EasyExcel.write(fileName, User.class) .sheet(sheetName) .doWrite(userList); long end = System.currentTimeMillis(); System.out.println("写入完成,共耗时:" + (double ) (end - begin) / 1000 + "秒" ); } }
EasyExcel导出Excel 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class ExcelReadTest { public static void main (String[] args) { AnalysisEventListener<User> listener = new AnalysisEventListener <User>() { @Override public void invoke (User user, AnalysisContext context) { System.out.println("读取到用户数据:" + user); } @Override public void doAfterAllAnalysed (AnalysisContext context) { System.out.println("数据导入完成" ); } }; String filePath = "EasyExcel/src/Excel/用户信息表.xlsx" ; String sheetName = "Sheet1" ; List<User> userList = EasyExcel.read(filePath, User.class, listener) .head(User.class) .sheet(sheetName) .doReadSync(); for (User user : userList) { System.out.println(user); } } }
常用JSON相关库 JSON JSON简介 JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,具有易于阅读和编写的特点。它以键值对的形式组织数据,并使用大括号 {} 来表示对象,用方括号 [] 来表示数组。
基本语法规则 键值对 :JSON使用键值对表示数据
键必须是字符串,使用双引号 “” 括起来。 值可以是字符串、数值、布尔值、空值、对象或者数组。 1 2 3 4 5 6 7 8 9 10 11 12 { "name" : "张三" , "age" : 30 , "isStudent" : true , "price" : 9.99 , "address" : null , "person" : { "gender" : "male" , "phone" : "13888888888" } , "hobbies" : [ "reading" , "swimming" , "traveling" ] }
字符串 :字符串是由双引号或单引号括起来的一段文本内容。可以包含任意字符,特殊字符需要使用转义字符进行表示。
1 2 3 4 { "message" : "Hello, World!" , "escaped" : "This string contains a \"quote\" character." }
数值 :数值可以是整数或浮点数。
1 2 3 4 { "age" : 30 , "price" : 9.99 }
布尔值 :布尔值表示真或假,可以是 true 或 false。
1 2 3 4 { "isStudent" : true , "isEmployed" : false }
空值 :空值表示一个不存在的值,可以用 null 表示。
对象 :JSONObject(JSON对象)是一个无序的键值对集合,其中键是字符串,值可以是任意合法的JSON数据类型。JSON对象使用大括号 {} 包裹,键值对之间使用冒号 : 分隔,每个键值对之间使用逗号 , 分隔。
1 2 3 4 { "person1" : { "name" : "张三" , "age" : 30 } , "person2" : { "name" : "李四" , "age" : 40 } }
数组 :JSONArray(JSON数组)是一个有序的、包含多个元素的数据集合。JSON数组使用方括号 [] 包裹,每个元素之间使用逗号 , 分隔。每个元素可以是任意合法的JSON数据类型。
1 2 3 4 [ { "name" : "张三" , "age" : 30 } , { "name" : "李四" , "age" : 40 } ]
常用的 JSON 库 Java中没有内置JSON的解析,因此使用JSON需要借助第三方类库,常用的有FastJson 、Jackson 和 Gson
FastJson :FastJson 是阿里巴巴开发的一款高性能 JSON 解析库。它具有极快的解析速度和较低的内存消耗,而且支持各种数据类型(包括复杂的嵌套对象和集合)。FastJson 也提供了灵活的 API,使得 JSON 的解析和生成变得简单而高效。Jackson :Jackson 是一个社区活跃度高且更新速度快的 JSON 解析库。它提供了多种功能强大的特性,包括对 JSON 和 Java 对象之间的双向转换、注解支持、流式处理等。Jackson 的性能也非常出色,是许多 Java 开发者的首选。Gson :Gson 是谷歌开发的一款功能丰富的 JSON 解析库。它可以方便地将 Java 对象和 JSON 进行互相转换,并且支持复杂对象的序列化和反序列化。Gson 也提供了易于使用的 API,使得 JSON 的解析和生成变得简单明了。JSON 库性能对比 解析速度:FastJson > Jackson > Gson
FastJson:FastJson 的解析速度非常快,因为它使用了一些高效的算法和优化技术。通常情况下,FastJson 的解析速度比其他库要更快一些。 Jackson:Jackson 的解析速度也很快,尤其是在处理大型 JSON 数据时表现出色。 Gson:Gson 的解析速度相对较慢,当处理大量数据时可能会比其他库稍微慢一些。 内存消耗:FastJson < Jackson < Gson
FastJson:FastJson 在内存消耗方面表现出色,通常比较省内存。 Jackson:Jackson 的内存消耗也相对较低,特别是在处理大型 JSON 数据时有优势。 Gson:Gson 在内存消耗方面可能稍高,当处理大量数据时可能会占用更多内存。 相关资料 在GitHub中,awesome-json 项目汇集了与 JSON 相关的各种库、工具、教程和资源
JSON 库:列出了多种编程语言中常用的 JSON 库和解析器,例如 Jackson、Gson、Fastjson 等。 文档和教程:提供了有关 JSON 的官方文档、教程和学习资源,可以帮助你深入了解 JSON 的各个方面和用法。 命令行工具:收录了一些命令行工具,可以用于处理和转换 JSON 数据。 在线工具和编辑器:介绍了一些在线的 JSON 工具和编辑器,方便你在线验证、格式化、压缩和编辑 JSON 数据。 可视化工具:推荐了一些用于可视化 JSON 数据的工具和库,可以以图表、树形结构等方式展示 JSON 数据。 校验和验证工具:提供了一些验证和校验 JSON 数据格式的工具和库,帮助你确保 JSON 数据的有效性和一致性。 JSON字符串与Java中字符串互相转换 使用Fastjson 1 2 3 4 5 String jsonString = JSON.toJSONString(jsonObject);JSONObject jsonObject = JSON.parseObject(jsonString);
使用Jackson 1 2 3 4 5 6 7 ObjectMapper objectMapper = new ObjectMapper ();String jsonString = objectMapper.writeValueAsString(jsonObject);JsonNode jsonNode = objectMapper.readTree(jsonString);
使用Gson 1 2 3 4 5 6 7 Gson gson = new Gson ();String jsonString = gson.toJson(jsonObject);JsonElement jsonElement = gson.fromJson(jsonString, JsonElement.class);
JSON数组与Java中List集合互相转换 使用Fastjson 1 2 3 4 5 List<MyEntity> list = JSON.parseArray(jsonArrayString, MyEntity.class); JSONArray jsonArray = JSONArray.parseArray(JSON.toJSONString(list));
使用Jackson 1 2 3 4 5 6 7 ObjectMapper objectMapper = new ObjectMapper ();List<MyEntity> list = objectMapper.readValue(jsonArrayString, new TypeReference <List<MyEntity>>() {}); String jsonArrayString = objectMapper.writeValueAsString(list);
使用Gson 1 2 3 4 5 6 7 Gson gson = new Gson ();List<MyEntity> list = gson.fromJson(jsonArrayString, new TypeToken <List<MyEntity>>() {}.getType()); String jsonArrayString = gson.toJson(list);
JSON对象与Java中Map集合互相转换 使用Fastjson 1 2 3 4 5 Map<String, Object> map = JSON.parseObject(jsonObjectString); JSONObject jsonObject = new JSONObject (map);
使用Jackson 1 2 3 4 5 6 7 ObjectMapper objectMapper = new ObjectMapper ();Map<String, Object> map = objectMapper.readValue(jsonObjectString, Map.class); String jsonObjectString = objectMapper.writeValueAsString(map);
使用Gson 1 2 3 4 5 6 7 Gson gson = new Gson ();Map<String, Object> map = gson.fromJson(jsonObjectString, new TypeToken <Map<String, Object>>() {}.getType()); String jsonObjectString = gson.toJson(map);
JSON对象与Java中实体类互相转换 使用Fastjson 1 2 3 4 5 MyEntity myEntity = JSON.parseObject(jsonString, MyEntity.class);String jsonString = JSON.toJSONString(myEntity);
使用Jackson 1 2 3 4 5 6 7 ObjectMapper objectMapper = new ObjectMapper ();MyEntity myEntity = objectMapper.readValue(jsonString, MyEntity.class);String jsonString = objectMapper.writeValueAsString(myEntity);
使用Gson 1 2 3 4 5 6 7 Gson gson = new Gson ();MyEntity myEntity = gson.fromJson(jsonString, MyEntity.class);String jsonString = gson.toJson(myEntity);
常用日志相关库 日志概述 日志简介 日志是指记录程序运行过程中产生的重要信息的一种技术手段。在软件开发和维护过程中,我们通常使用日志来记录程序的运行状态、错误信息、异常情况等等。
日志作用 日志有以下几个作用:
故障排查和问题定位 :当系统或应用程序发生错误、异常或故障时,日志可以提供关键的信息来帮助开发人员定位问题的根源和修复错误。性能分析与优化 :通过分析日志,可以了解系统或应用程序的性能瓶颈、资源利用情况和潜在的瓶颈点,从而进行优化和改进,提高系统性能。安全审计与追踪 :记录用户的操作日志和事件可以用于安全审计和追踪。当出现安全漏洞、入侵威胁或数据泄露时,日志可以提供审计证据和追踪攻击路径。业务分析与决策支持 :通过分析用户行为日志和业务日志,可以获取有关用户行为、偏好和需求的信息,帮助企业做出准确的决策和制定合适的业务策略。监控和预警 :监控系统和应用程序的日志可以提供实时的运行状态信息,用于监测系统是否正常运行并及时发现潜在的问题,在关键事件发生时触发预警机制。日志级别 Java日志系统使用不同的日志级别来表示日志信息的重要性和严重程度。常见的日志级别包括:
TRACE(跟踪) :最低级别,提供最详细的日志信息,适用于追踪程序的执行流程。DEBUG(调试) :用于调试目的的详细信息,例如变量值、方法调用等。INFO(信息) :用于记录程序运行时的一般信息,如应用程序启动、某个操作完成等。WARN(警告) :表示可能存在潜在问题或异常情况,但不会导致程序崩溃。ERROR(错误) :指示出现了错误或异常情况,可能导致程序无法正常运行。FATAL(严重错误) :最高级别,表示致命错误,可能导致应用程序崩溃。日志库 日志库简介 在日志相关库中,通常存在两种概念:日志系统和日志门面。
日志系统 (Logging System):日志系统通常是指实现了日志记录功能的库或组件。日志系统定义了日志记录的级别(例如调试、信息、警告、错误等),并提供了将日志消息写入不同目标(如控制台输出、文件、数据库等)的方法。常见的日志系统有Java Util Logging(JUL)、Log4j、Log4j 2、Logback等。日志门面 (Logging Facade):日志门面是一个抽象层,不实现日志功能,仅整合日志系统,提供统一的API接口。日志门面的目的是将具体的日志系统与应用程序解耦,使开发人员能够以统一的方式编写日志代码,而无需关注底层日志系统的具体实现细节。通过使用日志门面,应用程序可以方便地切换底层日志系统,而不会对代码产生太大的影响。常见的日志门面有SLF4J、Apache Commons Logging等。日志系统 常见的日志系统有JUL、Log4j、Log4j 2、Logback等。
Java Util Logging(JUL) :JUL 是 Java 标准库自带的日志系统,提供了基本的日志功能。JUL 的优点是简单易用,无需引入额外的依赖,可以通过配置文件或代码进行配置。但功能相对较为简单,不如其他日志系统提供的特性丰富。Log4j :Log4j 是Apache 的一个开源项目,具有广泛的功能和灵活的配置选项。支持多种输出目标(如文件、控制台、Socket)和日志级别,可以通过配置文件进行配置。Log4j 还提供了丰富的过滤器和格式化选项,可以灵活地控制日志记录的行为。Log4j2 :Log4j 2 是 Log4j 的升级版本,提供了更高的性能和更丰富的特性。Log4j 2 支持异步日志记录,可以大大提高应用程序的性能。还引入了插件机制,使得扩展和定制变得更加容易,同时支持灵活的归档策略,可以方便地管理和维护日志文件。Logback :Logback 是一个功能强大且高性能的日志组件,是 Log4j 的继任者。支持多线程环境和异步日志记录,并提供了丰富的配置选项。还支持通过 Groovy 脚本进行配置,使得配置更加灵活和动态化。Logback 的性能表现优秀,可以适用于高负载的应用程序。日志门面 常见的日志门面有SLF4J、Apache Commons Logging等。
SLF4J :SLF4J (Simple Logging Facade for Java):提供了一个简单的日志门面接口,可以与多种日志实现框架集成。通过使用 SLF4J,开发人员可以在应用程序中使用统一的日志 API 进行日志记录操作,而不需要直接依赖特定的日志实现。SLF4J 可以与其他日志系统(如Log4j、Log4j 2、Logback)无缝集成,通过配置文件或代码来指定具体的日志实现。Commons Logging :Apache 的一个顶级项目,最初名为Jakarta Commons Logging(JCL),后来更名为Apache Commons Logging 提供了简单的API用于在应用程序中进行日志记录。可以与多种日志系统集成,如JCL、SLF4J等。通过配置文件或代码来指定底层的日志实现,开发人员可以根据需要选择合适的日志系统。日志库使用 方案一:SLF4J + Logback 步骤一:引入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <dependencies > <dependency > <groupId > org.slf4j</groupId > <artifactId > slf4j-api</artifactId > <version > 1.7.32</version > </dependency > <dependency > <groupId > ch.qos.logback</groupId > <artifactId > logback-classic</artifactId > <version > 1.2.6</version > </dependency > </dependencies >
步骤二:配置文件,创建一个logback.xml或logback-spring.xml文件,用于配置Logback的日志输出方式、格式等。
1 2 3 4 5 6 7 8 9 10 11 <configuration > <appender name ="console" class ="ch.qos.logback.core.ConsoleAppender" > <encoder > <pattern > %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern > </encoder > </appender > <root level ="info" > <appender-ref ref ="console" /> </root > </configuration >
步骤三:使用 API
1 2 3 4 5 6 7 8 9 10 public class MyClass { private static final Logger LOG = LoggerFactory.getLogger(MyClass.class); public void doSomething () { LOG.debug("Debug message" ); LOG.info("Info message" ); LOG.warn("Warning message" ); LOG.error("Error message" ); } }
方案二:Commons Logging + Log4j2 步骤一:引入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <dependencies > <dependency > <groupId > commons-logging</groupId > <artifactId > commons-logging</artifactId > <version > 1.2</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-core</artifactId > <version > 2.7</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-api</artifactId > <version > 2.7</version > </dependency > </dependencies >
步骤二:配置文件,创建一个log4j2.xml文件,用于配置Log4j2的日志输出方式、格式等。
1 2 3 4 5 6 7 8 9 10 11 12 13 <Configuration > <Appenders > <Console name ="console" target ="SYSTEM_OUT" > <PatternLayout pattern ="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" /> </Console > </Appenders > <Loggers > <Root level ="info" > <AppenderRef ref ="console" /> </Root > </Loggers > </Configuration >
步骤三:使用 API
1 2 3 4 5 6 7 8 9 10 public class MyClass { private static final Log LOG = LogFactory.getLog(MyClass.class); public void doSomething () { LOG.debug("Debug message" ); LOG.info("Info message" ); LOG.warn("Warning message" ); LOG.error("Error message" ); } }
单元测试相关 单元测试概括 单元测试简介 单元测试是软件开发中的一种测试方法,用于验证程序的最小可测试单元(通常是函数或方法)是否按照预期进行工作。
Java常用的单元测试框架 在Java开发中,常用的测试框架有以下几种:
JUnit :JUnit是Java中最常用的单元测试框架。它提供了一套用于编写和运行单元测试的API,并支持断言(assertions)和测试装置(test fixtures)等功能。JUnit可以与各种开发工具和持续集成系统集成,是Java开发中必备的测试框架。TestNG :TestNG是一个灵活的测试框架,可以用于编写各种类型的测试,包括单元测试、集成测试和端到端测试等。它支持并行执行测试,提供了丰富的注解和配置选项,可以方便地管理测试用例和测试报告。Mockito :Mockito是一个用于Java单元测试中模拟(mock)对象的框架。它能够创建和操作模拟对象,以便更容易地进行测试驱动开发。Mockito提供了简洁的API,使得模拟对象的创建和行为设置变得简单而直观。PowerMock :PowerMock是一个扩展了Mockito和其他测试框架的工具,可以用于处理一些更复杂的测试场景,例如对静态方法、私有方法和构造函数进行模拟。它提供了一些特殊的注解和API,使得在单元测试中处理这些特殊情况更加方便。JUnit4 JUnit4简介 JUnit是Java中最常用的单元测试框架。它提供了一套用于编写和运行单元测试的API,并支持断言(assertions)和测试装置(test fixtures)等功能。JUnit可以与各种开发工具和持续集成系统集成,是Java开发中必备的测试框架。Maven依赖如下
1 2 3 4 5 6 <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.13.2</version > </dependency >
JUnit4注解 @Test: 用于标记一个测试方法。被标记的方法应该是公共的、无参数的,并且没有返回值。@Before: 用于标记在每个测试方法执行之前需要执行的方法。通常用于设置测试的前提条件。@After: 用于标记在每个测试方法执行之后需要执行的方法。通常用于清理测试过程中创建的资源。@BeforeClass: 用于标记在所有测试方法执行之前需要执行的方法。被标记的方法必须是静态的。@AfterClass: 用于标记在所有测试方法执行之后需要执行的方法。被标记的方法必须是静态的。@Ignore: 用于标记一个测试方法或测试类,表示忽略该方法或类的测试。@RunWith: 用于指定自定义的测试运行器。测试运行器负责控制测试的执行流程。@Rule: 用于定义测试规则,例如 Timeout 规则、Expected Exception 规则等。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @RunWith(JUnit4.class) public class JunitTest { @Rule public Timeout globalTimeout = Timeout.seconds(1 ); @Ignore @Test public void ignoredTestMethod () { System.out.println("被忽略的测试方法" ); } @BeforeClass public static void setUpClass () { System.out.println("在所有测试方法执行之前执行一次的静态方法" ); } @Before public void setUp () { System.out.println("在每个测试方法执行之前执行的方法" ); } @Test public void testMethod1 () { System.out.println("第一个测试方法" ); } @Test public void testMethod2 () { System.out.println("第二个测试方法" ); } @After public void tearDown () { System.out.println("在每个测试方法执行之后执行的方法" ); } @AfterClass public static void tearDownClass () { System.out.println("在所有测试方法执行之后执行一次的静态方法" ); } }
JUnit4断言 JUnit 4中常用的断言方法如下:
**assertEquals(expected, actual)**:比较两个值是否相等。使用该断言时,如果预期值与实际值不相等,则测试将失败。 **assertNotEquals(unexpected, actual)**:比较两个值是否不相等。使用该断言时,如果预期值与实际值相等,则测试将失败。 **assertSame(expected, actual)**:验证两个对象是否为同一个对象(引用相等)。如果 expected 和 actual 不是同一个对象,则测试将失败。 **assertNotSame(unexpected, actual)**:验证两个对象是否不是同一个对象(引用不相等)。如果 unexpected 和 actual 是同一个对象,则测试将失败。 **assertTrue(condition)**:验证条件是否为真。使用该断言时,如果条件为假,则测试将失败。 **assertFalse(condition)**:验证条件是否为假。使用该断言时,如果条件为真,则测试将失败。 **assertNull(object)**:验证对象是否为null。使用该断言时,如果对象不为null,则测试将失败。 **assertNotNull(object)**:验证对象是否不为null。使用该断言时,如果对象为null,则测试将失败。 **assertArrayEquals(expectedArray, actualArray)**:比较两个数组是否相等。使用该断言时,如果预期数组与实际数组不相等,则测试将失败。 **assertThrows(expectedType, executable)**:验证代码块是否会抛出预期的异常。使用该断言时,如果代码块没有抛出预期的异常,则测试将失败。 **fail()**:标记测试失败。使用该断言时,无论条件如何,测试都将失败。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test public void testAddition () { System.out.println("JunitTest" ); assertEquals(5 , 3 ); assertNotEquals(5 , 3 ); assertSame(new Object (), new Object ()); assertNotSame(new Object (), new Object ()); assertTrue(true ); assertFalse(true ); assertNull(null ); assertNotNull(null ); assertArrayEquals(new int []{1 , 2 , 3 }, new int []{1 , 2 , 3 }); assertThrows(ArithmeticException.class, () -> { int result = 10 / 0 ; }); fail("This test intentionally fails" ); }
JUnit5 JUnit5简介 JUnit 5是JUnit测试框架的最新版本,提供了一些新的功能和改进
JUnit5注解 JUnit 5引入了一组新的注解,使用更一致的命名规则。
@Test :标记一个测试方法。@BeforeAll :标记一个静态方法,在所有测试方法之前执行一次。@BeforeEach :标记一个非静态方法,在每个测试方法之前执行一次。@AfterEach :标记一个非静态方法,在每个测试方法之后执行一次。@AfterAll :标记一个静态方法,在所有测试方法之后执行一次。@DisplayName :为测试类或测试方法指定展示名称。@Disabled :标记一个测试类或测试方法为禁用状态,不会执行该测试。@Tag :为测试类或测试方法添加标签,以便于在测试运行时进行筛选和分组。@Timeout :设置测试方法的超时时间。如果测试方法执行时间超过指定的时间,测试将被认为失败。@RepeatedTest :指定一个测试方法重复执行的次数。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Tag("unit") @DisplayName("Junit5Test") public class Junit5Test { @Test @Disabled public void testMethod1 () { } @Test @Timeout(5) @RepeatedTest(3) public void testMethod2 () { System.out.println("测试方法的代码" ); } @BeforeAll static void setupAll () { System.out.println("在所有测试方法之前执行一次" ); } @BeforeEach void setup () { System.out.println("在每个测试方法之前执行一次" ); } @AfterEach void cleanup () { System.out.println("在每个测试方法之后执行一次" ); } @AfterAll static void cleanupAll () { System.out.println("在所有测试方法之后执行一次" ); } }
TestNG TestNG是一个灵活的测试框架,可以用于编写各种类型的测试,包括单元测试、集成测试和端到端测试等。它支持并行执行测试,提供了丰富的注解和配置选项,可以方便地管理测试用例和测试报告。
Mockito Mockito是一个用于Java单元测试中模拟(mock)对象的框架。它能够创建和操作模拟对象,以便更容易地进行测试驱动开发。Mockito提供了简洁的API,使得模拟对象的创建和行为设置变得简单而直观。
PowerMock PowerMock是一个扩展了Mockito和其他测试框架的工具,可以用于处理一些更复杂的测试场景,例如对静态方法、私有方法和构造函数进行模拟。它提供了一些特殊的注解和API,使得在单元测试中处理这些特殊情况更加方便。