Spring Boot学习第五天
一、Spring Boot 异常处理
1 Spring Boot中捕获全局异常
- 在Spring Boot中处理全局异常,两个注解起了关键作用
@ControllerAdvice
:控制器增强,配合@ExceptionHandler
来增所有的控制方法@ExceptionHandler
:用来捕获@RequestMapping
抛出的异常;
- 首先定义一个处理异常的类,在类上加入
@ControllerAdvice
注解,标识此类为异常处理类 - 再自定义一个异常处理方法并加上
@ExceptionHandler
指定要处理的异常
/**
* 使用注解开发异常处理器,声明该类是一个Controller的通知类,声明后该类就会被加载成异常处理器
*/
@Component
@ControllerAdvice
@Slf4j
public class ExceptionAdvice {
/**
* 类中定义的方法携带@ExceptionHandler注解的会被作为异常处理器,后面添加实际处理的异常类型
* @param ex 异常对象
* @return
*/
@ExceptionHandler(NullPointerException.class)
@ResponseBody
public String doNullException(Exception ex) {
log.error("空指针异常");
return "空指针异常";
}
@ExceptionHandler(ArithmeticException.class)
@ResponseBody
public String doArithmeticException(Exception ex) {
return "ArithmeticException";
}
@ExceptionHandler(Exception.class)
@ResponseBody
public String doException(Exception ex) {
return "all";
}
}
- 在Controller方法自定义一个异常进行测试,通过debug方法也可以看到自定义的异常被捕获
#Swagger返回结果:
Code 200
Response body 空指针异常
#控制台结果:
2021-04-14 10:51:57.156 ERROR 14696 --- [nio-8082-exec-1] com.example.exception.ExceptionAdvice : 空指针异常
2021-04-14 10:51:57.263 ERROR 14696 --- [nio-8082-exec-1] o.s.web.servlet.HandlerExecutionChain : HandlerInterceptor.afterCompletion threw exception
java.lang.NullPointerException: null
2. 优化全局异常捕获
以上就基本的完成的异常处理,但是还不完善;需要改善的地方:
- 异常返回应该有一个统一的异常业务类,和异常常量类来封装异常错误信息;
实现步骤:
- 自定义一个业务异常类 BusinessException,用于处理业务异常
/**
* 自定义异常继承RuntimeException,覆盖父类所有的构造方法
* @author 沈晨曦
*/
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
public BusinessException(Throwable cause) {
super(cause);
}
public BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
- 定义一个SystemException用于处理系统异常
/**
* 自定义异常继承RuntimeException,覆盖父类所有的构造方法
* @author 沈晨曦
*/
public class SystemException extends RuntimeException {
public SystemException() {
}
public SystemException(String message) {
super(message);
}
public SystemException(String message, Throwable cause) {
super(message, cause);
}
public SystemException(Throwable cause) {
super(cause);
}
public SystemException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
- 定义一个枚举类来封装异常状态码和错误提示信息
/**
* 异常枚举类
* @author 沈晨曦
* @version 2021/4/14 11:04:29
*/
public enum ExceptionHttpCodeEnum {
/**
* 用于封装空指针异常
*/
NULLPOINTEXCEPTION(201, "空指针异常");
int code;
String errorMessage;
ExceptionHttpCodeEnum(int code, String errorMessage) {
this.code = code;
this.errorMessage = errorMessage;
}
public int getCode() {
return code;
}
public String getErrorMessage() {
return errorMessage;
}
}
- 在捕获到异常时,根据自己需求返回到业务异常还是系统异常
@ExceptionHandler(NullPointerException.class)
@ResponseBody
public BusinessException doNullException(Exception ex) {
log.error("空指针异常");
return new BusinessException(ExceptionHttpCodeEnum.NULLPOINTEXCEPTION.toString());
//return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR);
}
- Swagger在线测试,查看返回结果
{
"cause": null,
"stackTrace": [
{
"methodName": "doNullException",
"fileName": "ExceptionAdvice.java",
"lineNumber": 29,
"className": "com.example.exception.ExceptionAdvice",
"nativeMethod": false
},....省略中间
],
"localizedMessage": "空指针异常",
"message": "空指针异常",
"suppressed": []
}
3. 再次优化
可以看到上面的异常捕获大致完善,但是还能持续优化,响应结果过于长篇大幅,相对信息比较全,但是有更好的解决方案;
- 封装一个统一的返回类,所有结果通过此类返回
/**
* 通用的结果返回类
* @param <T>
*/
public class ResponseResult<T> implements Serializable {
private String host;
private Integer code;
private String errorMessage;
private T data;
public static ResponseResult errorResult(ExceptionHttpCodeEnum enums){
return setExceptionHttpCodeEnum(enums,enums.getErrorMessage());
}
private static ResponseResult setExceptionHttpCodeEnum(ExceptionHttpCodeEnum enums, String errorMessage){
return okResult(enums.getCode(),errorMessage);
}
public static ResponseResult okResult(int code, String msg) {
ResponseResult result = new ResponseResult();
return result.ok(code, null, msg);
}
public static ResponseResult okResult(Object data) {
ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getErrorMessage());
if(data!=null) {
result.setData(data);
}
return result;
}
}
- 测试
{
"host": null,
"code": 201,
"errorMessage": "空指针异常",
"data": null
}
4. 对Controller进行优化测试
- ResponseResult返回类新增一个构造方法
/** 成功响应方法
* @param o 响应对象
* @param success 成功响应码和message的封装
* @return 通用返回结果
*/
public static ResponseResult okResult(Object o, AppHttpCodeEnum success) {
ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getErrorMessage());
return result.ok(success.getCode(),o,success.getErrorMessage());
}
- 测试:
{
"host": null,
"code": 200,
"errorMessage": "操作成功",
"data": [
{
"id": 1,
"name": "韩雪",
"age": 18
},
{
"id": 3,
"name": "一栗小莎子",
"age": 24
},
{
"id": 4,
"name": "史蒂夫",
"age": 18
}
]
}
二、任务调度
1. Java中常见的定时任务比较
- Timer:jdk自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让程序按照某一个频度执行,但不能在指定时间运行,一般很少使用,主要用于非Spring项目简单的任务调度。
- Spring Task:Spring3.0以后自带的Task,可以将它看成一个轻量级的Quartz,使用起来比Quartz简单很多,在Spring应用中,直接使用@Scheduled注解即可,但对于集群项目比较麻烦,需要避免集群环境下任务被多次调用的情况,而且不能动态维护,任务启动以后不能修改、暂停等。
- Quartz:好用的第三方任务调度工具,可谓是企业级应用系统任务调度工具的老大。可以方便的在集群下使用、可以动态增加、删除、暂停等维护任务,动态定时任务更加灵活。而且,和Spring Boot集成非常方便。
2. Spring boot中集成task
- pom
<!-- Spring Boot启动器父类 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- Spring Boot web启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
-
配置属性:无
-
编码
- 启动类:只需要加上
@EnableScheduling
注解开启task - 自定义一个task类作为任务调度类,在类上加入
@Component
注解,再自定义一个任务调度方法,并加上@Scheduling
注解 @Scheduling
:此注解可以通过属性指定每隔一定时间执行此方法。以下代码是任务执行完5秒后继续执行此任务
@Scheduled (fixedDelay= 5000 ) public void doSomething() { // 做些什么 }
fixedDelay
:固定延迟。基于上一次任务结束的时间节点,计数达到指定时间重新执行;fixedRete
:固定速率。以上一个任务开始的时间节点为基准,每隔指定时间重新执行;initialDelay
:延迟初始化;指定在第一次调用之前的等待时间;cron= "*/5 * * * * MON-FRI"
:只会在工作日执行
- 启动类:只需要加上
3. Quartz
3.1 quart介绍
- 定义:Quartz是Job scheduling(作业调度)领域的一个开源项目,Quartz既可以单独使用也可以跟spring框架整合使用,在实际开发中一般会使用后者。使用Quartz可以开发一个或者多个定时任务,每个定时任务可以单独指定执行的时间,例如每隔1小时执行一次、每个月第一天上午10点执行一次、每个月最后一天下午5点执行一次等。
- 使用场景:定时清理垃圾资源、定时扫描、监控需要被处理的资源等;
- Quartz有3个核心要素:调度器(Scheduler)、任务(Job)、触发器(Trigger)。
Job(任务)
:是一个接口,有一个方法void execute(),可以通过实现该接口来定义需要执行的任务(具体的逻辑代码)。JobDetail
:Quartz每次执行job时,都重新创建一个Job实例,会接收一个Job实现类,以便运行的时候通过newInstance()的反射调用机制去实例化Job.JobDetail是用来描述Job实现类以及相关静态信息,比如任务在scheduler中的组名等信息。Trigger(触发器)
:描述触发Job执行的时间触发规则实现类SimpleTrigger和CronTrigger可以通过crom表达式定义出各种复杂的调度方案。Calendar
:是一些日历特定时间的集合。一个Trigger可以和多个 calendar关联,比如每周一早上10:00执行任务,法定假日不执行,则可以通过calendar进行定点排除。Scheduler(调度器)
:代表一个Quartz的独立运行容器。Trigger和JobDetail可以注册到Scheduler中。Scheduler可以将Trigger绑定到某一JobDetail上,这样当Trigger被触发时,对应的Job就会执行。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job.
3.2 quart 使用
- pom
<!--spring boot集成quartz-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
-
编码
- 自定义一个job类,继承
QuartzJobBean
类,并重写executeInternal方法,此方法内部写代码逻辑
public class DateTimeJob extends QuartzJobBean { @Override public void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException { //获取JobDetail中关联的数据 String msg = (String) jobExecutionContext.getJobDetail().getJobDataMap().get("msg"); System.out.println("current time :"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "---" + msg); } }
- 配置属性:
- 指定要执行的job类
@Configuration public class QuartzConfig { @Bean public JobDetail printTimeJobDetail() { /* newJob:指定job类,这里指定我们自定义的job类 withIdentity:取名 usingJobData:存入键值对 */ return JobBuilder.newJob(DateTimeJob.class) .withIdentity("DateTimeJob") .usingJobData("msg", "Hello Quartz") .storeDurably() .build(); } @Bean public Trigger printTimeJobTrigger() { CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/1 * * * * ?"); /* forJob:指定job withIdentity:给任务调取取名 */ return TriggerBuilder.newTrigger() .forJob(printTimeJobDetail()) .withIdentity("quartzTaskService") .withSchedule(cronScheduleBuilder) .build(); } }
- 自定义一个job类,继承
4. corn表达式
cron表达式分为七个域,之间使用空格分隔。其中最后一个域(年)可以为空。每个域都有自己允许的值和一些特殊字符构成。使用这些特殊字符可以使我们定义的表达式更加灵活。
下面是对这些特殊字符的介绍:
逗号(,):指定一个值列表,例如使用在月域上1,4,5,7表示1月、4月、5月和7月
横杠(-):指定一个范围,例如在时域上3-6表示3点到6点(即3点、4点、5点、6点)
星号(*):表示这个域上包含所有合法的值。例如,在月份域上使用星号意味着每个月都会触发
斜线(/):表示递增,例如使用在秒域上0/15表示每15秒
问号(?):只能用在日和周域上,但是不能在这两个域上同时使用。表示不指定
井号(#):只能使用在周域上,用于指定月份中的第几周的哪一天,例如6#3,意思是某月的第三个周五 (6=星期五,3意味着月份中的第三周)
L:某域上允许的最后一个值。只能使用在日和周域上。当用在日域上,表示的是在月域上指定的月份的最后一天。用于周域上时,表示周的最后一天,就是星期六
W:W 字符代表着工作日 (星期一到星期五),只能用在日域上,它用来指定离指定日的最近的一个工作日
- 案例:
“30 * * * * ?”
:每半分钟触发任务;
“30 10 * * * ?”
:每小时的10分30秒触发任务;
“30 10 1 * * ?”
“:每天1点10分30秒触发任务;
“30 10 1 20 * ?”
:每月20号1点10分30秒触发任务
“30 10 1 20 10 ? *”
:每年10月20号1点10分30秒触发任务;
“30 10 1 20 10 ? 2011”
:2011年10月20号1点10分30秒触发任务;
“30 10 1 ? 10 * 2011”
:2011年10月每天1点10分30秒触发任务;
“30 10 1 ? 10 SUN 2011”
:2011年10月每周日1点10分30秒触发任务;
“15,30,45 * * * * ?”
:每15秒,30秒,45秒时触发任务;
“15-45 * * * * ?”
:15到45秒内,每秒都触发任务;
“15/5 * * * * ?”
:每分钟的每15秒开始触发,每隔5秒触发一次;
“15-30/5 * * * * ?”
:每分钟的15秒到30秒之间开始触发,每隔5秒触发一次;
“0 0/3 * * * ?”
:每小时的第0分0秒开始,每三分钟触发一次;
“0 15 10 ? * MON-FRI”
:星期一到星期五的10点15分0秒触发任务;
“0 15 10 L * ?”
:每个月最后一天的10点15分0秒触发任务;
“0 15 10 LW * ?”
:每个月最后一个工作日的10点15分0秒触发任务;
“0 15 10 ? * 5L”
:每个月最后一个星期四的10点15分0秒触发任务;
“0 15 10 ? * 5#3”
:每个月第三周的星期四的10点15分0秒触发任务;
三、IOC涉及到的设计模式
1 策略模式
-
定义:策略模式也叫政策模式,是一种行为型设计模式,是一种比较简单的设计模式。策略模式采用了面向对象的继承和多态机制;
-
场景:
- 多个类只有在算法或行为上稍有不同的场景。
- 算法需要自由切换的场景。
- 需要屏蔽算法规则的场景;
-
注意:策略类不要太多,如果一个策略家族的具体策略数量超过4个,则需要考虑混合模式,解决策略类膨胀和对外暴露问题。在实际项目中,一般通过工厂方法模式来实现策略类的声明。
-
优点:
- 算法可以自由切换。
- 避免使用多重条件判断。
- 扩展性良好。
-
缺点:
- 策略类会增多。
- 所有策略类都需要对外暴露。
-
**在spring中的应用:**例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略,如:
- 当ApplicationContext指向 PathXmlApplicationContext子类时:
ApplicationContext ctx = new Class PathXmlApplicationContext("bean.xml"); Resource res = ctx.getResource("book.xml");
- 当ApplicationContext指向FileSystemXmlApplicationContext子类时
ApplicationContext ctx = new Class FileSystemXmlApplicationContext("bean.xml");
-
在项目中使用:
- 先定义一个接口
public interface Driveable { void drive(); }
- 定义接口的实现类,每一个实现就是一种策略
@Slf4j public class Car implements Driveable { @Override public void drive() { log.info("开着汽车出去浪~"); } }
@Slf4j public class Ship implements Driveable { @Override public void drive() { log.info("坐船出去浪~"); } }
- 测试
// 策略1 Driveable car = new Car(); new Person(car).hangOut(); // 策略2 Driveable ship = new Ship(); new Person(ship).hangOut();
- 结果
15:56:35.944 [main] INFO com.example.demo.ioc.Car - 开着汽车出去浪~ 15:56:35.948 [main] INFO com.example.demo.ioc.Ship - 坐船出去浪~
2 单例模式
- 定义:单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
- 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
- 注意:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
- spring中:bean默认为单例模式,又分为饿汉和懒汉
- 饿汉:一开始就加载实例,但是会造成性能浪费;
- 懒汉:延迟加载;bean默认就是延迟加载
- 单例bean的优势
- 减少了新生成实例的消耗:
- spring会通过反射会cglib来生成bean
- 给对象分配内存
- 减少了JVM垃圾回收
- bean复用,能够快速取到bean,在单例模式下,bean除了第一次生成,其余都是从缓存取
- 减少了新生成实例的消耗:
- 劣势:
- 单例bean有线程安全问题,所有请求都共享一个bean实例;
- 单例模式三要素:
- 构造方法私有化
- 静态属性指向实例
- public static的getInstance方法,返回第二部的静态属性;
- spring实现单例模式,使用@Scope属性指定为
singleton
- Spring 通过 ConcurrentHashMap 实现单例注册表的特殊方式实现单例模式。Spring 实现单例的核心代码:
// 通过 ConcurrentHashMap(线程安全) 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "'beanName' must not be null");
synchronized (this.singletonObjects) {
// 检查缓存中是否存在实例
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
//...省略
try {
singletonObject = singletonFactory.getObject();
}
//...省略
// 如果实例对象在不存在,我们注册到单例注册表中。
addSingleton(beanName, singletonObject);
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}
//将对象添加到单例注册表
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));
}
}
}
3 工厂模式
-
定义:工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
-
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
-
优点:
- 一个调用者想创建一个对象,只要知道其名称就可以了。
- 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
- 屏蔽产品的具体实现,调用者只关心产品的接口。
-
**缺点:**每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
-
使用场景:
- 日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。
- 数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。
- 设计一个连接服务器的框架,需要三个协议,“POP3”、“IMAP”、“HTTP”,可以把这三个作为产品类,共同实现一个接口。
-
**注意事项:**作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。
-
spring中:Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象
BeanFactory
:延迟注入(使用到某个 bean 的时候才会注入)。相比于ApplicationContext 来说会占用更少的内存,程序启动速度更快。ApplicationContext
:容器启动的时候,不管你用没用到,一次性创建所有 bean 。BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory ,除了有BeanFactory的功能还有额外更多功能,所以一般开发人员使用ApplicationContext会更多。
-
使用:
- 先定义一个接口:
public interface Shape { void draw(); }
- 定义该接口的实现
public class Rectangle implements Shape { @Override public void draw() { System.out.println("Inside Rectangle::draw() method."); } }
public class Square implements Shape { @Override public void draw() { System.out.println("Inside Square::draw() method."); } }
public class Circle implements Shape { @Override public void draw() { System.out.println("Inside Circle::draw() method."); } }
- 创建工厂生成对应实体类对象
public class ShapeFactory { //使用 getShape 方法获取形状类型的对象 public Shape getShape(String shapeType){ if(shapeType == null){ return null; } if(shapeType.equalsIgnoreCase("CIRCLE")){ return new Circle(); } else if(shapeType.equalsIgnoreCase("RECTANGLE")){ return new Rectangle(); } else if(shapeType.equalsIgnoreCase("SQUARE")){ return new Square(); } return null; } }
- 测试
public class FactoryPatternDemo { public static void main(String[] args) { ShapeFactory shapeFactory = new ShapeFactory(); //获取 Circle 的对象,并调用它的 draw 方法 Shape circle = shapeFactory.getShape("CIRCLE"); circle.draw(); //获取 Rectangle 的对象,并调用它的 draw 方法 Shape rectangle = shapeFactory.getShape("RECTANGLE"); rectangle.draw(); //获取 square 的对象,并调用它的 draw 方法 Shape square = shapeFactory.getShape("SQUARE"); square.draw(); } }
- 结果
Inside Circle::draw() method. Inside Rectangle::draw() method. Inside Square::draw() method.