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
相关推荐
苹果醋344 分钟前
快速玩转 Mixtral 8x7B MOE大模型!阿里云机器学习 PAI 推出最佳实践
spring boot·nginx·毕业设计·layui·课程设计
程序员大金1 小时前
基于SpringBoot+Vue+MySQL的装修公司管理系统
vue.js·spring boot·mysql
【D'accumulation】2 小时前
令牌主动失效机制范例(利用redis)注释分析
java·spring boot·redis·后端
2401_854391082 小时前
高效开发:SpringBoot网上租赁系统实现细节
java·spring boot·后端
wxin_VXbishe2 小时前
springboot合肥师范学院实习实训管理系统-计算机毕业设计源码31290
java·spring boot·python·spring·servlet·django·php
OEC小胖胖3 小时前
Spring Boot + MyBatis 项目中常用注解详解(万字长篇解读)
java·spring boot·后端·spring·mybatis·web
2401_857617623 小时前
SpringBoot校园资料平台:开发与部署指南
java·spring boot·后端
quokka563 小时前
Springboot 整合 logback 日志框架
java·spring boot·logback
计算机学姐3 小时前
基于SpringBoot+Vue的在线投票系统
java·vue.js·spring boot·后端·学习·intellij-idea·mybatis
落落落sss4 小时前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis