Spring + SpringMVC + SpringBoot

2,043 阅读28分钟

Spring相关概念

Spring对IOC的实现

IOC控制反转:控制反转是一种思想,将对象的创建和对象之间关系的维护交出去,交由第三方容器负责。可以降低程序耦合度,提高程序扩展力。

声明组件的注解

声明Bean的注解都有一个value属性,属性值用来指定Bean的ID,如果不指定默认是类名首字母变小写后的名字。

  • @Controller:将一个类标识为请求处理的控制器组件,用于接收用户的请求并处理相关的业务逻辑,最后返回相应的结果。
  • @Service:将一个类标识为业务逻辑层(Service)组件,用于封装复杂的业务操作,并提供事务管理、依赖注入等功能。
  • @Repository:将一个类标识为持久层(Repository)组件,主要用于实现数据访问和持久化操作。
  • @Component:通用的组件注解,并没有特定的业务含义。
  • @Mapper:MyBatis框架提供的一个注解,用于标识一个接口为Mapper接口,提供了数据库持久化操作的映射规则。

负责注入的注解

  1. @Value:用于将值赋给一个类的属性或方法参数。通过@Value注解,可以在运行时将值注入到被注解的属性或方法参数中。
  • 属性注入:可以通过@Value注解将值注入到交由容器管理的类的属性中。
  • 方法参数注入:可以通过@Value注解将值注入到方法参数中。

属性注入

@RestController  
public class TestController {  
    @Value("${wenxuan.name}")  
    private String name;  
}

方法参数注入

@Service
public class UserService {
    private final String name;

    @Autowired
    public UserService (@Value("${wenxuan.name}") String name) {
        this.name = name;
    }
}
  1. @AutoWired注解可以用来注入非简单类型,被翻译为自动装配,单独使用@AutoWired注解,默认是根据类型自动装配(byType)。

@AutoWired注解有一个属性:required

  • 如果该属性值为true,则表示被注入的Bean必须存在,如果不存在则报错。
  • 如果该属性值为false,则表示被注入的Bean是否存在都可以,如果存在则注入,如果不存在也不报错。
@RestController  
public class UserController {  
    @Autowired  
    private UserService userService;
}
  1. @Quafier注解可以用来注入非简单类型,用来指定注入的Bean的名称,也就是Bean的id(byName)。

@Quafier注解使用场景:当使用@AutoWired注解,来根据属性类型自动装配时,但属性类型有多个类匹配时,此时不知道使用哪个类对象进行装配,可以通过@Quafier注解指定Bean的name,从而实现唯一性。

@RestController  
public class UserController {  
    @Autowired  
    private UserService userService;
}
  1. @Resource

@Resource注解也可以用来注入非简单类型,@Resource注解查找Bean的方式:

  1. 如果@Resource注解有name属性值,根据name属性值去查找对应名称的Bean(byName);
  2. 如果没有name名称匹配的Bean,根据属性名去查找对应名称的Bean(byName);
  3. 如果没有属性名匹配的Bean,根据属性类型去查找对应类型的Bean(byType);
  4. 如果没有类型的Bean,则报错;如果有多个类型匹配的Bean,则报错。

Bean对象生命周期

Spring其实就是一个管理Bean对象的工厂,它负责所有Bean对象的创建、Bean对象的销毁等。Bean的生命周期就是Bean对象从创建到销毁的这个过程。

Bean的生命周期之五步

Bean的生命周期之五步.png

Bean生命周期分为五步:

  1. 实例化Bean,调用Bean的无参构造方法
  2. 给Bean属性赋值,调用Bean的set方法
  3. 初始化Bean,调用Bean的initBean方法(手动编写且配置)
  4. 使用Bean对象
  5. 销毁Bean,调用Bean的destroyBean方法(手动编写且配置)
public class User {
    private String name;

    public void setName(String name) {
        this.name = name;
        System.out.println("第2步:给属性赋值");
    }

    public User() {
        System.out.println("第1步:无参数构造方法执行");
    }

    public void initBean() {
        System.out.println("第3步:初始化方法执行");
    }

    public void destroyBean() {
        System.out.println("第5步:销毁方法执行");
    }
}

Bean的生命周期之七步

Bean的生命周期之七步.png

Bean的生命周期之七步:

  1. 实例化Bean,调用无参构造函数
  2. 给Bean属性赋值,调用set方法
  3. Bean前处理器before
  4. 初始化Bean,调用initBean方法(手动编写且配置)
  5. Bean后处理器after
  6. 使用Bean
  7. 销毁Bean,调用destroyBean方法(手动编写且配置)
public class LogBeanPostProcessor implements BeanPostProcessor {
    /**
     * @param bean Bean对象
     * @param beanName Bean对象的名称
     */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("Bean后处理器before执行");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("Bean后处理器after执行");
        return bean;
    }
}

new对象放入Spring容器中

public class BeanLifecycleTest {
    // 自己new的对象放到spring容器中
    @Test
    public void testRegisterBean() {
        User user = new User();

        DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();
        // 将user对象放到spring容器中
        defaultListableBeanFactory.registerSingleton("user", user);
        // 从spring容器中取出对象
        User userBean = defaultListableBeanFactory.getBean("user", User.class);
    }
}

面向切面编程AOP

AOP介绍

AOP:核心的业务是纵向的,将与业务逻辑无关的交叉业务代码单独的提取出来,形成一个独立的组件,然后以横向交叉的方式应用到业务流程中的过程。

AOP思想图.png

AOP的七大术语

  • 连接点:在程序的整个执行过程中,可以织入切面的位置。方法执行前后,异常抛出后等位置。
  • 切点:在程序执行流程中,真正植入切面的方法。
  • 通知:通知又叫做增强,具体要织入的diamagnetic。包括前置通知、后置通知、环绕通知、异常通知、最终通知。
  • 切面:切点 + 通知 等于 切面。
  • 织入:把通知应用到目标对象上的过程。
  • 代理对象:一个目标对象被织入通知后产生的新对象。
  • 目标对象:被织入通知的对象。

AOP中的七大术语.png

AOP的使用

  1. 添加maven依赖
<!-- 引入aop支持 -->  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-aop</artifactId>  
    <version>2.7.17</version>  
</dependency>
  1. 定义切面类:切面类需要使用@Aspect注解和@Component注解标识
@Aspect
@Component
public class ExceptionAOP {
}
  1. 在切面类中定义切入点
@Aspect
@Component
public class ExceptionAOP {
    // value编写表达式定义切入点,标识某一类方法,在通知中添加切入点,这些标识的方法就会得到对应的通知
    @Pointcut(value = "execution(* com.wenxuan.service..*(..))")
    public void pointCut() {
    }
}
  1. 在切面类中定义通知:在通知中标识哪些切点得到增强的通知
@Aspect
@Component
public class ExceptionAOP {
    
    @Pointcut(value = "execution(* com.wenxuan.service..*(..))")
    public void pointCut() {
    }

    // 前置通知,在切点执行之前执行的操作
    @Before("pointCut()")
    public void before(JoinPoint joinPoint) {
        // 逻辑代码
        System.out.println("前置通知");
    }
}

切入点的定义

切入点是一个表达式,可以直接定义在通知中,也可以通过@PointCut注解进行定义。

  • 直接定义在通知中:不需要额外的注解或方法来定义切入点。这种方式比较简洁,适用于直接在单个通知方法中使用的切入点。
  • 通过@PointCut注解定义:可以使得切入点表达式在多个通知方法中共享,提高代码重用性和可读性。

切点表达式:用来定义通知往哪些目标方法上切入

  • 访问控制修饰符:可选项,指定访问修饰符的方法,如果没有写表示四个权限都包括。
  • 返回值类型:必填项,表示返回值类型任意。
  • 全限定类名:可选项,两个点 “..” 代表当前包以及子包下的所有类,如果没有写表示所有类。
  • 方法名:必填项,* 表示所有的方法,set* 表示所有以set开头的方法(set方法)。
  • 形式参数列表:必填项,() 表示没有参数的方法,(..) 表示参数类型和个数随意的方法,(* ) 表示只有一个参数的方法,(* , String) 表示第一个参数类型随意第二个参数类型为String类型的方法。
  • 异常:可选项,省略时表示任意异常类型。
// 切点表达式的语法格式:
execution([访问控制修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])

// 切点表达式举例:
// 表示 service包下以delete开头的并且是public修饰的方法
execution(public * com.powernode.mall.service.*.delete*(..))
// 表示mall包下所有的方法
execution(* com.powernode.mall..*(..))
// 表示所有包下所有类的所有方法
execution(* *(..))

AOP通知的分类

AOP通知在切面类中定义,通知分为前置通知、后置通知、环绕通知、异常通知、最终通知。

  1. 前置通知通过@Before注解进行定义,切点执行前会执行的语句。
// 前置通知,在切点执行之前执行的操作
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
    // 逻辑代码
    System.out.println("前置通知");
}
  1. 后置通知通过@AfterReturningr注解进行定义,当切点正常执行结束后会执行的语句。
