Spring + SpringMVC + SpringBoot

SpringBoot对IOC的实现

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

声明组件的注解

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

@Controller

@Controller:将一个类标识为请求处理的控制器组件,用于接收用户的请求并处理相关的业务逻辑,最后返回相应的结果。

java 复制代码
/**  
* @author 文轩  
* @create 2023-11-23 21:23  
*/
@Controller
public class UserController {
}

@Service

@Service:将一个类标识为业务逻辑层(Service)组件,用于封装复杂的业务操作,并提供事务管理、依赖注入等功能。

java 复制代码
/**  
* @author 文轩  
* @create 2023-11-23 21:54  
*/  
@Service  
public class UserService {
}

@Repository

@Repository:将一个类标识为持久层(Repository)组件,主要用于实现数据访问和持久化操作。

java 复制代码
/**  
* @author 文轩  
* @create 2023-11-23 21:24  
*/  
@Repository  
public class UserRepository {  
}

@Component

@Component注解只是一个通用的组件注解,并没有特定的业务含义。

java 复制代码
@Component
public class MyComponent {
}

@Mapper

@Mapper注解是MyBatis框架提供的一个注解,用于标识一个接口为Mapper接口,提供了数据库持久化操作的映射规则。

java 复制代码
/**  
* @author 文轩  
* @create 2023-11-23 21:24  
*/  
@Mapper  
public interface UserMapper {
}

负责注入的注解

@Value

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

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

属性注入

java 复制代码
/**  
* @author 文轩  
* @create 2023-11-25 9:50  
*/  
@RestController  
public class TestController {  
    @Value("${wenxuan.name}")  
    private String name;  
}

方法参数注入

java 复制代码
@Service
public class UserService {
    private final String name;

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

@Autowired

@AutoWired注解可以用来注入非简单类型,被翻译为自动装配,单独使用@AutoWired注解,默认是根据类型自动装配(byType)。

@AutoWired注解有一个属性:required

  • 如果该属性值为true,则表示被注入的Bean必须存在,如果不存在则报错。
  • 如果该属性值为false,则表示被注入的Bean是否存在都可以,如果存在则注入,如果不存在也不报错。
java 复制代码
/**  
* @author 文轩  
* @create 2023-11-23 21:23  
*/  
@RestController  
public class UserController {  
    @Autowired  
    private UserService userService;
}

@Quafier

@Quafier注解可以用来注入非简单类型,用来指定注入的Bean的名称,也就是Bean的id(byName)。

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

java 复制代码
/**  
* @author 文轩  
* @create 2023-11-23 22:23  
*/  
@RestController  
public class UserController {  
    @Autowired  
    private UserService userService;
}

@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生命周期分为五步:

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

编写Bean

java 复制代码
package com.wenxuan.spring.bean;

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的生命周期之七步:

  1. 实例化Bean,调用无参构造函数
  2. 给Bean属性赋值,调用set方法
  3. Bean前处理器before
  4. 初始化Bean,调用initBean方法(手动编写且配置)
  5. Bean后处理器after
  6. 使用Bean
  7. 销毁Bean,调用destroyBean方法(手动编写且配置)
java 复制代码
package com.wenxuan.spring.bean;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

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;
    }
}

Bean的生命周期之十步

Aware的相关接口有:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware

  • 当Bean实现BeanNameAware接口时,Spring会将Bean的名字(Bean标签中的id属性值)传递给该接口的setBeanName方法,并且在第三步时Spring自动调用setBeanName方法。
  • 当Bean实现BeanClassLoader接口时,Spring会将该Bean的类加载器传递给该接口的setBeanClassLoader方法,并且在第三步时Spring会自动调用setBeanClassLoader方法。
  • 当Bean实现BeanFactroy接口时,Spring会将该Bean的工厂类对象传递给该接口的setBeanFactroy方法,并且在第三步时Spring会自动调用setBeanFactroy方法。

new对象放入Spring容器中

java 复制代码
package com.wenxuan.spring.test;

import com.powernode.spring.bean.User;
import org.junit.Test;
import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;

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的七大术语

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

AOP的使用

添加maven依赖

