目录

Spring基础

目录

概念

架构图

./Spring.assets/image-20240624102301926.png

(1)核心层

  • Core Container:核心容器,这个模块是Spring最核心的模块,其他的都需要依赖该模块

(2)AOP层

  • AOP:面向切面编程,它依赖核心层容器,目的是==在不改变原有代码的前提下对其进行功能增强==
  • Aspects:AOP是思想,Aspects是对AOP思想的具体实现

(3)数据层

  • Data Access:数据访问,Spring全家桶中有对数据访问的具体实现技术
  • Data Integration:数据集成,Spring支持整合其他的数据层解决方案,比如Mybatis
  • Transactions:事务,Spring中事务管理是Spring AOP的一个具体实现,也是后期学习的重点内容

(4)Web层

  • 这一层的内容将在SpringMVC框架具体学习

(5)Test层

  • Spring主要整合了Junit来完成单元测试和集成测试

IoC:反转控制(Inverse Of Control)

业务层实现

1
2
3
4
5
6
public class BookServiceImpl implemnts BookService{
    private BookDao bookDao = new BookDaoImpl();
    public void save(){
    	bookDao.save();
    }
}

数据层实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class BookDaoImpl implements BookDao{
    public void save(){
        System.out.println("book dao save...");
    }
}
public class BookDaoImp12 implements BookDao{
    public void save(){
        System.out.println("book dao save ...2");
    }
}
1
注意

(1)业务层需要调用数据层的方法,就需要在业务层new数据层的对象

(2)如果数据层的实现类发生变化,那么业务层的代码也需要跟着改变,发生变更后,都需要进行编译打包和重部署

(3)所以,代码存在问题:==耦合度偏高==

解决方案

使用对象时,在程序中==不要主动使用new产生对象==,转换为由==外部==提供对象

IOC

(1)什么是控制反转?

  • 使用对象时,由主动new产生对象转换为由==外部==提供对象,此过程中对象创建控制权由程序转移到外部,此思想称为控制反转。
    • 业务层要用数据层的类对象,以前是自己 new
    • 现在自己不new了,交给 别人[外部]来创建对象
    • 别人[外部]就反转控制了数据层对象的创建权
    • 这种思想就是控制反转

(2)Spring和IOC之间的关系是什么?

  • Spring技术对IOC思想进行了实现
  • Spring提供了一个容器,称为==IOC容器==,用来充当IOC思想中的"外部"
  • IOC思想中的 别人[外部]指的就是Spring的IOC容器

(3)IOC容器的作用以及内部存放的是什么?

  • IOC容器负责对象的创建、初始化等一系列工作,其中包含了数据层和业务层的类对象
  • 被创建或被管理的对象在IOC容器中统称为==Bean==
  • IOC容器中放的就是一个个的Bean对象

(4)当IOC容器中创建好service和dao对象后,程序能正确执行么?

  • 不行,因为service运行需要依赖dao对象
  • IOC容器中虽然有service和dao对象
  • 但是service对象和dao对象没有任何关系
  • 需要把dao对象交给service,也就是说要绑定service和dao对象之间的关系

像这种在容器中建立对象与对象之间的绑定关系就要用到DI

DI:依赖注入(Dependency Injection)

业务层实现

依赖dao对象运行

1
2
3
4
5
6
public class BookServiceImpl implemnts BookService{
    private BookDao bookDao;
    public void save(){
    	bookDao.save();
    }
}

数据层实现

1
2
3
4
5
public class BookDaoImpl implements BookDao{
    public void save(){
        System.out.println("book dao save...");
    }
}

./Spring.assets/image-20240624103454563.png

(1)什么是依赖注入呢?

  • 在容器中建立bean与bean之间的依赖关系的整个过程,称为依赖注入
    • 业务层要用数据层的类对象,以前是自己 new
    • 现在自己不new了,靠 别人[外部其实指的就是IOC容器]来给注入进来
    • 这种思想就是依赖注入

(2)IOC容器中哪些bean之间要建立依赖关系呢?

  • 这个需要程序员根据业务需求提前建立好关系,如业务层需要依赖数据层,service就要和dao建立依赖关系

这两个概念的最终目标就是:==充分解耦==,具体实现靠:

  • 使用IOC容器管理bean(IOC)
  • 在IOC容器内将有依赖关系的bean进行关系绑定(DI)
  • 最终结果为:使用对象时不仅可以直接从IOC容器中获取,并且获取到的bean已经绑定了所有的依赖关系

入门案例

IOC

思想

(1)Spring是使用容器来管理bean对象的,管理什么?

  • 主要管理项目中所使用到的类对象,比如(Service和Dao)

(2)如何将被管理的对象告知IOC容器?

  • 使用配置文件

(3)被管理的对象交给IOC容器,要想从容器中获取对象,就先得思考如何获取到IOC容器?

  • Spring框架提供相应的接口

(4)IOC容器得到后,如何从容器中获取bean?

  • 调用Spring框架提供对应接口中的方法

(5)使用Spring导入哪些坐标?

  • 用别人的东西,就需要在pom.xml添加对应的依赖

实现

需求分析:将BookServiceImpl和BookDaoImpl交给Spring管理,并从容器中获取对应的bean对象进行方法调用。

1.创建Maven的java项目

2.pom.xml添加Spring的依赖jar包

3.创建BookService,BookServiceImpl,BookDao和BookDaoImpl四个类

4.resources下添加spring配置文件,并完成bean的配置

5.使用Spring提供的接口完成IOC容器的创建

6.从容器中获取对象进行方法调用

步骤1:创建Maven项目

./Spring.assets/image-20240624104226278.png

步骤2:添加Spring的依赖jar包

pom.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
</dependencies>

步骤3:添加案例中需要的类

创建BookService,BookServiceImpl,BookDao和BookDaoImpl四个类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public interface BookDao {
    public void save();
}
public class BookDaoImpl implements BookDao {
    public void save() {
        System.out.println("book dao save ...");
    }
}
public interface BookService {
    public void save();
}
public class BookServiceImpl implements BookService {
    private BookDao bookDao = new BookDaoImpl();
    public void save() {
        System.out.println("book service save ...");
        bookDao.save();
    }
}

步骤4:添加spring配置文件

resources下添加spring配置文件applicationContext.xml,并完成bean的配置

./Spring.assets/image-20240624104304677.png

步骤5:在配置文件中完成bean的配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       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">
 
    <!--bean标签标示配置bean
    	id属性标示给bean起名字
    	class属性表示给bean定义类型
	-->
	<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
    <bean id="bookService" class="com.itheima.service.impl.BookServiceImpl"/>

</beans>

==注意事项:bean定义时id属性在同一个上下文中(配置文件)不能重复==

步骤6:获取IOC容器

使用Spring提供的接口完成IOC容器的创建,创建App类,编写main方法

1
2
3
4
5
6
public class App {
    public static void main(String[] args) {
        //获取IOC容器
		ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); 
    }
}

步骤7:从容器中获取对象进行方法调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class App {
    public static void main(String[] args) {
        //获取IOC容器
		ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); 
//        BookDao bookDao = (BookDao) ctx.getBean("bookDao");
//        bookDao.save();
        BookService bookService = (BookService) ctx.getBean("bookService");
        bookService.save();
    }
}

步骤8:运行程序

测试结果为:

./Spring.assets/image-20240624104345214.png

Spring的IOC入门案例已经完成,但是在 BookServiceImpl的类中依然存在 BookDaoImpl对象的new操作,它们之间的耦合度还是比较高,解决就需要用到下面的 DI:依赖注入

DI:Dependency Injection:依赖注入

思路

(1)要想实现依赖注入,必须要基于IOC管理Bean

  • DI的入门案例要依赖于前面IOC的入门案例

(2)Service中使用new形式创建的Dao对象是否保留?

  • 需要删除掉,最终要使用IOC容器中的bean对象

(3)Service中需要的Dao对象如何进入到Service中?

  • 在Service中提供方法,让Spring的IOC容器可以通过该方法传入bean对象

(4)Service与Dao间的关系如何描述?

  • 使用配置文件

实现

需求:基于IOC入门案例,在BookServiceImpl类中删除new对象的方式,使用Spring的DI完成Dao层的注入

1.删除业务层中使用new的方式创建的dao对象

2.在业务层提供BookDao的setter方法

3.在配置文件中添加依赖注入的配置

4.运行程序调用方法

步骤1: 去除代码中的new

在BookServiceImpl类中,删除业务层中使用new的方式创建的dao对象

1
2
3
4
5
6
7
8
9
public class BookServiceImpl implements BookService {
    //删除业务层中使用new的方式创建的dao对象
    private BookDao bookDao;

    public void save() {
        System.out.println("book service save ...");
        bookDao.save();
    }
}

步骤2:为属性提供setter方法

在BookServiceImpl类中,为BookDao提供setter方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class BookServiceImpl implements BookService {
    //删除业务层中使用new的方式创建的dao对象
    private BookDao bookDao;

    public void save() {
        System.out.println("book service save ...");
        bookDao.save();
    }
    //提供对应的set方法
    public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }
}

步骤3:修改配置完成注入

在配置文件中添加依赖注入的配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       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">
    <!--bean标签标示配置bean
    	id属性标示给bean起名字
    	class属性表示给bean定义类型
	-->
    <bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>

    <bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
        <!--配置server与dao的关系-->
        <!--property标签表示配置当前bean的属性
        		name属性表示配置哪一个具体的属性
        		ref属性表示参照哪一个bean
		-->
        <property name="bookDao" ref="bookDao"/>
    </bean>

</beans>

==注意:配置中的两个bookDao的含义是不一样的==

  • name=“bookDao"中 bookDao的作用是让Spring的IOC容器在获取到名称后,将首字母大写,前面加set找对应的 setBookDao()方法进行对象注入
  • ref=“bookDao"中 bookDao的作用是让Spring能在IOC容器中找到id为 bookDao的Bean对象给 bookService进行注入
  • 综上所述,对应关系如下:

./Spring.assets/image-20240624122840444.png

步骤4:运行程序

运行,测试结果为:

./Spring.assets/image-20240624122854579.png

Bean

基础配置

  • 用name=”” 起别名,可以多个,用",“分割

  • Bean的作用范围:Spring默认创建单例。创建非单例需要在配置中加入scope=“prototype”。默认singleton

  • 适合交给容器进行管理的bean

    • 表现层对象
    • 业务层对象
    • 数据层对象
    • 工具对象
  • 不适合交给容器进行管理的bean

    • 封装实体的域对象

实例化

构造方法

显示提供或不提供无参的构造方法

1
<bean id="userDao" class="dao.impl.UserDaoImpl"/>

静态工厂

配置工厂类和工厂类中的哪个方法

1
<bean id="userDao" class="factory.StaticFactoryBean" factory-method="createUserDao" />

实例工厂

需要创建工厂的对象

配置:先配置工厂对象的bean 配置factory-method factory-bean

1
2
<bean id="factoryBean" class="factory.DynamicFactoryBean"/>
<bean id="userDao" factory-bean="factoryBean" factory-method="createUserDao"/>

改进:使用FactoryBean实例化Bean

1
2
3
4
5
public class UserDaoFactoryBean implements FactoryBean <UserDao> {
    public UserDao getObject() throws Exception {return new UserDaoImpl();}
    public Class<?> getObjectType() {return userDao.class;}
    public boolean isSingleton() {return false;} //改为非单例
}
1
<bean id="userDao" class="factory.UserDaoFactoryBean"/>

Bean的生命周期

配置:init-method,destory-method

  • ctx.registerShutdownHook(); 注册关闭🪝,可以使虚拟机关闭前先关容器
  • ctx.close(); 暴力关闭

也可以通过使用接口来实现:

  • InitializingBean
  • DisposableBean

依赖注入

Setter注入

  • 引用类型

Spring.assets/image-20240312125021478.png

  • 简单类型
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class UserDaoImpl implements UserDao {
    private String company;
    private int age;
    public void setCompany(String company) {
    	this.company = company;
    }
    public void setAge(int age) {
    	this.age = age;
    }
    public void save() {
        System.out.println(company+"==="+age);
        System.out.println("UserDao save method running....");
    }
}
1
2
3
4
<bean id="userDao" class="dao.impl.UserDaoImpl">
    <property name="company" value="传智播客"></property>
    <property name="age" value="15"></property>
</bean>

构造器注入

  • 引用类型
  • 简单类型

自动装配

  • Ioc容器根据bean所依赖的资源在容器中自动查找并注入到bean中的过程称为自动装配

  • 方式:

    • 按类型(常用)
    1
    2
    
    <bean id="bookDao" class="dao.impl.BookDaoImpl"/>
    <bean id="bookService" class="service.impl.BookServiceImpl" autowire="byType"/>
    
    • 按名称
    1
    2
    
    <bean id="bookDao" class="dao.impl.BookDaoImpl"/>
    <bean id="bookService" class="service.impl.BookServiceImpl" autowire="byName"/>
    
    • 按构造方法
    • 不启用自动装配
  • 自动装配用于引用类型依赖注入,不能对简单类型进行操作

  • 使用按类型装配时(byType)必须保障容器中相同类型的bean唯一,推荐使用

  • 使用按名称装配时(byName)必须保障容器中具有指定名称的bean,因变量名与配置耦合,不推荐使用

  • 自动装配优先级低于 setter注入与 构造器注入,同时出现时自动装配配置失效

集合注入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<bean id="userDao" class="dao.impl.UserDaoImpl">
    <property name="strList">
        <list>
            <value>aaa</value>
            <value>bbb</value>
            <value>ccc</value>
        </list>
    </property>
</bean>

<bean id="u1" class="domain.User"/>
<bean id="u2" class="domain.User"/>

<bean id="userDao" class="dao.impl.UserDaoImpl">
    <property name="userMap">
        <map>
            <entry key="user1" value-ref="u1"/>
            <entry key="user2" value-ref="u2"/>
        </map>
    </property>
</bean>

<bean id="userDao" class="dao.impl.UserDaoImpl">
    <property name="properties">
        <props>
            <prop key="p1">aaa</prop>
            <prop key="p2">bbb</prop>
            <prop key="p3">ccc</prop>
        </props>
    </property>
</bean>

管理第三方程序

加载properties

两个bookDao的含义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       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">
    <!--
    bean标签标示配置bean
    id属性标示给bean起名字
    class属性表示给bean定义类型,
    得是具体的实现类而不是接口,要靠这个造对象的
    -->
    <bean id="bookDao" class="BookDaoImpl"/>

    <!--主要变化在这里-->
    <bean id="bookService" name="myBookService" class="BookServiceImpl">
        <!--配置server与dao的关系-->
        <!--
            property标签表示配置当前bean的属性
            name属性表示配置哪一个具体的属性(这里是配置bookService的bookDao属性)
            ref属性表示参照哪一个bean(参照当前配置文件中的bookDao)
        -->
        <property name="myBookDao"🚩这个是寻找setMyBookDao的方法 ref="bookDao"🚩这个是参照id为bookDao的bean></property>
    </bean>
</beans>
  • name=“bookDao"中bookDao的作用是让Spring的IOC容器在获取到名称后,将首字母大写,前面加set找对应的setBookDao()方法进行对象注入
  • ref=“bookDao"中bookDao的作用是让Spring能在IOC容器中找到id为bookDao的Bean对象给bookService进行注入
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class BookServiceImpl implements BookService {
    //private BookDao bookDao = new BookDaoImpl();
    private BookDao b;🚩这个名字随便
    public void save() {
        System.out.println("book service save ...");
        b.save();
    }

    public void setMyBookDao(BookDao bookDao) {🚩和配置文件对应setMyBookDao
        this.b = bookDao;
        System.out.println("set方法被调用啦");
    }
}

容器

创建容器

1
2
方式一类路径加载配置文件
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
1
2
方式二文件路径加载配置文件
ApplicationContext ctx = new FileSystemXmlApplicationContext("D:\\applicationContext.xml");
1
2
加载多个配置文件
ApplicationContext ctx = new ClassPathXmlApplicationContext("bean1.xml","bean2.xml");

获取bean

1
2
3
4
5
6
方式一使用bean名称获取
BookDao bookDao = (BookDao)ctx.getBean("bookDao");
方式二使用bean名称获取并指定类型
BookDao bookDao = ctx.getBean("bookDao", BookDao.class);
方式三使用bean类型获取
BookDao bookDao = ctx.getBean(BookDao.class);

注解开发

注解开发定义bean

1
2
3
4
5
使用@Component:定义bean
@Component("bookDao")
public class BookDaoImpl implements BookDao
@Component
public class BookServiceImpl implements BookService
1
2
核心配置文件中通过组件扫描加载bean
<context:component-scan base-package="com.itheima"/>
  • Spring提供@Component注解的三个衍生注解
    • @Controller:用于表现层bean定义
    • @Service:用于业务层bean定义
    • @Repository:用于数据层bean定义

纯注解开发模型

  • 使用java类替代配置文件
1
2
3
@Configuration
@ComponentScan("com.itheima")
public class SpringConfig{}
  • @Configuration注解用于设定当前类为配置类
  • @ComponentScan注解用于设定扫描路径,此注解只能添加一次,多个数据请用数组格式
1
@ComponentScan({"service","dao"})

加载方式需要变换:

1
2
3
4
//加载配置文件初始化容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
//加载配置类初始化容器
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);🚩🚩🚩