@Aspect
@Component
public class ExceptionAOP {
    @Pointcut(value = "execution(* com.wenxuan.service..*(..))")
    public void pointCut() {
    }
    
    /**
     * 后置返回通知
     * 1. 方法参数
     *      如果参数中的第一个参数为JoinPoint,则第二个参数为返回值的信息
     *      如果参数中的第一个参数不为JoinPoint,则第一个参数为returning中对应的参数
     * 2. @AfterReturning注解
     *      value:用于指定被拦截的目标方法
     *      returning:用于指定目标方法的返回值绑定的参数名称,如果参数类型为Object则可以匹配任何目标返回值,否则匹配指定目标返回值
     *      argNames:用于指定目标方法的参数名称列表,参数名称之间用逗号分隔。
     */
    @AfterReturning(value = "pointCut()",returning = "keys")
    public void doAfterReturningAdvice1(JoinPoint joinPoint, Object keys){
        System.out.println("后置通知的返回值 = " + keys);
    }

    @AfterReturning(value = "pointCut()",returning = "keys",argNames = "keys")
    public void doAfterReturningAdvice2(String keys){
        System.out.println("后置通知的返回值 = " + keys);
    }
}

  1. 环绕通知通过@Around注解进行定义,环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
@Aspect
@Component
public class ExceptionAOP {
    @Pointcut(value = "execution(* com.wenxuan.service..*(..))")
    public void pointCut() {
    }

    /**
     * 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
     */
    @Around(value = "pointCut()")
    public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
        System.out.println("环绕通知开始");
        Object result = null;
        try {
            result = proceedingJoinPoint.proceed();  // 执行目标方法
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        System.out.println("环绕通知结束");

        // 可以在此处修改目标方法的返回值
        return result;
    }
}

  1. 异常通知通过@AfterThrowing注解进行定义,当切点抛出异常会执行的代码,类似于catch。
@Aspect
@Component
public class ExceptionAOP {
    @Pointcut(value = "execution(* com.wenxuan.service..*(..))")
    public void pointCut() {
    }

    /**
     * 异常通知
     * @AfterThrowing注解:
     * 1. value:用于指定被拦截的目标方法,可以使用表达式语言来指定
     * 2. pointcut:与value属性功能相同用于指定被拦的目标方法,可以表达式语言指定。
     * 3. throwing:用于指定目标方法抛出的异常绑定的参数名称,可以在后续逻辑中使用该参数获取目标方法抛出的异常。
     * 4. argNames:用于指定目标方法的参数名称列表,参数名称之间用逗号分隔。
     */
    @AfterThrowing(value = "pointCut()",throwing = "exception")
    public void doAfterThrowingAdvice(JoinPoint joinPoint, Throwable exception){
        //目标方法名
        System.out.println("目标方法发生异常:" + exception);
    }

}
  1. 最终通知通过@After注解进行定义,方法不管是否抛出异常都会执行的通知,类似于final语句块中的语句。
@Aspect
@Component
public class ExceptionAOP {
    @Pointcut(value = "execution(* com.wenxuan.service..*(..))")
    public void pointCut() {
    }
    
    /**
     * 后置最终通知(目标方法只要执行完了就会执行后置通知方法)
     * @param joinPoint
     */
    @After(value = "pointCut()")
    public void doAfterAdvice(JoinPoint joinPoint){
        System.out.println("最终通知");
    }
}

JoinPoint对象

基本上每个通知中都可以在第一个形参中声明JoinPoint对象,JoinPoint兑现中封装了切入点的一系列信息。

//返回目标对象,即被代理的对象
Object getTarget();

//返回切入点的参数
Object[] getArgs();

//返回切入点的Signature
Signature getSignature();

//返回切入的类型,比如method-call,field-get等等
String getKind();

ProceedingJoinPoint是JoinPoint的实现类,环绕通知中第一个形参必须是ProceedingJoinPoint对象,这个对象中定义了proceed方法,表示执行目标方法并得到返回值。

@SpringBootApplication注解

@SpringBootApplication用于标注主程序类,该类有一个主方法

@SpringBootApplication注解的定义如下

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication{}    
  1. @SpringBootConfiguration注解:里面有一个@Configuration注解,标识这是一个配置类。
  2. @Component注解:指定扫描哪些类,Spring中的注解,默认扫描主程序类所在包下和其所有子包。
  3. @EnableAutoConfiguration注解:里面有一个@AutoConfigurationPackage注解,将指定目录下的所有组件自动导入到Spring容器中,将扫描路径注册到全局,给其他组件查询。

Object的划分

  1. VO(Value Object):VO 是值对象的缩写,表示值对象。它通常是一种用于封装数据的简单 Java 类,用于在各个层之间传递数据。VO 主要用于表示业务领域中的数据对象,不包含业务逻辑,一般只包含属性和对应的 getter 和 setter 方法。
  2. TO(Transfer Object):TO 是传输对象的缩写,表示传输对象。它用于在服务端之间传输数据,主要用于网络传输或跨进程通信。TO 通常包含客户端和服务器端共同需要的数据和操作,且不包含业务逻辑。TO 可以看作是业务逻辑无关的数据传输模型。
  3. PO(Persistent Object):PO 是持久化对象的缩写,表示持久化对象。它是与数据库表相映射的 Java 对象,在数据库操作中起到数据持久化的作用。PO 一般与数据库表的字段一一对应,并提供对应的 getter 和 setter 方法。PO 可以使用 ORM(对象关系映射)框架自动生成,简化了与数据库的交互。
  4. DO(Domain Object):领域对象的缩写,表示从现实世界中抽象出来的有形或无形的业务实体。
  5. BO(Business Object):BO是业务对象的缩写,用于封装业务逻辑和数据操作,通常用于表示业务实体或业务逻辑处理的对象。
  6. POJO(Plain Ordinary Java Object):普通的Java对象,是一个简单的Java类,只有属性字段和getter、setter方法。POJO是DO/DTO/BO/VO的统称。
  7. DAO(Data Access Object):DAO主要负责对数据库或其他数据存储设备的访问和操作,包括数据的增删改查等操作。夹在业务逻辑和数据库资源中间,配合VO提供数据的CRUD操作。

MVC三层架构

Controller层的作用

  1. 处理请求,接受和校验数据
  2. 调用service层提供的方法,进行业务处理
  3. 接受service层处理完的数据。封装页面指定的vo

SpringBoot的使用

SpringBoot创建Web项目

  1. 添加maven依赖
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.17</version>
    <relativePath/> 
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
  1. 编写启动类
package com.wenxuan;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author 文轩
 * @create 2023-11-23 21:15
 */
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

SpringBoot请求处理

Rest风格的使用

不同的请求路径对应不同的功能

  • get请求:获取资源
  • post请求:添加资源
  • put请求:修改资源
  • delete请求:删除资源
@RestController
public class UserController {

    @GetMapping("/user")
    public String getUser(){
        return "GET-张三";
    }

    @PostMapping("/user")
    public String saveUser(){
        return "POST-张三";
    }

    @PutMapping("/user")
    public String putUser(){
        return "PUT-张三";
    }

    @DeleteMapping("/user")
    public String deleteUser(){
        return "DELETE-张三";
    }
}

请求参数处理

  1. @RequestParam注解获取以键值对形式的参数

@RequestParam注解的required属性,默认为true表示前端必须传递参数,如果没有传递则表示路径不匹配。

单个键值对形式的参数

@GetMapping("/user")  
public User getUserByUserId(@RequestParam String id) {  
    return userService.getById(id);  
}

多个键值对形式的参数


// 通过Bean类接收,属性名和属性值进行设置
@PostMapping("/user")  
public boolean saveUser(@RequestParam User user) {  
    return userService.save(user);  
}

// 通过Map接收,key和value进行设置
@PostMapping("/user")  
public boolean saveUser(@RequestParam Map<String, String> params) {  
    return userService.save(params);  
}
  1. @PathVariable

@PathVariable获取URL路径上的参数

@PathVariable注解的required属性,默认为true表示前端必须传递参数,如果没有传递则表示路径不匹配。

// 比如URL:/user/1
@GetMapping("/user/{id}")  
public User getUserByUserId(@PathVariable String id) {  
    return userService.getById(id);  
}
  1. @RequestBody

@RequestBody注解接收JSON形式的参数

@RequestBody注解的required属性,默认为true表示前端必须传递参数,如果没有传递则表示路径不匹配。

@PostMapping("/user")  
public boolean saveUser(@RequestBody User user) {  
    return userService.save(user);  
}
  1. @RequestHeader获取请求头部分或者全部信息

获取请求头指定参数

@GetMapping("/header")  
public String getHeader(@RequestHeader("User-Agent") String userAgent) {  
    return userAgent;  
}

获取请求头所有参数

@GetMapping("/headerList")  
public Map<String, String> getHeaderList(@RequestHeader Map<String, String> headerList) {  
    System.out.println(headerList);  
    return headerList;  
}
  1. @CookieValue获取Cookie值
@GetMapping("/cookie")  
public String cookie(@CookieValue("name") String name) {  
    return name;  
}
  1. @RequestAttribute获取请求域中的值