xml 复制代码
<!-- 引入aop支持 -->  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-aop</artifactId>  
    <version>2.7.17</version>  
</dependency>

定义切面类

切面类需要使用@Aspect注解和@Component注解标识

java 复制代码
package com.wenxuan.aop;

import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * @author 文轩
 * @create 2023-11-25 12:27
 */
@Aspect
@Component
public class ExceptionAOP {
}

在切面类中定义切入点

java 复制代码
package com.wenxuan.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * @author 文轩
 * @create 2023-11-25 12:27
 */
@Aspect
@Component
public class ExceptionAOP {
    // value编写表达式定义切入点,标识某一类方法,在通知中添加切入点,这些标识的方法就会得到对应的通知
    @Pointcut(value = "execution(* com.wenxuan.service..*(..))")
    public void pointCut() {
    }
}

在切面类中定义通知

在通知中标识哪些切点得到增强的通知

java 复制代码
package com.wenxuan.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * @author 文轩
 * @create 2023-11-25 12:27
 */
@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类型的方法。
  • 异常:可选项,省略时表示任意异常类型。
java 复制代码
// 切点表达式的语法格式:
execution([访问控制修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])

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

AOP通知的分类

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

前置通知

前置通知通过@Before注解进行定义,切点执行前会执行的语句。

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

后置通知

前置通知通过@AfterReturningr注解进行定义,当切点正常执行结束后会执行的语句。

java 复制代码
package com.wenxuan.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * @author 文轩
 * @create 2023-11-25 12:27
 */
@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);
    }
}

环绕通知

环绕通知通过@Around注解进行定义,环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。

java 复制代码
package com.wenxuan.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * @author 文轩
 * @create 2023-11-25 12:27
 */
@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;
    }
}

异常通知

异常通知通过@AfterThrowing注解进行定义,当切点抛出异常会执行的代码,类似于catch。

java 复制代码
package com.wenxuan.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * @author 文轩
 * @create 2023-11-25 12:27
 */
@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);
    }

}

最终通知

最终通知通过@After注解进行定义,方法不管是否抛出异常都会执行的通知,类似于final语句块中的语句。

java 复制代码
package com.wenxuan.aop;

import org.aspectj.lang.JoinPoint;  
import org.aspectj.lang.annotation.After;  
import org.aspectj.lang.annotation.Aspect;  
import org.aspectj.lang.annotation.Pointcut;  
import org.springframework.stereotype.Component;

/**
 * @author 文轩
 * @create 2023-11-25 12:27
 */
@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兑现中封装了切入点的一系列信息。

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

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

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

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

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

SpringBoot整合事务

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

SpringBoot实现事务

添加maven依赖

xml 复制代码
<!-- 引入事务支持 -->  
<dependency>  
    <groupId>org.springframework</groupId>  
    <artifactId>spring-tx</artifactId>  
    <version>5.2.7.RELEASE</version>  
</dependency>

编写启动类

在启动类中添加@EnableTransactionManagement注解

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

java 复制代码
package com.wenxuan;  
  
import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
import org.springframework.transaction.annotation.EnableTransactionManagement;  
  
/**  
* @author 文轩  
* @create 2023-11-23 21:15  
*/  
@SpringBootApplication  
@EnableTransactionManagement  
public class Application {  
    public static void main(String[] args) {  
        SpringApplication.run(Application.class, args);  
    }  
}

使用事务

java 复制代码
package com.wenxuan.controller;

import com.wenxuan.bean.User;
import com.wenxuan.service.UserService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/**
 * @author 文轩
 * @create 2023-11-23 21:23
 */
@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注解详解

@Transaction注解的定义

java 复制代码
package org.springframework.transaction.annotation;

@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 {};	
}

@Transaction注解事务的传播行为

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

java 复制代码
// 设置事务的传播行为
@Transactional(propagation = Propagation.REQUIRED)

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

java 复制代码
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;
    }
}

@Transaction注解事务的隔离级别

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

java 复制代码
// 设置事务的隔离级别
@Transactional(isolation = Isolation.READ_COMITTED)

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

java 复制代码
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;
    }
}

@Transactional失效的场景

@Transactional注解不生效的场景

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

