spring

Posted m0_56426304

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了spring相关的知识,希望对你有一定的参考价值。

目录

动态代理

AOP 概述

基于XML的AOP配置

   环境搭建

配置步骤

切入点表达式说明

环绕通知

基于注解的 AOP 配置

环境搭建

配置步骤

环绕通知注解配置

切入点表达式注解


动态代理

  特点:字节码随用随创建,随用随加载

  作用:不修改源码的基础上对方法增强

  分类:

         基于接口的动态代理

               涉及的类:Proxy

               提供者:JDK官方

         基于子类的动态代理

               涉及的类:Enhancer

               提供者:第三方cglib库

如何创建代理对象:

          接口的动态代理:使用Proxy类中的newProxyInstance方法

          子类的动态代理:使用Enhancer类中的create方法

创建代理对象的要求:

          接口的动态代理:被代理类最少实现一个接口,如果没有则不能使用

          子类的动态代理:被代理类不能是最终类

newProxyInstance方法的参数:

          ClassLoader:类加载器  

                  他是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法

          Class[]:字节码数组

                   他是用于让代理对象和被代理对象有相同方法。固定写法

          InvocationHandler:用于提供增强的代码

                   他是让我们写如何代理的。我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须是匿名内部类;此接口的实现类都是谁用谁写

create方法的参数:
     Class:字节码
         它是用于指定被代理对象的字节码
     Callback:用于提供增强的代码
         它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的;此接口的实现类都是谁用谁写
        我们一般写的都是该接口的子接口实现类:MethodInterceptor                 

基于接口的动态代理代码

       Client类

/**
 * 模拟一个消费者
 */
public class Client {

    public static void main(String[] args) {
        final Producer producer = new Producer();

        /**
         * 动态代理:
         *  特点:字节码随用随创建,随用随加载
         *  作用:不修改源码的基础上对方法增强
         *  分类:
         *      基于接口的动态代理
         *      基于子类的动态代理
         *  基于接口的动态代理:
         *      涉及的类:Proxy
         *      提供者:JDK官方
         *  如何创建代理对象:
         *      使用Proxy类中的newProxyInstance方法
         *  创建代理对象的要求:
         *      被代理类最少实现一个接口,如果没有则不能使用
         *  newProxyInstance方法的参数:
         *      ClassLoader:类加载器
         *          它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。
         *      Class[]:字节码数组
         *          它是用于让代理对象和被代理对象有相同方法。固定写法。
         *      InvocationHandler:用于提供增强的代码
         *          它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
         *          此接口的实现类都是谁用谁写。
         */
       IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
                producer.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 作用:执行被代理对象的任何接口方法都会经过该方法
                     * 方法参数的含义
                     * @param proxy   代理对象的引用
                     * @param method  当前执行的方法
                     * @param args    当前执行方法所需的参数
                     * @return        和被代理对象方法有相同的返回值
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //提供增强的代码
                        Object returnValue = null;

                        //1.获取方法执行的参数
                        Float money = (Float)args[0];
                        //2.判断当前方法是不是销售
                        if("saleProduct".equals(method.getName())) {
                            returnValue = method.invoke(producer, money*0.8f);
                        }
                        return returnValue;
                    }
                });
        proxyProducer.saleProduct(10000f);
    }
}

IProducer接口

/**
 * 对生产厂家要求的接口
 */
public interface IProducer {

    /**
     * 销售
     * @param money
     */
    public void saleProduct(float money);

    /**
     * 售后
     * @param money
     */
    public void afterService(float money);
}

Producer类

/**
 * 一个生产者
 */
public class Producer implements IProducer{

    /**
     * 销售
     * @param money
     */
    public void saleProduct(float money){
        System.out.println("销售产品,并拿到钱:"+money);
    }

    /**
     * 售后
     * @param money
     */
    public void afterService(float money){
        System.out.println("提供售后服务,并拿到钱:"+money);
    }
}

基于子类的动态代理代码

Producer类

/**
 * 一个生产者
 */
public class Producer {

    /**
     * 销售
     * @param money
     */
    public void saleProduct(float money){
        System.out.println("销售产品,并拿到钱:"+money);
    }

    /**
     * 售后
     * @param money
     */
    public void afterService(float money){
        System.out.println("提供售后服务,并拿到钱:"+money);
    }
}

Cilent类

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * 模拟一个消费者
 */
public class Client {

    public static void main(String[] args) {
        final Producer producer = new Producer();

        /**
         * 动态代理:
         *  特点:字节码随用随创建,随用随加载
         *  作用:不修改源码的基础上对方法增强
         *  分类:
         *      基于接口的动态代理
         *      基于子类的动态代理
         *  基于子类的动态代理:
         *      涉及的类:Enhancer
         *      提供者:第三方cglib库
         *  如何创建代理对象:
         *      使用Enhancer类中的create方法
         *  创建代理对象的要求:
         *      被代理类不能是最终类
         *  create方法的参数:
         *      Class:字节码
         *          它是用于指定被代理对象的字节码。
         *
         *      Callback:用于提供增强的代码
         *          它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
         *          此接口的实现类都是谁用谁写。
         *          我们一般写的都是该接口的子接口实现类:MethodInterceptor
         */
        Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
            /**
             * 执行北地阿里对象的任何方法都会经过该方法
             * @param proxy
             * @param method
             * @param args
             *    以上三个参数和基于接口的动态代理中invoke方法的参数是一样的
             * @param methodProxy :当前执行方法的代理对象
             * @return
             * @throws Throwable
             */
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                //提供增强的代码
                Object returnValue = null;

                //1.获取方法执行的参数
                Float money = (Float)args[0];
                //2.判断当前方法是不是销售
                if("saleProduct".equals(method.getName())) {
                    returnValue = method.invoke(producer, money*0.8f);
                }
                return returnValue;
            }
        });
        cglibProducer.saleProduct(12000f);
    }
}

接口的动态代理:

new InvocationHander()

    作用:执行被代理对象的任何接口方法都会经过该方法

    参数:

            proxy 代理对象的引用

            method 当前执行的方法

            args 当前执行方法所需的参数

    返回值:和被代理对象方法有相同的返回值

ps.匿名内部类访问外部成员变量时,外部成员变量需要用final修饰

子类的动态代理

           三个参数和基于接口的动态代理中invoke方法的参数是一样的
           methodProxy :当前执行方法的代理对象

AOP 概述

AOP :全称是 Aspect Oriented Programming 即:面向切面编程
简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强
AOP 的作用及优势
作用:
在程序运行期间,不修改源码对已有方法进行增强。
优势:
减少重复代码
提高开发效率
维护方便
AOP 的实现方式
使用动态代理技术
AOP 相关术语
Joinpoint( 连接点 ):
所谓连接点是指那些被拦截到的点。在 spring , 这些点指的是方法 , 因为 spring 只支持方法类型的连接点
Pointcut( 切入点 ):
所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义
Advice( 通知 / 增强 ):
所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知
通知的类型:前置通知 , 后置通知 , 异常通知 , 最终通知 , 环绕通知
Introduction( 引介 ):
引介是一种特殊的通知在不修改类代码的前提下 , Introduction 可以在运行期为类动态地添加一些方 法或 Field
Target( 目标对象 ):
  代理的目标对象
Weaving(织入):  
是指把增强应用到目标对象来创建新的代理对象的过程。
spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。
  Proxy(代理):
一个类被 AOP 织 入增强后,就产生一个结果代理类。
Aspect( 切面 ):
是切入点和通知(引介)的结合

基于XML的AOP配置

spring中基于XML的AOP配置步骤
    1、把通知Bean也交给spring来管理
    2、使用aop:config标签表明开始AOP的配置
    3、使用aop:aspect标签表明配置切面
            id属性:是给切面提供一个唯一标识
            ref属性:是指定通知类bean的Id。
    4、在aop:aspect标签的内部使用对应标签来配置通知的类型
           我们现在示例是让printLog方法在切入点方法执行之前之前:所以是前置通知
           aop:before:表示配置前置通知
                method属性:用于指定Logger类中哪个方法是前置通知
                pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强

   环境搭建

第一步:准备必要的代码

实体类,业务层和持久层代码

第二步:拷贝必备的 jar 包到工程的 lib 目录
此处要拷贝 spring ioc aop 两组 jar
第三步:创建 spring 的配置文件并导入约束
此处要导入 aop 的约束
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans

            http://www.springframework.org/schema/beans/spring-beans.xsd

            http://www.springframework.org/schema/aop

            http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>