@GetMapping("/attribute")  
public String attribute(@RequestAttribute("name") String name) {  
    return name;  
}

响应数据的处理

通过@RequestBody注解标识类或者方法,可以使响应数据以JSON形式返回

@RestController注解的作用等价于@Controller + @RequestBody

@ResponseBody  
@GetMapping("/user/{id}")  
public User getUser(@PathVariable String id) {  
    return userService.select(id);  
}

请求转发

方式1:使用 "forward" 关键字

// 类的注解不能使用@RestController 要用@Controller
@RequestMapping("/forward")
public String forward() {
    return "forward:/index.html";
}

方式2:使用servlet 提供的API

// 类的注解可以使用@RestController,也可以使用@Controller
@RequestMapping("/forward")
public void forward(HttpServletRequest request, HttpServletResponse response) throws Exception {
	request.getRequestDispatcher("/index").forward(request, response);
}

请求重定向

方式1:使用 "redirect" 关键字

// 类的注解不能使用@RestController,要用@Controller
@RequestMapping("/redirect")
public String redirect() {
    return "redirect:/index.html";
}

方式2:使用servlet 提供的API

@RequestMapping("/redirect”)
public void redirect(HttpServletResponse response) throws IOException {
    response.sendRedirect("/index.html");
}

SpringBoot实现事务

事务:在一个业务流程当中,通常需要多条DML(insert delete update)语句共同联合才能完成,这多条DML语句必须同时成功,或者同时失败,这样才能保证数据的安全。可以通过事务保证业务中多条DML语句同时成功或者同时失败。

SpringBoot实现事务的步骤

  1. 添加maven依赖
<!-- 引入事务支持 -->  
<dependency>  
    <groupId>org.springframework</groupId>  
    <artifactId>spring-tx</artifactId>  
    <version>5.2.7.RELEASE</version>  
</dependency>
  1. 在启动类中添加@EnableTransactionManagement注解

Spring推荐的方式,是将@EnableTransactionManagement加到被@Configuration注解的类上,而@SpringBootApplication被@SpringBootConfiguration注解,@SpringBootConfiguration又被@Configuration,所以可以将@EnableTransactionManagement注解加到被@SpringBootApplication注解的类上。

@SpringBootApplication  
@EnableTransactionManagement  
public class Application {  
    public static void main(String[] args) {  
        SpringApplication.run(Application.class, args);  
    }  
}
  1. 使用事务
@RestController
public class UserController {

    @Resource
    private UserService userService;

    /**
     * 在需要使用事务的方法上添加@Transactional注解即可,
     * 这个方法中执行的SQL语句就会要么全部执行成功,要么全部执行失败
     */
    @Transactional
    @GetMapping("/insert")
    public User saveUser() {
        User user = new User();
        user.setName("小李");
        user.setId("3");
        userService.insert(user);
        return user;
    }
}

@Transaction注解介绍

  1. @Transaction注解的定义
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    
    // 设置事务管理器的名称,可以通过名称指定使用哪个事务管理器进行事务管理。
    @AliasFor("transactionManager")
    String value() default "";

    // 设置事务的传播行为。在使用嵌套事务的情况下,该属性可以控制事务的传播方式
    Propagation propagation() default Propagation.REQUIRED;		

    // 事务隔离级别
    Isolation isolation() default Isolation.DEFAULT;			

    // 事务超时时间
    int timeout() default -1;								  			

    // 设置事务只读:设置为true表示该事务中只能执行select语句,不能执行DML语句
    boolean readOnly() default false;						  

    // 设置哪些异常回滚事务
    Class<? extends Throwable>[] rollbackFor() default {};		

    // 设置哪些异常不回滚事务
    Class<? extends Throwable>[] noRollbackFor() default {};	
}
  1. @Transaction注解事务的传播行为

事务的传播行为:在使用嵌套事务的情况下,嵌套的事务的设置情况通过事务的传播行为进行设置。

// 设置事务的传播行为
@Transactional(propagation = Propagation.REQUIRED)

事务的传播行为有如下几种

package org.springframework.transaction.annotation;

public enum Propagation {
    REQUIRED(0),	// 支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】
    SUPPORTS(1),	// 支持当前事务,如果当前没有事务,就以非事务方式执行【有就加入,没有就不管了】
    MANDATORY(2),	// 必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常【有就加入,没有就抛异常】
    REQUIRES_NEW(3),// 开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】,两个事务之间没有关系
    NOT_SUPPORTED(4),// 以非事务方式运行,如果有事务存在,挂起当前事务【不支持事务,存在就挂起】
    NEVER(5),		// 以非事务方式运行,如果有事务存在,抛出异常【不支持事务,存在就抛异常】
    NESTED(6);		// 如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】

    private final int value;

    private Propagation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}
  1. @Transaction注解事务的隔离级别

为了保证并发读取数据的正确性,提出事务的隔离级别。事务的隔离级别越好,并发读取数据越正确,但是性能就越差。

// 设置事务的隔离级别
@Transactional(isolation = Isolation.READ_COMITTED)

事务的隔离级别有如下四种:

package org.springframework.transaction.annotation;

public enum Isolation {
    DEFAULT(-1),			// 默认的隔离级别
    READ_UNCOMMITTED(1),	// 读未提交
    READ_COMMITTED(2),		// 读已提交
    REPEATABLE_READ(4),		// 可重复读
    SERIALIZABLE(8);		// 序列化

    private final int value;

    private Isolation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}
  1. @Transactional失效的场景

@Transactional注解不生效的场景

  • 数据库引擎不支持事务(MySQL的MyIsam引擎不支持事务)
  • 注解所在的类没有被加载成Bean,也就是没有被Spring容器管理
  • 注解所在的方法不是public修饰的
  • 注解所在的方法发生自调用问题
  • 所在数据源是否加载事务管理器
  • 注解的扩展配置propagation错误

SpringBoot容器添加数据

@Configuration + @Bean

@Configuration:告诉SpringBoot这是一个配置类,有一个proxyBeanMethods属性,true表示配置类中方法返回的组件对象是单例(默认值)的,false表示配置类中方法返回的组件对象不是单例的。

@Bean:表示方法是一个组件,将返回的Bean对象交由SpringBoot容器管理。

@Configuration
public class UserConfig {
    @Bean
    public User getUser() {
        return new User("1", "zs");
    }
}

@Configuration + @Import

@Configuration:告诉SpringBoot这是一个配置类,有一个proxyBeanMethods属性,true表示配置类中方法返回的组件对象是单例(默认值)的,false表示配置类中方法返回的组件对象不是单例的。

@Import注解在被@Configuration注解修饰的类中使用,导入特定的Bean类对象。

@Configuration
@Import({User.class})
public class UserConfig {
}

@Conditional注解

@Conditional注解用于条件装配,使用该注解可以使得在满足某些条件下才进行注入组件,@Conditional注解使用在配置方法或者配置类上,@Conditional注解有很多子注解,代表不同的条件:

  • @ConditionalOnBean:表示当存在某个组件时,才注入组件
  • @ConditionalOnMissingBean:表示不存在某个组件时,才注入组件
  • @ConditionalOnClass:表示存在某个类时,才注入组件
  • @ConditionalOnMissingClass:表示当不存在某个组件时,才注入组件
  • @ConditionalOnProperty:表示当存在配置的存在指定属性和属性值时,才注入组件
  • @ConditionalOnJava:表示如果是Java应用时,才会注入组件

@Conditional注解和@Bean注解结合使用

@Configuration
public class UserConfig {
    @Bean
    @ConditionalOnBean(SexEnum.class)
    public User getUser() {
        return new User("1", "zs");
    }
}

@Conditional注解和@Import注解结合使用

@Configuration
@Import({User.class})
@ConditionalOnBean(SexEnum.class)
public class UserConfig {
}

SpringBoot实现拦截器

  1. 定义拦截器
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("目标方法执行之前执行该方法");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("目标方法执行完成后执行该方法");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("页面渲染之后执行该方法");
    }
}
  1. 注册拦截器
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                //所有请求都被拦截包括静态资源
                .addPathPatterns("/**")
                //放行的请求
                .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**");
    }
}

自定义SpringBootStarter

添加maven依赖

<!-- 指定父依赖为springboot应用依赖,可以快速构建springboot应用 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.17</version>
    <relativePath/>
</parent>

<!-- springboot启动器依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- springboot自动配置模块 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
</dependency>

编写读取配置文件的属性类

配置属性类:获取配置文件内容

@ConfigurationProperties(prefix = "wenxuan")
public class HelloProperties {
    private String name;
    private String address;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

编写starter使用服务类

向外部提供一个服务类,starter使用者可以使用服务类对象

public class HelloService {

    private String name;
    private String address;


    public HelloService(String name, String address) {
        this.name = name;
        this.address = address;
    }

    public String sayHello(){
        return "你好!我的名字叫 " + name + ",我来自 " + address;
    }
}

编写自动配置类

自动配置类:使得外部可以通过注解方式获取服务类,将HelloProperties类属性注入到HelloService对象中。

// Configuration:表示这是一个配置类
// EnableConfigurationProperties:启用对@ConfigurationProperties注解标注的类的支持
@Configuration
@EnableConfigurationProperties(HelloProperties.class)
public class HelloServiceAutoConfiguration {