SpringBoot创建Web项目

添加maven依赖

xml 复制代码
<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>

编写启动类

java 复制代码
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请求:删除资源
java 复制代码
package com.wenxuan.controller;

import org.springframework.web.bind.annotation.*;


/**
 * @author 文轩
 * @create 2023-11-23 21:23
 */
@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-张三";
    }
}

请求参数处理

@RequestParam

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

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

单个键值对形式的参数

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

多个键值对形式的参数

java 复制代码
// 通过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);  
}

@PathVariable

@PathVariable获取URL路径上的参数

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

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

@RequestBody

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

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

java 复制代码
@PostMapping("/user")  
public boolean saveUser(@RequestBody User user) {  
    return userService.save(user);  
}

@RequestHeader

@RequestHeader获取请求头部分或者全部信息

获取请求头指定参数

java 复制代码
@GetMapping("/header")  
public String getHeader(@RequestHeader("User-Agent") String userAgent) {  
    return userAgent;  
}

获取请求头所有参数

java 复制代码
@GetMapping("/headerList")  
public Map<String, String> getHeaderList(@RequestHeader Map<String, String> headerList) {  
    System.out.println(headerList);  
    return headerList;  
}

@CookieValue

@CookieValue获取Cookie值

java 复制代码
@GetMapping("/cookie")  
public String cookie(@CookieValue("name") String name) {  
    return name;  
}

@RequestAttribute

@RequestAttribute获取请求域中的值

java 复制代码
@GetMapping("/attribute")  
public String attribute(@RequestAttribute("name") String name) {  
    return name;  
}

请求数据相应

响应JSON数据

通过@RequestBody注解标识类或者方法

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

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

请求转发

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

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

方式2:使用servlet 提供的API

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

重定向

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

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

方式2:使用servlet 提供的API

java 复制代码
@RequestMapping("/redirect")
public void redirect(HttpServletResponse response) throws IOException {
    response.sendRedirect("/index.html");
}

SpringBoot容器

@Configuration + @Bean

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

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

java 复制代码
package com.wenxuan.config;

import com.wenxuan.bean.User;
import com.wenxuan.enums.SexEnum;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 文轩
 * @create 2023-11-25 23:15
 */
@Configuration
public class UserConfig {

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

@Configuration + @Import

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

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

java 复制代码
package com.wenxuan.config;

import com.wenxuan.bean.User;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * @author 文轩
 * @create 2023-11-25 23:15
 */
@Configuration
@Import({User.class})
public class UserConfig {
}

@Conditional注解

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

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

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

java 复制代码
package com.wenxuan.config;

import com.wenxuan.bean.User;
import com.wenxuan.enums.SexEnum;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * @author 文轩
 * @create 2023-11-25 23:15
 */
@Configuration
public class UserConfig {

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

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

java 复制代码
package com.wenxuan.config;

import com.wenxuan.bean.User;
import com.wenxuan.enums.SexEnum;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * @author 文轩
 * @create 2023-11-25 23:15
 */
@Configuration
@Import({User.class})
@ConditionalOnBean(SexEnum.class)
public class UserConfig {
}

读取yaml配置文件属性

@Value

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

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

属性注入

java 复制代码
/**  
* @author 文轩  
* @create 2023-11-25 9:50  
*/  
@RestController  
public class TestController {  
    @Value("${wenxuan.name}")  
    private String name;  
}

方法参数注入

java 复制代码
@Service
public class UserService {
    private final String name;

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

@ConfigurationProperties

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

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

yaml 复制代码
car:  
name: BYD  
price: 10000

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

java 复制代码
package com.wenxuan.bean;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author 文轩
 * @create 2023-11-25 23:34
 */
@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;
    }
}

开启Car配置绑定功能,把这个Car这个组件自动注册到容器中

java 复制代码
package com.wenxuan.config;

import com.wenxuan.bean.Car;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

/**
 * @author 文轩
 * @create 2023-11-25 23:38
 */
// 开启Car配置绑定功能,把这个Car这个组件自动注册到容器中
@EnableConfigurationProperties(Car.class)
public class CarConfig {
}

@SpringBootApplication注解

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

@SpringBootApplication注解的定义如下

java 复制代码
@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容器中,将扫描路径注册到全局,给其他组件查询。

静态资源的访问

静态资源的默认路径

如果静态资源放在以下类路径下,可以通过当前项目根路径/资源名进行访问