第四步:配置 spring 的 ioc 
<!-- 配置 service --> <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"> <property name="accountDao" ref="accountDao"></property>
</bean>
<!-- 配置 dao --> <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
<property name="dbAssit" ref="dbAssit"></property>
</bean>
<!-- 配置数据库操作对象 --> <bean id="dbAssit" class="com.itheima.dbassit.DBAssit"> <property name="dataSource" ref="dataSource"></property>
<!-- 指定 connection 和线程绑定 --> <property name="useCurrentConnection" value="true"></property>
</bean>
<!-- 配置数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql:///spring_day02"></property> <property name="user" value="root"></property> <property name="password" value="1234"></property>
</bean>
第五步:抽取公共代码制作成通知
/**
* 事务控制类
* @author 黑马程序员
* @Company http://www.ithiema.com
* @Version 1.0
*/
public class TransactionManager {
//定义一个 DBAssit
private DBAssit dbAssit ;
public void setDbAssit(DBAssit dbAssit) {
this.dbAssit = dbAssit;
}
//开启事务
public void beginTransaction() {
try {
dbAssit.getCurrentConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}
//提交事务
public void commit() {
try {
dbAssit.getCurrentConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
//回滚事务
public void rollback() {
try {
dbAssit.getCurrentConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
//释放资源
public void release() {
try {
dbAssit.releaseConnection();
} catch (Exception e) {
e.printStackTrace();
}
}
}

配置步骤

第一步:把通知类用 bean 标签配置起来
<!-- 配置通知 -->
<bean id="txManager" class="com.itheima.utils.TransactionManager">
<property name="dbAssit" ref="dbAssit"></property>
</bean>

第二步:使用 aop:config 声明 aop 配置
aop:config:
作用: 用于声明开始 aop 的配置
< aop:config >
<!-- 配置的代码都写在此处 -->
</ aop:config >
第三步:使用 aop:aspect 配置切面
aop:aspect:
作用:
用于配置切面
属性:
id :给切面提供一个唯一标识。
ref :引用配置好的通知类 bean id
< aop:aspect id = "txAdvice" ref = "txManager" >
     <!-- 配置通知的类型要写在此处 -->
</ aop:aspect >
第四步:使用 aop:pointcut 配置切入点表达式
aop:pointcut
作用:
用于配置切入点表达式。就是指定对哪些类的哪些方法进行增强。
属性:
expression :用于定义切入点表达式。
id :用于给切入点表达式提供一个唯一标识
<aop:pointcut expression="execution(
public void com.itheima.service.impl.AccountServiceImpl.transfer(
java.lang.String, java.lang.String, java.lang.Float)
)" id="pt1"/>

第五步:使用 aop:xxx 配置对应的通知类型
aop:before
作用:
    用于配置前置通知。指定增强的方法在切入点方法之前执行
属性:
    method: 用于指定通知类中的增强方法名称
    ponitcut-ref :用于指定切入点的表达式的引用
    poinitcut :用于指定切入点表达式
执行时间点:
    切入点方法执行之前执行
< aop:before method = "beginTransaction" pointcut-ref = "pt1" />
aop:after-returning
作用:
    用于配置后置通知
属性:
    method: 指定通知中方法的名称。
    pointct: 定义切入点表达式
    pointcut-ref: 指定切入点表达式的引用
执行时间点:
    切入点方法正常执行之后。它和异常通知只能有一个执行
< aop:after-returning method = "commit" pointcut-ref = "pt1" />
aop:after-throwing
作用:
    用于配置异常通知
属性:
method 指定通知中方法的名称。
pointct 定义切入点表达式
pointcut-ref 指定切入点表达式的引用
执行时间点:
切入点方法执行产生异常后执行。它和后置通知只能执行一个
< aop:after-throwing method = "rollback" pointcut-ref = "pt1" />
aop:after
作用:
    用于配置最终通知
属性:
    method: 指定通知中方法的名称。
    pointct: 定义切入点表达式
    pointcut-ref: 指定切入点表达式的引用
执行时间点:
    无论切入点方法执行时是否有异常,它都会在其后面执行。
< aop:after method = "release" pointcut-ref = "pt1" />

切入点表达式说明

execution: 匹配方法的执行 ( 常用 )
execution( 表达式 )
表达式语法: execution ( [ 修饰符 ] 返回值类型 包名 . 类名 . 方法名 ( 参数 ))
写法说明:
全匹配方式:
public void
com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
访问修饰符可以省略
void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
返回值可以使用 * 号,表示任意返回值
*
com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
包名可以使用 * 号,表示任意包,但是有几级包,需要写几个 *
* *.*.*.*.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
使用 .. 来表示当前包,及其子包
* com..AccountServiceImpl.saveAccount(com.itheima.domain.Account)
类名可以使用 * 号,表示任意类
* com..*.saveAccount(com.itheima.domain.Account)
方法名可以使用 * 号,表示任意方法
* com..*.*( com.itheima.domain.Account)
参数列表可以使用 * ,表示参数可以是任意数据类型,但是必须有参数
* com..*.*(*)
参数列表可以使用 .. 表示有无参数均可,有参数可以是任意类型
* com..*.*(..)
全通配方式:
* *..*.*(..)
注:
通常情况下,我们都是对业务层的方法进行增强,所以切入点表达式都是切到业务层实现类。
execution( * com.itheima.service.impl.*.*(..) )

环绕通知

配置方式 :
<aop:config>
<aop:pointcut
expression="execution(* com.itheima.service.impl.*.*(..))"
id="pt1"/>
<aop:aspect id="txAdvice" ref="txManager">
<!-- 配置环绕通知 -->
<aop:around method="transactionAround" pointcut-ref="pt1"/>
</aop:aspect>
</aop:config>
aop:around
作用:
用于配置环绕通知
属性:
method :指定通知中方法的名称。
pointct :定义切入点表达式
pointcut-ref :指定切入点表达式的引用
说明:
它是 spring 框架为我们提供的一种可以在代码中手动控制增强代码什么时候执行的方式。
注意:
通常情况下,环绕通知都是独立使用的
/**
* 环绕通知
* @param pjp
* spring 框架为我们提供了一个接口:ProceedingJoinPoint,它可以作为环绕通知的方法参数。
* 在环绕通知执行时,spring 框架会为我们提供该接口的实现类对象,我们直接使用就行。
* @return
*/
public Object transactionAround(ProceedingJoinPoint pjp) {
//定义返回值
Object rtValue = null;
try {
//获取方法执行所需的参数
Object[] args = pjp.getArgs();
//前置通知:开启事务
beginTransaction();
//执行方法
rtValue = pjp.proceed(args);
//后置通知:提交事务
commit();
}catch(Throwable e) {
//异常通知:回滚事务
rollback();
e.printStackTrace();
}finally {
//最终通知:释放资源
release();
}
return rtValue;
}

基于注解的 AOP 配置

环境搭建

第一步:准备必要的代码和 jar 包
实体类,业务层和持久层代码
第二步:在配置文件中导入 context 的名称空间
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置数据库操作对象 -->
<bean id="dbAssit" class="com.itheima.dbassit.DBAssit">
<property name="dataSource" ref="dataSource"></property>
<!-- 指定 connection 和线程绑定 -->
<property name="useCurrentConnection" value="true"></property>
</bean>
<!-- 配置数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql:///spring_day02"></property>
<property name="user" value="root"></property>
<property name="password" value="1234"></property>
</bean>
</beans>

第三步:把资源使用注解配置
/**
* 账户的业务层实现类
* @author 黑马程序员
* @Company http://www.ithiema.com
* @Version 1.0
*/
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
@Autowired
private IAccountDao accountDao;
}
/**
* 账户的持久层实现类
* @author 黑马程序员
* @Company http://www.ithiema.com
* @Version 1.0
*/
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
@Autowired
private DBAssit dbAssit ;
}

第四步:在配置文件中指定 spring 要扫描的包

<!-- 告知 spring,在创建容器时要扫描的包 -->
<context:component-scan base-package="com.itheima"></context:component-scan>

配置步骤

第一步:把通知类也使用注解配置
/**
* 事务控制类
* @author 黑马程序员
* @Company http://www.ithiema.com
* @Version 1.0
*/
@Component("txManager")
public class TransactionManager {
//定义一个 DBAssit
@Autowired
private DBAssit dbAssit ;
}

第二步:在通知类上使用@Aspect 注解声明为切面
作用:
把当前类声明为切面类
/**
* 事务控制类
* @author 黑马程序员
* @Company http://www.ithiema.com
* @Version 1.0
*/
@Component("txManager")
@Aspect//表明当前类是一个切面类
public class TransactionManager {
//定义一个 DBAssit
@Autowired
private DBAssit dbAssit ;
}

第三步:在增强的方法上使用注解配置通知
@Before
作用:
把当前方法看成是前置通知。
属性:
value :用于指定切入点表达式,还可以指定切入点表达式的引用。
//开启事务
@Before("execution(* com.itheima.service.impl.*.*(..))")
public void beginTransaction() {
try {
dbAssit.getCurrentConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}

@AfterReturning
作用:
把当前方法看成是后置通知。
属性:
value :用于指定切入点表达式,还可以指定切入点表达式的引用
//提交事务
@AfterReturning("execution(* com.itheima.service.impl.*.*(..))")
public void commit() {
try {
dbAssit.getCurrentConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}

@AfterThrowing
作用:
把当前方法看成是异常通知。
属性:
value :用于指定切入点表达式,还可以指定切入点表达式的引用
//回滚事务
@AfterThrowing("execution(* com.itheima.service.impl.*.*(..))")
public void rollback() {
try {
dbAssit.getCurrentConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}

@After
作用:
把当前方法看成是最终通知。
属性:
value :用于指定切入点表达式,还可以指定切入点表达式的引用
//释放资源
@After("execution(* com.itheima.service.impl.*.*(..))")
public void release() {
try {
dbAssit.releaseConnection();
} catch (Exception e) {
e.printStackTrace();
}
}

第四步:在 spring 配置文件中开启 spring 对注解 AOP 的支持

<!-- 开启 spring 对注解 AOP 的支持 -->
<aop:aspectj-autoproxy/>

环绕通知注解配置

@Around
作用:
把当前方法看成是环绕通知。
属性:

value:用于指定切入点表达式,还可以指定切入点表达式的引用。

/**
* 环绕通知
* @param pjp
* @return
*/
@Around("execution(* com.itheima.service.impl.*.*(..))")
public Object transactionAround(ProceedingJoinPoint pjp) {
//定义返回值
Object rtValue = null;
try {
//获取方法执行所需的参数
Object[] args = pjp.getArgs();
//前置通知:开启事务
beginTransaction();
//执行方法
rtValue = pjp.proceed(args);
//后置通知:提交事务
commit();
}catch(Throwable e) {
//异常通知:回滚事务
rollback();
e.printStackTrace();
}finally {
//最终通知:释放资源
release();
}
return rtValue;
}

切入点表达式注解

@Pointcut
作用:
指定切入点表达式
属性:
value :指定表达式的内容
@Pointcut( "execution(* com.itheima.service.impl.*.*(..))" )
private void pt1() {}

引用方式:

/**
* 环绕通知
* @param pjp
* @return
*/
@Around("pt1()")//注意:千万别忘了写括号
public Object transactionAround(ProceedingJoinPoint pjp) {
//定义返回值
Object rtValue = null;
try {
//获取方法执行所需的参数
Object[] args = pjp.getArgs();
//前置通知:开启事务
beginTransaction();
//执行方法
rtValue = pjp.proceed(args);
//后置通知:提交事务
commit();
}catch(Throwable e) {
//异常通知:回滚事务
rollback();
e.printStackTrace();
}finally {
//最终通知:释放资源
release();
}
return rtValue;
}
2.3.5 不使用 XML 的配置方式
@Configuration
@ComponentScan(basePackages="com.itheima")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}

以上是关于spring的主要内容,如果未能解决你的问题,请参考以下文章

Spring boot:thymeleaf 没有正确渲染片段

What's the difference between @Component, @Repository & @Service annotations in Spring?(代码片段

spring练习,在Eclipse搭建的Spring开发环境中,使用set注入方式,实现对象的依赖关系,通过ClassPathXmlApplicationContext实体类获取Bean对象(代码片段

Spring Rest 文档。片段生成时 UTF-8 中间字节无效 [重复]

解决spring-boot启动中碰到的问题:Cannot determine embedded database driver class for database type NONE(转)(代码片段

一张图帮你记忆,Spring Boot 应用在启动阶段执行代码的几种方式