注解开发bean的作用范围和生命周期

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Repository
@Scope("prototype")🚩🚩🚩@Scope控制生命周期单例写singleton
public class BookDaoImpl implements BookDao
public void save(){
	System.out.println("book dao save...");
    @PostConstruct🚩🚩🚩
    public void init(){System.out.println("init ...")
    @PreDestroy🚩🚩🚩
    public void destroy(){System.out.printin("destroy ...")
}

注解开发依赖注入

  • 只有自动装配
  • 用的是暴力反射
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Service
    public class BookServiceImpl implements BookService{
    @Autowired🚩🚩🚩开启自动装配模式不需要set了
    private BookDao bookDao;

    public void save(){System.out.println("book service save ...")
    bookDao.save();
}
如果要指定名称
@Service
    public class BookServiceImpl implements BookService{
    @Autowired
    @Qualifier("bookDao2")🚩🚩🚩无法单独使用
    private BookDao bookDao;

    public void save(){System.out.println("book service save ...")
    bookDao.save();
}

ℹ️@Qualifier

用于标记特定的 Bean

当有多个 Bean 类型相同时,@Qualifier 可以帮助 Spring 框架选择所需的 Bean。它通常与 @Autowired 注解一起使用,以精确指定要注入的 Bean。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public interface Animal {
    void speak();
}

@Component
@Qualifier("dog")
public class Dog implements Animal {
    @Override
    public void speak() {
        System.out.println("Woof!");
    }
}

@Component
@Qualifier("cat")
public class Cat implements Animal {
    @Override
    public void speak() {
        System.out.println("Meow!");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Component
public class AnimalService {

    private final Animal animal;

    @Autowired
    public AnimalService(@Qualifier("dog") Animal animal) {
        this.animal = animal;
    }

    public void makeAnimalSpeak() {
        animal.speak();
    }
}

@Qualifier("dog") 明确指定了要注入的 Bean 是 Dog,而不是 Cat


1
2
3
4
5
6
使用@Value:实现简单类型注入
@Repository ("bookDao")
public class BookDaoImpl implements BookDao{
    @Va1ue("100")
    private String connectionNum;
}
  • 加载properties文件
1
2
3
4
5
6
使用@PropertySource注解加载properties文件
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
public class SpringConfig{}
注意路径仅支持单一文件配置多文件请使用数组格式配置不允许使用通配符*
  • 第三方bean管理
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
将独立的配置类加入核心配置
方式一导入式
public class JdbcConfig
@Bean🚩
public DataSource dataSource(){
    DruidDataSource ds = new DruidDataSource();
    //相关配置
	return ds;
}
使用@Import注解手动加入配置类到核心配置此注解只能添加一次多个数据请用数组格式
@Configuration
@Import({JdbcConfig.class})
public class SpringConfig
  • 第三方bean依赖注入

TODO🧾 整合Mybatis

TODO🧾 整合JUnit

AOP 面向切面编程

  • 作用:在不惊动原始设计的基础上为其进行功能增强
  • Spring理念:无入侵式/无侵入式

Spring.assets/image-20240317193317258.png

  • 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
    • 在SpringAOP中,理解为方法的执行
  • 切入点(PointCut):匹配连接点的式子
    • 在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
      • 一个具体方法:com.itheima.dao包下的 BookDao接口中的无形参无返回值的 save方法
      • 匹配多个方法:所有的 save方法,所有的 get开头的方法,所有以 Dao结尾的接口中的任意方法,所有带有一个参数的方法
  • 通知(Advice):在切入点处执行的操作,也就是共性功能
    • 在SpringAOP中,功能最终以方法的形式呈现
  • 通知类:定义通知的类
  • 切面(Aspect):描述通知与切入点的对应关系

案例

Spring.assets/image-20240318155806488.png

实现步骤

  • 步骤一:添加spring依赖:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.10.RELEASE</version>🚩不支持JDK17,支持的需要6.1.2?6.2.x
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.4</version>
    </dependency>
    
  • 因为 spring-context中已经导入了 spring-aop,所以不需要再单独导入 spring-aop 导入AspectJ的jar包,AspectJ是AOP思想的一个具体实现,Spring有自己的AOP实现,但是相比于AspectJ来说比较麻烦,所以直接采用Spring整合 ApsectJ的方式进行AOP开发。

  • 步骤二:定义接口和实现类 准备环境的时候已经完成了

  • 步骤三:定义通知类和通知 通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印。 类名和方法名没有要求,可以任意。

1
2
3
4
5
public class MyAdvice {
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}
  • 步骤四:定义切入点 BookDaoImpl中有两个方法,分别是update()和save(),要增强的是update方法,那么该如何定义呢?
1
2
3
4
5
6
7
8
9
public class MyAdvice {
    @Pointcut("execution(void mtmn.BookDaoImpl.update())")
    private void pt() {
    }

    public void method() {
        System.out.println(System.currentTimeMillis());
    }
}

说明:

  • 切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑。
  • execution及后面编写的内容,之后会专门去学习。
  • 步骤五:制作切面 切面是用来描述通知和切入点之间的关系,如何进行关系的绑定?
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class MyAdvice {

    @Pointcut("execution(void com.mtmn.dao.BookDao.update())")
    private void pt(){}
  
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置

说明:@Before翻译过来是之前,也就是说通知会在切入点方法执行之前执行,除此之前还有其他四种类型

这里就会在执行update()之前,来执行method(),输出当前时间的毫秒值

  • 步骤六:将通知类配给容器并标识其为切面类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Component
@Aspect
public class MyAdvice {

    @Pointcut("execution(void com.mtmn.dao.impl.BookDaoImpl.update())")
    private void pt() {
    }

    @Before("pt()")
    public void method() {
        System.out.println(System.currentTimeMillis());
    }
}
  • 步骤七:开启注解格式AOP功能 在Spring配置类上,使用 @EnableAspectJAutoProxy注解
1
2
3
4
5
@Configuration
@ComponentScan("com.mtmn")
@EnableAspectJAutoProxy
public class SpringConfig {
}
  • 步骤八:运行程序 这次再来调用update()
1
2
3
4
5
6
7
public class App {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = context.getBean(BookDao.class);
        bookDao.update();
    }
}

AOP工作流程

  • 配的切入点只有使用了才会读取
  • 切入点匹配上会创建代理对象

AOP切入点表达式

  • 切入点:要进行增强的方法
  • 切入点表达式:要进行增强的方法的描述方式

按接口描述或按实现类描述都可以

Spring.assets/image-20240318162148427.png

标准格式:

动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)

eg:execution(public User service.UserService.findById(int))

  • execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
  • public:访问修饰符,还可以是public,private等,可以省略
  • User:返回值,写返回值类型
  • service:包名,多级包使用点连接
  • UserService:类/接口名称
  • findById:方法名
  • int:参数,直接写参数的类型,多个类型用逗号隔开
  • 异常名:方法定义中抛出指定异常,可以省略

通配符

*:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现

书写技巧

对于切入点表达式的编写其实是很灵活的,那么在编写的时候,有没有什么好的技巧让用用:

  • 所有代码按照标准规范开发,否则以下技巧全部失效
  • 描述切入点通常 描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了
  • 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述
  • 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用 *通配快速描述
  • 包名书写尽量不使用 ..匹配,效率过低,常用 *做单个包描述匹配,或精准匹配
  • 接口名/类名书写名称与模块相关的采用 *匹配,例如UserService书写成 *Service,绑定业务层接口名
  • 方法名书写以 动词进行 精准匹配,名词采用 *匹配,例如 getById书写成 getBy*selectAll书写成 selectAll
  • 参数规则较为复杂,根据业务方法灵活调整
  • 通常 不使用异常作为 匹配规则

AOP通知类型

AOP通知共分为5种类型:

前置通知

后置通知

环绕通知(重点)

返回后通知(了解)

抛出异常后通知(了解)

Spring.assets/977e3848j00rji77q0019d000ni00ekp.jpg

环绕通知注意事项

  1. 环绕通知必须依赖形参 ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
  2. 通知中如果未使用 ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
  3. 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型
  4. 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
  5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常

TODO🧾 万次执行效率案例

TODO🧾 AOP通知获取数据

案例:百度网盘密码兼容处理

Spring.assets/image-20240319103434283.png

APP:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package bdwp;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        ResourceService service = context.getBean(ResourceService.class);
        boolean flag = service.openURL("https://pan.baidu.com/xx", "root");
        System.out.println(flag);
    }
}

ResourceDao:

1
2
3
4
5
package bdwp;

public interface ResourceDao {
    boolean readResource(String url,String password);
}

ResourceDaoImpl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package bdwp;

import org.springframework.stereotype.Repository;

@Repository
public class ResourceDaoImpl implements ResourceDao {
    public boolean readResource(String url, String password) {
        //模拟校验
        return password.equals("root");
    }
}

ResourceService:

1
2
3
4
5
package bdwp;

public interface ResourceService {
    public boolean openURL(String url,String password);
}

ResourceServiceImpl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package bdwp;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ResourceServiceImpl implements ResourceService {
    @Autowired
    private ResourceDao resourceDao;
    public boolean openURL(String url, String password) {
        return resourceDao.readResource(url,password);
    }
}

SpringConfig:

1
2
3
4
5
6
7
8
9
package bdwp;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("bdwp")
public class SpringConfig {
}
  • 开启SpringAOP的注解

    SpringConfig

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package bdwp;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan("bdwp")
🚩@EnableAspectJAutoProxy
public class SpringConfig {
}
  • 编写通知类

    MyAdvice

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package bdwp;

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;

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* bdwp.*Service.*(..))")
    public void pt(){}

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable
    {
        Object[] args = pjp.getArgs();
        for (int i = 0; i < args.length; i++) {
            if((String.class).equals(args[i].getClass())) {
                args[i] = args[i].toString().trim();
            }
        }
        Object res = pjp.proceed(args);
        return res;
    }
}

切面的优先级

给切面类增加 @Order注解,并指定具体的数字,值越小优先级越高

1
2
3
4
5
6
7
8
9
@Order(2)
@Component
@Aspect
public class ValidationAspect {   
     @Before("execution(public int com.yl.spring.aop.ArithmeticCalculator.*(..))")
    public void vlidateArgs(JoinPoint joinPoint) {
        System.out.println("validate: " + Arrays.asList(joinPoint.getArgs()));
    }
}

Spring事务

转账案例

将事务开在业务层

  • 开事务:在方法上加注解:🚩@Transactional(一般写在接口上)

  • 配事务管理器

    1
    2
    3
    4
    5
    6
    
    @Bean
    public PlatformTransactionManager platformTransactionManager(DataSource dataSource){
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
    

    DataSourceTransactionManager 主要用于 JDBC 数据源的事务管理。

  • 开启事务注解🚩@EnableTransactionManagement

    1
    2
    3
    4
    5
    6
    7
    
    @Configuration
    @ComponentScan("yhzz")
    @PropertySource("jdbc.properties")
    @EnableTransactionManagement
    @Import({JdbcConfig.class, MyBatisConfig.class})
    public class SpringConfig {
    }
    

使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import org.springframework.transaction.annotation.Transactional;

@Service
public class MyService {

    @Transactional
    public void someTransactionalMethod() {
        // 业务逻辑,Spring 会自动管理事务
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class MyService {

    private final PlatformTransactionManager transactionManager;

    @Autowired
    public MyService(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public void someTransactionalMethod() {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                // 业务逻辑
            }
        });
    }
}

如果有多个事务管理器

可以使用 @Primary 注解来标记主要的事务管理器,或者在 @Transactional 注解中指定特定的事务管理器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Bean
@Primary
public PlatformTransactionManager primaryTransactionManager(DataSource dataSource) {
    DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
    transactionManager.setDataSource(dataSource);
    return transactionManager;
}

@Bean
public PlatformTransactionManager secondaryTransactionManager(SecondaryDataSource secondaryDataSource) {
    DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
    transactionManager.setDataSource(secondaryDataSource);
    return transactionManager;
}
1
2
3
4
@Transactional(transactionManager = "secondaryTransactionManager")
public void someOtherTransactionalMethod() {
    // 业务逻辑
}

事务角色

Spring.assets/image-20240319153630525.png

  • 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
  • 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法

事务属性

事务配置

属性 作用 示例
readOnly 设置是否为只读事务 readOnly = true 只读事务
timeout 设置事务超时时间 timeout = -1(永不超时)
rollbackFor 设置事务回滚异常(class) rollbackFor{NullPointException.class}
rollbackForClassName 设置事务回滚异常(String) 同上格式为字符串
noRollbackFor 设置事务不回滚异常(class) noRollbackFor{NullPointExceptior.class}
noRollbackForClassName 设置事务不回滚异常(String) 同上格式为字符串
isolation 设置事务隔离级别 isolation = Isolation. DEFAULT
propagation 设置事务传播行为
  • 属性在**@Transactional**注解的参数上进行设置

可能存在的问题:

Spring.assets/image-20240319154844178.png

  • 事务传播行为:事务协调员对事务管理员所携带事务的处理态度

事务传播行为的可选值

传播属性 事务管理员 事务协调员
REQUIRED(默认) 开启T 加入T
新建T2
REQUIRES_NEW 开启T 新建T2
新建T2
SUPPORTS 开启T 加入T
NOT_SUPPORTED 开启T
MANDTORY 开启T 加入T
ERROR
NEVER 开启T ERROR
NESTED 设置savePoint,一旦事务回滚,事务将回滚到savePoint处,交由客户响应提交/回滚
  • 修改logService改变事务的传播行为
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Service
public class LogServiceImpl implements LogService {
    @Autowired
    private LogDao logDao;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void log(String out, String in, Double money) {
        logDao.log(out + "向" + in + "转账" + money + "元");
    }
}

SpringMVC

概述

./Spring.assets/631969fb16f2c2beb1dfbade.jpg

  • 如果所有的处理都交给Servlet来处理的话,所有的东西都耦合在一起,对后期的维护和扩展极其不利
    • 所以将后端服务器Servlet拆分成三层,分别是web、service和dao
    • web层主要由 servlet来处理,负责页面请求和数据的收集以及响应结果给前端
      • service层主要负责业务逻辑的处理
      • dao层主要负责数据的增删改查操作
  • servlet处理请求和数据时,存在一个问题:一个 servlet只能处理一个请求
  • 针对web层进行优化,采用MVC设计模式,将其设计为Controller、View和Model
    • controller负责请求和数据的接收,接收后将其转发给 service进行业务处理
    • service根据需要会调用 dao对数据进行增删改查
    • dao把数据处理完后,将结果交给 serviceservice再交给 controller
    • controller根据需求组装成 ModelViewModelView组合起来生成页面,转发给前端浏览器
    • 这样做的好处就是 controller可以处理多个请求,并对请求进行分发,执行不同的业务操作

随着互联网的发展,上面的模式因为是同步调用,性能慢慢的跟不是需求,所以异步调用慢慢的走到了前台,是现在比较流行的一种处理方式。

./Spring.assets/631969df16f2c2beb1dfa3f2.jpg

  • 因为是异步调用,所以后端不需要返回View视图,将其去除
  • 前端如果通过异步调用的方式进行交互,后端就需要将返回的数据转换成JSON格式进行返回
  • SpringMVC主要负责的就是
    • controller如何接收请求和数据
    • 如何将请求和数据转发给业务层
    • 如何将响应数据转换成JSON发回到前端
  • SpringMVC是一种基于Java实现MVC模型的轻量级Web框架
    • 优点
      • 使用简单、开发快捷(相比较于Servlet)
      • 灵活性强

ℹ️为什么一个servlet只能同时处理一个请求,而使用MVC之后的controller可以处理多个请求

这个问题涉及到 Servlet 的工作机制和 Spring MVC 中 Controller 的工作机制的差异。理解这两者的区别可以帮助更好地理解为什么一个 Servlet 通常只能处理一个请求,而 Spring MVC 的 Controller 可以处理多个请求。

Servlet 工作机制

Servlet 是 Java EE 中的服务器端组件,用于处理客户端请求并生成动态响应。Servlet 通过 Servlet 容器(如 Tomcat)进行管理。以下是 Servlet 的工作机制:

  1. 单线程模型(已经过时,不常用):在早期的 Servlet API 中,有一种单线程模型(通过实现 SingleThreadModel 接口),在这种模型下,每个请求都由一个独立的 Servlet 实例来处理,避免了线程安全问题。但是这种方式资源消耗大,不推荐使用。
  2. 多线程模型:大多数 Servlet 容器采用的是多线程模型,在这种模型下,同一个 Servlet 实例可以被多个线程同时访问。每个请求都会有一个独立的线程来处理,但这些线程共享同一个 Servlet 实例,因此需要注意线程安全问题。
    • 线程安全问题:如果 Servlet 中有共享的实例变量(成员变量),多个线程同时访问这些变量可能会导致数据不一致和其他线程安全问题。因此,开发者需要确保 Servlet 是线程安全的。

Spring MVC 中的 Controller 工作机制

Spring MVC 是一个基于 MVC(Model-View-Controller)设计模式的框架,用于构建 Web 应用程序。Spring MVC 中的 Controller 也由 Servlet 容器管理,但它的工作机制和传统的 Servlet 有一些不同:

  1. DispatcherServlet:Spring MVC 使用一个特殊的 Servlet,称为 DispatcherServlet,作为前端控制器(Front Controller)。所有的请求都会先经过 DispatcherServlet,然后由它分发给具体的处理器(Controller)。
  2. Controller 是普通的 Java 对象:Spring MVC 中的 Controller 只是一些标注了 @Controller@RestController 注解的普通 Java 类(POJO)。这些 Controller 由 Spring 的 IoC 容器管理,并且通常是单例的。
  3. 处理多个请求:由于 DispatcherServlet 使用多线程模型,每个请求都会由一个独立的线程来处理。Spring MVC 通过 DispatcherServlet 分发请求到对应的 Controller 方法上,这些方法通常是无状态的(不存储任何成员变量),因此可以同时处理多个请求,而不会出现线程安全问题。

为什么 Controller 可以处理多个请求

  1. 无状态的处理方法:Spring MVC 的 Controller 方法通常是无状态的,它们不依赖于共享的实例变量,因此不同线程可以安全地同时调用这些方法。
  2. Spring 的并发支持:Spring 框架本身对并发和线程安全有很好的支持。Controller 方法通常是无状态的,Spring 会通过依赖注入来管理任何需要的状态或服务,从而确保线程安全。
  3. Servlet 容器的线程模型:Spring MVC 的 Controller 是运行在 Servlet 容器的多线程环境中,这意味着每个请求都会由一个独立的线程处理。由于 Controller 方法本身是线程安全的,它们可以同时处理多个请求。

Servlet配置流程

  1. 创建web工程(Maven结构)
  2. 设置tomcat服务器,加载web工程(tomcat插件)
  3. 导入坐标(Servlet)
  4. 定义处理请求的功能类(UserServlet)
  5. 设置请求映射(配置映射关系)

SpringMVC配置流程

  1. 创建web工程(Maven结构)
  2. 设置tomcat服务器,加载web工程(tomcat插件)
  3. 导入坐标(SpringMVC+Servlet)
  4. 定义处理请求的功能类(UserController)
  5. 设置请求映射(配置映射关系)
  6. 将SpringMVC设定加载到Tomcat容器中
  • SpringMVC+Servlet坐标
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!--servlet-->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>
<!--springmvc-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>
  • 创建SpringMVC控制器类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//定义Controller,使用@Controller定义Bean
@Controller
public class UserController {
    //设置当前访问路径,使用@RequestMapping
    @RequestMapping("/save")
    //设置当前对象的返回值类型
    @ResponseBody
    public String save(){
        System.out.println("user save ...");
        return "{'module':'SpringMVC'}";
    }
}
  • 初始化SpringMVC环境(同Spring环境),设定SpringMVC加载对应的Bean
1
2
3
4
5
6
7
//创建SpringMVC的配置文件,加载controller对应的bean
@Configuration

@ComponentScan("controller")
public class SpringMvcConfig {

}
  • 初始化Servlet容器,加载SpringMVC环境,并设置SpringMVC技术处理的请求
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
//定义一个servlet容器的配置类,在里面加载Spring的配置,继承AbstractDispatcherServletInitializer并重写其方法
public class ServletContainerInitConfig extends AbstractDispatcherServletInitializer {
    //加载SpringMvc容器配置
    protected WebApplicationContext createServletApplicationContext() {
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(SpringMvcConfig.class);
        return context;
    }
  
    //设置哪些请求归SpringMvc处理
    protected String[] getServletMappings() {
        //所有请求都交由SpringMVC处理
        return new String[]{"/"};
    }

    //加载Spring容器配置
    protected WebApplicationContext createRootApplicationContext() {
        return null;
    }
}

./Spring.assets/image-20240323172758318.png

工作流程解析

  • 服务器启动,执行ServletContainerInitConfig类,初始化web容器
  • 执行createServletApplicationContext方法,创建了WebApplicationContext对象
  • 加载SpringMvcConfig配置类
  • 执行 @ComponentScan加载对应的bean
  • 加载 UserController,每个 @RequestMapping的名称对应一个具体的方法
  • 执行 getServletMappings方法,设定SpringMVC拦截请求的路径规则

单次请求过程

  1. 发送请求 http://localhost:8080/save
  2. web容器发现该请求满足SpringMVC拦截规则,将请求交给SpringMVC处理
  3. 解析请求路径/save
  4. 由/save匹配执行对应的方法save()上面的第5步已经将请求路径和方法建立了对应关系,通过 /save就能找到对应的 save()方法
  5. 执行 save()
  6. 检测到有 @ResponseBody直接将 save()方法的返回值作为响应体返回给请求方

Bean加载控制

项目结构 com.mtmn下有 configcontrollerservicedao这四个包

  • config目录存入的是配置类,写过的配置类有:

    • ServletContainersInitConfig
    • SpringConfig
    • SpringMvcConfig
    • JdbcConfig
    • MybatisConfig
  • controller目录存放的是 SpringMVCcontroller

  • service目录存放的是 service接口和实现类

  • dao目录存放的是 dao/Mapper接口

controller、service和dao这些类都需要被容器管理成bean对象,那么到底是该让 SpringMVC加载还是让 Spring加载呢?

  • SpringMVC控制的bean

    • 表现层bean,也就是 controller包下的类
  • Spring控制的bean

    • 业务bean(Service)
    • 功能bean(DataSource,SqlSessionFactoryBean,MapperScannerConfigurer等)

分析清楚谁该管哪些bean以后,接下来要解决的问题是如何让 SpringSpringMVC分开加载各自的内容。

解决方案:

  • 加载Spring控制的bean的时候,排除掉SpringMVC控制的bean

具体实现:

  • 方式一:Spring加载的bean设定扫描范围 com.mtmn,排除掉 controller包内的bean
1
2
3
4
5
6
7
8
@Configuration
@ComponentScan(value = "blog",
    excludeFilters = @ComponentScan.Filter( // 同时也有includeFilters
            type = FilterType.ANNOTATION, // 过滤注解类型
            classes = Controller.class
    ))
public class SpringConfig {
}

🚩🚩🚩SpringConfig里的@Configuration也要注释掉

  • 出现问题的原因是

    • Spring配置类扫描的包是 com.mtmn
    • SpringMVC的配置类,SpringMvcConfig上有一个 @Configuration注解,也会被Spring扫描到
    • SpringMvcConfig上又有一个 @ComponentScan,把controller类又给扫描进来了
    • 所以如果不把 @ComponentScan注释掉,Spring配置类将Controller排除,但是因为扫描到SpringMVC的配置类,又将其加载回来,演示的效果就出不来
    • 解决方案,也简单,把SpringMVC的配置类移出Spring配置类的扫描范围即可。
  • 方式二:Spring加载的bean设定扫描范围为精确扫描,具体到 service包,dao包等

1
2
3
4
@Configuration
@ComponentScan({"xxx.dao","xxx.service"})
public class SpringConfig {
}
  • 方式三:不区分Spring与SpringMVC的环境,加载到同一个环境中(了解即可)
  • 有了Spring的配置类,要想在tomcat服务器启动将其加载,需要修改ServletContainersInitConfig
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class ServletContainerInitConfig extends AbstractDispatcherServletInitializer {
    //加载SpringMvc配置
    protected WebApplicationContext createServletApplicationContext() {
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(🚩SpringMvcConfig.class);
        return context;
    }
  
    //设置哪些请求归SpringMvc处理
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    //加载Spring容器配置
    protected WebApplicationContext createRootApplicationContext() {
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(🚩SpringConfig.class);
        return context;
    }
}

🚩🚩🚩更方便的配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class ServletContainerInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {

    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{SpringConfig.class};
    }

    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{SpringMvcConfig.class};
    }

    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

请求

设置映射路径

  • 导入坐标

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    <!--servlet-->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
    <!--springmvc-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
    
  • 问题:

UserController有一个save方法,访问路径为 http://localhost/save

BookController也有一个save方法,访问路径为 http://localhost/save

当访问 http://localhost/save的时候,到底是访问 UserController还是 BookController?

法一:修改路径名 @RequestMapping("/user/save”) 缺点:耦合度高

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Controller
public class UserController {

    @RequestMapping("/user/save")
    @ResponseBody
    public String save(){
        System.out.println("user save ..");
        return "{'module':'user save'}";
    }

    @RequestMapping("/user/delete")
    @ResponseBody
    public String delete(){
        System.out.println("user delete ..");
        return "{'module':'user delete'}";
    }
}

法二:类和方法都加

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Controller
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/save")
    @ResponseBody
    public String save(){
        System.out.println("user save ..");
        return "{'module':'user save'}";
    }

    @RequestMapping("/delete")
    @ResponseBody
    public String delete(){
        System.out.println("user delete ..");
        return "{'module':'user delete'}";
    }
}
  • 当类上和方法上都添加了 @RequestMapping注解,前端发送请求的时候,要和两个注解的value值相加匹配才能访问到。
  • @RequestMapping注解value属性前面加不加 /都可以

请求参数

请求路径设置好后,只要确保页面发送请求地址和后台Controller类中配置的路径一致,就可以接收到前端的请求

  • GET:http://localhost:7777/user/commonParam?name=test&age=18

  • POST:./Spring.assets/image-20240323201535370.png

  • 解决Post中文乱码:配置过滤器

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    🚩import org.springframework.web.filter.CharacterEncodingFilter;
    
    public class ServletContainersInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
        protected Class<?>[] getRootConfigClasses() {
            return new Class[0];
        }
    
        protected Class<?>[] getServletConfigClasses() {
            return new Class[]{SpringMvcConfig.class};
        }
    
        protected String[] getServletMappings() {
            return new String[]{"/"};
        }
    
        //处理乱码问题
        🚩@Override
        protected Filter[] getServletFilters() {
            CharacterEncodingFilter filter = new CharacterEncodingFilter();
            filter.setEncoding("utf-8");
            return new Filter[]{filter};
        }
    }
    

五种类型参数传递

普通

1
localhost:7777/user/commonParam?name=Helsing&age=1024
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Controller
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/commonParam")
    @ResponseBody
    public String commonParam(String name,int age){
        System.out.println("普通参数传递name --> " + name);
        System.out.println("普通参数传递age --> " + age);
        return "{'module':'commonParam'}";
    }
}
  • 如果形参与地址参数名不一致,例如地址参数名为 username,而形参变量名为 name,因为前端给的是 username,后台接收使用的是 name,两个名称对不上,会导致接收数据失败

http://localhost:7777/user/commonParam?username=Helsing&age=1024

解决方案:使用 @RequestParam注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Controller
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/commonParam")
    @ResponseBody
    public String commonParam(@RequestParam("username") String name, int age){
        System.out.println("普通参数传递name --> " + name);
        System.out.println("普通参数传递age --> " + age);
        return "{'module':'commonParam'}";
    }
}

POJO

  • POJO参数:请求参数名与形参对象属性名相同,定义POJO类型形参即可接收参数
  • http://localhost:7777/user/pojoParam?username=Helsing&age=1024
1
2
3
4
5
6
7
//POJO参数:请求参数与形参对象中的属性对应即可完成参数传递
@RequestMapping("/pojoParam")
@ResponseBody
public String pojoParam(User user){
    System.out.println("POJO参数传递user --> " + user);
    return "{'module':'pojo param'}";
}
  • POJO参数接收,前端GET和POST发送请求数据的方式不变。
  • 请求参数key的名称要和POJO中属性的名称一致,否则无法封装。

嵌套POJO

http://localhost:7777/user/pojoParam?name=Helsing&age=1024&address.province=Beijing&address.city=Beijing

UserAddress:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package clazz;

public class UserAddress {
    private String name;
    private int age;

    private Address address;

    public Address getAddress() {
        return address;
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public UserAddress() {
    }

    public UserAddress(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        🚩this.address = address;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", address=" + address +
                '}';
    }
}
1
2
3
4
5
6
@RequestMapping("/pojoParam")
@ResponseBody
public String pojoParam(UserAddress user){
    System.out.println("POJO参数传递user --> " + user);
    return "{'module':'pojo param'}";
}

POJO参数传递user –> User{name=‘Helsing’, age=1024, address=Address{province=‘Beijing’, city=‘Beijing’}}

数组

  • 数组参数:请求参数名与形参对象属性名相同且请求参数为多个,定义数组类型即可接收参数
  • http://localhost:7777/user/arrayParam?hobbies=sing&hobbies=jump&hobbies=rap&hobbies=basketball
1
2
3
4
5
6
@RequestMapping("/arrayParam")
@ResponseBody
public String arrayParam(String[] hobbies){
    System.out.println("数组参数传递user --> " + Arrays.toString(hobbies));
    return "{'module':'array param'}";
}
  • 数组参数传递user –> [sing, jump, rap, basketball]

集合

http://localhost:7777/user/listParam?hobbies=sing&hobbies=jump&hobbies=rap&hobbies=basketball

❎错误方式:

1
2
3
4
5
6
@RequestMapping("/listParam")
@ResponseBody
public String listParam(List hobbies) {
    System.out.println("集合参数传递user --> " + hobbies);
    return "{'module':'list param'}";
}
  • 运行程序,报错

    1
    
    java.lang.IllegalArgumentException: Cannot generate variable name for non-typed Collection parameter type
    
    • 错误原因:SpringMVC将List看做是一个POJO对象来处理,将其创建一个对象并准备把前端的数据封装到对象中,但是List是一个接口无法创建对象,所以报错。

✅正确方式:

  • 使用 @RequestParam注解
1
2
3
4
5
6
@RequestMapping("/listParam")
@ResponseBody
public String listParam(@RequestParam List hobbies) {
    System.out.println("集合参数传递user --> " + hobbies);
    return "{'module':'list param'}";
}

知识点:@RequestParam

名称 @RequestParam
类型 形参注解
位置 SpringMVC控制器方法形参定义前面
作用 绑定请求参数与处理器方法形参间的关系
相关参数 required:是否为必传参数 defaultValue:参数默认值

JSON数据传输参数

常见的三种JSON数据类型:

  • json普通数组([“value1”,“value2”,“value3”,…])

  • json对象({key1:value1,key2:value2,…})

  • json对象数组([{key1:value1,…},{key2:value2,…}])

  • 导入坐标

    1
    2
    3
    4
    5
    
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.9.0</version>
    </dependency>
    
  • 开启SpringMVC注解支持

    使用 @EnableWebMvc,在SpringMVC的配置类中开启SpringMVC的注解支持,这里面就包含了将JSON转换成对象的功能。

Json普通数组

  • controller中添加

    1
    2
    3
    4
    5
    6
    
    @RequestMapping("/jsonArrayParam")
    @ResponseBody
    public String jsonArrayParam(@RequestBody List<String> hobbies) {
        System.out.println("JSON数组参数传递hobbies --> " + hobbies);
        return "{'module':'json array param'}";
    }
    

./Spring.assets/image-20240323205932289.png

JSON数组参数传递hobbies –> [chang, tiao, rap, lanqiu]

Json对象

1
2
3
4
5
6
@RequestMapping("/jsonPojoParam")
@ResponseBody
public String jsonArrayParam(@RequestBody UserAddress user) {
    System.out.println("JSON对象 --> " + user);
    return "{'module':'json array param'}";
}

http://localhost:7777/user/jsonPojoParam

./Spring.assets/image-20240323210420017.png

JSON对象 –> User{name=‘菲茨罗伊’, age=27, address=Address{province=‘外域’, city=‘萨尔沃’}}

Json对象数组

1
2
3
4
5
6
@RequestMapping("/jsonPojoListParam")
@ResponseBody
public String jsonPojoListParam(@RequestBody List<UserAddress> users) {
    System.out.println("JSON对象 --> " + users);
    return "{'module':'json array param'}";
}

http://localhost:7777/user/jsonPojoListParam

./Spring.assets/image-20240323210505831.png

JSON对象 –> [User{name=‘菲茨罗伊’, age=27, address=Address{province=‘外域’, city=‘萨尔沃’}}, User{name=‘罗伊’, age=7, address=Address{province=‘域’, city=‘尔沃’}}]

小结

SpringMVC接收JSON数据的实现步骤为:

  1. 导入jackson包
  2. 开启SpringMVC注解驱动,在配置类上添加 @EnableWebMvc注解
  3. 使用PostMan发送JSON数据
  4. Controller方法的参数前添加 @RequestBody注解

知识点1:@EnableWebMvc

名称 @EnableWebMvc
类型 配置类注解
位置 SpringMVC配置类定义上方
作用 开启SpringMVC多项辅助功能

知识点2:@RequestBody

名称 @RequestBody
类型 形参注解
位置 SpringMVC控制器方法形参定义前面
作用 将请求中请求体所包含的数据传递给请求参数,此注解一个处理器方法只能使用一次

@RequestBody@RequestParam区别

  • 区别
    • @RequestParam用于接收url地址传参,表单传参【application/x-www-form-urlencoded】
    • @RequestBody用于接收json数据【application/json】
  • 应用
    • 后期开发中,发送json格式数据为主,@RequestBody应用较广
    • 如果发送非json格式数据,选用 @RequestParam接收请求参数

日期类型

1
2
3
4
5
6
@RequestMapping("/dateParam")
@ResponseBody
public String dateParam(Date date) {
    System.out.println("参数传递date --> " + date);
    return "{'module':'date param'}";
}

SpringMVC默认支持的字符串转日期的格式为 yyyy/MM/dd

解决方案:使用 @DateTimeFormat注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@RequestMapping("/dateParam")
@ResponseBody
public String dateParam(Date date1,
                        @DateTimeFormat(pattern = "yyyy-MM-dd") Date date2,
                        @DateTimeFormat(pattern ="yyyy/MM/dd HH:mm:ss") Date date3) {
    System.out.println("参数传递date1 --> " + date1);
    System.out.println("参数传递date2 --> " + date2);
    System.out.println("参数传递date3 --> " + date3);
    return "{'module':'date param'}";
}

http://localhost:7777/user/dateParam?date1=2077/12/21&date2=1997-02-13

参数传递date1 –> Tue Dec 21 00:00:00 CST 2077 参数传递date2 –> Thu Feb 13 00:00:00 CST 1997 参数传递date3 –> Fri Sep 09 16:34:07 CST 2022

知识点:@DateTimeFormat

名称 @DateTimeFormat
类型 形参注解
位置 SpringMVC控制器方法形参前面
作用 设定日期时间型数据格式
相关属性 pattern:指定日期时间格式字符串

实现原理:

  • 前端传递字符串,后端使用日期Date接收
  • 前端传递JSON数据,后端使用对象接收
  • 前端传递字符串,后端使用Integer接收
  • 后台需要的数据类型有很多种
  • 在数据的传递过程中存在很多类型的转换

:谁来做这个类型转换?

  • :SpringMVC

:SpringMVC是如何实现类型转换的?

  • :SpringMVC中提供了很多类型转换接口和实现类
  1. Converter接口 注意:Converter所属的包为 org.springframework.core.convert.converter
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/**
*    S: the source type
*    T: the target type
*/
@FunctionalInterface
public interface Converter<S, T> {
    @Nullable
    //该方法就是将从页面上接收的数据(S)转换成想要的数据类型(T)返回
    T convert(S source);
}
  1. HttpMessageConverter接口

该接口是实现对象与JSON之间的转换工作 注意:需要在SpringMVC的配置类把 @EnableWebMvc当做标配配置上去,不要省略

响应

响应页面

  • 添加页面
1
2
3
4
5
6
7
8
9
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
HELLO SPRING MVC!!
</body>
</html>
  • 修改UserController
1
2
3
4
5
6
7
8
@RequestMapping("/toJumpPage")
//注意
🚩//1.此处不能添加@ResponseBody,如果加了该注入,会直接将page.jsp当字符串返回前端
//2.方法需要返回String
public String toJumpPage(){
    System.out.println("跳转页面");
    return "page.jsp";
}
  • 访问http://localhost:7777/toJumpPage

./Spring.assets/image-20240324120432962.png

响应数据

文本数据

  • 添加
1
2
3
4
5
6
7
8
@RequestMapping("toText")
🚩🚩🚩//此时就需要添加@ResponseBody,将`response text`当成字符串返回给前端
//如果不写@ResponseBody,则会将response text当成页面名去寻找,找不到报404
@ResponseBody
public String toText(){
    System.out.println("返回纯文本数据");
    return "response text";
}
  • 访问http://localhost:7777/toText

./Spring.assets/image-20240324120621204.png

json数据

POJO
  • 添加
1
2
3
4
5
6
7
8
9
@RequestMapping("toJsonPojo")
@ResponseBody
public User toJsonPojo(){
    System.out.println("返回json对象数据");
    User user = new User();
    user.setName("Helsing");
    user.setAge(9527);
    return user;
}
  • 访问http://localhost:7777/toJsonPojo

./Spring.assets/image-20240324120946168.png

HttpMessageConverter接口帮实现了对象与JSON之间的转换工作,只需要在 SpringMvcConfig配置类上加上 @EnableWebMvc注解即可

POJO集合
  • 添加
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    @RequestMapping("toJsonList")
    @ResponseBody
    public List<User> toJsonList(){
        List<User> users = new ArrayList<User>();

        User user1 = new User();
        user1.setName("马文");
        user1.setAge(27);
        users.add(user1);

        User user2 = new User();
        user2.setName("马武");
        user2.setAge(28);
        users.add(user2);

        return users;
    }
  • 访问http://localhost:7777/toJsonList

./Spring.assets/image-20240324121051113.png

知识点:@ResponseBody

名称 @ResponseBody
类型 方法\类注解
位置 SpringMVC控制器方法定义上方和控制类上
作用 设置当前控制器返回值作为响应体, 写在类上,该类的所有方法都有该注解功能
相关属性 pattern:指定日期时间格式字符串

说明:

  • 该注解可以写在类上或者方法上
  • 写在类上就是该类下的所有方法都有 @ReponseBody功能
  • 当方法上有@ReponseBody注解后
    • 方法的返回值为字符串,会将其作为文本内容直接响应给前端
    • 方法的返回值为对象,会将对象转换成JSON响应给前端

此处又使用到了类型转换,内部还是通过 HttpMessageConverter接口完成的,所以 Converter除了前面所说的功能外,它还可以实现:

  • 对象转Json数据(POJO -> json)
  • 集合转Json数据(Collection -> json)

REST风格

  • 按照REST风格访问资源时,使用行为动作区分对资源进行了何种操作

    • http://localhost/users 查询全部用户信息 GET(查询)
    • http://localhost/users/1 查询指定用户信息 GET(查询)
    • http://localhost/users 添加用户信息 POST(新增/保存)
    • http://localhost/users 修改用户信息 PUT(修改/更新)
    • http://localhost/users/1 删除用户信息 DELETE(删除)
  • REST提供了对应的架构方式,按照这种架构方式设计项目可以降低开发的复杂性,提高系统的可伸缩性

    REST中规定 GET/POST/PUT/DELETE针对的是查询/新增/修改/删除,但如果非要使用 GET请求做删除,这点在程序上运行是可以实现的

    但是如果大多数人都遵循这种风格,你不遵循,那你写的代码在别人看来就有点莫名其妙了,所以最好还是遵循REST风格

  • 描述模块的名称通常使用复数,也就是加s的格式描述,表示此类的资源,而非单个的资源,例如 usersbooksaccounts

RESTful

  • 根据REST风格对资源进行访问称为 RESTful
  • save
1
2
3
4
5
6
@RequestMapping(value = "/users", method = RequestMethod.POST)
@ResponseBody
public String save(@RequestBody User user) {
    System.out.println("user save ..." + user);
    return "{'module':'user save'}";
}
1
2
3
4
{
    "name":"菲茨罗伊",
    "age":"27"
}

./Spring.assets/image-20240324153021156.png

user save …User{name=‘菲茨罗伊’, age=27}

  • delete
1
2
3
4
5
6
@RequestMapping(value = "/users/{id}",method = RequestMethod.DELETE)
@ResponseBody
public String delete(🚩🚩🚩@PathVariable Integer id){
    System.out.println("user delete ..." + id);
    return "{'module':'user delete'}";
}

./Spring.assets/image-20240324153337938.png

疑问:如果方法形参的名称和路径 {}中的值不一致,该怎么办? 例如 "/users/{id}"delete(@PathVariable Integer userId)

  • 解答:如果这两个值不一致,就无法获取参数,此时可以在注解后面加上属性,让注解的属性值与 {}中的值一致即可,具体代码如下
1
2
3
4
5
6
@RequestMapping(value = "/users/{id}",method = RequestMethod.DELETE)
@ResponseBody
public String delete(@PathVariable("id") Integer userId){
    System.out.println("user delete ..." + userId);
    return "{'module':'user delete'}";
}

疑问:如果有多个参数需要传递该如何编写? 前端发送请求时使用 localhost:8080/users/9421/Tom,路径中的 9421Tom就是想传递的两个参数

  • 解答:在路径后面再加一个 /{name},同时在方法参数中增加对应属性即可
1
2
3
4
5
6
@RequestMapping(value = "/users/{id}/{name}",method = RequestMethod.DELETE)
@ResponseBody
public String delete(@PathVariable("id") Integer userId,@PathVariable String name){
    System.out.println("user delete ..." + userId + ":" + name);
    return "{'module':'user delete'}";
}

./Spring.assets/image-20240324153720925.png

user delete …1:w

  • update

使用put调用

1
2
3
4
5
6
@RequestMapping(value = "/users",method = RequestMethod.PUT)
@ResponseBody
public String update(@RequestBody User user){
    System.out.println("user update ..." + user);
    return "{'module':'user update'}";
}

./Spring.assets/image-20240324153824872.png

user update …User{name=‘菲茨罗伊’, age=27}

  • 根据ID查询
1
2
3
4
5
6
@RequestMapping(value = "/users/{id}",method = RequestMethod.GET)
@ResponseBody
public String getById(@PathVariable Integer id){
    System.out.println("user getById ..." + id);
    return "{'module':'user getById'}";
}

./Spring.assets/image-20240324155247838.png

user getById …1

  • 查询所有
1
2
3
4
5
6
7
    @RequestMapping(value = "users", method = RequestMethod.GET)
    @ResponseBody
    public String getAll()
    {
        System.out.println("user getAll ...");
        return "{'module':'user getAll'}";
    }

./Spring.assets/image-20240324155302428.png

user getAll …

整体代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package controller;

import clazz.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller
public class UserController {
    @RequestMapping(value = "/users", method = RequestMethod.POST)
    @ResponseBody
    public String save(@RequestBody User user) {
        System.out.println("user save ..." + user);
        return "{'module':'user save'}";
    }

    @RequestMapping(value = "/users/{id}",method = RequestMethod.DELETE)
    @ResponseBody
    public String delete(@PathVariable Integer id){
        System.out.println("user delete ..." + id);
        return "{'module':'user delete'}";
    }

    @RequestMapping(value = "/users/{id}/{name}",method = RequestMethod.DELETE)
    @ResponseBody
    public String delete(@PathVariable("id") Integer userId,@PathVariable String name){
        System.out.println("user delete ..." + userId + ":" + name);
        return "{'module':'user delete'}";
    }

    @RequestMapping(value = "/users",method = RequestMethod.PUT)
    @ResponseBody
    public String update(@RequestBody User user){
        System.out.println("user update ..." + user);
        return "{'module':'user update'}";
    }

    @RequestMapping(value = "users/{id}", method = RequestMethod.GET)
    @ResponseBody
    public String getById(@PathVariable Integer id)
    {
        System.out.println("user getById ..." + id);
        return "{'module':'user getById'}";
    }

    @RequestMapping(value = "users", method = RequestMethod.GET)
    @ResponseBody
    public String getAll()
    {
        System.out.println("user getAll ...");
        return "{'module':'user getAll'}";
    }
}

从整体代码来看,有些臃肿,好多代码都是重复的,下一小节就会来解决这个问题

  • 小结

    • 设定Http请求动作(动词)

      1
      
      @RequestMapping(value="",method = RequestMethod.POST|GET|PUT|DELETE)
      
    • 设定请求参数(路径变量)

      1
      2
      3
      4
      
      @RequestMapping(value="/users/{id}",method = RequestMethod.DELETE)
      @ReponseBody
      public String delete(@PathVariable Integer id){
      }
      

知识点:@PathVariable

名称 @PathVariable
类型 形参注解
位置 SpringMVC控制器方法形参定义前面
作用 绑定路径参数与处理器方法形参间的关系,要求路径参数名与形参名一一对应

三个注解 @RequestBody@RequestParam@PathVariable之间的区别和应用

  • 区别
    • @RequestParam用于接收url地址传参或表单传参
    • @RequestBody用于接收JSON数据
    • @PathVariable用于接收路径参数,使用{参数名称}描述路径参数
  • 应用
    • 后期开发中,发送请求参数超过1个时,以JSON格式为主,@RequestBody应用较广
    • 如果发送非JSON格式数据,选用 @RequestParam接收请求参数
    • 采用 RESTful进行开发,当参数数量较少时,例如1个,可以采用 @PathVariable接收请求路径变量,通常用于传递id值

RESTful快速开发

做完了上面的 RESTful的开发,就感觉好麻烦,主要体现在以下三部分

  • 每个方法的 @RequestMapping注解中都定义了访问路径 /users,重复性太高。
    • 解决方案:将 @RequestMapping提到类上面,用来定义所有方法共同的访问路径。
  • 每个方法的 @RequestMapping注解中都要使用method属性定义请求方式,重复性太高。
    • 解决方案:使用 @GetMapping@PostMapping@PutMapping@DeleteMapping代替
  • 每个方法响应json都需要加上 @ResponseBody注解,重复性太高。
    • 解决方案:
      • @ResponseBody提到类上面,让所有的方法都有 @ResponseBody的功能
      • 使用 @RestController注解替换 @Controller@ResponseBody注解,简化书写
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package controller;

import clazz.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@RequestMapping("users")
@RestController
public class UserController {
    @PostMapping
    public String save(@RequestBody User user) {
        System.out.println("user save ..." + user);
        return "{'module':'user save'}";
    }

    @DeleteMapping("{id}")
    public String delete(@PathVariable("id") Integer id){
        System.out.println("user delete ..." + id);
        return "{'module':'user delete'}";
    }

    @DeleteMapping("{id}/{name}")
    public String delete(@PathVariable("id") Integer userId,@PathVariable("name") String name){
        System.out.println("user delete ..." + userId + ":" + name);
        return "{'module':'user delete'}";
    }

    @PutMapping
    public String update(@RequestBody User user){
        System.out.println("user update ..." + user);
        return "{'module':'user update'}";
    }

    @GetMapping("{id}")
    public String getById(@PathVariable Integer id)
    {
        System.out.println("user getById ..." + id);
        return "{'module':'user getById'}";
    }

    @GetMapping
    public String getAll()
    {
        System.out.println("user getAll ...");
        return "{'module':'user getAll'}";
    }
}

user getAll … user getById …1 user delete …1 user update …User{name=‘菲茨罗伊’, age=27} user save …User{name=‘菲茨罗伊’, age=27}

案例

页面访问处理

  • 步骤一:导入提供好的静态页面
  • 步骤二:访问pages目录下的books.html
    • 打开浏览器访问 http://localhost:7777/pages/book.html,报404,为什么呢?
    • SpringMvcConfig拦截了所有资源路径
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Configuration
public class SpringMvcSupport extends WebMvcConfigurationSupport {
    //设置静态资源访问过滤,当前类需要设置为配置类,并被扫描加载
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        //当访问/pages/xxx时候,从/pages目录下查找内容
        registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
        registry.addResourceHandler("/js/**").addResourceLocations("/js/");
        registry.addResourceHandler("/css/**").addResourceLocations("/css/");
        registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/");
    }
}

SSM整合

流程分析

  1. 创建工程

    • 创建一个Maven的web工程

    • pom.xml添加SSM需要的依赖jar包

    • 编写Web项目的入口配置类,实现

      1
      
      AbstractAnnotationConfigDispatcherServletInitializer
      

      重写以下方法

      • getRootConfigClasses() :返回Spring的配置类 –> 需要 SpringConfig配置类
      • getServletConfigClasses() :返回SpringMVC的配置类 –> 需要 SpringMvcConfig配置类
      • getServletMappings() : 设置SpringMVC请求拦截路径规则
      • getServletFilters() :设置过滤器,解决POST请求中文乱码问题
  2. SSM整合(重点是各个配置的编写)

    • SpringConfig

      • 标识该类为配置类,使用 @Configuration

      • 扫描 Service所在的包,使用 @ComponentScan

      • Service层要管理事务,使用 @EnableTransactionManagement

      • 读取外部的 properties配置文件,使用 @PropertySource

      • 整合 Mybatis

        需要引入Mybatis相关配置类,使用 @Import

        • 第三方数据源配置类 JdbcConfig
        • 构建DataSource数据源,DruidDataSouroce,需要注入数据库连接四要素,使用 @Bean@Value
        • 构建平台事务管理器,DataSourceTransactionManager,使用 @Bean
        • Mybatis配置类 MybatisConfig
        • 构建 SqlSessionFactoryBean并设置别名扫描与数据源,使用 @Bean
        • 构建 MapperScannerConfigurer并设置DAO层的包扫描
    • SpringMvcConfig

      • 标识该类为配置类,使用 @Configuratio
      • 扫描 Controller所在的包,使用 @ComponentScan
      • 开启SpringMVC注解支持,使用 @EnableWebMvc
  3. 功能模块(与具体的业务模块有关)

    • 创建数据库表

    • 根据数据库表创建对应的模型类

    • 通过Dao层完成数据库表的增删改查(接口+自动代理)

    • 编写 Service层(Service接口+实现类)

      • @Service
      • @Transactional
      • 整合Junit对业务层进行单元测试
        • @RunWith
        • @ContextConfiguration
        • @Test
    • 编写 Controller

      • 接收请求 @RequestMapping@GetMapping@PostMapping@PutMapping@DeleteMapping
      • 接收数据 简单、POJO、嵌套POJO、集合、数组、JSON数据类型
        • @RequestParam
        • @PathVariable
        • @RequestBody
      • 转发业务层
        • @Autowired
      • 响应结果
        • @ResponseBody

整合配置

分析完毕之后,就一步步来完成的SSM整合

  • 步骤一:创建Maven的web项目
  • 步骤二:导入坐标
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.example</groupId>
        <artifactId>LearnWeb</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>L_SSM</artifactId>
    <packaging>war</packaging>
    <name>L_SSM Maven Webapp</name>
    <url>http://maven.apache.org</url>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.6</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.3.0</version>
        </dependency>

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

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

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.0</version>
        </dependency>
    </dependencies>
    <build>
        <finalName>L_SSM</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <port>7777</port>
                    <path>/</path>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  • 步骤三:创建项目包结构
    • config目录存放的是相关的配置类
    • controller编写的是Controller类
    • dao存放的是Dao接口,因为使用的是Mapper接口代理方式,所以没有实现类包
    • service存的是Service接口,service.impl存放的是Service实现类
    • resources:存入的是配置文件,如jdbc.properties
    • domain:存放类
    • webapp:目录可以存放静态资源
    • test/java:存放的是测试类

./Spring.assets/image-20240326121308066.png

  • 步骤四:创建jdbc.properties
1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///learn?useSSL=false
jdbc.username=cdd
jdbc.password=1
  • 步骤五:创建JdbcConfig配置类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource()
    {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

    @Bean
    public PlatformTransactionManager platformTransactionManager(DataSource dataSource)
    {
        DataSourceTransactionManager ds = new DataSourceTransactionManager();
        ds.setDataSource(dataSource);
        return ds;
    }
}
  • 步骤六:创建MyBatisConfig配置类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package config;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;

public class MybatisConfig {
    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource)
    {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setTypeAliasesPackage("domain");
        return factoryBean;
    }
  
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer()
    {
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        mapperScannerConfigurer.setBasePackage("dao");
        return mapperScannerConfigurer;
    }
}

ℹ️ Mybatis配置类

public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource)

作用

  • 创建并配置 SqlSessionFactoryBean 实例,用于管理MyBatis的 SqlSession 和数据库连接。
  • 将数据源(DataSource)设置到 SqlSessionFactoryBean 中,以便MyBatis可以与数据库进行交互。
  • 设置类型别名包(Type Aliases Package),告诉MyBatis在哪里寻找领域对象(Domain Objects)的别名。

配置说明

  • DataSource dataSource:作为方法参数传入,通常在Spring中配置好的数据源,用于连接数据库。
  • setTypeAliasesPackage("domain"):指定MyBatis扫描哪个包下的Java类,这些类会作为别名注册,可以在SQL映射文件中使用类名作为别名来代替全限定类名。

public MapperScannerConfigurer mapperScannerConfigurer()

作用

  • 创建并配置 MapperScannerConfigurer 实例,用于扫描指定包路径下的MyBatis Mapper接口。
  • 这些Mapper接口通常定义了与数据库交互的SQL操作,MyBatis会自动将它们注册为Spring Bean。

配置说明

  • setBasePackage("dao"):指定MyBatis扫描哪个包下的Mapper接口,这些接口中定义了SQL操作,可以通过注解或XML配置文件与SQL语句进行映射。

  • 步骤七:创建SpringConfig配置类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@ComponentScan("service")
@PropertySource("classpath:jdbc.properties")🚩🚩🚩不加classpath报错
@EnableTransactionManagement
@Import({JdbcConfig.class, MybatisConfig.class})
public class SpringConfig {
}
  • 步骤八:创建SpringMvcConfig配置类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@ComponentScan("controller")
@EnableWebMvc
public class SpringMvcConfig {
}
  • 步骤九:创建ServletContainersInitConfig配置类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package config;

import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

import javax.servlet.Filter;

public class ServletContainerInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{SpringConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{SpringMvcConfig.class};
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter filter = new CharacterEncodingFilter();
        filter.setEncoding("utf-8");
        return new Filter[]{filter};
    }
}

功能模块开发

需求:对表tbl_book进行新增、修改、删除、根据ID查询和查询所有

  • 步骤一:创建数据库及表
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use learn;
create table tbl_book
(
    id          int primary key auto_increment,
    type        varchar(20),
    `name`      varchar(50),
    description varchar(255)
);

insert into `tbl_book`(`id`, `type`, `name`, `description`)
values (1, '计算机理论', 'Spring实战 第五版', 'Spring入门经典教程,深入理解Spring原理技术内幕'),
       (2, '计算机理论', 'Spring 5核心原理与30个类手写实践', '十年沉淀之作,手写Spring精华思想'),
       (3, '计算机理论', 'Spring 5设计模式', '深入Spring源码刨析Spring源码中蕴含的10大设计模式'),
       (4, '计算机理论', 'Spring MVC+Mybatis开发从入门到项目实战',
        '全方位解析面向Web应用的轻量级框架,带你成为Spring MVC开发高手'),
       (5, '计算机理论', '轻量级Java Web企业应用实战', '源码级刨析Spring框架,适合已掌握Java基础的读者'),
       (6, '计算机理论', 'Java核心技术 卷Ⅰ 基础知识(原书第11版)',
        'Core Java第11版,Jolt大奖获奖作品,针对Java SE9、10、11全面更新'),
       (7, '计算机理论', '深入理解Java虚拟机', '5个纬度全面刨析JVM,大厂面试知识点全覆盖'),
       (8, '计算机理论', 'Java编程思想(第4版)', 'Java学习必读经典,殿堂级著作!赢得了全球程序员的广泛赞誉'),
       (9, '计算机理论', '零基础学Java(全彩版)', '零基础自学编程的入门图书,由浅入深,详解Java语言的编程思想和核心技术'),
       (10, '市场营销', '直播就这么做:主播高效沟通实战指南', '李子柒、李佳奇、薇娅成长为网红的秘密都在书中'),
       (11, '市场营销', '直播销讲实战一本通', '和秋叶一起学系列网络营销书籍'),
       (12, '市场营销', '直播带货:淘宝、天猫直播从新手到高手', '一本教你如何玩转直播的书,10堂课轻松实现带货月入3W+');
  • 步骤二:编写模型类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package domain;

import java.util.Objects;

public class Book {
    private Integer id;
    private String type;
    private String name;
    private String description;

    public Book() {
    }

    public Book(Integer id, String type, String name, String description) {
        this.id = id;
        this.type = type;
        this.name = name;
        this.description = description;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        };
        if (o == null || getClass() != o.getClass()) {
            return false;
        };
        Book book = (Book) o;
        return Objects.equals(id, book.id) && Objects.equals(type, book.type) && Objects.equals(name, book.name) && Objects.equals(description, book.description);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, type, name, description);
    }

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", type='" + type + '\'' +
                ", name='" + name + '\'' +
                ", description='" + description + '\'' +
                '}';
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getName() {
        return name;
    }

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

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}
  • 步骤三:编写Dao接口
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package dao;

import domain.Book;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import java.util.List;

public interface BookDao {
    @Insert(value = "insert into tbl_book values (null, #{type}, #{name}, #{description})")
    void save(Book book);
    @Update(value = "update tbl_book set type=#{type}, name=#{name}, description=#{description} where id=#{id}")
    void update(Book book);
    @Delete(value = "delete from tbl_book where id=#{id}")
    void delete(Integer id);
    @Select(value = "select * from tbl_book")
    List<Book> getAll();
    @Select(value = "select * from tbl_book where id=#{id}")
    Book getById(Integer id);
}
  • 步骤四:编写Service接口及其实现类
  • BookService
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package service;

import domain.Book;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Transactional
public interface BookService {
    /**
     * 保存
     * @param book
     * @return
     */
    boolean save(Book book);

    /**
     * 修改
     * @param book
     * @return
     */
    boolean update(Book book);

    /**
     * 按id删除
     * @param id
     * @return
     */
    boolean delete(Integer id);

    /**
     * 按id查询
     * @param id
     * @return
     */
    Book getById(Integer id);

    /**
     * 查询所有
     * @return
     */
    List<Book> getAll();
}
  • BookServiceImpl
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package service.impl;

import dao.BookDao;
import domain.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import service.BookService;

import java.util.List;

@Service
public class BookServiceImpl implements BookService {

    @Autowired
    private BookDao bookDao;

    @Override
    public boolean save(Book book) {
        bookDao.save(book);
        return true;
    }
    @Override
    public boolean update(Book book) {
        bookDao.update(book);
        return true;
    }
    @Override
    public boolean delete(@PathVariable Integer id) {
        bookDao.delete(id);
        return true;
    }
    @Override
    public Book getById(Integer id) {
        return bookDao.getById(id);
    }
    @Override
    public List<Book> getAll() {
        return bookDao.getAll();
    }
}
  • 步骤五:编写Controller类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package controller;

import domain.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import service.BookService;

import java.util.List;

@RestController
@RequestMapping("/books")
public class BookController {
    @Autowired
    private BookService bookService;

    @PostMapping
    public boolean save(@RequestBody Book book) {
        return bookService.save(book);
    }

    @PutMapping
    public boolean update(@RequestBody Book book) {
        return bookService.update(book);
    }

    @DeleteMapping("/{id}")
    public boolean delete(@PathVariable Integer id) {
        return bookService.delete(id);
    }

    @GetMapping("/{id}")
    public Book getById(@PathVariable Integer id) {
        return bookService.getById(id);
    }

    @GetMapping
    public List<Book> getAll() {
        return bookService.getAll();
    }
}

统一结果封装

  • 目前已经有 三种数据类型返回给前端了,随着业务的增长,需要返回的数据类型就会 越来越多。那么前端开发人员在解析数据的时候就比较 凌乱了,所以对于前端来说,如果后端能返回一个 统一的数据结果,前端在解析的时候就可以按照一种方式进行解析,开发就会变得更加简单

  • 所以现在需要解决的问题就是 如何将返回的结果数据进行统一,具体如何来做,大体思路如下

    • 为了封装返回的结果数据:创建结果模型类,封装数据到data属性中
      • 可以设置data的数据类型为 Object,这样data中就可以放任意的结果类型了,包括但不限于上面的 boolean对象集合对象
    • 为了封装返回的数据是何种操作,以及是否操作成功:封装操作结果到code属性中
      • 例如增删改操作返回的都是 true,那怎么分辨这个 true到底是 还是 还是 呢?就通过这个 code来区分
    • 操作失败后,需要封装返回错误信息提示给用户:封装特殊消息到message(msg)属性中
      • 例如查询或删除的目标不存在,会返回null,那么此时需要提示 查询/删除的目标不存在,请重试!
  • boolean

规则可以自己定 这里前三位是固定的 第四位表示不同的操作 末位表示成功/失败,1成功,0失败

1
2
3
4
{
    "code":20011,
    "data":true
}
  • 对象

这里的末尾是0,表示失败操作 第四位是2,区别于上面的1,表示是不同的操作类型 msg给用户提示信息,不是必有项

1
2
3
4
5
{
    "code":20020,
    "data":null,
    "msg":"数据查询失败,请重试!"
}
  • 对象集合

这里末尾操作是1,表示成功操作 data中显示的是对象集合 没有msg

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
    "code":20031,
    "data":[
        {
            "id":1,
            "type":"计算机理论",
            "name":"Spring实战 第5版",
            "description":"Spring入门经典教程"
        },
        {
            "id":2,
            "type":"计算机理论",
            "name":"Spring 5核心原理与30个类手写实战",
            "description":"十年沉淀之作"
        }
    ]
}
  • 结果类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package domain;

public class Result {
    //描述统一格式中的编码,用于区分操作,可以简化配置0或1表示成功失败
    private Integer code;
    //描述统一格式中的数据
    private Object data;
    //描述统一格式中的消息,可选属性
    private String msg;

    public Result() {
    }

    //构造器可以根据自己的需要来编写
    public Result(Integer code, Object data) {
        this.code = code;
        this.data = data;
    }

    public Result(Integer code, Object data, String msg) {
        this.code = code;
        this.data = data;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    @Override
    public String toString() {
        return "Result{" +
                "code=" + code +
                ", data=" + data +
                ", msg='" + msg + '\'' +
                '}';
    }
}
  • 错误码类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package domain;

public class Code {
    public static final Integer SAVE_OK = 20011;
    public static final Integer UPDATE_OK = 20021;
    public static final Integer DELETE_OK = 20031;
    public static final Integer GET_OK = 20041;

    public static final Integer SAVE_ERR = 20010;
    public static final Integer UPDATE_ERR = 20020;
    public static final Integer DELETE_ERR = 20030;
    public static final Integer GET_ERR = 20040;
}
  • 修改BookController
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package controller;

import domain.Book;
import domain.Code;
import domain.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import service.BookService;

import java.util.List;

@RestController
@RequestMapping("/books")
public class BookController {
    @Autowired
    private BookService bookService;

    @PostMapping
    public Result save(@RequestBody Book book) {
        boolean flag = bookService.save(book);
        return new Result(flag ? Code.SAVE_OK : Code.SAVE_ERR, flag);
    }

    @PutMapping
    public Result update(@RequestBody Book book) {
        boolean flag = bookService.update(book);
        return new Result(flag ? Code.UPDATE_OK : Code.UPDATE_ERR, flag);
    }

    @DeleteMapping("/{id}")
    public Result delete(@PathVariable Integer id) {
        boolean flag = bookService.delete(id);
        return new Result(flag ? Code.DELETE_OK : Code.DELETE_ERR, flag);
    }

    @GetMapping("/{id}")
    public Result getById(@PathVariable Integer id) {
        Book book = bookService.getById(id);
        Integer code = book == null ? Code.GET_ERR : Code.GET_OK;
        String msg = book == null ? "数据查询失败,请重试!" : "";
        return new Result(code, book, msg);
    }

    @GetMapping
    public Result getAll() {
        List<Book> books = bookService.getAll();
        Integer code = books == null ? Code.GET_ERR : Code.GET_OK;
        String msg = books == null ? "数据查询失败,请重试!" : "";
        return new Result(code, books, msg);
    }
}

统一异常处理

异常的种类,以及出现异常的原因:

  • 框架内部抛出的异常:因 使用不合规导致
  • 数据层抛出的异常:因使用 外部服务器故障导致(例如:服务器访问超时)
  • 业务层抛出的异常:因 业务逻辑书写错误导致(例如:遍历业务书写操作,导致索引越界异常等)
  • 表现层抛出的异常:因 数据收集校验等规则导致(例如:不匹配的数据类型间转换导致异常)
  • 工具类抛出的异常:因工具类 书写不严谨健壮性不足导致(例如:必需要释放的连接,长时间未释放等)

在开发的 任何一个位置都可能会出现异常,而且这些异常是 不能避免的,所以就需要对这些异常来 进行处理

思考:

各个层级均出现异常,那么异常处理代码要写在哪一层?

所有的异常均抛出到表现层进行处理

异常的种类很多,表现层如何将所有的异常都处理到呢?

异常分类

表现层处理异常,每个方法中单独书写,代码书写量巨大,且意义不强,如何解决呢?

AOP

异常处理器

  • contorller.ProjectExceptionAdvice
1
2
3
4
5
6
7
8
@RestControllerAdvice
public class ProjectExceptionAdvice {
    @ExceptionHandler(Exception.class)
    public Result doException(Exception ex) {
        System.out.println("嘿嘿,逮到一个异常~");
        return new Result(666, null, "嘿嘿,逮到一个异常~");
    }
}

知识点:@RestControllerAdvice

说明:此注解自带 @ResponseBody注解与 @Component注解,具备对应的功能

名称 @RestControllerAdvice
类型 类注解
位置 Rest风格开发的控制器增强类定义上方
作用 为Rest风格开发的控制器类做增强

知识点:@ExceptionHandler

说明:此类方法可以根据处理的异常不同,制作多个方法分别处理对应的异常

名称 @ExceptionHandler
类型 方法注解
位置 专用于异常处理的控制器方法上方
作用 设置指定异常的处理方案,功能等同于控制器方法, 出现异常后终止原始控制器执行,并转入当前方法执行

项目异常处理方案

异常分类

因为异常的种类有很多,如果每一个异常都对应一个 @ExceptionHandler,那得写多少个方法来处理各自的异常,所以在处理异常之前,需要对异常进行一个分类:

  • 业务异常(BusinessException)

    • 规范的用户行为产生的异常
      • 用户在页面输入内容的时候未按照指定格式进行数据填写,如在年龄框输入的是字符串
    • 不规范的用户行为操作产生的异常
      • 如用户手改URL,故意传递错误数据 localhost:8080/books/略略略
  • 系统异常(SystemException)

    • 项目运行过程中可预计,但无法避免的异常
      • 如服务器宕机
  • 其他异常(Exception)

    • 编程人员未预期到的异常
      • 如:系统找不到指定文件

将异常分类以后,针对不同类型的异常,要提供具体的解决方案

异常解决方案

  • 业务异常(BusinessException)

    • 发送对应消息传递给用户,提醒规范操作
      • 大家常见的就是提示用户名已存在或密码格式不正确等
  • 系统异常(SystemException)

    • 发送固定消息传递给用户,安抚用户
      • 系统繁忙,请稍后再试
      • 系统正在维护升级,请稍后再试
      • 系统出问题,请联系系统管理员等
    • 发送特定消息给运维人员,提醒维护
      • 可以发送短信、邮箱或者是公司内部通信软件
    • 记录日志
      • 发消息给运维和记录日志对用户来说是不可见的,属于后台程序
  • 其他异常(Exception)

    • 发送固定消息传递给用户,安抚用户
    • 发送特定消息给编程人员,提醒维护(纳入预期范围内)
      • 一般是程序没有考虑全,比如未做非空校验等
    • 记录日志

具体实现

  • 思路:

    1. 先通过自定义异常,完成 BusinessExceptionSystemException的定义
    2. 将其他异常包装成自定义异常类型
    3. 在异常处理器类中对不同的异常进行处理
  • 步骤一:自定义异常类

  • exception.SystemException

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package exception;

public class SystemException extends RuntimeException {
    private Integer code;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public SystemException() {
    }

    public SystemException(Integer code) {
        this.code = code;
    }

    public SystemException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    public SystemException(Integer code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }
}
  • exception.BussinessException
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package exception;

public class BusinessException extends RuntimeException{
    private Integer code;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public BusinessException() {
    }

    public BusinessException(Integer code) {
        this.code = code;
    }

    public BusinessException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    public BusinessException(Integer code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }
}
  • 让自定义异常类继承 RuntimeException的好处是,后期在抛出这两个异常的时候,就不用在 try..catch..throws
  • 自定义异常类中添加 code属性的原因是为了更好的区分异常是来自哪个业务的
  • 步骤二:将其他异常包成自定义异常
  • Code报错码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package domain;

public class Code {
    public static final Integer SAVE_OK = 20011;
    public static final Integer UPDATE_OK = 20021;
    public static final Integer DELETE_OK = 20031;
    public static final Integer GET_OK = 20041;

    public static final Integer SAVE_ERR = 20010;
    public static final Integer UPDATE_ERR = 20020;
    public static final Integer DELETE_ERR = 20030;
    public static final Integer GET_ERR = 20040;

    public static final Integer SYSTEM_ERR = 50001;
    public static final Integer SYSTEM_TIMEOUT_ERR = 50002;
    public static final Integer SYSTEM_UNKNOW_ERR = 59999;

    public static final Integer BUSINESS_ERR = 60001;
}
  • 模拟异常
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package service.impl;

import exception.BusinessException;
import dao.BookDao;
import domain.Book;
import domain.Code;
import exception.SystemException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import service.BookService;

import java.util.List;

@Service
public class BookServiceImpl implements BookService {

    @Autowired
    private BookDao bookDao;

    @Override
    public boolean save(Book book) {
        bookDao.save(book);
        return true;
    }
  
    @Override
    public boolean update(Book book) {
        bookDao.update(book);
        return true;
    }
  
    @Override
    public boolean delete(@PathVariable Integer id) {
        bookDao.delete(id);
        return true;
    }
  
    @Override
    public Book getById(Integer id) {
        //模拟业务异常,包装成自定义异常
        if(id == 1){
            throw new BusinessException(Code.BUSINESS_ERR,"你别给我乱改URL噢");
        }
        //模拟系统异常,将可能出现的异常进行包装,转换成自定义异常
        try{
            int i = 1/0;
        }catch (Exception e){
            throw new SystemException(Code.SYSTEM_TIMEOUT_ERR,"服务器访问超时,请重试!",e);
        }
        return bookDao.getById(id);
    }
  
    @Override
    public List<Book> getAll() {
        return bookDao.getAll();
    }
}
  • 步骤三:处理器类中处理自定义异常
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@RestControllerAdvice
public class ProjectExceptionAdvice {
    @ExceptionHandler(SystemException.class)
    public Result doSystemException(SystemException ex) {
        return new Result(ex.getCode(), null, ex.getMessage());
    }

    @ExceptionHandler(BusinessException.class)
    public Result doBusinessException(BusinessException ex) {
        return new Result(ex.getCode(), null, ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public Result doException(Exception ex) {
        return new Result(Code.SYSTEM_UNKNOW_ERR, null, "系统繁忙,请稍后再试!");
    }
}

./Spring.assets/631d597316f2c2beb1d20ccd.jpg

前后端协议联调

SpringMVC对静态资源放行

  • 新建SpringMVCSupport类,继承 WebMvcConfigurationSupport,并重写 addResourceHandlers()方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class SpringMvcSupport extends WebMvcConfigurationSupport {
    //设置静态资源访问过滤,当前类需要设置为配置类,并被扫描加载
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        //当访问/pages/xxx时候,从/pages目录下查找内容
        registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
        registry.addResourceHandler("/js/**").addResourceLocations("/js/");
        registry.addResourceHandler("/css/**").addResourceLocations("/css/");
        registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/");
    }
}
  • 同时也需要让SpringMvcConfig扫描到的配置类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@ComponentScan({"controller", "config"})
@EnableWebMvc
public class SpringMvcConfig {
}
  • 前端页面
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
<!DOCTYPE html>

<html>
<head>
  <!-- 页面meta -->
  <meta charset="utf-8">
  <title>SpringMVC案例</title>
  <!-- 引入样式 -->
  <link rel="stylesheet" href="../plugins/elementui/index.css">
  <link rel="stylesheet" href="../plugins/font-awesome/css/font-awesome.min.css">
  <link rel="stylesheet" href="../css/style.css">
</head>

<body class="hold-transition">

<div id="app">

  <div class="content-header">
    <h1>图书管理</h1>
  </div>

  <div class="app-container">
    <div class="box">
      <div class="filter-container">
        <el-input placeholder="图书名称" style="width: 200px;" class="filter-item"></el-input>
        <el-button class="dalfBut">查询</el-button>
        <el-button type="primary" class="butT" @click="openSave()">新建</el-button>
      </div>

      <el-table size="small" current-row-key="id" :data="dataList" stripe highlight-current-row>
        <el-table-column type="index" align="center" label="序号"></el-table-column>
        <el-table-column prop="type" label="图书类别" align="center"></el-table-column>
        <el-table-column prop="name" label="图书名称" align="center"></el-table-column>
        <el-table-column prop="description" label="描述" align="center"></el-table-column>
        <el-table-column label="操作" align="center">
          <template slot-scope="scope">
            <el-button type="primary" size="mini" @click="openEdit(scope.row)">编辑</el-button>
            <el-button size="mini" type="danger" @click="deleteBook(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>

      <div class="pagination-container">
        <el-pagination
                class="pagiantion"
                @current-change="handleCurrentChange"
                :current-page="pagination.currentPage"
                :page-size="pagination.pageSize"
                layout="total, prev, pager, next, jumper"
                :total="pagination.total">
        </el-pagination>
      </div>

      <!-- 新增标签弹层 -->
      <div class="add-form">
        <el-dialog title="新增图书" :visible.sync="dialogFormVisible">
          <el-form ref="dataAddForm" :model="formData" :rules="rules" label-position="right"
                   label-width="100px">
            <el-row>
              <el-col :span="12">
                <el-form-item label="图书类别" prop="type">
                  <el-input v-model="formData.type"/>
                </el-form-item>
              </el-col>
              <el-col :span="12">
                <el-form-item label="图书名称" prop="name">
                  <el-input v-model="formData.name"/>
                </el-form-item>
              </el-col>
            </el-row>
            <el-row>
              <el-col :span="24">
                <el-form-item label="描述">
                  <el-input v-model="formData.description" type="textarea"></el-input>
                </el-form-item>
              </el-col>
            </el-row>
          </el-form>
          <div slot="footer" class="dialog-footer">
            <el-button @click="dialogFormVisible = false">取消</el-button>
            <el-button type="primary" @click="saveBook()">确定</el-button>
          </div>
        </el-dialog>
      </div>

    </div>

    <!-- 编辑标签弹层 -->
    <div class="add-form">
      <el-dialog title="编辑检查项" :visible.sync="dialogFormVisible4Edit">
        <el-form ref="dataEditForm" :model="formData" :rules="rules" label-position="right" label-width="100px">
          <el-row>
            <el-col :span="12">
              <el-form-item label="图书类别" prop="type">
                <el-input v-model="formData.type"/>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="图书名称" prop="name">
                <el-input v-model="formData.name"/>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row>
            <el-col :span="24">
              <el-form-item label="描述">
                <el-input v-model="formData.description" type="textarea"></el-input>
              </el-form-item>
            </el-col>
          </el-row>
        </el-form>
        <div slot="footer" class="dialog-footer">
          <el-button @click="dialogFormVisible4Edit = false">取消</el-button>
          <el-button type="primary" @click="handleEdit()">确定</el-button>
        </div>
      </el-dialog>
    </div>
  </div>
</div>
</body>

<!-- 引入组件库 -->
<script src="../js/vue.js"></script>
<script src="../plugins/elementui/index.js"></script>
<script type="text/javascript" src="../js/jquery.min.js"></script>
<script src="../js/axios-0.18.0.js"></script>

<script>
  var vue = new Vue({

    el: '#app',

    data: {
      dataList: [],//当前页要展示的分页列表数据
      formData: {},//表单数据
      dialogFormVisible: false,//增加表单是否可见
      dialogFormVisible4Edit: false,//编辑表单是否可见
      pagination: {},//分页模型数据,暂时弃用
    },

    //钩子函数,VUE对象初始化完成后自动执行
    created() {
      this.getAll();
    },

    methods: {
      // 重置表单
      resetForm() {
        this.formData = {};
      },

      // 弹出添加窗口
      openSave() {
        this.dialogFormVisible = true;
        //每次弹出表单的时候,都重置一下数据
        this.resetForm();
      },

      //添加
      saveBook() {
        axios.post("/books",this.formData).then((res)=>{
          //20011是成功的状态码,成功之后就关闭对话框,并显示添加成功
          if (res.data.code == 20011){
            this.dialogFormVisible = false;
            this.$message.success("添加成功")
            //20010是失败的状态码,失败后给用户提示信息
          }else if(res.data.code == 20010){
            this.$message.error("添加失败");
            //如果前两个都不满足,那就是SYSTEM_UNKNOW_ERR,未知异常了,显示未知异常的错误提示信息安抚用户情绪
          }else {
            this.$message.error(res.data.msg);
          }
        }).finally(()=>{
          this.getAll();
        })
      },

      //主页列表查询
      getAll() {
        axios.get("/books").then((res)=>{
          this.dataList = res.data.data;
        })
      },
      openEdit(row) {
        axios.get("/books/" + row.id).then((res) => {
          if (res.data.code == 20041) {
            this.formData = res.data.data;
            this.dialogFormVisible4Edit = true;
          } else {
            this.$message.error(res.data.msg);
          }
        });
      },
      deleteBook(row) {
        this.$confirm("此操作永久删除当前数据,是否继续?","提示",{
          type:'info'
        }).then(()=> {
          axios.delete("/books/" + row.id).then((res) => {
            if (res.data.code == 20031) {
              this.$message.success("删除成功")
            } else if (res.data.code == 20030) {
              this.$message.error("删除失败")
            }
          }).finally(() => {
            this.getAll();
          });
        }).catch(()=>{
          this.$message.info("取消删除操作")
        })
      },
      handleEdit() {
        axios.put("/books", this.formData).then((res) => {
          if (res.data.code == 20021) {
            this.dialogFormVisible4Edit = false;
            this.$message.success("修改成功")
          } else if (res.data.code == 20020) {
            this.$message.error("修改失败")
          } else {
            this.$message.error(res.data.msg);
          }
        }).finally(() => {
          this.getAll();
        });
      }
    }
  })
</script>
</html>

拦截器

./Spring.assets/631e964416f2c2beb1ece79e.jpg

  1. 浏览器发送一个请求,会先到Tomcat服务器的web服务器
  2. Tomcat服务器接收到请求后,会先去判断请求的是 静态资源还是 动态资源
  3. 如果是静态资源,会直接到Tomcat的项目部署目录下直接访问
  4. 如果是动态资源,就需要交给项目的后台代码进行处理
  5. 在找到具体的方法之前,可以去配置过滤器(可以配置多个),按照顺序进行执行(在这里就可以进行权限校验)
  6. 然后进入到中央处理器(SpringMVC中的内容),SpringMVC会根据配置的规则进行拦截
  7. 如果满足规则,则进行处理,找到其对应的 Controller类中的方法进行之星,完成后返回结果
  8. 如果不满足规则,则不进行处理
  9. 这个时候,如果需要在每个Controller方法执行的前后添加业务,具体该如何来实现?
    • 这个就是拦截器要做的事
  • 拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行
    • 作用
      • 在指定的方法调用前后执行预先设定的代码
      • 阻止原始方法的执行
    • 总结:拦截器就是用来作增强
  • 但是这个拦截器貌似跟之前学的过滤器很像啊,不管是从作用上来看还是从执行顺序上来看
    • 那么拦截器和过滤器之间的区别是什么呢?
      • 归属不同:Filter属于Servlet技术,而Interceptor属于SpringMVC技术
      • 拦截内容不同:Filter对所有访问进行增强,Interceptor仅对SpringMVC的访问进行增强

./Spring.assets/631e9b5416f2c2beb1f1d381.jpg

拦截器案例

  • 创建拦截器类:controller.interceptor下创建 ProjectInterceptor类,实现 HandlerInterceptor接口,并重写其中的三个方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package controller.interceptor;

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

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

@Component
//注意当前类必须受Spring容器控制
public class ProjectInterceptor implements HandlerInterceptor {
    @Override
    //原始方法调用前执行的内容
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle");
        return true;
    }
  
    @Override
    //原始方法调用后执行的内容
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle");
    }
  
    @Override
    //原始方法调用完成后执行的内容
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion");
    }
}
  • 配置拦截器:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package config;

import controller.interceptor.ProjectInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class SpringMvcSupport extends WebMvcConfigurationSupport {
    @Autowired
    private ProjectInterceptor projectInterceptor;
    //设置静态资源访问过滤,当前类需要设置为配置类,并被扫描加载

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        //配置拦截器,拦截路径是/books,只会拦截/books,拦截不到/books/1
        registry.addInterceptor(projectInterceptor).addPathPatterns("/books");
    }
  
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        //配置拦截器,查看源码发现,参数是个可变形参,可以设置任意多个拦截路径
        registry.addInterceptor(projectInterceptor).addPathPatterns("/books","/books/*");
    }

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        //当访问/pages/xxx时候,从/pages目录下查找内容
        registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
        registry.addResourceHandler("/js/**").addResourceLocations("/js/");
        registry.addResourceHandler("/css/**").addResourceLocations("/css/");
        registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/");
    }
}
  • SpringMvc添加SpringMvcSupport包扫描
1
2
3
4
5
@Configuration
@ComponentScan({"com.mtmn.controller", "com.mtmn.config"})
@EnableWebMvc
public class SpringMvcConfig {
}

拦截器中的 preHandler方法,如果返回true,则代表放行,会执行原始 Controller类中要请求的方法,如果返回 false,则代表拦截,后面的就不会再执行了。

  • 简化SpringMvcSupport的编写 可以让 SpringMvcConfig类实现 WebMvcConfigurer接口,然后直接在 SpringMvcConfig中写 SpringMvcSupport的东西,这样就不用再写 SpringMvcSupport类了,全都在 SpringMvcConfig中写
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Configuration
@ComponentScan("controller")
@EnableWebMvc
//实现WebMvcConfigurer接口可以简化开发,但具有一定的侵入性
public class SpringMvcConfig implements WebMvcConfigurer {
    @Autowired
    private ProjectInterceptor projectInterceptor;

    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //对静态资源放行
        registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
    }

    public void addInterceptors(InterceptorRegistry registry) {
        //配置拦截器
        registry.addInterceptor(projectInterceptor).addPathPatterns("/books", "/books/*");
    }
}
  • 拦截器的执行流程

./Spring.assets/631ebfbd16f2c2beb1165a09.jpg

  • 当有拦截器后,请求会先进入 preHandle方法,

    • 如果方法返回 true,则放行继续执行后面的handle(Controller的方法)和后面的方法
    • 如果返回 false,则直接跳过后面方法的执行。

拦截器参数

前置处理方法

原始方法之前运行preHandle

1
2
3
4
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    System.out.println("preHandle");
    return true;
}
  • request:请求对象
  • response:响应对象
  • handler:被调用的处理器对象,本质上是一个方法对象,对反射中的Method对象进行了再包装

使用request对象可以获取请求数据中的内容,如获取请求头的 Content-Type

1
2
3
4
5
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String name = request.getHeader("Content-Type");
    System.out.println("preHandle.." + name);
    return true;
}

控制台输出如下,成功输出了Content-Type application/json

preHandle…application/json book save …Book{书名=‘书名测试数据’, 价格=0.0} postHandle afterCompletion

使用handler参数,可以获取方法的相关信息

1
2
3
4
5
6
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    HandlerMethod handlerMethod = (HandlerMethod) handler;
    String methodName = handlerMethod.getMethod().getName();
    System.out.println("preHandle.." + methodName);
    return true;
}

控制台输出如下,成功输出了方法名 save

preHandle…save book save …Book{书名=‘书名测试数据’, 价格=0.0} postHandle afterCompletion

后置处理方法

原始方法运行后运行,如果原始方法被拦截,则不执行

1
2
3
4
//原始方法调用后执行的内容
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    System.out.println("postHandle");
}

前三个参数和上面的是一致的。 modelAndView:如果处理器执行完成具有返回结果,可以读取到对应数据与页面信息,并进行调整 因为现在都是返回json数据,所以该参数的使用率不高。

完成处理方法

拦截器最后执行的方法,无论原始方法是否执行

1
2
3
4
//原始方法调用完成后执行的内容
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    System.out.println("afterCompletion");
}

前三个参数与上面的是一致的。 ex:如果处理器执行过程中出现异常对象,可以针对异常情况进行单独处理 因为现在已经有全局异常处理器类,所以该参数的使用率也不高。 这三个方法中,最常用的是 preHandle,在这个方法中可以通过返回值来决定是否要进行放行,可以把业务逻辑放在该方法中,如果满足业务则返回 true放行,不满足则返回 false拦截。

配置多个拦截器

  • 步骤一:创建拦截器类 直接复制一份改个名,改个输出语句
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Component
//注意当前类必须受Spring容器控制
public class ProjectInterceptor2 implements HandlerInterceptor {

    //原始方法调用前执行的内容
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle..222");
        return true;
    }

    //原始方法调用后执行的内容
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle..222");
    }

    //原始方法调用完成后执行的内容
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion..222");
    }
}
  • 步骤二:配置拦截器类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Configuration
@ComponentScan("controller")
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {
    @Autowired
    private ProjectInterceptor projectInterceptor;
    @Autowired
    private ProjectInterceptor2 projectInterceptor2;

    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(projectInterceptor).addPathPatterns("/books", "/books/*");
        registry.addInterceptor(projectInterceptor2).addPathPatterns("/books", "/books/*");
    }
}
  • 重新启动服务器,使用PostMan发送请求,控制台输出如下

preHandle… preHandle…222 book getById …9527 postHandle…222 postHandle afterCompletion…222 afterCompletion

  • 当配置多个拦截器时,形成拦截器链
  • 拦截器链的运行顺序参照拦截器添加顺序为准
  • 当拦截器中出现对原始处理器的拦截,后面的拦截器均终止运行
  • 当拦截器运行中断,仅运行配置在前面的拦截器的afterCompletion操作

./Spring.assets/631ecaa216f2c2beb121be79.jpg

  • preHandle:与配置顺序相同,必定运行
  • postHandle:与配置顺序相反,可能不运行
  • afterCompletion:与配置顺序相反,可能不运行。

Maven进阶

分模块开发实现

前面已经完成了SSM整合,接下来就基于SSM整合的项目来实现对项目的拆分。

环境准备

复制一份之前的ssm项目,重命名为 maven_01_ssm ./Spring.assets/63201b4a16f2c2beb149296e.jpg

抽取domain层

  • 步骤一:创建新模块

    • 创建一个名为 maven_02_pojo的maven项目
  • 步骤二:项目中创建domain包

    • maven_02_pojo中创建 com.mtmn.domain包,并将 maven_01_ssm的Book类拷贝到该包中
  • 步骤三:删除原项目中的domain包

    • 删除后,maven_01_ssm项目中用到 Book的类中都会爆红
    • 要想解决上述问题,需要在 maven_01_ssm中添加 maven_02_pojo的依赖。
  • 步骤四:在maven_01_ssm 项目的pom.xml添加maven_02_pojo 的依赖

    1
    2
    3
    4
    5
    
    <dependency>
        <groupId>com.mtmn</groupId>
        <artifactId>maven_02_pojo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    
    • 因为添加了依赖,所以在 maven_01_ssm中就已经能找到Book类,所以刚才的爆红提示就会消失。
  • 步骤五:编译maven_01_ssm项目控制台会报错

    Failed to execute goal on project maven_01_ssm: Could not resolve dependencies for project com.mtmn:maven_01_ssm:jar:1.0-SNAPSHOT: Could not find artifact com.mtmn:maven_02_pojo:jar:1.0-SNAPSHOT -> [Help 1]

    意思就是找不到maven_02_pojo这个jar包

    • 为啥找不到呢?
      • 原因是Maven会从本地仓库找对应的jar包,但是本地仓库又不存在该jar包所以会报错。
      • 在IDEA中是有 maven_02_pojo这个项目,所以只需要将 maven_02_pojo项目安装到本地仓库即可。
  • 步骤六:将项目安装本地仓库

    • 将需要被依赖的项目 maven_02_pojo,使用maven的 install命令,把其安装到Maven的本地仓库中
    • 之后再次执行 maven_01_ssmcompile的命令后,就已经能够成功编译。

抽取dao层

  • 步骤一:创建新模块

    • 创建一个名为 maven_03_dao的maven项目
  • 步骤二:项目中创建 dao

    • maven_03_dao项目中创建 com.mtmn.dao包,并将 maven_01_ssm中BookDao类拷贝到该包中

    • 在maven_03_dao中会有如下几个问题需要解决下

      • 项目maven_03_dao的BookDao接口中Book类找不到报错

      • 解决方案在 maven_03_dao项目的pom.xml中添加 maven_02_pojo项目

        1
        2
        3
        4
        5
        
        <dependency>
            <groupId>com.mtmn</groupId>
            <artifactId>maven_02_pojo</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        
      • 项目maven_03_dao的BookDao接口中,Mybatis的增删改查注解报错

      • 解决方案在 maven_03_dao项目的pom.xml中添加 mybatis的相关依赖

         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.6</version>
        </dependency>
        
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
        
  • 最后记得使用maven的 install命令,把其安装到Maven的本地仓库中

  • 步骤三:删除原项目中的 dao

    • 删除Dao包以后,因为 maven_01_ssm中的BookServiceImpl类中有使用到Dao的内容,所以需要在 maven_01_ssm的pom.xml添加 maven_03_dao的依赖
    1
    2
    3
    4
    5
    
    <dependency>
        <groupId>com.mtmn</groupId>
        <artifactId>maven_03_dao</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    
  • 步骤四:运行测试

    • 启动Tomcat服务器,访问 http://localhost:7777/pages/books.html
    • 将抽取后的项目进行运行,测试之前的增删改查功能依然能够使用。

小结

对于项目的拆分,大致会有如下几个步骤

  1. 创建Maven模块
  2. 书写模块代码
    • 分模块开发需要先针对模块功能进行设计,再进行编码。不会先将工程开发完毕,然后进行拆分。拆分方式可以按照功能拆也可以按照模块拆。
  3. 通过maven指令安装模块到本地仓库(install 指令)
    • 由于maven指令只能安装到自己电脑的仓库里,那么团队内部开发需要发布模块功能,需要到团队内部可共享的仓库中(私服),私服后面会讲解。

依赖管理

现在已经能把项目拆分成一个个独立的模块,当在其他项目中想要使用独立出来的这些模块,只需要在其pom.xml使用 <dependency>标签来进行jar包的引入即可。 <dependency>其实就是依赖,关于依赖管理里面都涉及哪些内容,就一个个来学习下:

  • 依赖传递
  • 可选依赖
  • 排除依赖

先来说说什么是依赖:

  • 依赖指当前项目运行所需的jar一个项目可以设置多个依赖。
  • 格式为:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!--设置当前项目所依赖的所有jar-->
<dependencies>
    <!--设置具体的依赖-->
    <dependency>
        <!--依赖所属群组id-->
        <groupId>org.springframework</groupId>
        <!--依赖所属项目id-->
        <artifactId>spring-webmvc</artifactId>
        <!--依赖版本号-->
        <version>5.2.10.RELEASE</version>
    </dependency>
</dependencies>

依赖传递与冲突问题

首先要明白一点:依赖是具有传递性的 ./Spring.assets/63203b1c16f2c2beb16ca62c.jpg

说明:A代表自己的项目;B,C,D,E,F,G代表的是项目所依赖的jar包;D1和D2、E1和E2代表是相同jar包的不同版本

  1. A依赖了B和C,B和C有分别依赖了其他jar包,所以在A项目中就可以使用上面所有jar包,这就是所说的依赖传递
  2. 依赖传递有直接依赖和间接依赖
    • 相对于A来说,A直接依赖B和C,间接依赖了D1,E1,G,F,D2和E2
    • 相对于B来说,B直接依赖了D1和E1,间接依赖了G
    • 直接依赖和间接依赖是一个相对的概念
  3. 因为有依赖传递的存在,就会导致jar包在依赖的过程中出现冲突问题,具体什么是冲突?Maven是如何解决冲突的?

这里所说的 依赖冲突是指项目依赖的某一个jar包,有多个不同的版本,因而造成类包版本冲突。

  • 情况一: 在maven_01_ssm的pom.xml中添加两个不同版本的Junit依赖:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
</dependencies>

调换位置,刷新maven面板,会发现,maven的dependencies面板上总是显示使用的是后加载的jar包 于是得出一个结论:

  • 特殊优先:当同级配置了相同资源的不同版本,后配置的覆盖先配置的。
  • 情况二:路径优先:当依赖中出现相同的资源时,层级越深,优先级越低,层级越浅,优先级越高
    • A通过B间接依赖到E1
    • A通过C间接依赖到E2
    • A就会间接依赖到E1和E2,Maven会按照层级来选择,E1是2度,E2是3度,所以最终会选择E1
  • 情况三:声明优先:当资源在相同层级被依赖时,配置顺序靠前的覆盖配置顺序靠后的
    • A通过B间接依赖到D1
    • A通过C间接依赖到D2
    • D1和D2都是两度,这个时候就不能按照层级来选择,需要按照声明来,谁先声明用谁,也就是说B在C之前声明,这个时候使用的是D1,反之则为D2

但是对于上面的结果,也不用刻意去记,一切以maven的dependencies面板上显示的为准

可选依赖和排除依赖

依赖传递介绍完以后,思考一个问题,假如

  • maven_01_ssm 依赖了 maven_03_dao
  • maven_03_dao 依赖了 maven_02_pojo
  • 因为现在有依赖传递,所以 maven_01_ssm能够使用到 maven_02_pojo的内容
  • 如果说现在不想让 maven_01_ssm依赖到 maven_02_pojo,有哪些解决方案?

说明:在真实使用的过程中,maven_01_ssm中是需要用到 maven_02_pojo的,这里只是用这个例子描述的需求。因为有时候,maven_03_dao出于某些因素的考虑,就是不想让别人使用自己所依赖的 maven_02_pojo

  • 方案一:可选依赖

    • 可选依赖指对外隐藏当前所依赖的资源—不透明
    • maven_03_dao的pom.xml,在引入 maven_02_pojo的时候,添加 optional
    1
    2
    3
    4
    5
    6
    7
    
    <dependency>
        <groupId>com.mtmn</groupId>
        <artifactId>maven_02_pojo</artifactId>
        <version>1.0-SNAPSHOT</version>
        <!--可选依赖是隐藏当前工程所依赖的资源,隐藏后对应资源将不具有依赖传递-->
        <optional>true</optional>
    </dependency>
    
  • 方案二:排除依赖

    • 排除依赖指主动断开依赖的资源,被排除的资源无需指定版本—不需要
    • 前面已经通过可选依赖实现了阻断 maven_02_pojo的依赖传递,对于排除依赖,则指的是已经有依赖的事实,也就是说 maven_01_ssm项目中已经通过依赖传递用到了 maven_02_pojo,此时需要做的是将其进行排除,所以接下来需要修改 maven_01_ssm的pom.xml
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    <dependency>
        <groupId>com.mtmn</groupId>
        <artifactId>maven_03_dao</artifactId>
        <version>1.0-SNAPSHOT</version>
        <!--排除依赖是隐藏当前资源对应的依赖关系-->
        <exclusions>
            <!--这里可以排除多个依赖,只要你有需求-->
            <exclusion>
                <groupId>com.mtmn</groupId>
                <artifactId>maven_02_pojo</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    

介绍完这两种方式后,简单来梳理下,就是

  • A依赖B,B依赖CC通过依赖传递会被 A使用到,现在要想办法让 A不去依赖 C
  • 可选依赖是在B上设置 <optional>A不知道有 C的存在,
  • 排除依赖是在A上设置 <exclusions>A知道有 C的存在,主动将其排除掉。

聚合与继承

的项目已经从以前的单模块,变成了现在的多模块开发。项目一旦变成了多模块开发以后,就会引发一些问题,在这一节中主要会学习两个内容 聚合继承,用这两个知识来解决下分模块后的一些问题。

聚合

./Spring.assets/63205b6416f2c2beb18ff5db.jpg

  • 分模块开发后,需要将这四个项目都安装到本地仓库,目前只能通过项目Maven面板的install来安装,并且需要安装四个,如果的项目足够多,那一个个install也挺麻烦的
  • 如果四个项目都已经安装成功,当ssm_pojo发生变化后,就得将ssm_pojo重新安装到maven仓库,但是为了确保对ssm_pojo的修改不会影响到其他模块(比如将pojo类中的一个属性删除,如果其他模块调用了这个属性,那必然报错),需要对所有模块重新编译,看看有没有问题。然后还需要将所有模块再install一遍

项目少的话还好,但是如果项目多的话,一个个操作项目就容易出现漏掉或重复操作的问题,所以就想能不能抽取一个项目,把所有的项目管理起来,以后再想操作这些项目,只需要操作抽取的这个项目,这样就省事儿多了

这就要用到接下来讲的 聚合

  • 所谓聚合:将多个模块组织成一个整体,同时进行项目构建的过程称为聚合
  • 聚合工程:通常是一个不具有业务功能的 工程
  • 作用:使用聚合工程可以将多个工程编组,通过对聚合工程的构建,实现对所包含的所有模块进行同步构建
    • 当工程中某个模块发生更新后,必须保障工程中与更新模块关联的模块同步更新,此时就可以使用聚合工程来解决批量模块同步构建的问题

具体实现步骤如下:

  • 步骤一:创建一个空的maven项目
  • 步骤二:将项目打包方式改为 pom
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.mtmn</groupId>
    <artifactId>maven_00_parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!--设置打包方式-->
    <packaging>pom</packaging>🚩🚩🚩

</project>

说明:项目的打包方式,接触到的有三种,分别是

  • jar:默认情况,说明该项目为java项目
  • war:说明该项目为web项目
  • pom:说明该项目为聚合或继承(后面会讲)项目
  • 步骤三:pom.xml添加所要管理的项目
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.mtmn</groupId>
    <artifactId>maven_00_parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!--设置打包方式-->
    <packaging>pom</packaging>

    <!--设置管理的项目名称-->
    <modules>
        <module>../maven_01_ssm</module>
        <module>../maven_02_pojo</module>
        <module>../maven_03_dao</module>
    </modules>

</project>
  • 步骤四:使用聚合统一管理项目 在maven面板上点击compile,会发现所有受管理的项目都会被执行编译,这就是聚合工程的作用

[INFO] maven_02_pojo Maven Webapp … SUCCESS [ 0.677 s] [INFO] maven_03_dao … SUCCESS [ 0.027 s] [INFO] maven_01_ssm Maven Webapp … SUCCESS [ 0.100 s] [INFO] maven_00_parent … SUCCESS [ 0.001 s]

说明:聚合工程管理的项目在进行运行的时候,会按照项目与项目之间的依赖关系来自动决定执行的顺序和配置的顺序无关。 虽然配置的顺序是 123,但是执行的时候按照依赖关系编译是 231

那么聚合的知识就讲解完了,最后总结一句话就是,聚合工程主要是用来管理项目。

继承

已经完成了使用聚合工程去管理项目,聚合工程进行某一个构建操作,其他被其管理的项目也会执行相同的构建操作。那么接下来,再来分析下,多模块开发存在的另外一个问题,重复配置的问题,先来看张图: ./Spring.assets/632063ce16f2c2beb19992ee.jpg

  • spring-webmvcspring-jdbc在三个项目模块中都有出现,这样就出现了重复的内容
  • spring-test只在ssm_crm和ssm_goods中出现,而在ssm_order中没有,这里是部分重复的内容
  • 使用的spring版本目前是 5.2.10.RELEASE,假如后期要想升级spring版本,所有跟Spring相关jar包都得被修改,涉及到的项目越多,维护成本越高

面对上面这些问题,就得用到接下来要学习的 继承

  • 所谓继承:描述的是两个工程间的关系,与java中的继承类似,子工程可以继承父工程中的配置信息,常见于依赖关系的继承
  • 作用:
    • 简化配置
    • 减少版本冲突

接下来,到程序中去看看继承该如何实现

  • 步骤一:创建一个空的Maven项目并将其打包方式设置为pom
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.mtmn</groupId>
    <artifactId>maven_00_parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!--设置打包方式-->
    <packaging>pom</packaging>
  
</project>
  • 步骤二:在子工程中设置其父工程
1
2
3
4
5
6
7
8
<!--配置当前工程继承自parent工程-->
<parent>
    <artifactId>maven_00_parent</artifactId>
    <groupId>com.mtmn</groupId>
    <version>1.0-SNAPSHOT</version>
    <!--配置父项目的pom.xml路径-->
    <relativePath>../maven_00_parent/pom.xml</relativePath>