    private HelloProperties helloProperties;

    public HelloServiceAutoConfiguration(HelloProperties helloProperties) {
        this.helloProperties = helloProperties;
    }

    // ConditionalOnMissingBean:表示当HelloService类没有注入时才会注入
    @Bean
    @ConditionalOnMissingBean
    public HelloService helloService() {
        return new HelloService(helloProperties.getName(), helloProperties.getAddress());
    }
}

编写SpringBootStarter配置文件

在resources目录下创建META-INF/spring.factories:让spring可以找到自动装配类,让自动配置生效,如果有多个自动配置类,之间使用逗号分割。

# com.wenxuan.config.HelloServiceAutoConfiguration:自动装配类的全类名
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.wenxuan.config.HelloServiceAutoConfiguration

使用自定义的starter

  1. 将jar包导入maven仓库中,通过mvn install
  2. 在项目中的pom.xml配置文件中添加自定义SpringBootStarter的依赖
<!--导入自定义starter-->
<dependency>
    <groupId>com.wenxuan</groupId>
    <artifactId>hello-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
  1. 编写配置文件application.yml
wenxuan:
  name: xiaoming
  address: beijing
  1. 通过注解获取服务类
@Autowired
private HelloService helloService;

SpringBoot全局异常处理

同一处理控制器中的异常通过@ControllerAdvice注解和@ExceptionHandler注解结合实现:

  • @ControllerAdvice: 提供了全局性的控制器Advice,可以实现同一处理控制器中的异常、全局数据绑定、页面跳转。
  • @ExceptionHandler:定义全局的异常处理方法,在控制器中使用改注解可以指定处理特定异常的方法,当控制器中抛出对应类型的异常,Spring会调用这个方法进行处理。
// 表示异常处理器作用于使用RestController注解的类抛出的遗产
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@RestController
public class ExceptionConfiguration {

    // 表示当程序出现ConstraintViolationException或者BindException异常时会调用该方法
    @ExceptionHandler({ConstraintViolationException.class, BindException.class})
    public String validatorException(Exception ex, HttpServletRequest request) {
        ex.printStackTrace();
        String msg = null;
        if(ex instanceof ConstraintViolationException){
            ConstraintViolationException constraintViolationException = (ConstraintViolationException)ex;
            Set<ConstraintViolation<?>> violations = constraintViolationException.getConstraintViolations();
            ConstraintViolation<?> next = violations.iterator().next();
            msg = next.getMessage();
        }else if(ex instanceof BindException){
            BindException bindException = (BindException)ex;
            msg = bindException.getBindingResult().getFieldError().getDefaultMessage();
        }
        return msg;
    }
}

SpringBoot编写测试类

  1. 添加maven依赖
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-test</artifactId>  
    <scope>test</scope>  
</dependency>  
  
<dependency>  
    <groupId>junit</groupId>  
    <artifactId>junit</artifactId>  
    <scope>test</scope>  
</dependency>

编写测试类:在test目录下面编写测试类

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {
    @Resource
    private UserService userService;

    @Test
    public void testUserService() {
        userService.test();
    }
}

SpringBoot解决跨域问题

// 使用@WebFilter注解标记该类为一个过滤器,并指定filterName为"CorsFilter"
@WebFilter(filterName = "CorsFilter ")
@Configuration
public class CorsFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse servletResponse = (HttpServletResponse) response;
        // 设置响应头,允许所有跨域请求访问
        servletResponse.setHeader("Access-Control-Allow-Origin","*");
        //  设置响应头,允许发送Cookie等凭证
        servletResponse.setHeader("Access-Control-Allow-Credentials", "true");
        // 设置响应头允许的请求方法
        servletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PATCH, DELETE, PUT");
        // 设置响应头,设置预检请求的有效期
        servletResponse.setHeader("Access-Control-Max-Age", "3600");
        // 设置响应头,允许的请求头
        servletResponse.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        // 将请求传递给下一个过器或目标资源
        chain.doFilter(request, response);
    }
}

SpringBoot实现枚举常量

开发过程中,当出现一组常量时不能在代码中直接用整数或者字符串表示,应该将这组常量抽取出来定义成一个枚举类,这样子可以可以提高代码的可读性和可维护性。

package com.wenxuan.common.constant;

public class UserConstant {

    public enum  SexStatusEnum{
        MAN(0,"男"),
        WOMAN(1,"女");
        
        private int code;
        private String msg;

        PurchaseStatusEnum(int code,String msg){
            this.code = code;
            this.msg = msg;
        }

        public int getCode() {
            return code;
        }

        public String getMsg() {
            return msg;
        }
    }

    public enum  MemberStatusEnum{
        DAY_MEMBER(0,"一天会员"),
        WEEK_MEMBER(1,"一周会员"),
        MONTH_MEMBER(2,"月度会员")
        QUARTER_MEMBER(3,"季度会员"),
        YEAR_MEMBER(4, "年度会员");
        private int code;
        private String msg;

        PurchaseDetailStatusEnum(int code,String msg){
            this.code = code;
            this.msg = msg;
        }

        public int getCode() {
            return code;
        }

        public String getMsg() {
            return msg;
        }
    }
}

SpringBoot业务状态码规范

这里举出一种业务状态码开发规范,还有很多种业务状态码开发规范,可以自行搜索。

  1. 错误码定义规则为五位数字,正确码为00000
  2. 前两位表示业务场景,最后三位表示错误码
  3. 维护错误码后需要错误提示,所以将其定义为枚举类型

例如:

  • 0:表示成功的状态码
  • 10开头:表示通用的错误状态码
  • 11开头:表示商品业务的错误状态码
  • 12开头:表示订单业务的错误状态码

其中比如10开头的状态码

  • 10000:系统未知异常
  • 10001:参数格式校验失败
public enum BizCodeEnum {
    SUCCESS(0, "操作成功"),
    UNKNOW_EXCEPTION(10000, "系统未知异常"),
    VALID_EXCEPTION(10001, "参数格式校验失败")
    ;

    private int code;
    private String message;

    BizCodeEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

SpringBoot接口统一响应

编写接口统一响应类的前提是需要先添加Spring业务状态码规范类

@NoArgsConstructor
@AllArgsConstructor
@Data
public class ResponseData<T> implements Serializable {

    /**
     * 状态码
     */
    private int code;
    /**
     * 状态码描述信息
     */
    private String message;
    /**
     * 请求返回信息
     */
    private T data;

    public ResponseData(int code, String message) {
        this.code = code;
        this.message = message;
    }


    /**
     * 成功但不携带返回参数
     */
    public static <T> ResponseData<T> success() {
        return new ResponseData<>(BizCodeEnum.SUCCESS.getCode(), BizCodeEnum.SUCCESS.getMessage());
    }

    /**
     * 成功但不携带返回参数
     */
    public static <T> ResponseData<T> success(String message) {
        return new ResponseData<>(BizCodeEnum.SUCCESS.getCode(), message);
    }

    /**
     * 成功,且携带返回参数
     * @param data 返回参数
     */
    public static <T> ResponseData<T> success(T data) {
        return new ResponseData<>(BizCodeEnum.SUCCESS.getCode(), BizCodeEnum.SUCCESS.getMessage(), data);
    }

    /**
     * 成功,且携带返回参数
     * @param data 返回参数
     */
    public static <T> ResponseData<T> success(String message, T data) {
        return new ResponseData<>(BizCodeEnum.SUCCESS.getCode(), message, data);
    }

    /**
     * 失败,自定义返回码且不包含返回参数
     *
     * @param code 返回码
     * @param message 返回信息
     */
    public static <T> ResponseData<T> error(int code, String message) {
        return new ResponseData<>(code, message);
    }

    /**
     * 失败,自定义返回码且包含返回参数
     * @param code 返回码
     * @param message 返回信息
     * @param data 返回参数
     */
    public static <T> ResponseData<T> error(int code, String message, T data) {
        return new ResponseData<>(code, message, data);
    }

    /**
     * 失败,使用枚举类返回码且不包含返回参数
     * @param bizCodeEnum 返回码枚举类
     */
    public static <T> ResponseData<T> error(BizCodeEnum bizCodeEnum) {
        return new ResponseData<>(bizCodeEnum.getCode(), bizCodeEnum.getMessage());
    }

    /**
     * 失败,使用枚举类返回码且包含返回参数
     *
     * @param bizCodeEnum 返回码枚举类
     * @param data 返回参数
     */
    public static <T> ResponseData<T> error(BizCodeEnum bizCodeEnum, T data) {
        return new ResponseData<>(bizCodeEnum.getCode(), bizCodeEnum.getMessage(), data);
    }
}

SpringBoot使用yaml文件

设置应用上下文路径

SpringBoot默认的上下文路径为空,可以通过下面方式设置项目的上下文路径:

server:
  servlet:
    context-path: /api

yaml文件中定义变量

如果yaml文件中存在多个地方使用同一个值,可以自定义变量通过${变量名}在多处引用。

# 定义变量
wenxuan:
  redis:
    ip: localhost
    
# 使用变量:通过 ${变量名} 的方式使用
spring:
  redis:
    host: ${wenxuan.redis.ip}

yaml设置Date的日期格式化

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss

SpringBoot读取yaml文件