  • /static
  • /public
  • /resources
  • /META-INF/resources

修改静态资源的路径

修改静态资源访问的前缀

yaml 复制代码
# 修改静态资源访问的前缀后,访问静态资源需要URL后面需要加上前缀才能访问到
spring:
  mvc:
    # 当前项目根路径/**  修改为  当前项目根路径/res/**
    static-path-pattern: /res/**   

修改默认的静态资源路径

yaml 复制代码
spring:
  resources:
    # 需要将静态资源存在/resources/res目录下
    static-locations: [classpath:/res/]  

自定义Favicon

将需要设置为Favicon的文件重命名为favicon.ico,将favicon.ico文件放到静态资源存放的路径下即可。

自定义错误页

定义静态错误页

resources下的static目录下,新建error目录,在其中新建各种静态错误页面,如 404、500,也可以模糊处理,如4xx、5xx 等,当程序运行出错时,会自动根据错误代码(如500)找到相应的错误页面(如/static/error/500.html),给予展示。

自定义跳转到指定错误页面

注册错误页面

java 复制代码
package com.wenxuan.config;

import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.ErrorPageRegistrar;
import org.springframework.boot.web.server.ErrorPageRegistry;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

/**
 * @author 文轩
 * @create 2023-11-26 9:58
 */
@Component
public class ErrorPageConfig implements ErrorPageRegistrar {
    @Override
    public void registerErrorPages(ErrorPageRegistry registry) {
        ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/error/404");
        ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500");
        registry.addErrorPages(error404Page, error500Page);
    }
}

controller进行拦截

java 复制代码
@RequestMapping("/error/{status}")
public String errorPage(@PathVariable int status){
    // status是对应的状态码
    System.out.println("status = " + status);
    if(status == 404) {
        return "redirect:https://localhost/400.html";
    } else if(status == 500) {
        return "redirect:https://localhost/500.html";
    }
    return "redirect:https://localhost/error.html";
}

SpringBoot实现拦截器

SpringBoot实现拦截器

定义拦截器

java 复制代码
package com.wenxuan.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author 文轩
 * @create 2023-11-26 10:12
 * 定义拦截器
 */
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("页面渲染之后执行该方法");
    }
}

注册拦截器

java 复制代码
package com.wenxuan.config;

import com.wenxuan.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author 文轩
 * @create 2023-11-26 10:13
 */
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {

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

SpringBoot实现文件上传和下载

SpringBoot实现文件上传

java 复制代码
package com.wenxuan.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

/**
 * @author 文轩
 * @create 2023-11-26 13:26
 */
@RestController
@RequestMapping("file")
public class FileController {

    @Value("${file.upload.path}")
    private String uploadFilePath;

    @RequestMapping("/upload")
    public String upload(@RequestParam("files") MultipartFile[] files) {
        for(int i = 0; i < files.length; i++){
            // 获取上传文件的文件名
            String fileName = files[i].getOriginalFilename();

            // 获取当前时间,作为目录分割文件
            LocalDate currentDate = LocalDate.now();
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
            String formattedDate = currentDate.format(formatter);

            // 设置上传文件的保存路径
            File dest = new File(uploadFilePath + '/' + formattedDate + "/" + fileName);
            if (!dest.getParentFile().exists()) {
                dest.getParentFile().mkdirs();
            }
            try {
                files[i].transferTo(dest);
            } catch (Exception e) {
                System.out.println("上传失败:" + e);
                return "error";
            }
        }
        return "success";
    }
}

SpringBoot实现文件下载

java 复制代码
package com.wenxuan.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.*;

/**
 * @author 文轩
 * @create 2023-11-26 13:26
 */
@RestController
@RequestMapping("file")
public class FileController {

    @Value("${file.download.path}")
    private String downloadFilePath;

    @RequestMapping("/download")
    public String download(HttpServletResponse response, @RequestParam("fileName") String fileName){
        File file = new File(uploadFilePath +'/'+ fileName);
        // 文件存在校验
        if(!file.exists()){
            return "下载文件不存在";
        }

        // 重置response对象,保证response对象为初始状态
        response.reset();

        // 获取文件响应类型
        MimetypesFileTypeMap fileTypeMap = new MimetypesFileTypeMap();
        String contentType = fileTypeMap.getContentType(file);
        // 设置下载文件响应类型
        if (contentType != null) {
            response.setContentType(contentType);
        } else {
            response.setContentType("application/octet-stream");
        }

        response.setCharacterEncoding("utf-8");
        response.setContentLength((int) file.length());
        // 设置Content-Disposition头部字段,表示将文件作附件下载
        response.setHeader("Content-Disposition", "attachment;filename=" + fileName );

        try(BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));) {
            byte[] buff = new byte[1024];
            OutputStream os  = response.getOutputStream();
            int i = 0;
            while ((i = bis.read(buff)) != -1) {
                os.write(buff, 0, i);
                os.flush();
            }
        } catch (IOException e) {
            return "下载失败";
        }
        return "下载成功";
    }
}