</parent>
  • 步骤三:优化子项目共有依赖导入问题

    1. 将子项目共同使用的jar包都抽取出来,维护在父项目的pom.xml中
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.mtmn</groupId>
        <artifactId>maven_00_parent</artifactId>
        <version>1.0-SNAPSHOT</version>
        <!--设置打包方式-->
        <packaging>pom</packaging>
    
        <!--设置管理的项目名称-->
        <modules>
            <module>../maven_01_ssm</module>
            <module>../maven_02_pojo</module>
            <module>../maven_03_dao</module>
        </modules>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-webmvc</artifactId>
                <version>5.2.10.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-jdbc</artifactId>
                <version>5.2.10.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-test</artifactId>
                <version>5.2.10.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>3.5.6</version>
            </dependency>
    
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis-spring</artifactId>
                <version>1.3.0</version>
            </dependency>
    
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.46</version>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.1.16</version>
            </dependency>
    
            <dependency>
                <groupId>javax.servlet</groupId>
                <artifactId>javax.servlet-api</artifactId>
                <version>3.1.0</version>
                <scope>provided</scope>
            </dependency>
    
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>2.9.0</version>
            </dependency>
    
        </dependencies>
    </project>
    
    1. 删除子项目中已经被抽取到父项目的pom.xml中的jar包
      • 删除完后,你会发现父项目中有依赖对应的jar包,子项目虽然已经将重复的依赖删除掉了,但是刷新的时候,子项目中所需要的jar包依然存在。
      • 当项目的 <parent>标签被移除掉,会发现多出来的jar包依赖也会随之消失。
    2. 在父项目中修改jar包的版本,刷新后,子项目中的jar包版本也随之变化
  • 那么现在就可以解决了刚才提到的第一个问题,将子项目中的公共jar包抽取到父工程中进行统一添加依赖,这样做的可以简化配置,并且当父工程中所依赖的jar包版本发生变化,所有子项目中对应的jar包版本也会跟着更新。

  • 步骤四:优化子项目依赖版本问题 如果把所有用到的jar包都管理在父项目的pom.xml,看上去更简单些,但是这样就会导致有很多项目引入了过多自己不需要的jar包。如上面看到的这张图: ./Spring.assets/632063ce16f2c2beb19992ee.jpg 如果把所有的依赖都放在了父工程中进行统一维护,就会导致ssm_order项目中多引入了 spring-test的jar包,如果这样的jar包过多的话,对于ssm_order来说也是一种"负担”。

那针对于这种部分项目有的jar包,该如何管理优化呢?

  1. 在父工程中的pom.xml中定义依赖管理
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!--定义依赖管理-->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
  1. 将maven_01_ssm的pom.xml中的junit依赖删除掉,刷新Maven 刷新后,在maven_01_ssm项目中找不到junit依赖,所以得出一个结论 <dependencyManagement>标签不真正引入jar包,而是配置可供子项目选择的jar包依赖 子项目要想使用它所提供的这些jar包,需要自己添加依赖,并且不需要指定 <version>
  2. 在maven_01_ssm的pom.xml添加junit的依赖
1
2
3
4
5
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>

注意:这里就不需要添加版本了,这样做的好处就是当父工程 dependencyManagement标签中的版本发生变化后,子项目中的依赖版本也会跟着发生变化

  1. 在maven_03_dao的pom.xml添加junit的依赖
1
2
3
4
5
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>

这个时候,maven_01_ssm和maven_03_dao这两个项目中的junit版本就会跟随着父项目中的标签 dependencyManagement中junit的版本发生变化而变化。不需要junit的项目就不需要添加对应的依赖即可(maven_02_pojo中就没添加)

至此继承就已经学习完了,总结来说,继承可以帮助做两件事

  • 将所有项目公共的jar包依赖提取到父工程的pom.xml中,子项目就可以不用重复编写,简化开发
  • 将所有项目的jar包配置到父工程的 dependencyManagement标签下,实现版本管理,方便维护
    • dependencyManagement标签不真正引入jar包,只是管理jar包的版本
    • 子项目在引入的时候,只需要指定groupId和artifactId,不需要加version
    • dependencyManagement标签中jar包版本发生变化,所有子项目中用到该jar包的地方对应的版本会自动随之更新

最后总结一句话就是,父工程主要是用来快速配置依赖jar包和管理项目中所使用的资源

  • 小结

    • 继承的实现步骤:

      • 创建Maven模块,设置打包类型为pom
      1
      
      <packaging>pom</packaging>
      
      • 在父工程的pom文件中配置依赖关系(子工程将沿用父工程中的依赖关系),一般只抽取子项目中公有的jar包
      1
      2
      3
      4
      5
      6
      7
      8
      
      <dependencies>
          <dependency>
              <groupId>org.springframework</groupId>
              <artifactId>spring-webmvc</artifactId>
              <version>5.2.10.RELEASE</version>
          </dependency>
          ...
      </dependencies>
      
      • 在父工程中配置子工程中可选的依赖关系
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      
      <dependencyManagement>
          <dependencies>
              <dependency>
                  <groupId>com.alibaba</groupId>
                  <artifactId>druid</artifactId>
                  <version>1.1.16</version>
              </dependency>
          </dependencies>
          ...
      </dependencyManagement>
      
      • 在子工程中配置当前工程所继承的父工程
      1
      2
      3
      4
      5
      6
      7
      8
      
      <!--定义该工程的父工程-->
      <parent>
          <groupId>com.mtmn</groupId>
          <artifactId>maven_01_parent</artifactId>
          <version>1.0-RELEASE</version>
          <!--填写父工程的pom文件,可以不写-->
          <relativePath>../maven_01_parent/pom.xml</relativePath>
      </parent>
      
      • 在子工程中配置使用父工程中可选依赖的坐标
      1
      2
      3
      4
      5
      6
      
      <dependencies>
          <dependency>
              <groupId>com.alibaba</groupId>
              <artifactId>druid</artifactId>
          </dependency>
      </dependencies>
      

注意事项:

  1. 子工程中使用父工程中的可选依赖时,仅需要提供群组id和项目id,无需提供版本,版本由父工程统一提供,避免版本冲突
  2. 子工程中还可以定义父工程中没有定义的依赖关系,只不过不能被父工程进行版本统一管理。

聚合与继承的区别

聚合与继承分别的作用:

  • 聚合用于快速构建项目,对项目进行管理
  • 继承用于快速配置和管理子项目中所使用jar包的版本

聚合和继承的相同点:

  • 聚合与继承的pom.xml文件打包方式均为pom,可以将两种关系制作到同一个pom文件中
  • 聚合与继承均属于设计型模块,并无实际的模块内容

聚合和继承的不同点:

  • 聚合是在当前模块中配置关系,聚合可以感知到参与聚合的模块有哪些
  • 继承是在子模块中配置关系,父模块无法感知哪些子模块继承了自己

属性

问题分析

前面已经在父工程中的dependencyManagement标签中对项目中所使用的jar包版本进行了统一的管理,但是如果在标签中有如下的内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.6</version>
</dependency>

如果现在想更新Spring的版本,就会发现依然需要更新多个jar包的版本,这样的话还是有可能出现漏改导致程序出问题,而且改起来也是比较麻烦。 问题清楚后,需要解决的话,就可以参考咱们java基础所学习的变量,声明一个变量,在其他地方使用该变量,当变量的值发生变化后,所有使用变量的地方也会跟着变化 例如

1
String spring_version = "5.2.10.RELEASE";

然后将依赖的版本号替换成 spring_version

1
2
3
4
5
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>spring_version</version>
</dependency>

解决步骤

  • 步骤一:父工程中定义属性
1
2
3
4
<properties>
        <spring.version>5.2.10.RELEASE</spring.version>
        <mybatis.version>3.5.6</mybatis.version>
</properties>
  • 步骤二:修改依赖的version
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>${spring.version}</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>${spring.version}</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>${spring.version}</version>
</dependency>

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>${mybatis.version}</version>
</dependency>

此时,只需要更新父工程中properties标签中所维护的jar包版本,所有子项目中的版本也就跟着更新。当然除了将spring相关版本进行维护,可以将其他的jar包版本也进行抽取,这样就可以对项目中所有jar包的版本进行统一维护

说明:使用 properties标签来定义属性,在 properties标签内自定义标签名当做属性名,自定义标签内的值即为属性值 例如:<spring.version>5.2.10.RELEASE</spring.version>,属性名为 spring.version,属性值为 5.2.10.RELEASE 在其他地方引用变量时用 ${变量名}

配置文件加载属性

Maven中的属性已经介绍过了,现在也已经能够通过Maven来集中管理Maven中依赖jar包的版本。但是又有新的需求,就是想让Maven对于属性的管理范围能更大些,比如之前项目中的 jdbc.properties,这个配置文件中的属性,能不能也来让Maven进行管理呢?

答案是肯定的,具体的实现步骤如下

  • 步骤一:父工程定义属性
1
2
3
4
5
6
7
8
<properties>
    <spring.version>5.2.10.RELEASE</spring.version>
    <mybatis.version>3.5.6</mybatis.version>
    <jdbc.driver>com.mysql.jdbc.Driver</jdbc.driver>
    <jdbc.url>jdbc:mysql://localhost:13306/ssm_db?useSSL=false</jdbc.url>
    <jdbc.username>root</jdbc.username>
    <jdbc.password>PASSWORD</jdbc.password>
</properties>
  • 步骤二:jdbc.properties文件中引用属性
1
2
3
4
jdbc.driver=${jdbc.driver}
jdbc.url=${jdbc.url}
jdbc.username=${jdbc.username}
jdbc.password=${jdbc.password}
  • 步骤三:设置maven过滤文件范围 直接在properties中引用属性,看起来怪怪的,properties怎么能直接用到maven中配置的属性呢? 所以还需要来配置一下,让 maven_01_ssm/src/main/resources目录下的 jdbc.properties文件可以解析 ${}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<build>
    <resources>
        <!--设置资源目录-->
        <resource>
            <directory>../maven_01_ssm/src/main/resources</directory>
            <!--设置能够解析${},默认是false -->
            <filtering>true</filtering>
        </resource>
    </resources>
</build>
  • 步骤四:测试是否生效 测试的时候,只需要将maven_01_ssm项目进行打包,然后在本地仓库观察打包结果中最终生成的内容是否为Maven中配置的内容。

  • 存在的问题 如果不只是 maven_01_ssm项目需要有属性被父工程管理,如果还有多个项目需要配置,该如何实现呢?

    • 方式一
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    <build>
        <resources>
            <!--设置资源目录,并设置能够解析${}-->
            <resource>
                <directory>../maven_01_ssm/src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
            <resource>
                <directory>../maven_02_pojo/src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
            ...
        </resources>
    </build>
    

    可以一个一个配,但是项目足够多的话,这样还是比较繁琐的

    • 方式二:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    <build>
        <resources>
            <!--
                ${project.basedir}: 当前项目所在目录,子项目继承了父项目,
                相当于所有的子项目都添加了资源目录的过滤
            -->
            <resource>
                <directory>${project.basedir}/src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
    

说明:如果打包过程中出现错误 Error assembling WAR: webxml attribute is required 原因就是Maven发现你的项目为web项目,就会去找web项目的入口web.xml(配置文件配置的方式),发现没有找到,就会报错。

  • 解决方案1:在maven_02_ssm项目的 src\main\webapp\WEB-INF\添加一个web.xml文件
1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
</web-app>
  • 解决方案2: 配置maven打包war时,忽略web.xml检查
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-war-plugin</artifactId>
            <version>3.2.3</version>
            <configuration>
                <failOnMissingWebXml>false</failOnMissingWebXml>
            </configuration>
        </plugin>
    </plugins>
</build>

上面所使用的都是Maven的自定义属性,除了 ${project.basedir},它属于Maven的内置系统属性。

在Maven中的属性分为:

  • 自定义属性(常用)
  • 内置属性
  • Setting属性
  • Java系统属性
  • 环境变量属性
属性分类 引用格式 示例
自定义属性 ${自定义属性名} ${spring.vension}
内置属性 ${内置属性名} ${basedir}、${version}
setting属性 ${setting.属性名} ${settings.localRepository}
ava系统属性 ${系统属性分类.系统属性名} ${user.home}
环境变量属性 ${env.环境变量属性名} ${env.JAVA_HOME}

版本管理

关于这个版本管理解决的问题是,在Maven创建项目和引用别人项目的时候,都看到过如下内容:

1
2
3
4
5
6
7
<groupId>com.mtmn</groupId>
<artifactId>maven_00_parent</artifactId>
<version>1.0-SNAPSHOT</version>

<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>

这里面有两个单词,SNAPSHOT和RELEASE,它们所代表的含义是什么呢?

  • SNAPSHOT(快照版本)
    • 项目开发过程中临时输出的版本,称为快照版本
    • 快照版本会随着开发的进展不断更新
  • RELEASE(发布版本)
    • 项目开发到一定阶段里程碑后,向团队外部发布较为稳定的版本,这种版本所对应的构件文件是稳定的
    • 即便进行功能的后续开发,也不会改变当前发布版本内容,这种版本称为发布版本

除了上面的工程版本,还经常能看到一些发布版本:

  • alpha版:内测版,bug多不稳定内部版本不断添加新功能
  • beta版:公测版,不稳定(比alpha稳定些),bug相对较多不断添加新功能
  • 纯数字版

对于这些版本,简单认识下即可

多环境配置与应用

多环境开发

./Spring.assets/632146b616f2c2beb1667b38.jpg

  • 平常都是在自己的开发环境进行开发
  • 当开发完成后,需要把开发的功能部署到测试环境供测试人员进行测试使用
  • 等测试人员测试通过后,会将项目部署到生成环境上线使用。
  • 这个时候就有一个问题是,不同环境的配置是不相同的,如不可能让三个环境都用一个数据库,所以就会有三个数据库的url配置,
  • 在项目中如何配置?
  • 要想实现不同环境之间的配置切换又该如何来实现呢?

maven提供配置多种环境的设定,帮助开发者在使用过程中快速切换环境。具体实现步骤如下

  • 步骤一:父工程配置多个环境,并指定默认激活环境
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<profiles>
    <!--开发环境-->
    <profile>
        <id>env_dep</id>
        <properties>
            <jdbc.url>jdbc:mysql://127.1.1.1:3306/ssm_db</jdbc.url>
        </properties>
        <!--设定是否为默认环境-->
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    <!--生产环境-->
    <profile>
        <id>env_pro</id>
        <properties>
            <jdbc.url>jdbc:mysql://127.2.2.2:3306/ssm_db</jdbc.url>
        </properties>
    </profile>
    <!--测试环境-->
    <profile>
        <id>env_test</id>
        <properties>
            <jdbc.url>jdbc:mysql://127.3.3.3:3306/ssm_db</jdbc.url>
        </properties>
    </profile>
</profiles>
  • 步骤二:执行install查看env_dep环境是否生效 在本地仓库找到打包的war包,看看jdbc.properties配置文件中的url是否为 jdbc:mysql://127.1.1.1:3306/ssm_db
  • 步骤三:切换默认环境为生产环境
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<profiles>
    <!--开发环境-->
    <profile>
        <id>env_dep</id>
        <properties>
            <jdbc.url>jdbc:mysql://127.1.1.1:3306/ssm_db</jdbc.url>
        </properties>

    </profile>
    <!--生产环境-->
    <profile>
        <id>env_pro</id>
        <properties>
            <jdbc.url>jdbc:mysql://127.2.2.2:3306/ssm_db</jdbc.url>
        </properties>
        <!--设定是否为默认环境-->
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    <!--测试环境-->
    <profile>
        <id>env_test</id>
        <properties>
            <jdbc.url>jdbc:mysql://127.3.3.3:3306/ssm_db</jdbc.url>
        </properties>
    </profile>
</profiles>
  • 步骤四:执行install并查看env_pro环境是否生效 查看到的结果为: jdbc:mysql://127.2.2.2:3306/ssm_db 虽然已经能够实现不同环境的切换,但是每次切换都是需要手动修改,如何来实现在不改变代码的前提下完成环境的切换呢?
  • 步骤无:命令行实现环境切换 在命令后加上环境id: mvn install -P env_test
  • 步骤六:执行安装并查看env_test环境是否生效 查看到的结果为: jdbc:mysql://127.3.3.3:3306/ssm_db

所以总结来说,对于多环境切换只需要两步即可:

  • 父工程中定义多环境
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<profiles>
<profile>
    <id>环境名称</id>
        <properties>
        <key>value</key>
        </properties>
        <activation>
        <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    ...
</profiles>
  • 使用多环境(构建过程)
1
mvn 指令 -P 环境定义ID

跳过测试

前面在执行 install指令的时候,Maven都会按照顺序从上往下依次执行,每次都会执行 test,

对于 test来说有它存在的意义,

  • 可以确保每次打包或者安装的时候,程序的正确性,假如测试已经通过在没有修改程序的前提下再次执行打包或安装命令,由于顺序执行,测试会被再次执行,就有点耗费时间了。
  • 功能开发过程中有部分模块还没有开发完毕,测试无法通过,但是想要把其中某一部分进行快速打包,此时由于测试环境失败就会导致打包失败。

遇到上面这些情况的时候,就想跳过测试执行下面的构建命令,具体实现方式有很多:

  • 方式一:IDEA工具实现跳过测试 IDEA的maven面板上有一个按钮,点击之后可以跳过测试,不过此种方式会跳过所有的测试,如果想更精细的控制哪些跳过,哪些不跳过,那么就需要使用配置插件的方式来完成了
  • 方式二:配置插件实现跳过测试 在父工程中的pom.xml中添加测试插件配置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.12.4</version>
            <configuration>
                <skipTests>false</skipTests>
                <!--排除掉不参与测试的内容-->
                <excludes>
                    <exclude>**/BookServiceTest.java</exclude>
                </excludes>
            </configuration>
        </plugin>
    </plugins>
</build>

skipTests:如果为true,则跳过所有测试,如果为false,则不跳过测试 excludes:哪些测试类不参与测试,即排除,针对skipTests为false来设置的 includes:哪些测试类要参与测试,即包含,针对skipTests为true来设置的

  • 方式三:命令行跳过测试 使用Maven的命令行,mvn 指令 -D skipTests

注意事项:

  • 执行的项目构建指令必须包含测试生命周期,否则无效果。例如执行compile生命周期,不经过test生命周期。
  • 该命令可以不借助IDEA,直接使用cmd命令行进行跳过测试,需要注意的是cmd要在pom.xml所在目录下进行执行。

私服

这一小节,主要学习的内容是:

  • 私服简介
  • 私服仓库分类
  • 资源上传与下载

首先来说一说什么是私服

私服简介

团队开发现状分析 ./Spring.assets/63215ba216f2c2beb17b6286.jpg

  1. 张三负责ssm_crm的开发,自己写了一个ssm_pojo模块,要想使用直接将ssm_pojo安装到本地仓库即可
  2. 李四负责ssm_order的开发,需要用到张三所写的ssm_pojo模块,这个时候如何将张三写的ssm_pojo模块交给李四呢?
  3. 如果直接拷贝,那么团队之间的jar包管理会非常混乱而且容器出错,这个时候就想能不能将写好的项目上传到中央仓库,谁想用就直接联网下载即可
  4. Maven的中央仓库不允许私人上传自己的jar包,那么就得换种思路,自己搭建一个类似于中央仓库的东西,把自己的内容上传上去,其他人就可以从上面下载jar包使用
  5. 这个类似于中央仓库的东西就是接下来要学习的私服

所以到这就有两个概念,一个是私服,一个是中央仓库

  • 私服:公司内部搭建的用于存储Maven资源的服务器
  • 远程仓库:Maven开发团队维护的用于存储Maven资源的服务器

所以说:

  • 私服是一台独立的服务器,用于解决团队内部的资源共享与资源同步问题

搭建Maven私服的方式有很多,介绍其中一种使用量比较大的实现方式:

  • Nexus
    • Sonatype公司的一款maven私服产品
    • 下载地址:https://help.sonatype.com/repomanager3/download

关于nexus的下载安装和初次登录,这里就不介绍了

私服仓库分类

私服资源操作流程分析: ./Spring.assets/6321630c16f2c2beb1836959.jpg

  1. 在没有私服的情况下,自己创建的服务都是安装在Maven的本地仓库中
  2. 私服中也有仓库,要把自己的资源上传到私服,最终也是放在私服的仓库中
  3. 其他人要想使用你所上传的资源,就需要从私服的仓库中获取
  4. 当要使用的资源不是自己写的,是远程中央仓库有的第三方jar包,这个时候就需要从远程中央仓库下载,每个开发者都去远程中央仓库下速度比较慢(中央仓库服务器在国外)
  5. 私服就再准备一个仓库,用来专门存储从远程中央仓库下载的第三方jar包,第一次访问没有就会去远程中央仓库下载,下次再访问就直接走私服下载
  6. 前面在介绍版本管理的时候提到过有 SNAPSHOTRELEASE,如果把这两类的都放到同一个仓库,比较混乱,所以私服就把这两个种jar包放入不同的仓库
  7. 上面已经介绍了有三种仓库,一种是存放 SNAPSHOT的,一种是存放 RELEASE还有一种是存放从远程仓库下载的第三方jar包,那么在获取资源的时候要从哪个仓库种获取呢?
  8. 为了方便获取,将所有的仓库编成一个组,只需要访问仓库组去获取资源。

所有私服仓库总共分为三大类:

  1. 宿主仓库hosted
    • 保存无法从中央仓库获取的资源
    • 自主研发
    • 第三方非开源项目,比如Oracle,因为是付费产品,所以中央仓库没有
  2. 代理仓库proxy
    • 代理远程仓库,通过nexus访问其他公共仓库,例如中央仓库
  3. 仓库组group
    • 将若干个仓库组成一个群组,简化配置
    • 仓库组不能保存资源,属于设计型仓库
仓库类别 英文名称 功能 关联操作
宿主仓库 hosted 保存自主研发+第三方资源 上传
代理仓库 proxy 代理连接中央仓库 下载
仓库组 group 为仓库编组简化下载操作 下载

本地仓库访问私服配置

  • 通过IDEA将开发的模块上传到私服,中间是要经过本地Maven的
  • 本地Maven需要知道私服的访问地址以及私服访问的用户名和密码
  • 私服中的仓库很多,Maven最终要把资源上传到哪个仓库?
  • Maven下载的时候,又需要携带用户名和密码到私服上找对应的仓库组进行下载,然后再给IDEA
  • 上面所说的这些内容,需要在本地Maven的配置文件 settings.xml中进行配置。

  • 步骤一:私服上配置仓库 新建两个仓库,type选hosted,version policy 一个选release,一个选snapshot
  • 步骤二:配置本地Maven对私服的访问权限
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<server>
    <!--id就是刚刚创建的仓库名-->
    <id>blog-snapshot</id>
    <username>admin</username>
    <password>7d1f6527-cf26-4be3-974a-de177d75cb87</password>
</server>
<server>
    <id>blog-release</id>
    <username>admin</username>
    <password>7d1f6527-cf26-4be3-974a-de177d75cb87</password>
</server>
  • 步骤三:配置私服的访问路径
1
2
3
4
5
6
7
8
<mirror>
    <!--配置仓库组的ID-->
    <id>maven-public</id>
    <!--*代表所有内容都从私服获取-->
    <mirrorOf>*</mirrorOf>
    <!--私服仓库组maven-public的访问路径-->
    <url>http://localhost:8081/repository/maven-public/</url>
</mirror>

最后记得将新创建的两个仓库加入到maven-public的成员中,至此本地仓库就能与私服进行交互了

私服资源的下载和上传

本地仓库与私服已经建立了连接,接下来就需要往私服上上传资源和下载资源,具体的实现步骤如下

  • 步骤一:配置工程上传私服的具体位置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<distributionManagement>
    <repository>
        <id>blog-release</id>
        <url>http://localhost:8081/repository/blog-release/</url>
    </repository>
    <snapshotRepository>
        <id>blog-snapshot</id>
        <url>http://localhost:8081/repository/blog-snapshot/</url>
    </snapshotRepository>
</distributionManagement>
  • 步骤二:发布资源到私服 maven面板中运行 deploy,或者执行maven命令 mvn deploy

注意:

  • 要发布的项目都需要配置 distributionManagement标签,要么在自己的pom.xml中配置,要么在其父项目中配置,然后子项目中继承父项目即可。
  • 如果报401错误,尝试将maven的setting.xml文件复制到 C:\Users\username\.m2目录下,然后在重新进行deploy

发布成功,在私服中就能看到了 ./Spring.assets/6321707016f2c2beb1908d86.jpg

现在发布是在blog-snapshot仓库中,如果想发布到blog-release仓库中就需要将项目pom.xml中的version修改成RELEASE即可。

1
2
3
4
<groupId>com.mtmn</groupId>
<artifactId>maven_00_parent</artifactId>
<!--<version>1.0-SNAPSHOT</version>-->
<version>1.0-RELEASE</version>

如果私服中没有对应的jar,会去中央仓库下载,速度很慢。可以配置让私服去阿里云中下载依赖。 修改maven-central的Remote storage为 http://maven.aliyun.com/nexus/content/groups/public

至此私服的搭建就已经完成,相对来说有点麻烦,但是步骤都比较固定

SpringBoot

SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来 简化Spring应用的 初始搭建以及 开发过程。 使用了Spring框架后已经简化了的开发,而SpringBoot又是对Spring开发进行简化的,可想而知SpringBoot使用的简单及广泛性。 既然SpringBoot是用来简化Spring开发的,那就先回顾一下,以SpringMVC开发为例

  • 创建新模块,选择Spring初始化,并配置模块相关基础信息

./Spring.assets/image-20240404164951200.png

./Spring.assets/image-20240404165014127.png

要换成java8需要换源 https://start.aliyun.com/

IDEA2023版本创建Spring项目只能勾选17和21却无法使用Java8的完美解决方案

  • 选择当前模块需要使用的技术集
  • 开发控制器类
1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/books")
public class BookController {
    @GetMapping("/{id}")
    public String getById(@PathVariable Integer id) {
        System.out.println("get id ==> " + id);
        return "hello,spring boot!";
    }
}
  • 运行自动生成的Application类

./Spring.assets/image-20240404165256992.png

./Spring.assets/image-20240404165453018.png

对比一下 Spring 程序和 SpringBoot 程序。

类/配置文件 Spring SpringBoot
pom文件中的坐标 手工添加 勾选添加
web3.e配置类 手工制作
Spring/SpringMVC配置类 手工制作
控制器 手工制作 手工制作

基于Idea的 Spring Initializr 快速构建 SpringBoot 工程时需要联网。

不用idea也可以:

首先进入SpringBoot官网 https://spring.io/projects/spring-boot ,拉到页面最下方,会有一个 Quickstart your project 然后点击 Spring Initializr超链接

SpringBoot工程快速启动

  • 问题引入 以后和前端开发人员协同开发,而前端开发人员需要测试前端程序就需要后端开启服务器,这就受制于后端开发人员。为了摆脱这个受制,前端开发人员尝试着在自己电脑上安装 TomcatIdea ,在自己电脑上启动后端程序,这显然不现实。 后端可以将 SpringBoot 工程打成 jar 包,该 jar 包运行不依赖于 TomcatIdea 这些工具也可以正常运行,只是这个 jar 包在运行过程中连接和自己程序相同的 Mysql 数据库即可,这样就可以解决这个问题。
  • 那现在问题就是如何打包 由于在构建 SpringBoot 工程时已经在 pom.xml 中配置了如下插件
1
2
3
4
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

所以只需要使用 Mavenpackage 指令打包就会在 target 目录下生成对应的 Jar 包。

注意:该插件必须配置,不然打好的 jar 包也是有问题的。

  • 启动 进入 jar 包所在位置,在 命令提示符 中输入如下命令
1
java -jar springboot_01_quickstart-0.0.1-SNAPSHOT.jar

执行上述命令就可以看到 SpringBoot 运行的日志信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
D:\Study\SpringBoot\springboot_01_quickstart\target>java -jar springboot_01_quickstart-0.0.1-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.3)

2022-09-14 21:01:13.777  INFO 15992 --- [           main] c.b.Springboot01QuickstartApplication    : Starting Springboot01QuickstartApplication v0.0.1-SNAPSHOT using Java 1.8.0_321 on Kyle with PID 15992 (D:\Study\SpringBoot\springboot_01_quickstart\target\springboot_01_quickstart-0.0.1-SNAPSHOT.jar started by Kyle in D:\Study\SpringBoot\springboot_01_quickstart\target)
2022-09-14 21:01:13.783  INFO 15992 --- [           main] c.b.Springboot01QuickstartApplication    : No active profile set, falling back to 1 default profile: "default"
2022-09-14 21:01:14.855  INFO 15992 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-09-14 21:01:14.869  INFO 15992 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-09-14 21:01:14.870  INFO 15992 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.65]
2022-09-14 21:01:14.977  INFO 15992 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-09-14 21:01:14.977  INFO 15992 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1150 ms
2022-09-14 21:01:15.306  INFO 15992 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-09-14 21:01:15.314  INFO 15992 --- [           main] c.b.Springboot01QuickstartApplication    : Started Springboot01QuickstartApplication in 1.888 seconds (JVM running for 2.252)
2022-09-14 21:01:26.096  INFO 15992 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-09-14 21:01:26.096  INFO 15992 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2022-09-14 21:01:26.098  INFO 15992 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 2 ms
get id ==> 9527

SpringBoot概述

  • 自动配置。这个是用来解决 Spring 程序配置繁琐的问题
  • 起步依赖。这个是用来解决 Spring 程序依赖设置繁琐的问题

切换web服务器

现在启动工程使用的是 tomcat 服务器,那能不能不使用 tomcat 而使用 jetty 服务器。而要切换 web 服务器就需要将默认的 tomcat 服务器给排除掉,怎么排除呢?需要用到前面学的知识 排除依赖,使用 exclusion 标签

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <groupId>org.springframework.boot</groupId>
        </exclusion>
    </exclusions>
</dependency>

然后还要引入 jetty 服务器。

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

接下来再次运行引导类,在日志信息中就可以看到使用的是jetty服务器

1
2
2022-09-15 10:54:36.713  INFO 10308 --- [           main] o.s.b.w.e.j.JettyServletWebServerFactory : Server initialized with port: 8080
2022-09-15 10:54:36.715  INFO 10308 --- [           main] org.eclipse.jetty.server.Server          : jetty-9.4.48.v20220622; 

Jetty比Tomcat更轻量级,可扩展性更强(相较于Tomcat),谷歌应用引擎(GAE)已经全面切换为]etty

小结:通过切换服务器,不难发现在使用 SpringBoot 换技术时只需要导入该技术的 起步依赖即可。

配置文件

格式一:properties

1
server.port=80
  • 改端口号
  • ./Spring.assets/image-20240404185845399.png

格式二:yml

1
2
server:
	port: 81		🚩冒号后必须加空格

格式三:yaml

1
2
server:
	port: 82		🚩冒号后必须加空格
  • 主要写yml格式
  • 优先级:properties > yml > yaml
  • 改日志级别
  • ./Spring.assets/image-20240404191448219.png

yaml格式

YAML(YAML Ain’t Markup Language),一种数据序列化格式。这种格式的配置文件在近些年已经占有主导地位,那么这种配置文件和前期使用的配置文件是有一些优势的,先看之前使用的配置文件。

  • 最开始使用的是 xml ,格式如下:
1
2
3
4
5
<enterprise>
    <name>Helsing</name>
    <age>16</age>
    <tel>400-957-241</tel>
</enterprise>
  • properties 类型的配置文件如下
1
2
3
enterprise.name=Helsing
enterprise.age=16
enterprise.tel=400-957-241
  • yaml 类型的配置文件内容如下
1
2
3
4
enterprise:
  name: Helsing
  age: 16
  tel: 400-957-241
  • 通过对比,得出yaml的优点有:
    • 容易阅读
      • yaml 类型的配置文件比 xml 类型的配置文件更容易阅读,结构更加清晰
    • 容易与脚本语言交互(暂时还体会不到,后面会了解)
    • 以数据为核心,重数据轻格式
      • yaml 更注重数据,而 xml 更注重格式
  • YAML 文件扩展名:
    • .yml (主流)
    • .yaml

上面两种后缀名都可以,以后使用更多的还是 yml 的。


  • yml语法规则

    • 大小写敏感
    • 属性层级关系使用多行描述,每行结尾使用冒号结束
    • 使用缩进表示层级关系,同层级左侧对齐,只允许使用空格(不允许使用Tab键)
      • 空格的个数并不重要,只要保证同层级的左侧对齐即可。
    • 属性值前面添加空格(属性名与属性值之间使用冒号+空格作为分隔)
    • ## 表示注释

    核心规则:数据前面要加空格与冒号隔开

    • 数组数据在数据书写位置的下方使用减号作为数据开始符号,每行书写一个数据,减号与数据间空格分隔,例如
    1
    2
    3
    4
    5
    6
    7
    8
    
    enterprise:
      name: Helsing
      age: 16
      tel: 400-957-241
      subject:
        - Java
        - Python
        - C#
    

yaml配置文件数据读取

环境准备

  • 修改 resource目录下的 application.yml配置文件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
lesson: SpringBoot

server:
  port: 80

enterprise:
  name: Helsing
  age: 16
  tel: 400-957-241
  subject:
    - Java
    - Python
    - C#
  • com.mtmn.domain包下新建一个Enterprise类,用来封装数据
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.mtmn.domain;

import java.util.Arrays;

public class Enterprise {
    private String name;
    private int age;
    private String tel;
    private String[] subject;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getTel() {
        return tel;
    }

    public void setTel(String tel) {
        this.tel = tel;
    }

    public String[] getSubject() {
        return subject;
    }

    public void setSubject(String[] subject) {
        this.subject = subject;
    }

    @Override
    public String toString() {
        return "Enterprise{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", tel='" + tel + '\'' +
                ", subject=" + Arrays.toString(subject) +
                '}';
    }
}