  1. @Value方式

@Value:用于将值赋给一个类的属性或方法参数。通过@Value注解,可以在运行时将值注入到被注解的属性或方法参数中。

  • 属性注入:可以通过@Value注解将值注入到交由容器管理的类的属性中。
  • 方法参数注入:可以通过@Value注解将值注入到方法参数中。

属性注入

@RestController  
public class TestController {  
    @Value("${wenxuan.name}")  
    private String name;  
}

方法参数注入

@Service
public class UserService {
    private final String name;

    @Autowired
    public UserService (@Value("${wenxuan.name}") String name) {
        this.name = name;
    }
}
  1. @ConfigurationProperties方式

@ConfigurationProperties注解用于将类和某个配置文件进行绑定,可以自动获取配置文件的值(资源绑定),@ConfigurationProperties注解有一个属性prefix,指定获取配置文件中以什么开头的值。

在application.yaml配置文件中编写需要绑定的数据

car:  
  name: BYD  
  price: 10000

编写Bean类,定义需要绑定的属性

@ConfigurationProperties(prefix = "car")
@Component
public class Car {
    private String name;

    private Integer price;

    public void setName(String name) {
        this.name = name;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public Integer getPrice() {
        return price;
    }
}

SpringBoot整合各种中间件

SpringBoot整合数据源

数据源:给程序员提供Connection对象的,都叫做数据源(datasource)。数据源实际上是一套规范(接口),接口全路径名:javax.sql.DataSource(JDK规范)。所有的数据源都实现了DataSource接口,重写了接口中的方法。

添加maven依赖

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.32</version>
</dependency>

编写application.yaml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db_account
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver

驱动类driver-class-name

  • mysql-connector-java版本为5:driver-class-name: com.mysql.jdbc.Driver
  • mysql-connector-java版本为8:driver-class-name: com.mysql.cj.jdbc.Driver

连接地址url

  • mysql-connector-java版本为5:jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&useSSL=false
  • mysql-connector-java版本为8:jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false

SpringBoot整合第三方数据源

常见的第三方数据源:druid、c3p0、dbcp。

添加maven依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.17</version>
</dependency>

在application.yaml配置文件中切换数据源

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db_account
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

druid数据源其他配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    # 初始化时建立物理连接的个数
    initialSize: 5
    # 最小连接池数量
    minIdle: 5
    # 最大连接池数量
    maxActive: 201
    # 获取连接时最大等待时间,单位毫秒
    maxWait: 60000
    # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
    timeBetweenEvictionRunsMillis: 60000
    # 连接保持空闲而不被驱逐的最小时间
    minEvictableIdleTimeMillis: 300000
    # 用来检测连接是否有效的sql,要求是一个查询语句
    validationQuery: SELECT 1 FROM DUAL
    # 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
    testWhileIdle: true
    # 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
    testOnBorrow: false
    # 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
    testOnReturn: false
    # 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
    poolPreparedStatements: true
    # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    filters: stat,wall,log4j
    # 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。
    maxPoolPreparedStatementPerConnectionSize: 20
    # 合并多个DruidDataSource的监控数据
    useGlobalDataSourceStat: true
    # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

SpringBoot整合Redis

  1. 添加Maven依赖
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-data-redis</artifactId>  
</dependency>
  1. 注册Redis连接信息
spring:
	redis:
		host: redis
		port: 6379
		password: 123456
		database: 0				# 指定操作的0号数据库
	jedis:
		pool:
			# Redis连接池配置
			max-active: 8		# 最大连接数
			max-wait: 1ms		# 连接池最大阻塞等待时间
			max-idle: 4		# 连接池中的最大空闲连接数
			min-idle: 0		# 连接池最小空闲连接
  1. 添加Redis工具类
/**
 * @author 文轩
 * @create 2024-03-03 17:15
 *
 * Redis缓存工具类
 */
@SuppressWarnings(value = { "all"})
@Component
public class RedisCacheUtil {
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout) {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit) {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key) {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key) {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection) {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList) {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key) {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext()) {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key) {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey) {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 删除Hash中的数据
     *
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey) {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern) {
        return redisTemplate.keys(pattern);
    }
}

SpringBoot整合Redisson

Redisson是一个基于Redis的Java客户端,它提供了一系列的分布式Java对象和服务,以便于在Java应用程序中使用分布式锁、分布式集合、分布式Map、分布式队列等分布式数据结构和分布式服务。

Redisson官方文档:github.com/redisson/re…

  1. 添加maven依赖
<dependency>  
    <groupId>org.redisson</groupId>  
    <artifactId>redisson</artifactId>  
</dependency>
  1. 编写配置类
@Configuration  
public class MyRedissonConfig {  
  
    @Bean(destroyMethod = "shutdown")  
    public RedissonClient redisson() {  
        Config config = new Config();  
        config.useSingleServer().setAddress("redis://localhost.84:6379");  

        RedissonClient redissonClient = Redisson.create(config);  
        return redissonClient;  
    }  
}
  1. 使用RedissonClient
@Controller
public class IndexController {

    @Autowired
    RedissonClient redissonClient;

    @ResponseBody
    @RequestMapping("/hello")
    public String hello() {
        // 获取一把锁
        RLock lock = redissonClient.getLock("lock");

        // 加锁,加锁失败会阻塞式等待
        lock.lock();
        try {
            // 执行业务代码
            Thread.sleep(30000);
        } catch (Exception e) {

        } finally {
            // 解锁
            lock.unlock();
        }
        return "hello";
    }
}

SpringBoot整合SpringCache

  1. 添加maven依赖
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-data-redis</artifactId>  
</dependency>

<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-cache</artifactId>  
</dependency>
  1. 编写配置信息
spring:
  redis:
    host: localhost
    password: 123456
    port: 6379
  cache:
    type: redis
    redis:  
        time-to-live: 3600000 # 有效时间,毫秒为单位  
        key-prefix: CACHE_ # KEY前缀  
        use-key-prefix: true # 是否使用前缀  
        cache-null-values: true # 是否缓存空值,防止缓存穿透
  1. 编写SpringCache的配置类
@Configuration
@EnableCaching
@EnableConfigurationProperties(CacheProperties.class)
public class MyCacheConfig {

    @Bean
    RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();

        // 配置数据序列化
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        // 设置配置文件的数据
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        if(redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if(redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if(redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if(!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }

        return config;
    }
}
  1. 使用SpringCache,SpringCache提供注解方式使用缓存
  • @Cacheable:被注解的方法在执行前会先查询缓存,如果缓存中存在对应的数据,则直接返回缓存数据,不会执行方法体的代码。如果缓存中不存在对应的数据,则将方法返回结果添加到缓存中。
  • @CacheEvict:被注解的方法执行后会清空缓存中的对应数据,可以配置在方法执行前或者执行后清空缓存。
  • @CachePut:被注解的方法会执行方法体的代码,并将返回值更新到缓存中,即每次方法调用都会执行方法体并更新缓存。
  • @Caching:用于组合多个缓存操作的注解,可以在同一个方法上同时使用多个缓存注解。
  • @CacheConfig:用于在类级别上定义缓存的一些公共配置,如缓存的名称、缓存管理器等。

SpringBoot整合日志

  1. 添加maven依赖
<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.20</version>
    </dependency>

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.36</version>
    </dependency>
</dependencies>
  1. 添加log4j的配置文件
# log4j的设置  
log4j.rootLogger=info,stdout,D,E  
  
# 日志信息默认输出到控制抬  
log4j.appender.stdout=org.apache.log4j.ConsoleAppender  
log4j.appender.stdout.Target=System.out  
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout  
log4j.appender.stdout.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n  
  
# DEBUG 级别以上的日志输出到 logs/debug.log  
log4j.appender.D=org.apache.log4j.DailyRollingFileAppender  
log4j.appender.D.File=/logs/debug.log  
log4j.appender.D.Append=true  
log4j.appender.D.Threshold=DEBUG  
log4j.appender.D.layout=org.apache.log4j.PatternLayout  
log4j.appender.D.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n  
  
# ERROR 级别以上的日志到 logs/error.log  
log4j.appender.E=org.apache.log4j.DailyRollingFileAppender  
log4j.appender.E.File=/logs/error.log  
log4j.appender.E.Append=true  
log4j.appender.E.Threshold=ERROR  
log4j.appender.E.layout=org.apache.log4j.PatternLayout  
log4j.appender.E.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
  1. @Slf4j的使用
package com.wenxuan.test;  
  
import lombok.extern.slf4j.Slf4j;  
  
/**  
* @author 文轩  
* @create 2024-01-17 18:40  
*/  
@Slf4j  
public class Test {  
    public static void main(String[] args) {  
        log.error("error");  
    }  
}

SpringBoot整合Swagger

Swagger官网:swagger.io/

  1. 添加依赖
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>3.0.0</version>
</dependency>
  1. 解决Spring2.6以上匹配规则错误
spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
  1. 编写Swagger配置类
@Configuration
@EnableOpenApi
public class SwaggerConfig {
    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.OAS_30)
                .apiInfo(apiInfo()).enable(true)
                .select()
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("wenxuan-Swagger")       // 文档标题
                .description("文轩编程")         // 文档描述信息
                // 文档作者信息
                .contact(new Contact("文轩", "https://xxx","xxx@163.com"))
                // 文档版本号
                .version("v1.0")
                .build();
    }
}
  1. 编写接口
@Api(tags = "测试 Swagger")
@RestController
@RequestMapping("/swagger")
public class SwaggerController {