SpringBoot实现将文件出到浏览器

java 复制代码
package com.wenxuan.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * @author 文轩
 * @create 2023-11-26 13:26
 */
@RestController
@RequestMapping("file")
public class FileController {

    @Value("${file.path}")
    private String filePath;

    @RequestMapping("/outputBrowser")
    public String outputBrowser(HttpServletResponse response, @RequestParam("fileName") String fileName) {
        // 获取绝对路径
        File file = new File(filePath +'/'+ fileName);

        if(!file.exists()){
            return "输出文件不存在";
        }

        // 获取文件响应类型
        String contentType = null;
        Path path = Paths.get(fileName);
        try {
            contentType =  Files.probeContentType(path);
        } catch (IOException e) {
            e.printStackTrace();
            return "获取文件响应类型失败";
        }

        try(
                // 获取输入流,读取文件内容
                FileInputStream fileInputStream = new FileInputStream(file);
                // 获取输出流,输出到浏览器
                ServletOutputStream outputStream = response.getOutputStream();) {
            // 设置响应类型
            response.setContentType(contentType + ";character=UTF-8");
            int len = 0;
            byte[] bytes = new byte[1024];
            while((len = fileInputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, len);
                outputStream.flush();
            }
        } catch (IOException e) {
            System.out.println("文件输出失败:" + e);
            return "error";
        }
        return "success";
    }
}

SpringBoot实现数据源

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

SpringBoot配置数据源

添加maven依赖

xml 复制代码
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.32</version>
</dependency>

编写application.yaml

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

配置第三方数据源

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

添加maven依赖

xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.17</version>
</dependency>

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

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数据源其他配置

yaml 复制代码
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
相关推荐
NE_STOP2 小时前
SpringBoot--简单入门
java·spring
seventeennnnn2 小时前
谢飞机的Java高级开发面试:从Spring Boot到分布式架构的蜕变之旅
spring boot·微服务架构·java面试·分布式系统·电商支付
超级小忍3 小时前
服务端向客户端主动推送数据的几种方法(Spring Boot 环境)
java·spring boot·后端
张小洛4 小时前
Spring AOP 设计解密:代理对象生成、拦截器链调度与注解适配全流程源码解析
java·后端·spring·spring aop·aop
时间会给答案scidag4 小时前
报错 400 和405解决方案
vue.js·spring boot
Wyc724095 小时前
SpringBoot
java·spring boot·spring
ladymorgana6 小时前
【Spring Boot】HikariCP 连接池 YAML 配置详解
spring boot·后端·mysql·连接池·hikaricp
neoooo6 小时前
别慌,Java只有值传递——一次搞懂“为啥我改了它还不变”!
java·后端·spring
GJCTYU8 小时前
spring中@Transactional注解和事务的实战理解附代码
数据库·spring boot·后端·spring·oracle·mybatis
linweidong8 小时前
七牛云Java开发面试题及参考答案(60道面试题汇总)
spring·读写分离·aop·cap·java开发·spring ioc·java面经