读取配置文件

  • 方式一:

    使用 @Value注解

    • 使用 @Value("表达式") 注解可以从配合文件中读取数据,注解中用于读取属性名引用方式是:${一级属性名.二级属性名……}
    • 可以在 BookController 中使用 @Value 注解读取配合文件数据,如下
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    @RestController
    @RequestMapping("/books")
    public class BookController {
        @Value("${lesson}")
        private String lesson;
        @Value("${server.port}")
        private Integer port;
        @Value("${enterprise.subject[0]}")
        private String subject_0;
    
        @GetMapping("/{id}")
        public String getById(@PathVariable Integer id) {
            System.out.println(lesson);
            System.out.println(port);
            System.out.println(subject_0);
            return "hello , spring boot!";
        }
    }
    
    • 使用PostMan发送请求,控制台输出如下,成功获取到了数据
    1
    2
    3
    
    SpringBoot
    80
    Java
    
  • 方式二:

    使用Environment对象

    • 上面方式读取到的数据特别零散,SpringBoot 还可以使用 @Autowired 注解注入 Environment 对象的方式读取数据。这种方式 SpringBoot 会将配置文件中所有的数据封装到 Environment 对象中,如果需要使用哪个数据只需要通过调用 Environment 对象的 getProperty(String name) 方法获取。具体代码如下
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    @RestController
    @RequestMapping("/books")
    public class BookController {
        @Autowired
        private Environment environment;
    
        @GetMapping("/{id}")
        public String getById(@PathVariable Integer id) {
            System.out.println(environment.getProperty("lesson"));
            System.out.println(environment.getProperty("enterprise.name"));
            System.out.println(environment.getProperty("enterprise.subject[1]"));
            return "hello , spring boot!";
        }
    }
    
    • 使用PostMan发送请求,控制台输出如下,成功获取到了数据
    1
    2
    3
    
    SpringBoot
    Helsing
    Python
    

    注意:这种方式在开发中很少用,因为框架内含大量数据

  • ⭐️⭐️⭐️方式三: 常用

    使用自定义对象`SpringBoot

    • 还提供了将配置文件中的数据封装到自定义的实体类对象中的方式。具体操作如下:

      • 将实体类 bean 的创建交给 Spring 管理。

        • 在类上添加 @Component 注解
      • 使用 @ConfigurationProperties

        注解表示加载配置文件

        • 在该注解中也可以使用 prefix 属性指定只加载指定前缀的数据
      • BookController 中进行注入

    • 具体代码如下

      • Enterprise 实体类内容如下:
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      
      @Component
      @ConfigurationProperties(prefix = "enterprise")
      public class Enterprise {
          private String name;
          private int age;
          private String tel;
          private String[] subject;
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          public int getAge() {
              return age;
          }
      
          public void setAge(int age) {
              this.age = age;
          }
      
          public String getTel() {
              return tel;
          }
      
          public void setTel(String tel) {
              this.tel = tel;
          }
      
          public String[] getSubject() {
              return subject;
          }
      
          public void setSubject(String[] subject) {
              this.subject = subject;
          }
      
          @Override
          public String toString() {
              return "Enterprise{" +
                      "name='" + name + '\'' +
                      ", age=" + age +
                      ", tel='" + tel + '\'' +
                      ", subject=" + Arrays.toString(subject) +
                      '}';
          }
      }
      
      • BooKController内容如下
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      
      @RestController
      @RequestMapping("/books")
      public class BookController {
          @Autowired
          private Enterprise enterprise;
      
          @GetMapping("/{id}")
          public String getById(@PathVariable Integer id) {
              System.out.println(enterprise);
              System.out.println(enterprise.getAge());
              System.out.println(enterprise.getName());
              System.out.println(enterprise.getTel());
              return "hello , spring boot!";
          }
      }
      
      • 使用PostMan发送请求,控制台输出如下,成功获取到了数据
      1
      2
      3
      4
      
      Enterprise{name='Helsing', age=16, tel='400-957-241', subject=[Java, Python, C#]}
      16
      Helsing
      400-957-241
      

      可能遇到的问题:

      ./Spring.assets/image-20240404193211988.png

      • 在Enterprise实体类上遇到 Spring Boot Configuration Annotation Processor not configured警告提示

      解决方案

      • pom.xml中添加如下依赖即可
      1
      2
      3
      4
      5
      
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-configuration-processor</artifactId>
          <optional>true</optional>
      </dependency>
      

多环境配置

以后在工作中,对于开发环境、测试环境、生产环境的配置肯定都不相同,比如开发阶段会在自己的电脑上安装 mysql ,连接自己电脑上的 mysql 即可,但是项目开发完毕后要上线就需要改配置,将环境的配置改为线上环境的。 来回的修改配置会很麻烦,而 SpringBoot 给开发者提供了多环境的快捷配置,需要切换环境时只需要改一个配置即可。不同类型的配置文件多环境开发的配置都不相同,接下来对不同类型的配置文件进行说明

yaml文件

application.yml 中使用 --- 来分割不同的配置,内容如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
## 开发环境
spring:
  profiles: dev ## 给开发环境取的名
server:
  port: 80
---
## 生产环境
spring:
  profiles: pro ## 给生产环境取的名
server:
  port: 81
---
## 测试环境
spring:
  profiles: test ## 给测试环境起的名
server:
  port: 82

上面配置中 spring.profiles 是用来给不同的配置起名字的。而如何告知 SpringBoot 使用哪段配置呢?可以使用如下配置来启用都一段配置

1
2
3
4
## 设置启用的环境
spring:
  profiles:
    active: dev  ## 表示使用的是开发环境的配置

综上所述,application.yml 配置文件内容如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
  profiles:
    active: dev
---
## 开发环境
spring:
  profiles: dev ## 给开发环境取的名
server:
  port: 80
---
## 生产环境
spring:
  profiles: pro ## 给生产环境取的名
server:
  port: 81
---
## 测试环境
spring:
  profiles: test ## 给测试环境起的名
server:
  port: 82

注意:在上面配置中给不同配置起名字的 spring.profiles 配置项已经过时。最新用来起名字的配置项是

1
2
3
4
5
## 开发环境
spring:
  config:
    activate:
      on-profile: dev ## 给开发环境取的名

那现在就可以尝试启用不同的环境,来观察启用端口号,证明是否真的启用了不同的环境

properties文件

properties 类型的配置文件配置多环境需要定义不同的配置文件

  • application-dev.properties 是开发环境的配置文件。在该文件中配置端口号为 80
1
server.port=80
  • application-test.properties 是测试环境的配置文件。在该文件中配置端口号为 81
1
server.port=81
  • application-pro.properties 是生产环境的配置文件。在该文件中配置端口号为 82
1
server.port=82
  • SpringBoot 只会默认加载名为 application.properties 的配置文件,所以需要在 application.properties 配置文件中设置启用哪个配置文件,配置如下:
1
spring.profiles.active=pro

命令行启动参数设置

使用 SpringBoot 开发的程序以后都是打成 jar 包,通过 java -jar xxx.jar 的方式启动服务的。那么就存在一个问题,如何切换环境呢?因为配置文件打到的jar包中了。

知道 jar 包其实就是一个压缩包,可以解压缩,然后修改配置,最后再打成jar包就可以了。这种方式显然有点麻烦,而 SpringBoot 提供了在运行 jar 时设置开启指定的环境的方式,如下

1
java -jar xxx.jar --spring.profiles.active=test

那么这种方式能不能临时修改端口号呢?也是可以的,可以通过如下方式

1
java -jar xxx.jar --server.port=9421

当然也可以同时设置多个配置,比如即指定启用哪个环境配置,又临时指定端口,如下

1
java -jar xxx.jar -server.port=9421 --spring.profiles.active=pro

那现在命令行配置的端口号是9421,配置文件中的端口号为82,那么结果将会是多少呢? 测试后就会发现命令行设置的端口号优先级高(也就是使用的是命令行设置的端口号),配置的优先级其实 SpringBoot 官网已经进行了说明,详情参见 https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config 如果使用了多种方式配合同一个配置项,优先级高的生效。

配置文件分类

有这样的场景,开发完毕后需要测试人员进行测试,由于测试环境和开发环境的很多配置都不相同,所以测试人员在运行的工程时需要临时修改很多配置,如下

1
java –jar springboot.jar –-spring.profiles.active=test --server.port=85 --server.servlet.context-path=/heima --server.tomcat.connection-timeout=-1 …… …… …… …… ……

针对这种情况,SpringBoot 定义了配置文件不同的放置的位置;而放在不同位置的优先级时不同的。

  • SpringBoot中4级配置文件放置位置:

    • 1级:classpath:application.yml
    • 2级:classpath:config/application.yml
    • 3级:file :application.yml
    • 4级:file :config/application.yml

    说明:级别越高的优先级越高

下面验证这个优先级

1级和2级

1级就是resource目录下的 application.yml,2级是在resource目录下新建一个config文件,在其中新建 application.yml

1级

1
2
server:
  port: 80

2级

1
2
server:
  port: 81

启动引导类,控制台输出的为81端口

Tomcat initialized with port(s): 81 (http)

3级和4级

先将工程打成一个jar包,进入到jar包的目录下,创建 application.yml 配置文件,而在该配合文件中将端口号设置为 82jar 包所在位置创建 config 文件夹,在该文件夹下创建 application.yml 配置文件,而在该配合文件中将端口号设置为 83

3级

1
2
server:
  port: 82

4级

1
2
server:
  port: 83

在命令行使用以下命令运行程序

1
java -jar springboot_06_config_file-0.0.1-SNAPSHOT.jar

运行后日志信息如下,端口为83

Tomcat initialized with port(s): 83 (http)

通过这个结果可以得出一个结论 config下的配置文件优先于类路径下的配置文件。

整合Junit

首先回顾一下 Spring 整合 junit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class UserServiceTest {
  
    @Autowired
    private BookService bookService;
  
    @Test
    public void testSave(){
        bookService.save();
    }
}

使用 @RunWith 注解指定运行器,使用 @ContextConfiguration 注解来指定配置类或者配置文件。

SpringBoot 整合 junit 特别简单,分为以下三步完成

  • 在测试类上添加 SpringBootTest 注解
  • 使用 @Autowired 注入要测试的资源
  • 定义测试方法进行测试

环境准备

  • 创建一个新的SpringBoot工程
  • 在com.mtmn.service包下创建BookService接口
1
2
3
public interface BookService {
    void save();
}
  • 在com.mtmn.service.impl包下创建BookService接口的实现类,并重写其方法
1
2
3
4
5
6
7
@Service
public class BookServiceImpl implements BookService {
    @Override
    public void save() {
        System.out.println("book service is running ..");
    }
}

编写测试类

test/java 下创建 com.mtmn 包,在该包下创建测试类,将 BookService 注入到该测试类中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@SpringBootTest
class Springboot02JunitApplicationTests {
    @Autowired
    private BookService bookService;

    @Test
    void contextLoads() {
        bookService.save();
    }

}

运行测试方法,控制台成功输出

book service is running …

注意:这里的引导类所在包必须是测试类所在包及其子包。

例如:

  • 引导类所在包是 com.mtmn
  • 测试类所在包是 com.mtmn

如果不满足这个要求的话,就需要在使用 @SpringBootTest 注解时,使用 classes 属性指定引导类的字节码对象。如 @SpringBootTest(classes = XxxApplication.class)

整合Mybatis

回顾Spring整合MyBatis

之前Spring整合MyBatis时,需要定义很多配置类

  • SpringConfig配置类
1
2
3
4
5
6
@Configuration
@ComponentScan("com.mtmn")
@PropertySource("jdbc.properties")
@Import({JdbcConfig.class, MyBatisConfig.class})
public class SpringConfig {
}
  • 导入JdbcConfig配置类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }
}
  • 导入MyBatisConfig配置类
    • 定义 SqlSessionFactoryBean
    • 定义映射配置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class MyBatisConfig {
    //定义bean,SqlSessionFactoryBean,用于产生SqlSessionFactory对象
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
        SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
        //设置模型类的别名扫描
        sqlSessionFactory.setTypeAliasesPackage("com.mtmn.domain");
        //设置数据源
        sqlSessionFactory.setDataSource(dataSource);
        return sqlSessionFactory;
    }
  
    //定义bean,返回MapperScannerConfigurer对象
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.mtmn.dao");
        return msc;
    }
}

SpringBoot整合MyBatis

  • 创建一个新的模块 注意选择技术集的时候,要勾选 MyBatis FrameworkMySQL Driver
  • 建库建表
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CREATE TABLE tbl_book
(
    id          INT PRIMARY KEY AUTO_INCREMENT,
    `type`      VARCHAR(20),
    `name`      VARCHAR(50),
    description VARCHAR(255)
);

INSERT INTO `tbl_book`(`id`, `type`, `name`, `description`)
VALUES (1, '计算机理论', 'Spring实战 第五版', 'Spring入门经典教程,深入理解Spring原理技术内幕'),
       (2, '计算机理论', 'Spring 5核心原理与30个类手写实践', '十年沉淀之作,手写Spring精华思想'),
       (3, '计算机理论', 'Spring 5设计模式', '深入Spring源码刨析Spring源码中蕴含的10大设计模式'),
       (4, '计算机理论', 'Spring MVC+Mybatis开发从入门到项目实战',
        '全方位解析面向Web应用的轻量级框架,带你成为Spring MVC开发高手'),
       (5, '计算机理论', '轻量级Java Web企业应用实战', '源码级刨析Spring框架,适合已掌握Java基础的读者'),
       (6, '计算机理论', 'Java核心技术 卷Ⅰ 基础知识(原书第11版)',
        'Core Java第11版,Jolt大奖获奖作品,针对Java SE9、10、11全面更新'),
       (7, '计算机理论', '深入理解Java虚拟机', '5个纬度全面刨析JVM,大厂面试知识点全覆盖'),
       (8, '计算机理论', 'Java编程思想(第4版)', 'Java学习必读经典,殿堂级著作!赢得了全球程序员的广泛赞誉'),
       (9, '计算机理论', '零基础学Java(全彩版)', '零基础自学编程的入门图书,由浅入深,详解Java语言的编程思想和核心技术'),
       (10, '市场营销', '直播就这么做:主播高效沟通实战指南', '李子柒、李佳奇、薇娅成长为网红的秘密都在书中'),
       (11, '市场营销', '直播销讲实战一本通', '和秋叶一起学系列网络营销书籍'),
       (12, '市场营销', '直播带货:淘宝、天猫直播从新手到高手', '一本教你如何玩转直播的书,10堂课轻松实现带货月入3W+');
  • 定义实体类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Book {
    private Integer id;
    private String type;
    private String name;
    private String description;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getName() {
        return name;
    }

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

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", type='" + type + '\'' +
                ", name='" + name + '\'' +
                ", description='" + description + '\'' +
                '}';
    }
}
  • 定义dao接口 在com.mtmn.dao包下定义BookDao接口
1
2
3
4
public interface BookDao {
    @Select("select * from tbl_book where id = #{id}")
    Book getById(Integer id);
}
  • 定义测试类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@SpringBootTest
class Springboot03MybatisApplicationTests {

    @Autowired
    private BookDao bookDao;

    @Test
    void contextLoads() {
        Book book = bookDao.getById(1);
        System.out.println(book);
    }

}
  • 编写配置
1
2
3
4
5
6
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot_db?serverTimezone=UTC
    username: root
    password: PASSWORD
  • 测试 运行测试方法,会报错 No qualifying bean of type 'com.mtmn.dao.BookDao',没有类型为“com.mtmn.dao.BookDao”的限定bean 为什么会出现这种情况呢?之前在配置MyBatis时,配置了如下内容
1
2
3
4
5
6
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
    MapperScannerConfigurer msc = new MapperScannerConfigurer();
    msc.setBasePackage("com.mtmn.dao");
    return msc;
}

Mybatis 会扫描接口并创建接口的代码对象交给 Spring 管理,但是现在并没有告诉 Mybatis 哪个是 dao 接口。 而要解决这个问题需要在 BookDao 接口上使用 @MapperBookDao 接口修改为

1
2
3
4
5
@Mapper
public interface BookDao {
    @Select("select * from tbl_book where id = #{id}")
    Book getById(Integer id);
}

注意: SpringBoot 版本低于2.4.3(不含),Mysql驱动版本大于8.0时,需要在url连接串中配置时区 jdbc:mysql:///learn?serverTimezone=UTC,或在MySQL数据库端配置时区解决此问题

  • 使用Druid数据源 现在并没有指定数据源,SpringBoot 有默认的数据源,也可以指定使用 Druid 数据源,按照以下步骤实现
1
2
3
4
5
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.12</version>
</dependency>
  • application.yml 修改配置文件配置 可以通过 spring.datasource.type 来配置使用什么数据源。配置文件内容可以改进为
1
2
3
4
5
6
7
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot_db?serverTimezone=UTC
    username: root
    password: PASSWORD.
    type: com.alibaba.druid.pool.DruidDataSource

案例

./Spring.assets/image-20240407152238819.png

  • pom.xml
  •   1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    
      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
          <groupId>com.mtmn</groupId>
          <artifactId>LearnSB</artifactId>
          <version>0.0.1-SNAPSHOT</version>
          <name>LearnSB</name>
          <description>LearnSB</description>
          <properties>
              <java.version>1.8</java.version>
              <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
              <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
              <spring-boot.version>2.7.6</spring-boot.version>
          </properties>
          <dependencies>
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-web</artifactId>
              </dependency>
    
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-test</artifactId>
                  <scope>test</scope>
              </dependency>
    
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-configuration-processor</artifactId>
                  <optional>true</optional>
              </dependency>
    
              <dependency>
                  <groupId>com.alibaba</groupId>
                  <artifactId>druid</artifactId>
                  <version>1.1.12</version>
              </dependency>
    
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-web</artifactId>
              </dependency>
              <dependency>
                  <groupId>org.mybatis.spring.boot</groupId>
                  <artifactId>mybatis-spring-boot-starter</artifactId>
                  <version>2.2.2</version>
              </dependency>
    
              <dependency>
                  <groupId>com.mysql</groupId>
                  <artifactId>mysql-connector-j</artifactId>
                  <scope>runtime</scope>
              </dependency>
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-test</artifactId>
                  <scope>test</scope>
              </dependency>
          </dependencies>
          <dependencyManagement>
              <dependencies>
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-dependencies</artifactId>
                      <version>${spring-boot.version}</version>
                      <type>pom</type>
                      <scope>import</scope>
                  </dependency>
              </dependencies>
          </dependencyManagement>
    
          <build>
              <plugins>
                  <plugin>
                      <groupId>org.apache.maven.plugins</groupId>
                      <artifactId>maven-compiler-plugin</artifactId>
                      <version>3.8.1</version>
                      <configuration>
                          <source>1.8</source>
                          <target>1.8</target>
                          <encoding>UTF-8</encoding>
                      </configuration>
                  </plugin>
                  <plugin>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-maven-plugin</artifactId>
                      <version>${spring-boot.version}</version>
                      <configuration>
                          <mainClass>com.mtmn.LearnSbApplication</mainClass>
                          <skip>true</skip>
                      </configuration>
                      <executions>
                          <execution>
                              <id>repackage</id>
                              <goals>
                                  <goal>repackage</goal>
                              </goals>
                          </execution>
                      </executions>
                  </plugin>
              </plugins>
          </build>
      </project>
    
  • domain.Book
  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    
      package com.mtmn.domain;
    
      import java.util.Objects;
    
      public class Book {
          private Integer id;
          private String type;
          private String name;
          private String description;
    
          public Book() {
          }
    
          public Book(Integer id, String type, String name, String description) {
              this.id = id;
              this.type = type;
              this.name = name;
              this.description = description;
          }
    
          @Override
          public boolean equals(Object o) {
              if (this == o) {
                  return true;
              };
              if (o == null || getClass() != o.getClass()) {
                  return false;
              };
              Book book = (Book) o;
              return Objects.equals(id, book.id) && Objects.equals(type, book.type) && Objects.equals(name, book.name) && Objects.equals(description, book.description);
          }
    
          @Override
          public int hashCode() {
              return Objects.hash(id, type, name, description);
          }
    
          @Override
          public String toString() {
              return "Book{" +
                      "id=" + id +
                      ", type='" + type + '\'' +
                      ", name='" + name + '\'' +
                      ", description='" + description + '\'' +
                      '}';
          }
    
          public Integer getId() {
              return id;
          }
    
          public void setId(Integer id) {
              this.id = id;
          }
    
          public String getType() {
              return type;
          }
    
          public void setType(String type) {
              this.type = type;
          }
    
          public String getName() {
              return name;
          }
    
          public void setName(String name) {
              this.name = name;
          }
    
          public String getDescription() {
              return description;
          }
    
          public void setDescription(String description) {
              this.description = description;
          }
      }
    
  • domain.Code
  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
      package com.mtmn.domain;
    
      public class Code {
          public static final Integer SAVE_OK = 20011;
          public static final Integer UPDATE_OK = 20021;
          public static final Integer DELETE_OK = 20031;
          public static final Integer GET_OK = 20041;
    
          public static final Integer SAVE_ERR = 20010;
          public static final Integer UPDATE_ERR = 20020;
          public static final Integer DELETE_ERR = 20030;
          public static final Integer GET_ERR = 20040;
    
          public static final Integer SYSTEM_ERR = 50001;
          public static final Integer SYSTEM_TIMEOUT_ERR = 50002;
          public static final Integer SYSTEM_UNKNOW_ERR = 59999;
    
          public static final Integer BUSINESS_ERR = 60001;
      }
    
  • domain.Result
  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    
      package com.mtmn.domain;
    
      public class Result {
          //描述统一格式中的编码,用于区分操作,可以简化配置0或1表示成功失败
          private Integer code;
          //描述统一格式中的数据
          private Object data;
          //描述统一格式中的消息,可选属性
          private String msg;
    
          public Result() {
          }
    
          //构造器可以根据自己的需要来编写
          public Result(Integer code, Object data) {
              this.code = code;
              this.data = data;
          }
    
          public Result(Integer code, Object data, String msg) {
              this.code = code;
              this.data = data;
              this.msg = msg;
          }
    
          public Integer getCode() {
              return code;
          }
    
          public void setCode(Integer code) {
              this.code = code;
          }
    
          public Object getData() {
              return data;
          }
    
          public void setData(Object data) {
              this.data = data;
          }
    
          public String getMsg() {
              return msg;
          }
    
          public void setMsg(String msg) {
              this.msg = msg;
          }
    
          @Override
          public String toString() {
              return "Result{" +
                      "code=" + code +
                      ", data=" + data +
                      ", msg='" + msg + '\'' +
                      '}';
          }
      }
    
  • service.BookService
  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    
      package com.mtmn.service;
    
      import com.mtmn.domain.Book;
      import org.springframework.transaction.annotation.Transactional;
      import java.util.List;
    
      @Transactional
      public interface BookService {
          /**
           * 保存
           * @param book
           * @return
           */
          boolean save(Book book);
    
          /**
           * 修改
           * @param book
           * @return
           */
          boolean update(Book book);
    
          /**
           * 按id删除
           * @param id
           * @return
           */
          boolean delete(Integer id);
    
          /**
           * 按id查询
           * @param id
           * @return
           */
          Book getById(Integer id);
    
          /**
           * 查询所有
           * @return
           */
          List<Book> getAll();
      }
    
  • service.impl.BookServiceImpl
  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
      package com.mtmn.service.impl;
    
      import com.mtmn.dao.BookDao;
      import com.mtmn.domain.Book;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Service;
      import org.springframework.web.bind.annotation.PathVariable;
      import com.mtmn.service.BookService;
    
      import java.util.List;
    
      @Service
      public class BookServiceImpl implements BookService {
    
          @Autowired
          private BookDao bookDao;
    
          @Override
          public boolean save(Book book) {
              return bookDao.save(book) > 0;
          }
          @Override
          public boolean update(Book book) {
              return bookDao.update(book) > 0;
          }
          @Override
          public boolean delete(@PathVariable Integer id) {
              return bookDao.delete(id) > 0;
          }
          @Override
          public Book getById(Integer id) {
              return bookDao.getById(id);
          }
          @Override
          public List<Book> getAll() {
              return bookDao.getAll();
          }
      }
    
  • dao.BookDao
  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
      package com.mtmn.dao;
    
      import com.mtmn.domain.Book;
      import org.apache.ibatis.annotations.*;
    
      import java.util.List;
    
      @Mapper
      public interface BookDao {
          @Insert(value = "insert into tbl_book values (null, #{type}, #{name}, #{description})")
          int save(Book book);
          @Update(value = "update tbl_book set type=#{type}, name=#{name}, description=#{description} where id=#{id}")
          int update(Book book);
          @Delete(value = "delete from tbl_book where id=#{id}")
          int delete(Integer id);
          @Select(value = "select * from tbl_book")
          List<Book> getAll();
          @Select(value = "select * from tbl_book where id=#{id}")
          Book getById(Integer id);
      }
    
  • controller.BookContorller
  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    
      package com.mtmn.contorller;
    
      import com.mtmn.domain.Book;
      import com.mtmn.domain.Code;
      import com.mtmn.domain.Result;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.web.bind.annotation.*;
      import com.mtmn.service.BookService;
    
      import java.util.List;
    
      @RestController
      @RequestMapping("/books")
      public class BookController {
          @Autowired
          private BookService bookService;
    
          @PostMapping
          public Result save(@RequestBody Book book) {
              boolean flag = bookService.save(book);
              return new Result(flag ? Code.SAVE_OK : Code.SAVE_ERR, flag);
          }
    
          @PutMapping
          public Result update(@RequestBody Book book) {
              boolean flag = bookService.update(book);
              return new Result(flag ? Code.UPDATE_OK : Code.UPDATE_ERR, flag);
          }
    
          @DeleteMapping("/{id}")
          public Result delete(@PathVariable Integer id) {
              boolean flag = bookService.delete(id);
              return new Result(flag ? Code.DELETE_OK : Code.DELETE_ERR, flag);
          }
    
          @GetMapping("/{id}")
          public Result getById(@PathVariable Integer id) {
              Book book = bookService.getById(id);
              Integer code = book == null ? Code.GET_ERR : Code.GET_OK;
              String msg = book == null ? "数据查询失败,请重试!" : "";
              return new Result(code, book, msg);
          }
    
          @GetMapping
          public Result getAll() {
              List<Book> books = bookService.getAll();
              Integer code = books == null ? Code.GET_ERR : Code.GET_OK;
              String msg = books == null ? "数据查询失败,请重试!" : "";
              return new Result(code, books, msg);
          }
      }
    
  • contorller.ProjectExceptionAdvice
  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
      package com.mtmn.contorller;
    
      import com.mtmn.domain.Code;
      import com.mtmn.domain.Result;
      import com.mtmn.exception.BusinessException;
      import com.mtmn.exception.SystemException;
      import org.springframework.web.bind.annotation.ExceptionHandler;
      import org.springframework.web.bind.annotation.RestControllerAdvice;
    
      @RestControllerAdvice
      public class ProjectExceptionAdvice {
          @ExceptionHandler(Exception.class)
          public Result doException(Exception ex) {
              return new Result(Code.SYSTEM_UNKNOW_ERR, null, "系统繁忙,请稍后再试!");
          }
    
          @ExceptionHandler(SystemException.class)
          public Result doSystemException(SystemException ex) {
              return new Result(ex.getCode(), null, ex.getMessage());
          }
    
          @ExceptionHandler(BusinessException.class)
          public Result doBusinessException(BusinessException ex) {
              return new Result(ex.getCode(), null, ex.getMessage());
          }
      }
    
  • exception.BusinessException
  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
      package com.mtmn.exception;
    
      public class BusinessException extends RuntimeException{
          private Integer code;
    
          public Integer getCode() {
              return code;
          }
    
          public void setCode(Integer code) {
              this.code = code;
          }
    
          public BusinessException() {
          }
    
          public BusinessException(Integer code) {
              this.code = code;
    
          }
    
          public BusinessException(Integer code, String message) {
              super(message);
              this.code = code;
          }
    
          public BusinessException(Integer code, String message, Throwable cause) {
              super(message, cause);
              this.code = code;
          }
      }
    
  • exception.SystemException
  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
      package com.mtmn.exception;
    
      public class SystemException extends RuntimeException {
          private Integer code;
    
          public Integer getCode() {
              return code;
          }
    
          public void setCode(Integer code) {
              this.code = code;
          }
    
          public SystemException() {
          }
    
          public SystemException(Integer code) {
              this.code = code;
    
          }
    
          public SystemException(Integer code, String message) {
              super(message);
              this.code = code;
          }
    
          public SystemException(Integer code, String message, Throwable cause) {
              super(message, cause);
              this.code = code;
          }
      }
    
  • static/pages/books.html
  •   1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    
      <!DOCTYPE html>
    
      <html>
      <head>
        <!-- 页面meta -->
        <meta charset="utf-8">
        <title>SpringMVC案例</title>
        <!-- 引入样式 -->
        <link rel="stylesheet" href="../plugins/elementui/index.css">
        <link rel="stylesheet" href="../plugins/font-awesome/css/font-awesome.min.css">
        <link rel="stylesheet" href="../css/style.css">
      </head>
    
      <body class="hold-transition">
    
      <div id="app">
    
        <div class="content-header">
          <h1>图书管理</h1>
        </div>
    
        <div class="app-container">
          <div class="box">
            <div class="filter-container">
              <el-input placeholder="图书名称" style="width: 200px;" class="filter-item"></el-input>
              <el-button class="dalfBut">查询</el-button>
              <el-button type="primary" class="butT" @click="openSave()">新建</el-button>
            </div>
    
            <el-table size="small" current-row-key="id" :data="dataList" stripe highlight-current-row>
              <el-table-column type="index" align="center" label="序号"></el-table-column>
              <el-table-column prop="type" label="图书类别" align="center"></el-table-column>
              <el-table-column prop="name" label="图书名称" align="center"></el-table-column>
              <el-table-column prop="description" label="描述" align="center"></el-table-column>
              <el-table-column label="操作" align="center">
                <template slot-scope="scope">
                  <el-button type="primary" size="mini" @click="openEdit(scope.row)">编辑</el-button>
                  <el-button size="mini" type="danger" @click="deleteBook(scope.row)">删除</el-button>
                </template>
              </el-table-column>
            </el-table>
    
            <div class="pagination-container">
              <el-pagination
                      class="pagiantion"
                      @current-change="handleCurrentChange"
                      :current-page="pagination.currentPage"
                      :page-size="pagination.pageSize"
                      layout="total, prev, pager, next, jumper"
                      :total="pagination.total">
              </el-pagination>
            </div>
    
            <!-- 新增标签弹层 -->
            <div class="add-form">
              <el-dialog title="新增图书" :visible.sync="dialogFormVisible">
                <el-form ref="dataAddForm" :model="formData" :rules="rules" label-position="right"
                         label-width="100px">
                  <el-row>
                    <el-col :span="12">
                      <el-form-item label="图书类别" prop="type">
                        <el-input v-model="formData.type"/>
                      </el-form-item>
                    </el-col>
                    <el-col :span="12">
                      <el-form-item label="图书名称" prop="name">
                        <el-input v-model="formData.name"/>
                      </el-form-item>
                    </el-col>
                  </el-row>
                  <el-row>
                    <el-col :span="24">
                      <el-form-item label="描述">
                        <el-input v-model="formData.description" type="textarea"></el-input>
                      </el-form-item>
                    </el-col>
                  </el-row>
                </el-form>
                <div slot="footer" class="dialog-footer">
                  <el-button @click="dialogFormVisible = false">取消</el-button>
                  <el-button type="primary" @click="saveBook()">确定</el-button>
                </div>
              </el-dialog>
            </div>
    
          </div>
    
          <!-- 编辑标签弹层 -->
          <div class="add-form">
            <el-dialog title="编辑检查项" :visible.sync="dialogFormVisible4Edit">
              <el-form ref="dataEditForm" :model="formData" :rules="rules" label-position="right" label-width="100px">
                <el-row>
                  <el-col :span="12">
                    <el-form-item label="图书类别" prop="type">
                      <el-input v-model="formData.type"/>
                    </el-form-item>
                  </el-col>
                  <el-col :span="12">
                    <el-form-item label="图书名称" prop="name">
                      <el-input v-model="formData.name"/>
                    </el-form-item>
                  </el-col>
                </el-row>
                <el-row>
                  <el-col :span="24">
                    <el-form-item label="描述">
                      <el-input v-model="formData.description" type="textarea"></el-input>
                    </el-form-item>
                  </el-col>
                </el-row>
              </el-form>
              <div slot="footer" class="dialog-footer">
                <el-button @click="dialogFormVisible4Edit = false">取消</el-button>
                <el-button type="primary" @click="handleEdit()">确定</el-button>
              </div>
            </el-dialog>
          </div>
        </div>
      </div>
      </body>
    
      <!-- 引入组件库 -->
      <script src="../js/vue.js"></script>
      <script src="../plugins/elementui/index.js"></script>
      <script type="text/javascript" src="../js/jquery.min.js"></script>
      <script src="../js/axios-0.18.0.js"></script>
    
      <script>
        var vue = new Vue({
    
          el: '#app',
    
          data: {
            dataList: [],//当前页要展示的分页列表数据
            formData: {},//表单数据
            dialogFormVisible: false,//增加表单是否可见
            dialogFormVisible4Edit: false,//编辑表单是否可见
            pagination: {},//分页模型数据,暂时弃用
          },
    
          //钩子函数,VUE对象初始化完成后自动执行
          created() {
            this.getAll();
          },
    
          methods: {
            // 重置表单
            resetForm() {
              this.formData = {};
            },
    
            // 弹出添加窗口
            openSave() {
              this.dialogFormVisible = true;
              //每次弹出表单的时候,都重置一下数据
              this.resetForm();
            },
    
            //添加
            saveBook() {
              axios.post("/books",this.formData).then((res)=>{
                //20011是成功的状态码,成功之后就关闭对话框,并显示添加成功
                if (res.data.code === 20011){
                  this.dialogFormVisible = false;
                  this.$message.success("添加成功")
                  //20010是失败的状态码,失败后给用户提示信息
                }else if(res.data.code === 20010){
                  this.$message.error("添加失败");
                  //如果前两个都不满足,那就是SYSTEM_UNKNOW_ERR,未知异常了,显示未知异常的错误提示信息安抚用户情绪
                }else {
                  this.$message.error(res.data.msg);
                }
              }).finally(()=>{
                this.getAll();
              })
            },
    
            //主页列表查询
            getAll() {
              axios.get("/books").then((res)=>{
                this.dataList = res.data.data;
              })
            },
            openEdit(row) {
              axios.get("/books/" + row.id).then((res) => {
                if (res.data.code === 20041) {
                  this.formData = res.data.data;
                  this.dialogFormVisible4Edit = true;
                } else {
                  this.$message.error(res.data.msg);
                }
              });
            },
            deleteBook(row) {
              this.$confirm("此操作永久删除当前数据,是否继续?","提示",{
                type:'info'
              }).then(()=> {
                axios.delete("/books/" + row.id).then((res) => {
                  if (res.data.code === 20031) {
                    this.$message.success("删除成功")
                  } else if (res.data.code === 20030) {
                    this.$message.error("删除失败")
                  }
                }).finally(() => {
                  this.getAll();
                });
              }).catch(()=>{
                this.$message.info("取消删除操作")
              })
            },
            handleEdit() {
              axios.put("/books", this.formData).then((res) => {
                if (res.data.code === 20021) {
                  this.dialogFormVisible4Edit = false;
                  this.$message.success("修改成功")
                } else if (res.data.code === 20020) {
                  this.$message.error("修改失败")
                } else {
                  this.$message.error(res.data.msg);
                }
              }).finally(() => {
                this.getAll();
              });
            }
          }
        })
      </script>
      </html>
    

Mybatis-Plus

MyBatisPlus入门案例与简介

入门案例

  • MyBatisPlus(简称MP)是基于MyBatis框架基础上开发的增强型工具,旨在简化开发,提高效率
  • 开发方式
    • 基于MyBatis使用MyBatisPlus
    • 基于Spring使用MyBatisPlus
    • 基于SpringBoot使用MyBatisPlus(重点)

由于刚刚才学完SpringBoot,所以现在直接使用SpringBoot来构建项目,官网的快速开始也是直接用的SpringBoot

  • 步骤一:创建数据库和表
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
CREATE TABLE user (
    id bigint(20) primary key auto_increment,
    name varchar(32) not null,
    password  varchar(32) not null,
    age int(3) not null ,
    tel varchar(32) not null
);
insert into user values(1,'Tom','tom',3,'18866668888');
insert into user values(2,'Jerry','jerry',4,'16688886666');
insert into user values(3,'Jock','123456',41,'18812345678');
insert into user values(4,'略略略','nigger',15,'4006184000');
  • 步骤二:创建SpringBoot工程 只需要勾选MySQL,不用勾选MyBatis了
  • 步骤三:补全依赖 导入德鲁伊和MyBatisPlus的坐标
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.1</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.16</version>
</dependency>
  • 步骤四:编写数据库连接四要素 还是将application的后缀名改为yml,以后配置都是用yml来配置 注意要设置一下时区,不然可能会报错(指高版本的mysql)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql:///learn?serverTimezone=UTC
    username: cdd
    password: 1
  
## mybatis的日志信息
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  • 步骤五:根据数据表来创建对应的模型类 注意id是Long类型,至于为什么是Long,接着往下看
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class User {
    private Long id;
    private String name;
    private String password;
    private Integer age;
    private String tel;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                ", age=" + age +
                ", tel='" + tel + '\'' +
                '}';
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getTel() {
        return tel;
    }

    public void setTel(String tel) {
        this.tel = tel;
    }
}
  • 步骤六:创建dao接口
1
2
3
@Mapper
public interface UserDao extends BaseMapper<User>{
}

只需要在类上方加一个 @Mapper注解,同时继承 BaseMapper<>,泛型写创建的模型类的类型 然后这样就能完成单表的CRUD了

  • 步骤七:测试 以后连简单的CRUD都不用写了 SpringBoot的测试类也是简单的一批,只需要一个 @SpringBootTest注解就能完成(创建SpringBoot工程的时候已经帮自动弄好了) 测试类里需要什么东西就用 @Autowired自动装配,测试方法上用 @Test注解
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@SpringBootTest
class MybatisplusApplicationTests {

    @Autowired
    private UserDao userDao;

    @Test
    void contextLoads() {
        List<User> users = userDao.selectList(null);
        for (User b : users) {
            System.out.println(b);
        }
    }
}

selectList() 方法的参数为 MP 内置的条件封装器 Wrapper,所以不填写就是无任何条件

现在运行测试方法,看看控制台

User{id=1, name=‘Tom’, password=‘tom’, age=3, tel=‘18866668888’} User{id=2, name=‘Jerry’, password=‘jerry’, age=4, tel=‘16688886666’} User{id=3, name=‘Jock’, password=‘123456’, age=41, tel=‘18812345678’} User{id=4, name=‘略略略’, password=‘nigger’, age=15, tel=‘4006184000’}

MyBatisPlus简介

MyBatisPlus的官网为:https://mp.baomidou.com/ ,没错就是个拼音,苞米豆,因为域名被抢注了,但是粉丝也捐赠了一个 https://mybatis.plus 域名

MP旨在成为MyBatis的最好搭档,而不是替换掉MyBatis,从名称上来看也是这个意思,一个MyBatis的plus版本,在原有的MyBatis上做增强,其底层仍然是MyBatis的东西,所以当然也可以在MP中写MyBatis的内容

对于MP的深入学习,可以多看看官方文档,锻炼自己自学的能力,毕竟不是所有知识都有像这样的网课,更多的还是自己看文档,挖源码。

MP的特性:

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

小结

SpringBoot集成MyBatisPlus非常的简单,只需要导入 MyBatisPlus的坐标,然后令dao类继承 BaseMapper,写上泛型,类上方加 @Mapper注解

可能存在的疑问:

  • 我甚至都没写在哪个表里查,为什么能自动识别是在我刚刚创建的表里查?
    • 注意创建的表,和对应的模型类,是同一个名,默认情况是在同名的表中查找
  • 那我要是表明和模型类的名不一样,那咋整?
    • 在模型类的上方加上 @TableName注解
      • 例如数据表叫 tb_user但数据类叫 User,那么就在User类上加 @TableName("tb_user")注解

标准数据层开发

标准的CRUD使用

先来看看MP给提供了哪些方法

功能 自定义接口 MP接口
新增 boolean save(T t) int insert(T t)
删除 boolean delete(int id) int deleteById(Serializable id)
修改 boolean update(T t) int updateById(T t)
根据id查询 T getById(int id) T selectById(Serializable id)
查询全部 List<T> getAll() List<T> selectList()
分页查询 PageInfo<T> getAll(int page,int size) IPage<T> selectPage(IPage<T> page)
按条件查询 List<T> getAll(Condition condition) IPage<T> selectPage(Wrapper<T>queryWrapper)

新增

1
int insert(T t)

参数类型是泛型,也就是当初继承BaseMapper的时候,填的泛型,返回值是int类型,0代表添加失败,1代表添加成功

1
2
3
4
5
6
7
8
9
@Test
void testInsert(){
    User user = new User();
    user.setName("Seto");
    user.setAge(23);
    user.setTel("4005129421");
    user.setPassword("MUSICIAN");
    userDao.insert(user);
}

随便写一个User的数据,运行程序,然后去数据库看看新增是否成功

1572364408896622593 Seto MUSICIAN 23 4005129421

这个主键自增id看着有点奇怪,但现在你知道为什么要将id设为long类型了吧

删除

1
int deleteByIds(Serializable id)
  • 参数类型为什么是一个序列化类 Serializable
  • 通过查看String的源码,你会发现String实现了Serializable接口,而且Number类也实现了Serializable接口
    • Number类又是Float,Double,Long等类的父类
    • 那现在能作为主键的数据类型,都已经是Serializable类型的子类了
    • MP使用Serializable类型当做参数类型,就好比用Object类型来接收所有类型一样
  • 返回值类型是int
    • 数据删除成功返回1
    • 未删除数据返回0。
  • 那下面就来删除刚刚添加的数据,注意末尾加个L
1
2
3
4
@Test
void testDelete(){
    userDao.deleteById(1572364408896622593L);
}

删除完毕之后,刷新数据库,看看是否删除成功

修改

1
int updateById(T t);
  • T:泛型,需要修改的数据内容,注意因为是根据ID进行修改,所以传入的对象中需要有ID属性值
  • int:返回值
    • 修改成功后返回1
    • 未修改数据返回0
1
2
3
4
5
6
7
@Test
void testUpdate(){
    User user = new User();
    user.setId(1L);
    user.setName("Alen");
    userDao.updateById(user);
}

修改功能只修改指定的字段,未指定的字段保持原样

根据ID查询

1
T selectById (Serializable id)
  • Serializable:参数类型,主键ID的值
  • T:根据ID查询只会返回一条数据
1
2
3
4
5
@Test
void testSelectById(){
    User user = userDao.selectById(1);
    System.out.println(user);
}

控制台输出如下

User(id=1, name=Alen, password=tom, age=3, tel=18866668888)

查询全部

1
List<T> selectList(Wrapper<T> queryWrapper)
  • Wrapper:用来构建条件查询的条件,目前没有可直接传为Null
1
2
3
4
5
6
7
@Test
void testSelectAll() {
    List<User> users = userDao.selectList(null);
    for (User u : users) {
        System.out.println(u);
    }
}

控制台输出如下

User(id=1, name=Alen, password=tom, age=3, tel=18866668888) User(id=2, name=Jerry, password=jerry, age=4, tel=16688886666) User(id=3, name=Jock, password=123456, age=41, tel=18812345678) User(id=4, name=传智播客, password=itcast, age=15, tel=4006184000)

  • 方法都测试完了,那你们有没有想过,这些方法都是谁提供的呢?
    • 想都不用想,肯定是当初继承的 BaseMapper,里面的方法还有很多,后面再慢慢学习

Lombok

  • 代码写到这,发现之前的dao接口,都不用自己写了,只需要继承BaseMapper,用他提供的方法就好了
  • 但是现在我还想偷点懒,毕竟懒是第一生产力,之前手写模型类的时候,创建好对应的属性,然后用IDEA的Alt+Insert快捷键,快速生成get和set方法,toSring,各种构造器(有需要的话)等
  • U1S1项目做这么久,写模型类都给我写烦了,有没有更简单的方式呢?
    • 答案当然是有的,可以使用Lombok,一个Java类库,提供了一组注解,来简化的POJO模型类开发

具体步骤如下

  • 步骤一:添加Lombok依赖
1
2
3
4
5
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <!--<version>1.18.12</version>-->
</dependency>

版本不用写,SpringBoot中已经管理了lombok的版本,

  • 步骤二:在模型类上添加注解

    Lombok常见的注解有:

    • @Setter:为模型类的属性提供setter方法
    • @Getter:为模型类的属性提供getter方法
    • @ToString:为模型类的属性提供toString方法
    • @EqualsAndHashCode:为模型类的属性提供equals和hashcode方法
    • @Data:是个组合注解,包含上面的注解的功能
    • @NoArgsConstructor:提供一个无参构造函数
    • @AllArgsConstructor:提供一个包含所有参数的构造函数
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private Long id;
        private String name;
        private String password;
        private Integer age;
        private String tel;
    }
    

说明:Lombok只是简化模型类的编写,之前的方法也能用 例如你有特殊的构造器需求,只想要name和password这两个参数,那么可以手写一个

1
2
3
4
public User(String name, String password) {
    this.name = name;
    this.password = password;
}

分页功能

基础的增删改查功能就完成了,现在进行分页功能的学习

1
IPage<T> selectPage(IPage<T> page, Wrapper<T> queryWrapper)
  • IPage用来构建分页查询条件
  • Wrapper:用来构建条件查询的条件,暂时没有条件可以传一个null
  • 返回值IPage是什么意思,后面会说明

具体的使用步骤如下

  • 步骤一:调用方法传入参数获取返回值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
void testSelectPage() {
    IPage<User> page = new Page<>(1, 3);
    userDao.selectPage(page, null);
    System.out.println("当前页码" + page.getCurrent());
    System.out.println("本页条数" + page.getSize());
    System.out.println("总页数" + page.getPages());
    System.out.println("总条数" + page.getTotal());
    System.out.println(page.getRecords());
}
  • 步骤二:设置分页拦截器

它能够自动拦截查询请求,并在SQL语句中加入分页逻辑,从而返回特定页的数据

1
2
3
4
5
6
7
8
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor myInterceptor = new MybatisPlusInterceptor();
        myInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return myInterceptor;
    }
}
  • 步骤三:运行测试程序 运行程序,结果如下,符合的预期

当前页码1 本页条数3 总页数2 总条数5 [User(id=1, name=Alen, password=tom, age=3, tel=18866668888), User(id=2, name=Jerry, password=jerry, age=4, tel=16688886666), User(id=3, name=Jock, password=123456, age=41, tel=18812345678)]

DQL编程控制

增删改查四个操作中,查询是非常重要的也是非常复杂的操作,这部分主要学习的内容有:

  • 条件查询方式
  • 查询投影
  • 查询条件设定
  • 字段映射与表名映射

条件查询

条件查询的类

  • MP将复杂的SQL查询语句都做了封装,使用编程的方式来完成查询条件的组合
  • 之前在写CRUD时,都看到了一个Wrapper类,当初都是赋一个null值,但其实这个类就是用来查询的

构建条件查询

  • QueryWrapper 小于用lt,大于用gt 回想之前在html页面中,如果需要用到小于号或者大于号,需要用对应的html实体来替换 小于号的实体是 <,大于号的实体是 >
1
2
3
4
5
6
7
8
@Test
void testQueryWrapper(){
    QueryWrapper<User> qw = new QueryWrapper<>();
    //条件为 age字段小于18
    qw.lt("age",18);
    List<User> userList = userDao.selectList(qw);
    System.out.println(userList);
}

运行测试方法,结果如下

[User(id=1, name=Alen, password=tom, age=3, tel=18866668888), User(id=2, name=Jerry, password=jerry, age=4, tel=16688886666), User(id=4, name=kyle, password=cyan, age=15, tel=4006184000)]

这种方法有个弊端,那就是字段名是字符串类型,没有提示信息和自动补全,如果写错了,那就查不出来

  • QueryWrapper的基础上,使用 lambda
1
2
3
4
5
6
7
8
@Test
void testQueryWrapper(){
    QueryWrapper<User> qw = new QueryWrapper<>();
    qw.lambda().lt(User::getAge,18);
    List<User> userList = userDao.selectList(qw);
    System.out.println(userList);
}
ser::getAget`,为lambda表达式中的,`类名::方法名
  • LambdaQueryWrapper 方式二解决了方式一的弊端,但是要多些一个lambda(),那方式三就来解决方式二的弊端,使用LambdaQueryWrapper,就可以不写lambda()
1
2
3
4
5
6
7
@Test
void testQueryWrapper(){
    LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
    lqw.lt(User::getAge,18);
    List<User> userList = userDao.selectList(lqw);
    System.out.println(userList);
}

多条件查询

上面三种都是单条件的查询,那现在想进行多条件的查询,该如何编写代码呢?

需求:查询表中年龄在10~30岁的用户信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
void testQueryWrapper(){
    LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
    //大于10
    lqw.gt(User::getAge,10);
    //小于30
    lqw.lt(User::getAge,30);
    List<User> userList = userDao.selectList(lqw);
    System.out.println(userList);
}

构建多条件的时候,还可以使用链式编程

1
2
3
4
5
6
7
@Test
void testQueryWrapper() {
    LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
    lqw.gt(User::getAge, 10).lt(User::getAge, 30);
    List<User> userList = userDao.selectList(lqw);
    System.out.println(userList);
}
  • 可能存在的疑问

    • MP怎么就知道你这俩条件是AND的关系呢,那我要是想用OR的关系,该咋整
  • 解答

    • 默认就是AND的关系,如果需要OR关系,用or()链接就可以了
    1
    
    lqw.gt(User::getAge, 10).or().lt(User::getAge, 30);
    

需求:查询年龄小于10,或者年龄大于30的用户信息

1
2
3
4
5
6
7
@Test
void testQueryWrapper() {
    LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
    lqw.lt(User::getAge, 10).or().gt(User::getAge, 30);
    List<User> userList = userDao.selectList(lqw);
    System.out.println(userList);
}

null值判定

  • 在做条件查询的时候,一般都会有很多条件供用户查询
  • 这些条件用户可以选择用,也可以选择不用
  • 之前是通过动态SQL来实现的
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<select id="selectByPageAndCondition" resultMap="brandResultMap">
    select *
    from tb_brand
    <where>
        <if test="brand.brandName != null and brand.brandName != '' ">
            and  brand_name like #{brand.brandName}
        </if>

        <if test="brand.companyName != null and brand.companyName != '' ">
            and  company_name like #{brand.companyName}
        </if>

        <if test="brand.status != null">
            and  status = #{brand.status}
        </if>
    </where>
    limit #{begin} , #{size}
</select>
  • 那现在试试在MP里怎么写

需求:查询数据库表中,根据输入年龄范围来查询符合条件的记录 用户在输入值的时候, 如果只输入第一个框,说明要查询大于该年龄的用户 如果只输入第二个框,说明要查询小于该年龄的用户 如果两个框都输入了,说明要查询年龄在两个范围之间的用户

  • 问题一:后台如果想接收前端的两个数据,该如何接收?

    • 可以使用两个简单数据类型,也可以使用一个模型类,但是User类中目前只有一个age属性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    @TableName("tb_user")
    @Data
    public class User {
        private Long id;
        private String name;
        private String password;
        private Integer age;
        private String tel;
    }
    
    • 使用一个age属性,如何去接收页面上的两个值呢?这个时候有两个解决方案

      • 方案一:添加属性age2,这种做法可以但是会影响到原模型类的属性内容
      • 方案二:新建一个模型类,让其继承User类,并在其中添加age2属性,UserQuery在拥有User属性后同时添加了age2属性。
      1
      2
      3
      4
      5
      
      @Data
      @TableName("tb_user")
      public class UserQuery extends User{
          private Integer age2;
      }
      
    • 环境准备好后,实现下刚才的需求:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    @Test
    void testQueryWrapper() {
        LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
        UserQuery uq = new UserQuery();
        uq.setAge(10);
        uq.setAge2(30);
    
        if (null != uq.getAge()) {
            lqw.gt(User::getAge, uq.getAge());
        }
    
        if (null != uq.getAge2()) {
            lqw.lt(User::getAge, uq.getAge2());
        }
    
        for (User user : userDao.selectList(lqw)) {
            System.out.println(user);
        }
    }
    
    • 上面的写法可以完成条件为非空的判断,但是问题很明显,如果条件多的话,每个条件都需要判断,代码量就比较大,来看MP给提供的简化方式
    • lt还有一个重载的方法,当condition为true时,添加条件,为false时,不添加条件
    1
    2
    3
    
    public Children lt(boolean condition, R column, Object val) {
        return this.addCondition(condition, column, SqlKeyword.LT, val);
    }
    
    • 故可以把if的判断操作,放到lt和gt方法中当做参数来写
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    @Test
    void testQueryWrapper() {
        LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
        UserQuery uq = new UserQuery();
        uq.setAge(10);
        uq.setAge2(30);
        lqw.gt(null != uq.getAge(), User::getAge, uq.getAge())
           .lt(null != uq.getAge2(), User::getAge, uq.getAge2());
        for (User user : userDao.selectList(lqw)) {
            System.out.println(user);
        }
    }
    

查询投影

查询指定字段

目前在查询数据的时候,什么都没有做默认就是查询表中所有字段的内容,所说的查询投影即不查询所有字段,只查询出指定内容的数据。

具体如何来实现?

1
2
3
4
5
6
7
8
@Test
void testQueryWrapper() {
    LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
    lqw.select(User::getName,User::getAge);
    for (User user : userDao.selectList(lqw)) {
        System.out.println(user);
    }
}

select(…)方法用来设置查询的字段列,可以设置多个

1
lqw.select(User::getName,User::getAge);

控制台输出如下

User(id=null, name=Alen, password=null, age=null, tel=null) User(id=null, name=Jerry, password=null, age=null, tel=null) User(id=null, name=Jock, password=null, age=null, tel=null) User(id=null, name=kyle, password=null, age=null, tel=null) User(id=null, name=Seto, password=null, age=null, tel=null)

如果使用的不是lambda,就需要手动指定字段

1
2
3
4
5
6
7
8
@Test
void testQueryWrapper() {
    QueryWrapper<User> qw = new QueryWrapper<>();
    qw.select("name", "age");
    for (User user : userDao.selectList(qw)) {
        System.out.println(user);
    }
}

聚合查询

需求:聚合函数查询,完成count、max、min、avg、sum的使用

  • count:总记录数
  • max:最大值
  • min:最小值
  • avg:平均值
  • sum:求和
  • count
  • max
  • min
  • avg
  • sum
1
2
3
4
5
6
7
8
@Test
void testQueryWrapper() {
    QueryWrapper<User> qw = new QueryWrapper<>();
    qw.select("count(*) as count");
    for (Map<String, Object> selectMap : userDao.selectMaps(qw)) {
        System.out.println(selectMap);
    }
}

控制台输出

{count=5}

分组查询

1
2
3
4
5
6
7
8
9
@Test
void testQueryWrapper() {
    QueryWrapper<User> qw = new QueryWrapper<>();
    qw.select("max(age) as maxAge");
    qw.groupBy("tel");
    for (Map<String, Object> selectMap : userDao.selectMaps(qw)) {
        System.out.println(selectMap);
    }
}

控制台输出如下

{maxAge=3} {maxAge=4} {maxAge=41} {maxAge=15} {maxAge=23}

注意:

  • 聚合与分组查询,无法使用lambda表达式来完成
  • MP只是对MyBatis的增强,如果MP实现不了,可以直接在DAO接口中使用MyBatis的方式实现

查询条件

前面只使用了lt()和gt(),除了这两个方法外,MP还封装了很多条件对应的方法

  • 范围匹配(> 、 = 、between)
  • 模糊匹配(like)
  • 空判定(null)
  • 包含性匹配(in)
  • 分组(group)
  • 排序(order)
  • ……

等值查询

需求:根据用户名和密码查询用户信息

1
2
3
4
5
6
7
@Test
void testQueryWrapper() {
    LambdaQueryWrapper<User> qw = new LambdaQueryWrapper<>();
    qw.eq(User::getName,"Seto").eq(User::getPassword,"MUSICIAN");
    User user = userDao.selectOne(qw);
    System.out.println(user);
}

控制台输出如下

User(id=1572385590169579521, name=Seto, password=MUSICIAN, age=23, tel=4005129421)

  • eq(): 相当于 =,对应的sql语句为
1
SELECT * FROM tb_user WHERE name = 'seto' AND password = 'MUSICIAN';
  • selectList:查询结果为多个或者单个
  • selectOne:查询结果为单个

范围查询

需求:对年龄进行范围查询,使用lt()、le()、gt()、ge()、between()进行范围查询

1
2
3
4
5
6
7
8
9
@Test
void testQueryWrapper() {
    LambdaQueryWrapper<User> qw = new LambdaQueryWrapper<>();
    qw.between(User::getAge,10,30);
    List<User> users = userDao.selectList(qw);
    for (User u : users) {
        System.out.println(u);
    }
}

控制台输出如下

User(id=4, name=kyle, password=cyan, age=15, tel=4006184000) User(id=1572385590169579521, name=Seto, password=MUSICIAN, age=23, tel=4005129421)

  • gt():大于(>)
  • ge():大于等于(>=)
  • lt():小于(<)
  • lte():小于等于(<=)
  • between():between ? and ?

模糊查询

需求:查询表中name属性的值以 J开头的用户信息,使用like进行模糊查询

1
2
3
4
5
6
7
8
9
@Test
void testQueryWrapper() {
    LambdaQueryWrapper<User> qw = new LambdaQueryWrapper<>();
    qw.likeRight(User::getName,"J");
    List<User> users = userDao.selectList(qw);
    for (User u : users) {
        System.out.println(u);
    }
}

控制台输出如下

User(id=2, name=Jerry, password=jerry, age=4, tel=16688886666) User(id=3, name=Jock, password=123456, age=41, tel=18812345678)

  • like():前后加百分号,如 %J%,相当于包含J的name
  • likeLeft():前面加百分号,如 %J,相当于J结尾的name
  • likeRight():后面加百分号,如 J%,相当于J开头的name

需求:查询表中name属性的值包含 e的用户信息,使用like进行模糊查询

1
2
3
4
5
6
7
8
9
@Test
void testQueryWrapper() {
    LambdaQueryWrapper<User> qw = new LambdaQueryWrapper<>();
    qw.like(User::getName,"e");
    List<User> users = userDao.selectList(qw);
    for (User u : users) {
        System.out.println(u);
    }
}

控制台输出如下

User(id=1, name=Alen, password=tom, age=3, tel=18866668888) User(id=2, name=Jerry, password=jerry, age=4, tel=16688886666) User(id=4, name=kyle, password=cyan, age=15, tel=4006184000) User(id=1572385590169579521, name=Seto, password=MUSICIAN, age=23, tel=4005129421)

排序查询

需求:查询所有数据,然后按照age降序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Test
void testQueryWrapper() {
    LambdaQueryWrapper<User> qw = new LambdaQueryWrapper<>();
    /**
        * condition :条件,返回boolean,
            当condition为true,进行排序,如果为false,则不排序
        * isAsc:是否为升序,true为升序,false为降序
        * columns:需要操作的列
    */
    qw.orderBy(true,false,User::getAge);
    List<User> users = userDao.selectList(qw);
    for (User u : users) {
        System.out.println(u);
    }
}

控制台输出如下

User(id=3, name=Jock, password=123456, age=41, tel=18812345678) User(id=1572385590169579521, name=Seto, password=MUSICIAN, age=23, tel=4005129421) User(id=4, name=kyle, password=cyan, age=15, tel=4006184000) User(id=2, name=Jerry, password=jerry, age=4, tel=16688886666) User(id=1, name=Alen, password=tom, age=3, tel=18866668888)

遇到想用的功能,先自己用一个试试,方法名和形参名都很见名知意,遇到不确定的用法,再去官方文档查阅资料

映射匹配兼容性

在上面的案例中,做查询的时候,数据表中的字段名与模型类中的属性名一致,查询的时候没有问题,那么问题就来了

  • 问题一:

    表字段与模型类编码属性不一致

    • 当表的列名和模型类的属性名发生不一致,就会导致数据封装不到模型对象,这个时候就需要其中一方做出修改,那如果前提是两边都不能改又该如何解决?
    • MP给提供了一个注解 @TableField,使用该注解可以实现模型类属性名和表的列名之间的映射关系
    • 例如表中密码字段为 pwd,而模型类属性名为 password,那就可以用 @TableField注解来实现他们之间的映射关系
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    @TableName("tb_user")
    @Data
    public class User {
        private Long id;
        private String name;
        @TableField("pwd")
        private String password;
        private Integer age;
        private String tel;
    }
    

    MyBatis 并没有提供类似 MyBatis-Plus 中的 @TableField 注解。对于字段映射,MyBatis 通常通过 XML 映射文件或注解来定义。

    例如,使用 XML 映射文件:

    1
    2
    3
    4
    
    <resultMap id="UserResultMap" type="User">
        <result property="id" column="user_id"/>
        <result property="password" column="pwd"/>
    </resultMap>
    

    或者使用注解:

    1
    2
    3
    4
    5
    6
    
    @Select("SELECT user_id, pwd FROM user WHERE user_id = #{id}")
    @Results({
        @Result(property = "id", column = "user_id"),
        @Result(property = "password", column = "pwd")
    })
    User selectUserById(int id);
    

  • 问题二:

    编码中添加了数据库中未定义的属性

    • 当模型类中多了一个数据库表不存在的字段,就会导致生成的sql语句中在select的时候查询了数据库不存在的字段,程序运行就会报错,错误信息为:Unknown column '多出来的字段名称' in 'field list'
    • 具体的解决方案用到的还是 @TableField注解,它有一个属性叫 exist,设置该字段是否在数据库表中存在,如果设置为false则不存在,生成sql语句查询的时候,就不会再查询该字段了。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    @TableName("tb_user")
    @Data
    public class User {
        private Long id;
        private String name;
        @TableField("pwd")
        private String password;
        private Integer age;
        private String tel;
        @TableField(exist = false)
        private Integer online;
    }
    
  • 问题三:

    采用默认查询开放了更多的字段查看权限

    • 查询表中所有的列的数据,就可能把一些敏感数据查询到返回给前端,这个时候就需要限制哪些字段默认不要进行查询。解决方案是 @TableField注解的一个属性叫 select,该属性设置默认是否需要查询该字段的值,true(默认值)表示默认查询该字段,false表示默认不查询该字段。
    • 例如像密码这种的敏感字段,不应该查询出来作为JSON返回给前端,不安全
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    @TableName("tb_user")
    @Data
    public class User {
        private Long id;
        private String name;
        @TableField(value = "pwd",select = false)
        private String password;
        private Integer age;
        private String tel;
        @TableField(exist = false)
        private Integer online;
    }
    

知识点:@TableField

名称 @TableField
类型 属性注解
位置 模型类属性定义上方
作用 设置当前属性对应的数据库表中的字段关系
相关属性 value(默认):设置数据库表字段名称 exist:设置属性在数据库表字段中是否存在,默认为true,此属性不能与value合并使用 select:设置属性是否参与查询,此属性与select()映射配置不冲突
  • 问题四:

    表名与编码开发设计不同步

    • 这个问题其实在一开始就解决过了,现在再来回顾一遍
    • 该问题主要是表的名称和模型类的名称不一致,导致查询失败,这个时候通常会报如下错误信息 Table 'databaseName.tableNaem' doesn't exist
    • 解决方案是使用MP提供的另外一个注解 @TableName来设置表与模型类之间的对应关系。

知识点:@TableName

名称 @TableName
类型 类注解
位置 模型类定义上方
作用 设置当前类对应于数据库表关系
相关属性 value(默认):设置数据库表名称

DML编程控制

查询相关的操作已经介绍完了,紧接着需要对另外三个,增删改进行内容的讲解。挨个来说明下,首先是新增(insert)中的内容。

id生成策略控制

前面在新增数据的时候,主键ID是一个很长的Long类型,现在想要主键按照数据表字段进行自增长,在解决这个问题之前,先来分析一下ID的生成策略

  • 不同的表,应用不同的id生成策略
    • 日志:自增(1 2 3 4)
    • 购物订单:特殊规则(线下购物发票,下次可以留意一下)
    • 外卖订单:关联地区日期等信息(这个我熟,举个例子10 04 20220921 13 14,例如10表示北京市,04表示朝阳区,20220921表示日期等)
    • 关系表:可以省略ID
    • ……
  • 不同的业务采用的ID生成方式应该是不一样的,那么在MP中都提供了哪些主键生成策略,以及该如何进行选择?
    • 在这里又需要用到MP的一个注解叫 @TableId

知识点:@TableId

名称 @TableId
类型 属性注解
位置 模型类中用于表示主键的属性定义上方
作用 设置当前类中主键属性的生成策略
相关属性 value(默认):设置数据库表主键名称 type:设置主键属性的生成策略,值查照IdType的枚举值

AUTO策略

  • 步骤一:设置生成策略为AUTO
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@TableName("tb_user")
@Data
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private String password;
    private Integer age;
    private String tel;
    @TableField(exist = false)
    private Integer online;
}
  • 步骤二:设置自动增量为5,将4之后的数据都删掉,防止影响的结果
  • 步骤三:运行新增方法
1
2
3
4
5
6
7
8
9
@Test
void testInsert(){
    User user = new User();
    user.setName("Helsing");
    user.setAge(531);
    user.setPassword("HELL_SING");
    user.setTel("4006669999");
    userDao.insert(user);
}

会发现,新增成功,并且主键id也是从5开始

进入源码来看看还有什么生成策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public enum IdType {
    AUTO(0),
    NONE(1),
    INPUT(2),
    ASSIGN_ID(3),
    ASSIGN_UUID(4),
    /** @deprecated */
    @Deprecated
    ID_WORKER(3),
    /** @deprecated */
    @Deprecated
    ID_WORKER_STR(3),
    /** @deprecated */
    @Deprecated
    UUID(4);

    private final int key;

    private IdType(int key) {
        this.key = key;
    }

    public int getKey() {
        return this.key;
    }
}
  • NONE: 不设置id生成策略
  • INPUT:用户手工输入id
  • ASSIGN_ID:雪花算法生成id(可兼容数值型与字符串型)
  • ASSIGN_UUID:以UUID生成算法作为id生成策略
  • 其他的几个策略均已过时,都将被ASSIGN_ID和ASSIGN_UUID代替掉。

拓展: 分布式ID是什么?

  • 当数据量足够大的时候,一台数据库服务器存储不下,这个时候就需要多台数据库服务器进行存储
  • 比如订单表就有可能被存储在不同的服务器上
  • 如果用数据库表的自增主键,因为在两台服务器上所以会出现冲突
  • 这个时候就需要一个全局唯一ID,这个ID就是分布式ID。

INPUT策略

  • 步骤一:将ID生成策略改为INPUT
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@TableName("tb_user")
@Data
public class User {
    @TableId(type = IdType.INPUT)
    private Long id;
    private String name;
    private String password;
    private Integer age;
    private String tel;
    @TableField(exist = false)
    private Integer online;
}
  • 步骤二:运行新增方法 注意这里需要手动设置ID了
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
void testInsert(){
    User user = new User();
    user.setId(6L);
    user.setName("Helsing");
    user.setAge(531);
    user.setPassword("HELL_SING");
    user.setTel("4006669999");
    userDao.insert(user);
}

查看数据库,ID确实是设置的值

ASSIGN_ID策略

  • 步骤一:设置生成策略为 ASSIGN_ID
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@TableName("tb_user")
@Data
public class User {
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;
    private String name;
    private String password;
    private Integer age;
    private String tel;
    @TableField(exist = false)
    private Integer online;
}
  • 步骤二:运行新增方法 这里就不要手动设置ID了
1
2
3
4
5
6
7
8
9
@Test
void testInsert(){
    User user = new User();
    user.setName("Helsing");
    user.setAge(531);
    user.setPassword("HELL_SING");
    user.setTel("4006669999");
    userDao.insert(user);
}

查看结果,生成的ID就是一个Long类型的数据,生成ID时,使用的是雪花算法 雪花算法(SnowFlake),是Twitter官方给出的算法实现 是用Scala写的。其生成的结果是一个64bit大小整数 ./Spring.assets/f836877aj00rjkmyw000gd000n0006tp.jpg

  1. 1bit,不用,因为二进制中最高位是符号位,1表示负数,0表示正数。生成的id一般都是用整数,所以最高位固定为0。
  2. 41bit-时间戳,用来记录时间戳,毫秒级
  3. 10bit-工作机器id,用来记录工作机器id,其中高位5bit是数据中心ID其取值范围0-31,低位5bit是工作节点ID其取值范围0-31,两个组合起来最多可以容纳1024个节点
  4. 序列号占用12bit,每个节点每毫秒0开始不断累加,最多可以累加到4095,一共可以产生4096个ID

ASSIGN_UUID策略

  • 步骤一:设置生成策略为ASSIGN_UUID
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@TableName("tb_user")
@Data
public class User {
    @TableId(type = IdType.ASSIGN_UUID)
    private String id;
    private String name;
    private String password;
    private Integer age;
    private String tel;
    @TableField(exist = false)
    private Integer online;
}
  • 步骤二:修改表的主键类型 主键类型设置为varchar,长度要大于32,因为UUID生成的主键为32位,如果长度小的话就会导致插入失败。
  • 步骤三:运行新增方法
1
2
3
4
5
6
7
8
9
@Test
void testInsert(){
    User user = new User();
    user.setName("Helsing");
    user.setAge(531);
    user.setPassword("HELL_SING");
    user.setTel("4006669999");
    userDao.insert(user);
}

ID生成策略对比

介绍完了这些主键ID的生成策略,那么以后开发用哪个呢?

  • NONE:不设置ID生成策略,MP不自动生成,约定于INPUT,所以这两种方式都需要用户手动设置(SET方法),但是手动设置的第一个问题就是容易出错,加了相同的ID造成主键冲突,为了保证主键不冲突就得做很多判定,实现起来较为复杂
  • AUTO:数据库ID自增,这种策略适合在数据库服务器只有一台的情况下使用,不可作为分布式ID使用
  • ASSIGN_UUID:可以在分布式的情况下使用,而且能够保证ID唯一,但是生成的主键是32位的字符串,长度过长占用空间,而且不能排序,查询性能也慢
  • ASSIGN_ID:可以在分布式的情况下使用,生成的是Long类型的数字,可以排序,性能也高,但是生成的策略与服务器时间有关,如果修改了系统时间,也有可能出现重复的主键
  • 综上所述,每一种主键的策略都有自己的优缺点,根据自己的项目业务需求的实际情况来使用,才是最明智的选择

简化配置

  • 模型类主键策略设置 如果要在项目中的每一个模型类上都需要使用相同的生成策略,比如你有Book表,User表,Student表,Score表等好多个表,如果你每一个表的主键生成策略都是ASSIGN_ID,那就可以用yml配置文件来简化开发,不用在每一个表的id上都加上 @TableId(type = IdType.ASSIGN_ID)
1
2
3
4
mybatis-plus:
  global-config:
    db-config:
      id-type: assign_id
  • 数据库表与模型类的映射关系

    MP会默认将模型类的类名名首字母小写作为表名使用,假如数据库表的名称都以 tb_开头,那么就需要将所有的模型类上添加 @TableName("tb_TABLENAME"),这样做很繁琐,有没有更简单的方式呢?

    • 可以在配置文件中设置表的前缀
    1
    2
    3
    4
    5
    
    mybatis-plus:
    global-config:
        db-config:
        id-type: assign_id
        table-prefix: tb_
    

    设置表的前缀内容,这样MP就会拿 tb_加上模型类的首字母小写,就刚好组装成数据库的表名(前提是你的表名得规范命名,别瞎起花里胡哨的名)。将User类的 @TableName注解去掉,再次运行新增方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    @Data
    public class User {
        @TableId(type = IdType.ASSIGN_ID)
        private Long id;
        private String name;
        private String password;
        private Integer age;
        private String tel;
    }
    

多记录操作

这部分其实没有新内容,MP已经提供好了针对多记录的删除和查询,自己多看看API就好了

需求:根据传入的id集合将数据库表中的数据删除掉。

1
2
3
4
5
6
7
8
@Test
void testDeleteByIds(){
    ArrayList<Long> list = new ArrayList<>();
    list.add(1572543345085964289L);
    list.add(1572554951983460354L);
    list.add(1572555035978534913L);
    userDao.deleteBatchIds(list);
}

执行成功后,数据库表中的数据就会按照指定的id进行删除。上面三个数据是我之前新增插入的,可以随便换成数据库中有的id

需求:根据传入的ID集合查询用户信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
void testSelectByIds() {
    ArrayList<Long> list = new ArrayList<>();
    list.add(1L);
    list.add(2L);
    list.add(3L);
    for (User user : userDao.selectBatchIds(list)) {
        System.out.println(user);
    }
}

控制台输出如下

User(id=1, name=Alen, password=tom, age=3, tel=18866668888) User(id=2, name=Jerry, password=jerry, age=4, tel=16688886666) User(id=3, name=Jock, password=123456, age=41, tel=18812345678)

逻辑删除

逻辑删除是删除操作中比较重要的一部分,先来讲个案例 ./Spring.assets/632b01a716f2c2beb12b5ef9.jpg

  • 这是一个员工和其所办理的合同表,一个员工可以办理多张合同表
  • 员工ID为1的张业绩,办理了三个合同,但是她现在想离职跳槽了,需要将员工表中的数据进行删除,执行DELETE操作
  • 如果表在设计的时候有主外键关系,那么同时也要将合同表中的张业绩的数据删掉 ./Spring.assets/dbe0c60cj00rjkmyx000xd000qt0056p.jpg
  • 后来公司要统计今年的总业绩,发现这数据咋对不上呢,业绩这么少,原因是张业绩办理的合同信息被删掉了
  • 如果只删除员工,却不删除员工对应的合同表数据,那么合同的员工编号对应的员工信息不存在,那么就会产生垃圾数据,出现无主合同,根本不知道有张业绩这个人的存在
  • 经过的分析之后,不应该将表中的数据删除掉,得留着,但是又得把离职的人和在职的人区分开,这样就解决了上述问题 ./Spring.assets/632b025316f2c2beb12c2d45.jpg
  • 区分的方式,就是在员工表中添加一列数据 deleted,如果为0说明在职员工,如果离职则将其改完1,(0和1所代表的含义是可以自定义的)

所以对于删除操作业务问题来说有:

  • 物理删除:业务数据从数据库中丢弃,执行的是delete操作
  • 逻辑删除:为数据设置是否可用状态字段,删除时设置状态字段为不可用状态,数据保留在数据库中,执行的是update操作

MP中逻辑删除具体该如何实现?

  • 步骤一:修改数据库表,添加 deleted列 字段名任意,类型int,长度1,默认值0(个人习惯,你随便)
  • 步骤二:实体类添加属性 还得修改对应的pojo类,增加delete属性(属性名也任意,对不上用 @TableField来添加映射关系 标识新增的字段为逻辑删除字段,使用 @TableLogic
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//表名前缀和id生成策略在yml配置文件写了
@Data
public class User {
    private Long id;
    private String name;
    private String password;
    private Integer age;
    private String tel;
    //新增delete属性
    //value为正常数据的值(在职),delval为删除数据的值(离职)
    @TableLogic(value = "0",delval = "1")
    private Integer deleted;
}
  • 步骤三:运行删除方法 没有就自己写一个呗
1
2
3
4
@Test
void testLogicDelete(){
    userDao.deleteById(1);
}

从测试结果来看,逻辑删除最后走的是update操作,执行的是 UPDATE tb_user SET deleted=1 WHERE id=? AND deleted=0,会将指定的字段修改成删除状态对应的值。

  • 思考:逻辑删除,对查询有没有影响呢?

    • 执行查询操作
    1
    2
    3
    4
    5
    6
    
    @Test
    void testSelectAll() {
        for (User user : userDao.selectList(null)) {
            System.out.println(user);
        }
    }
    

    从日志中可以看到执行的SQL语句如下,WHERE条件中,规定只查询deleted字段为0的数据

    1
    
    SELECT id,name,password,age,tel,deleted FROM tb_user WHERE deleted=0
    

    输出结果当然也没有ID为1的数据了

    User(id=2, name=Jerry, password=jerry, age=4, tel=16688886666, deleted=0) User(id=3, name=Jock, password=123456, age=41, tel=18812345678, deleted=0) User(id=4, name=kyle, password=cyan, age=15, tel=4006184000, deleted=0) User(id=6, name=Helsing, password=HELL_SING, age=531, tel=4006669999, deleted=0)

    • 如果还是想把已经删除的数据都查询出来该如何实现呢?
    1
    2
    3
    4
    5
    6
    
    @Mapper
    public interface UserDao extends BaseMapper<User> {
        //查询所有数据包含已经被删除的数据
        @Select("select * from tb_user")
        public List<User> selectAll();
    }
    
    • 如果每个表都要有逻辑删除,那么就需要在每个模型类的属性上添加 @TableLogic注解,如何优化?

      • 在配置文件中添加全局配置,如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      mybatis-plus:
          global-config:
          db-config:
              ## 逻辑删除字段名
              logic-delete-field: deleted
              ## 逻辑删除字面值:未删除为0
              logic-not-delete-value: 0
              ## 逻辑删除字面值:删除为1
              logic-delete-value: 1
      

      使用yml配置文件配置了之后,就不需要在模型类上用 @TableLogic注解了

介绍完逻辑删除,逻辑删除的本质为修改操作。如果加了逻辑删除字段,查询数据时也会自动带上逻辑删除字段。 执行的SQL语句为:

1
UPDATE tb_user SET deleted=1 WHERE id=? AND deleted=0

知识点:@TableLogic

名称 @TableLogic
类型 属性注解
位置 模型类中用于表示删除字段的属性定义上方
作用 标识该字段为进行逻辑删除的字段
相关属性 value:逻辑未删除值 delval:逻辑删除值

乐观锁

概念

在学乐观锁之前,还是先由一个案例来引入

  • 业务并发现象带来的问题:秒杀
    • 加入有100个商品在售,为了保证每个商品只能被一个人购买,如何保证不会超买或者重复卖
    • 对于这一类的问题,其实有很多的解决方案可以使用
    • 第一个最先想到的就是锁,锁在一台服务器中是可以解决的,但是如果在多台服务器下就没办法控制,比如12306有两台服务器在进行卖票,在两台服务器上都添加锁的话,那也有可能会在同一时刻有两个线程在卖票,还是会出现并发问题
    • 接下来介绍的这种方式就是针对于小型企业的解决方案,因为数据库本身的性能就是个瓶颈,如果对其并发超过2000以上的就需要考虑其他解决方案了

简单来说,乐观锁主要解决的问题是,当要更新一条记录的时候,希望这条记录没有被别人更新

实现思路

  • 数据库表中添加 version字段,比如默认值给个1
  • 第一个线程要修改数据之前,取出记录时,获取当前的version=1
  • 第二个线程要修改数据之前,取出记录时,获取当前的version=1
  • 第一个线程执行更新时
    • set version = newVersion where version = oldVersion
      • newVersion = version + 1
      • oldVersion = version
  • 第二个线程执行更新时
    • set version = newVersion where version = oldVersion
      • newVersion = version + 1
      • oldVersion = version
  • 假如这两个线程都来更新数据,第一个和第二个线程都可能先执行
    • 假如第一个线程先执行更新,会将version改为2
      • 那么第二个线程再更新的时候,set version = 2 where version = 1,此时数据库表的version已经是2了,所以第二个线程修改失败
    • 假如第二个线程先执行更新,会将version改为2
      • 那么第一个线程再更新的时候,set version = 2 where version = 1,此时数据库表的version已经是2了,所以第一个线程修改失败

实现步骤

  • 步骤一:数据库表添加列 加一列version,长度给个11,默认值设为1
  • 步骤二:在模型类中添加对应的属性
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Data
public class User {
    private Long id;
    private String name;
    private String password;
    private Integer age;
    private String tel;
    @TableLogic(value = "0", delval = "1")
    private Integer deleted;
    @Version
    private Integer version;
}
  • 步骤三:添加乐观锁拦截器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Configuration
public class MpConfig {
    @Bean
    public MybatisPlusInterceptor mpInterceptor() {
        //1.定义Mp拦截器
        MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
        //2.添加乐观锁拦截器
        mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return mpInterceptor;
    }
}
  • 步骤四:执行更新操作
1
2
3
4
5
6
7
8
@Test
void testUpdate(){
    //1. 先通过要修改的数据id将当前数据查询出来
    User user = userDao.selectById(1L);
    //2. 修改属性
    user.setName("Person");
    userDao.updateById(user);
}

查看日志的SQL语句

==> Preparing: UPDATE tb_user SET name=?, password=?, age=?, tel=?, version=? WHERE id=? AND version=? ==> Parameters: Person(String), tom(String), 3(Integer), 18866668888(String), 2(Integer), 1(Long), 1(Integer)

传递的是1(oldVersion),MP会将1进行加1,变成2,然后更新回到数据库中(newVersion)

大概分析完乐观锁的实现步骤以后,模拟一种加锁的情况,看看能不能实现多个人修改同一个数据的时候,只能有一个人修改成功。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
void testUpdate() {
    User userA = userDao.selectById(1L); //version=1
    User userB = userDao.selectById(1L); //version=1
    userB.setName("Jackson");
    userDao.updateById(userB);  //B修改完了之后,version=2
    userA.setName("Person");
    //A拿到的version是1,但现在的version已经是2了,那么A在执行 UPDATE ... WHERE version = 1时,就必然会失败
    userDao.updateById(userA);  
}

至此,乐观锁的实现就已经完成了

快速开发

代码生成器原理分析

官方文档地址:https://baomidou.com/pages/981406/

通过观察之前写的代码,会发现其中有很多重复的内容,于是MP抽取了这些重复的地方,做成了一个模板供使用 要想完成代码自动生成,需要有以下内容:

  • 模板: MyBatisPlus提供,可以自己提供,但是麻烦,不建议
  • 数据库相关配置:读取数据库获取表和字段信息
  • 开发者自定义配置:手工配置,比如ID生成策略

代码生成器实现

  • 步骤一:创建一个Maven项目
  • 步骤二:导入对应的jar包
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.1</version>
    </parent>
    <groupId>com.blog</groupId>
    <artifactId>mybatisplus_04_generator</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!--spring webmvc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--mybatisplus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.1</version>
        </dependency>

        <!--druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.16</version>
        </dependency>

        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>

        <!--代码生成器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.4.1</version>
        </dependency>

        <!--velocity模板引擎-->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.3</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • 步骤三:编写引导类
1
2
3
4
5
6
@SpringBootApplication
public class Mybatisplus04GeneratorApplication {
    public static void main(String[] args) {
        SpringApplication.run(Mybatisplus04GeneratorApplication.class, args);
    }
}
  • 步骤四:创建代码生成类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class CodeGenerator {
    public static void main(String[] args) {
        //1.获取代码生成器的对象
        AutoGenerator autoGenerator = new AutoGenerator();

        //设置数据库相关配置
        DataSourceConfig dataSource = new DataSourceConfig();
        dataSource.setDriverName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=UTC");
        dataSource.setUsername("root");
        dataSource.setPassword("YOURPASSWORD");
        autoGenerator.setDataSource(dataSource);

        //设置全局配置
        GlobalConfig globalConfig = new GlobalConfig();
        globalConfig.setOutputDir(System.getProperty("user.dir")+"/项目名/src/main/java");    //设置代码生成位置
        globalConfig.setOpen(false);    //设置生成完毕后是否打开生成代码所在的目录
        globalConfig.setAuthor("Kyle");    //设置作者
        globalConfig.setFileOverride(true);     //设置是否覆盖原始生成的文件
        globalConfig.setMapperName("%sDao");    //设置数据层接口名,%s为占位符,指代模块名称
        globalConfig.setIdType(IdType.ASSIGN_ID);   //设置Id生成策略
        autoGenerator.setGlobalConfig(globalConfig);

        //设置包名相关配置
        PackageConfig packageInfo = new PackageConfig();
        packageInfo.setParent("com.aaa");   //设置生成的包名,与代码所在位置不冲突,二者叠加组成完整路径
        packageInfo.setEntity("domain");    //设置实体类包名
        packageInfo.setMapper("dao");   //设置数据层包名
        autoGenerator.setPackageInfo(packageInfo);

        //策略设置
        StrategyConfig strategyConfig = new StrategyConfig();
        strategyConfig.setInclude("tb_user");  //设置当前参与生成的表名,参数为可变参数
        strategyConfig.setTablePrefix("tb_");  //设置数据库表的前缀名称,模块名 = 数据库表名 - 前缀名  例如: User = tb_user - tb_
        strategyConfig.setRestControllerStyle(true);    //设置是否启用Rest风格
        strategyConfig.setVersionFieldName("version");  //设置乐观锁字段名
        strategyConfig.setLogicDeleteFieldName("deleted");  //设置逻辑删除字段名
        strategyConfig.setEntityLombokModel(true);  //设置是否启用lombok
        autoGenerator.setStrategy(strategyConfig);
        //2.执行生成操作
        autoGenerator.execute();
    }
}

对于代码生成器中的代码内容,可以直接从官方文档中获取代码进行修改,https://baomidou.com/pages/981406/

  • 步骤五:运行程序

运行成功后,会在当前项目中生成很多代码,代码包含 controller,servicemapperentity

至此代码生成器就已经完成工作,能快速根据数据库表来创建对应的类,简化的代码开发。

初期还是不建议直接使用代码生成器,还是多自己手写几遍比较好

MP中Service的CRUD

回顾之前业务层代码的编写,编写接口和对应的实现类:

1
2
3
4
5
6
7
8
public interface UserService{

}

@Service
public class UserServiceImpl implements UserService{

}

接口和实现类有了以后,需要在接口和实现类中声明方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public interface UserService{
	public List<User> findAll();
}

@Service
public class UserServiceImpl implements UserService{
    @Autowired
    private UserDao userDao;
  
	public List<User> findAll(){
        return userDao.selectList(null);
    }
}

MP看到上面的代码以后就说这些方法也是比较固定和通用的,那我来帮你抽取下,所以MP提供了一个Service接口和实现类,分别是:IServiceServiceImpl,后者是对前者的一个具体实现。

以后自己写的Service就可以进行如下修改:

1
2
3
4
5
6
7
8
public interface UserService extends IService<User>{

}

@Service
public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserService{

}

修改以后的好处是,MP已经帮把业务层的一些基础的增删改查都已经实现了,可以直接进行使用。

编写测试类进行测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@SpringBootTest
class Mybatisplus04GeneratorApplicationTests {

    private IUserService userService;

    @Test
    void testFindAll() {
        List<User> list = userService.list();
        System.out.println(list);
    }
}