    @ApiOperation("test")
    @RequestMapping("/test")
    public String testNoParam() {
        return "wenxuan";
    }
}
  1. Swagger常用注解
  • @Api:用在Controller类上,说明该类的作用,通过tags标签指定某一类接口。
  • @ApiOperation:用在请求方法上,说明请求方法的作用,每一个接口的定义。
  • @ApiImplicitParam 和 @ApiImplicitParams:用于请求方法上,为单独的请求参数进行说明。
    • name:参数名,指定参数的名称
    • value:参数描述,用于描述参数的作用。
    • required:参数是否必传,默认为 false,设置为 true 表示必传。
    • dataTypeClass:参数的数据类型,可以是基本数据类型(如 int、String)也可以是复杂数据类型(如自定义类、集合等)
    • paramType:参数类型,包括 path(路径参数)、query(查询参数)、header(请求头参数)等。
    • example:参数的示例值,用于展示参数的示例。
  • @ApiModel:用在Modal类上,表示对类进行说明,用于实体类中的参数接收或接口响应说明。
  • @ApiModelProperty:用于Modal类的字段上,表示对 model 属性的说明。
    • value:属性描述,用于描述属性的作用。
    • name:属性名称,指定属性的名称。
    • dataType:属性的数据类型,可以是基本数据类型(如 int、String)也可以是复杂数据类型(如自定义类、集合等)。
    • required:属性是否必须,默认为 false,设置为 true 表示必须。
    • example:属性的示例值,用于展示属性的示例。
    • hidden:属性是否隐藏,默认为 false,设置为 true 表示在文档中隐藏该属性。
    • allowEmptyValue:属性是否允许为空值,默认为 false,设置为 true 表示允许为空值。
    • notes:属性的额外说明,用于对属性进行额外描述。
  • @ApiIgnore:用于请求方法的参数上,用来忽视自定义类型的参数。

注意:

  1. query参数存在多个参数,则通过使用@ApiImplicitParam注解实现,而无法通过@ApiModelProperty注解实现。
  2. query参数如果是单个单个接收,则无需使用@ApiIgnore注解。但如果是将多个参数封装成VO,则需要通过@ApiIgnore注解将请求方法上的参数忽略。
  3. body参数存在多个参数,则通过@ApiModelProperty注解实现,而无法通过@ApiImplicitParam注解实现。
  1. 实现分组

实现分组只需要修改Config配置类即可,将哪些Controller设置为同一个分组下面,那就在apis中添加即可。

@Configuration
@EnableOpenApi
public class SwaggerConfig {
    @Bean
    public Docket group1() {
        return new Docket(DocumentationType.OAS_30)
                .groupName("分组1")
                .apiInfo(apiInfo()).enable(true)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.wenxuan.web.controller1")
                // 可以添加扫描多个包下的Controller
                .or(RequestHandlerSelectors.basePackage("com.wenxuan.web.controller2")))
                .build();
    }

    @Bean
    public Docket group2() {
        return new Docket(DocumentationType.OAS_30)
                .groupName("分组2")
                .apiInfo(apiInfo()).enable(true)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.wenxuan.web.controller3"))
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("wenxuan-Swagger")       // 文档标题
                .description("文轩编程")         // 文档描述信息
                // 文档作者信息
                .contact(new Contact("文轩", "https://xxx","xxx@163.com"))
                // 文档版本号
                .version("v1.0")
                .build();
    }
}
  1. 访问接口文档:http://localhost:8080/swagger-ui/index.html

SpringBoot整合Knife4

Knife4官网:doc.xiaominfo.com/knife4j/doc…

knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui,取名knife4j是希望它能像一把匕首一样小巧、轻量、并且功能强悍。其底层是对Springfox的封装,使用方式也和Springfox一致,只是对接口文档UI进行了优化。

  1. 添加Maven依赖
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>
  1. 解决Spring2.6以上匹配规则错误
spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
  1. 编写Swagger配置类
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;

@Configuration
@EnableOpenApi
public class SwaggerConfig {
    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.OAS_30)
                .apiInfo(apiInfo()).enable(true)
                .select()
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("wenxuan-Swagger")       // 文档标题
                .description("文轩编程")         // 文档描述信息
                // 文档作者信息
                .contact(new Contact("文轩", "https://xxx","xxx@163.com"))
                // 文档版本号
                .version("v1.0")
                .build();
    }
}
  1. 编写接口
@Api(tags = "测试 Swagger")
@RestController
@RequestMapping("/swagger")
public class SwaggerController {

    @ApiOperation("test")
    @RequestMapping("/test")
    public String testNoParam() {
        return "wenxuan";
    }
}
  1. Swagger常用注解
  • @Api:用在Controller类上,说明该类的作用,通过tags标签指定某一类接口。
  • @ApiOperation:用在请求方法上,说明请求方法的作用,每一个接口的定义。
  • @ApiImplicitParam 和 @ApiImplicitParams:用于请求方法上,为单独的请求参数进行说明。
    • name:参数名,指定参数的名称
    • value:参数描述,用于描述参数的作用。
    • required:参数是否必传,默认为 false,设置为 true 表示必传。
    • dataTypeClass:参数的数据类型,可以是基本数据类型(如 int、String)也可以是复杂数据类型(如自定义类、集合等)
    • paramType:参数类型,包括 path(路径参数)、query(查询参数)、header(请求头参数)等。
    • example:参数的示例值,用于展示参数的示例。
  • @ApiModel:用在Modal类上,表示对类进行说明,用于实体类中的参数接收或接口响应说明。
  • @ApiModelProperty:用于Modal类的字段上,表示对 model 属性的说明。
    • value:属性描述,用于描述属性的作用。
    • name:属性名称,指定属性的名称。
    • dataType:属性的数据类型,可以是基本数据类型(如 int、String)也可以是复杂数据类型(如自定义类、集合等)。
    • required:属性是否必须,默认为 false,设置为 true 表示必须。
    • example:属性的示例值,用于展示属性的示例。
    • hidden:属性是否隐藏,默认为 false,设置为 true 表示在文档中隐藏该属性。
    • allowEmptyValue:属性是否允许为空值,默认为 false,设置为 true 表示允许为空值。
    • notes:属性的额外说明,用于对属性进行额外描述。
  • @ApiIgnore:用于请求方法的参数上,用来忽视自定义类型的参数。

注意:

  1. query参数存在多个参数,则通过使用@ApiImplicitParam注解实现,而无法通过@ApiModelProperty注解实现。
  2. query参数如果是单个单个接收,则无需使用@ApiIgnore注解。但如果是将多个参数封装成VO,则需要通过@ApiIgnore注解将请求方法上的参数忽略。
  3. body参数存在多个参数,则通过@ApiModelProperty注解实现,而无法通过@ApiImplicitParam注解实现。
  1. 实现分组

实现分组只需要修改Config配置类即可,将哪些Controller设置为同一个分组下面,那就在apis中添加即可。

@Configuration
@EnableOpenApi
public class SwaggerConfig {
    @Bean
    public Docket group1() {
        return new Docket(DocumentationType.OAS_30)
                .groupName("分组1")
                .apiInfo(apiInfo()).enable(true)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.wenxuan.web.controller1")
                // 可以添加扫描多个包下的Controller
                .or(RequestHandlerSelectors.basePackage("com.wenxuan.web.controller2")))
                .build();
    }

    @Bean
    public Docket group2() {
        return new Docket(DocumentationType.OAS_30)
                .groupName("分组2")
                .apiInfo(apiInfo()).enable(true)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.wenxuan.web.controller3"))
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("wenxuan-Swagger")       // 文档标题
                .description("文轩编程")         // 文档描述信息
                // 文档作者信息
                .contact(new Contact("文轩", "https://xxx","xxx@163.com"))
                // 文档版本号
                .version("v1.0")
                .build();
    }
}
  1. 访问接口文档:http://ip:port/doc.html

SpringBoot整合Validation

Validation:用于验证数据的机制,通常用于确保用户输入或传入的数据符合特定的规则或格式。在Web应用程序中,经常需要对用户提交的表单数据进行验证,以确保数据的完整性、正确性和安全性。

  1. 添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  1. 给TO中添加注解
public class AddUserVO {
    @Range(min = 0, message = "年龄不合法")
    private Integer age;
    @NotEmpty(message = "姓名不能为空")
    private String name;
    @Email(message = "邮箱不合法")
    private String email;
}
  1. 在Controller中开启校验
@PostMapping("save")
public String saveUser(@Validated @RequestBody AddUserVO addUserVO) {
    // 处理逻辑
    return "success";
}
  1. 校验异常统一处理

目的:使得当参数校验出现异常时,给前端的返回值也是符合程序的标准

编写校验统一处理前提是需要先添加SpringBoot接口统一响应类的

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseData handle(MethodArgumentNotValidException e) {
        List<FieldError> fieldErrors = e.getFieldErrors();
        List<ValidMessage> validMessages = new ArrayList<>();
        fieldErrors.forEach(item -> {
            String field = item.getField();
            String defaultMessage = item.getDefaultMessage();
            validMessages.add(new ValidMessage(field, defaultMessage));
        });
        return ResponseData.error(BizCodeEnum.VALID_EXCEPTION, validMessages);
    }
}

异常消息类如下:

@Data
@AllArgsConstructor
public class ValidMessage {
    private String field;
    private String message;
}
  1. Validation常用注解

作用在Bean类上的注解

注解说明
@AssertTrue用于boolean字段,该字段只能为true
@AssertFalse用于boolean字段,该字段只能为false
@CreditCardNumber对信用卡号进行一个大致的验证
@DecimalMax只能小于或等于该值
@DecimalMin只能大于或等于该值
@Email检查是否是一个有效的email地址
@Future检查该字段的日期是否是属于将来的日期
@Length(min=,max=)检查所属的字段的长度是否在min和max之间,只能用于字符串
@Max该字段的值只能小于或等于该值
@Min该字段的值只能大于或等于该值
@NotNull不能为null,用在任意类型的属性上
@NotBlank不能为NULL,但可以为空字符串,用在字符串属性上
@NotEmpty不能为NULL也不能为空字符串,只能用在字符串属性上
@Pattern(regexp=)被注释的元素必须符合指定的正则表达式
@URL(protocol=,host,port)检查是否是一个有效的URL,如果提供了protocol,host等,则该URL还需满足提供的条件
  1. 自定义校验注解

当Validation中提供的注解中不满足要求时,可以自定义校验注解实现校验功能。

比如实现一个校验需要满足指定值的注解:

编写一个自定义的校验注解

@Documented
// 关联自定义的校验器和自定义的校验注解,自定义校验器在下面编写的,这里可以填写多个
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
    // 设置校验失败默认的提示信息
    String message() default "不满足值";

    // 表示可以分组
    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    int[] vals() default {};
}

编写一个自定义校验器:校验的逻辑在这里实现

public class ListValueConstraintValidator 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);
    }
}

使用自定义校验注解

@ListValue(vals = {0, 1}, message = "显示状态不满足指定值")
private Integer showStatus;

SpringBoot整合Quartz

Quartz 是一个功能强大的开源任务调度框架,广泛应用于 Java 应用程序中。它提供了一套丰富的 API,用于创建、管理和调度定时任务。

  1. 添加Maven依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
  1. 添加yml配置
spring:
  quartz:
    job-store-type: jdbc # memory:表示使用内存存储任务,jdbc:表示使用数据库存储任务
    wait-for-jobs-to-complete-on-shutdown: true # 关闭时等待任务完成
    overwrite-existing-jobs: true # 可以覆盖已有的任务
    jdbc:
      # 控制Quartz是否在启动时初始化数据库表结构
      # embedded:表示Quartz将自动检查数据库中的相关表结构是否存在,如果不存在,则尝试创建它们。如果存在相关表结构,则不会进行任何修改。这是一种适用于嵌入式数据库的策略。
      # always:表示每次应用启动时,Quartz都会删除并重新创建相关表结构,即使数据库中已经存在这些表结构。这是一种适用于开发和测试环境的策略。
      # never:表示Quartz不会进行任何数据库表结构的初始化操作。这要求你确保数据库中已存在Quartz所需的表结构。这种策略适用于生产环境,其中通常会使用专门的数据库脚本或其他手动方式来管理Quartz表结构。
      initialize-schema: never
    properties:
      org:
        quartz:
          scheduler:
            instanceName: scheduler # 调度器实例名称
            instanceId: AUTO # 调度器实例ID自动生成
          jobStore:
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate # 使用完全兼容JDBC的驱动
            tablePrefix: QRTZ_ # QuartZ 表前缀
            useProperties: false # 取消将JobDataMap中的属性转为字符串存储
          # 线程池相关配置
          threadPool:
            threadCount: 25 # 线程池大小。默认为 10 。
            threadPriority: 5 # 线程优先级
            class: org.quartz.simpl.SimpleThreadPool
  1. 为任务类编写配置类
@Configuration
public class QuartzDeleteExpireLinkJobConfig {
    @Bean
    public JobDetail quartzTestJobDetail() {
        /**
         * 通过JobBuilder来创建一个Job实例
         * withIdentity(QuartzTestJob.class.getSimpleName()):为Job指定一个唯一的标识
         * storeDurably:设置Job为持久化的,即使没有被任何触发器关联也不会被删除
         * usingJobData("key", "value"):
         *  可以传值到Job中通过jobExecutionContext.getJobDetail().getJobDataMap().get("key")获取value值
         */
        return JobBuilder.newJob(QuartzDeleteExpireLinkJob.class)
                .withIdentity(QuartzDeleteExpireLinkJob.class.getSimpleName())
                .storeDurably()
                .build();
    }

    @Bean
    public Trigger quartzTestJobTrigger() {
        /**
         * 通过TriggerBuilder来创建一个Trigger实例
         * forJob(QuartzTestJob.class.getSimpleName()):指定要联接的Job的唯一标识
         * withIdentity(QuartzTestJob.class.getSimpleName()):为Trigger指定一个唯一的标识
         * withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1).repeatForever()):
         *      配置触发器的调度规则。这里使用了简单调度规则,即每隔1秒触发一次,并且无限重复执行
         * withSchedule(CronScheduleBuilder.cronSchedule("0 0 0 * * ?")):
         *      配置触发器的调度规则。这里使用了Cron表达式,表示每天的午夜(00:0000)触发一次。
         */
        return TriggerBuilder.newTrigger()
                .forJob(QuartzDeleteExpireLinkJob.class.getSimpleName())
                .withIdentity(QuartzDeleteExpireLinkJob.class.getSimpleName())
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1).repeatForever())
                .build();
    }
}
  1. 编写Service类
/**
 * @author 文轩
 * @create 2023-09-04 22:30
 */
@Service
public class QuartzService {


    public int deleteExpireLinkMappingDataTask() {
        System.out.println("执行...");
        return 1;
    }
}
  1. 编写任务类
@Component
public class QuartzDeleteExpireLinkJob extends QuartzJobBean {

    @Resource
    private QuartzService quartzService;


    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        quartzService.deleteExpireLinkMappingDataTask();
    }
}
  1. 将SQL文件执行

  2. Cron 表达式

Cron的语法:Seconds Minutes Hours DayofMonth Month DayofWeek

Cron举例

示例说明
0 0 0 * * *每天上午00:00执行任务
0 0 0,12,18 * * *每天0点、12 点、18点执行任务
0 0 12 * * 1每个星期1中午 12 点执行任务
0 15 10 5 * *每月 5 日上午 10 点 15 执行任务

Cron中的特殊字符

特殊字符含义解释
*所有可能的值比如每个月、每个月的每天
,列举多个值0,12,18表示0、12、18这些值
-范围值DayofMonth取值1-3,表示每月的1日到3日
/间隔值Minutes取值0、15,表示每隔15分钟执行一次
L最后值DayofMonth和DayofWeek可以使用,表示每月的最后一天或每周的最后一天

SpringBoot整合SpringTask

SpringBoot中已经集成了SpringTask,所以无需添加任何的依赖。

  1. 启用SpringTask,在启动类上添加
@EnableScheduling
  1. 编写定时任务
@Component
public class CronTask {
    // 这边可以编写corn表达式,每天凌晨十二点
    @Scheduled(cron = "0 0 0 * * *")
    public void cron() {
        log.info("执行定时任务...");
    }
}

SpringBoot整合hutool实现文件上传下载

  1. 添加相关依赖
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.17</version>
    <relativePath/>
</parent>

<dependencies>
    <!--spring-boot-web依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!--hutool-core依赖-->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-core</artifactId>
        <version>5.8.32</version>
    </dependency>

    <!--lombok依赖-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.20</version>
    </dependency>

</dependencies>
  1. 编写配置文件
server:
  port: 8080

params:
  # 文件上传路径,可根据需要修改
  fileDir: C:/temp/images
  1. 定义相关的Bean
  • 定义统一的响应状态码:参考上面《SpringBoot业务状态码规范》
  • 定义统一的返回信息:参考上面《SpringBoot接口统一响应》
  1. 实现文件上传和下载功能的接口
@RestController
@RequestMapping("/file")
public class FileLoadController {

    @Value("${params.fileDir}")
    private String fileDir;

    @Value("${params.fileDownloadUrl}")
    private String fileDownloadUrl;

    /**
     * 获取文件下载的路径
     * @param fileName 文件名称
     */
    private String getFileDownloadUrl(String fileName, HttpServletRequest request) {
        // 构建完整的 URL: http://localhost:8080/admin/
        String fullUrl = request.getScheme() + "://" +
                request.getServerName() + ":" +
                request.getServerPort() +
                request.getContextPath();
        fullUrl = fullUrl + fileDownloadUrl + "/" + fileName;
        return fullUrl;
    }

    /**
     * 根据文件类型获取文件保存路径
     * @param fileName 文件名称
     */
    private String getFilePath(String fileName) {
        // 获取文件原始名称
        if (fileName == null || !fileName.contains(".")) {
            return "others";
        }
        // 获取文件扩展名
        String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
        String fileType = null;

        switch (fileExtension) {
            case "jpg":
            case "jpeg":
            case "png":
            case "gif":
            case "svg":
                fileType = "images";
                break;
            case "txt":
            case "doc":
            case "docx":
            case "pdf":
                fileType = "documents";
                break;
            case "mp4":
            case "avi":
            case "mkv":
                fileType = "videos";
                break;
            default:
                fileType = "others";
                break;
        }
        return fileDir + "/" + fileType + "/" + fileName;
    }

    /**
     * 上传单个文件接口
     * @param file 文件名称
     */
    @PostMapping("/upload")
    public ResponseData<Object> uploadFile(@RequestParam("file") MultipartFile file, HttpServletRequest request) {
        if (file.isEmpty()) {
            throw new RuntimeException("上传的文件参数有误");
        }

        try {
            // 保存文件到指定路径
            String fileName = file.getOriginalFilename(); // 获取原始文件名
            // 生成新的文件名
            String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
            fileName = System.currentTimeMillis() + "." + fileExtension;
            File savedFile = FileUtil.writeFromStream(file.getInputStream(), getFilePath(fileName));

            // 拼接文件下载的路径
            String url = getFileDownloadUrl(fileName, request);
            Map<String, String> map = new HashMap<>();
            map.put("url", url);
            return ResponseData.success(map);
        } catch (Exception e) {
            throw new RuntimeException("文件写入失败");
        }
    }

    /**
     * 上传多个文件接口
     * @param files 文件名称
     */
    @PostMapping("/uploadMultiple")
    public ResponseData<List<String>> uploadFiles(@RequestParam("files") MultipartFile[] files, HttpServletRequest request) {
        if (files.length == 0) {
            // No files provided
            throw new RuntimeException("上传的文件参数有误");
        }

        List<String> urls = new ArrayList<>();
        try {
            for (MultipartFile file : files) {
                if (!file.isEmpty()) {
                    // 保存文件到指定路径
                    String fileName = file.getOriginalFilename(); // 获取原始文件名
                    // 生成新的文件名
                    String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
                    fileName = System.currentTimeMillis() + "." + fileExtension;
                    File savedFile = FileUtil.writeFromStream(file.getInputStream(), getFilePath(fileName));

                    // 拼接文件下载的路径
                    String url = getFileDownloadUrl(fileName, request);
                    urls.add(url);
                } else {
                    // 某个文件写入失败
                }
            }
            return ResponseData.success(urls);
        } catch (Exception e) {
            throw new RuntimeException("文件上传失败");
        }
    }


    /**
     * 文件下载接口
     * @param fileName 文件名称
     */
    @GetMapping("/download/{fileName}")
    public void downloadFile(@PathVariable("fileName") String fileName, HttpServletResponse response) {
        File file = new File(getFilePath(fileName));

        if (!file.exists()) {
            throw new RuntimeException(fileName + "文件不存在");
        }

        try {
            // 设置响应头
            response.setContentType("application/octet-stream");
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));

            // 将文件内容写入响应输出流
            FileUtil.writeToStream(file, response.getOutputStream());
        } catch (Exception e) {
            throw new RuntimeException("文件下载失败");
        }
    }

    /**
     * 文件输出到浏览器
     * @param fileName 文件名称
     */
    @GetMapping("/view/{fileName}")
    public void viewFile(@PathVariable("fileName") String fileName, HttpServletResponse response) {
        File file = new File(getFilePath(fileName));

        if (!file.exists()) {
            throw new RuntimeException(fileName + "文件不存在");
        }

        try {
            // 设置响应的Content-Type,根据文件类型动态设置
            String contentType = Files.probeContentType(file.toPath()); // 自动检测文件类型
            if (contentType == null) {
                contentType = "application/octet-stream"; // 默认类型
            }
            response.setContentType(contentType);

            // 将文件流写入响应
            FileUtil.writeToStream(file, response.getOutputStream());
        } catch (Exception e) {
            throw new RuntimeException("文件下载失败");
        }
    }
}

SpringBoot整合Mybatis

  1. 添加maven依赖
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.17</version>
    <relativePath/>
</parent>


<dependencies>
    <!--添加spring-boot-starter-web依赖-->
    <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.3</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
</dependencies>

  1. 编写yml配置
server:
  port: 8900

mybatis:
  # 配置mapper映射文件
  mapper-locations: classpath:mybatis/mapper/*.xml
  # 配置mybatis配置文件
  config-location: classpath:mybatis/mybatis-config.xml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3308/test?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456

# 日志级别可以是:info、debug、error
logging:
  level:
    com.wenxuan.mapper: debug
  1. mybatis配置文件:/resource/mybatis/mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>
</configuration>

  1. Controller的编写
@RestController 
public class UserController { 
    @Autowired private UserService userService; 
    
    @PostMapping("/save") 
    public User save(@RequestBody User user) { 
        return userService.save(user); 
    } 
}
  1. Service编写
@Service
public class UserService {
    @Resource
    private UserMapper userMapper;

    public void save(User user) {
        userMapper.save(user);
    }
}
  1. Mapper的编写
@Mapper
public interface UserMapper {
    void save(User user);
}
  1. Mapper对应配置文件

Mapper的路径:mybatis/mapper/UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wenxuan.mapper.UserMapper">

    <!--Bean属性名和表字段名的对应关系-->
    <resultMap id="userMapper" type="com.wenxuan.bean.User">
        <result property="id" column="id"/>
        <result property="name" column="name"/>
    </resultMap>

    <insert id="save" parameterType="com.wenxuan.bean.User">
        insert into user(id, name)
        values (#{id}, #{name})
    </insert>
</mapper>

  1. 编写启动类
@SpringBootApplication
@MapperScan("com.wenxuan.mapper")
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

SpringBoot整合Mybatis实现多数据源

  1. 添加Maven依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</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.3</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>

<dependency>
    <groupId>com.microsoft.sqlserver</groupId>
    <artifactId>mssql-jdbc</artifactId>
    <version>8.2.2.jre8</version>
</dependency>
  1. 编写数据源信息
spring:
  datasource:
    mysql:  # MySQL数据源信息
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false
      username: root
      password: 123456

    sqlserver:  # SQLServer数据源信息
      jdbc-url: jdbc:sqlserver://localhost:1433;databaseName=db-test
      username: sa
      password: wenxuan@2001
      driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
  1. 编写启动类
@SpringBootApplication
@MapperScan("com.wenxuan.mapper")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  1. 编写多数据源的配置类

MySQL对应的数据源配置类

@Configuration
// 指定数据源对应的mapper接口位置
@MapperScan(basePackages = "com.wenxuan.mapper.mysql", sqlSessionFactoryRef = "mysqlSqlSessionFactory")
public class MySQLDataSourceConfig {

    @Bean(name = "mysqlDataSource")
    // 指定数据源配置前缀
    @ConfigurationProperties(prefix = "spring.datasource.mysql")
    public DataSource mysqlDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "mysqlSqlSessionFactory")
    public SqlSessionFactory mysqlSqlSessionFactory(@Qualifier("mysqlDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        // 指定数据源对应的mapper xml文件位置
        sessionFactory.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/mysql/*.xml"));
        return sessionFactory.getObject();
    }

    @Bean(name = "mysqlSqlSessionTemplate")
    public SqlSessionTemplate mysqlSqlSessionTemplate(@Qualifier("mysqlSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

SQLServer数据源配置类

@Configuration
// 指定数据源对应的mapper接口位置
@MapperScan(basePackages = "com.wenxuan.mapper.sqlserver", sqlSessionFactoryRef = "sqlServerSqlSessionFactory")
public class SQLServerDataSourceConfig {

    @Bean(name = "sqlServerDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.sqlserver")
    public DataSource sqlServerDataSource() {
        return DataSourceBuilder.create().build();
    }

    // 指定数据源配置前缀
    @Bean(name = "sqlServerSqlSessionFactory")
    public SqlSessionFactory sqlServerSqlSessionFactory(@Qualifier("sqlServerDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        // 指定数据源对应的mapper xml文件位置
        sessionFactory.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/sqlserver/*.xml"));
        return sessionFactory.getObject();
    }

    @Bean(name = "sqlServerSqlSessionTemplate")
    public SqlSessionTemplate sqlServerSqlSessionTemplate(@Qualifier("sqlServerSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}