> 文档中心 > SSM三大框架:Spring,SpringMVC,Mybatis

SSM三大框架:Spring,SpringMVC,Mybatis


一、Spring

1. 关于Spring

Spring框架主要解决了创建对象、管理对象的问题。

在传统的开发中,当需要某个对象时,使用new关键字及类型的构造方法即可创建对象,例如:

Random random = new Random();

如果以上代码存在于某个方法中。则random就只是个局部变量,当方法运行结束,此变量就会被销毁!

在实际项目开发,许多对象被创建出来之后,应该长期存在于内存中,而不应该销毁,当需要使用这些对象时,通过某种方式获取对象即可,而不应该重新创建对象!

除了对象存在的时间(时长)以外,在实际项目开发中,还需要关注各个类型之间的依赖关系!例如:

public class UserMapper { public void insert() { // 向数据表中的“用户表”中插入数据    }    }
public class UserController { public UserMapper userMapper; public void reg() { userMapper.insert();    }    }

在以上示例代码中,可视为“UserController是依赖于UserMapper的”,也可以把UserMapper称之为“UserController的依赖项”。

当需要创建以上类型时,如果只是单纯的把UserController创建出来了,却没有关注其内部userMapper属性的值,甚至该属性没有值,则是错误的做法!

如果要使得UserController中的UserMapper属性是有值的,也非常简单,例如:

public class UserController { public UserMapper userMapper = new UserMapper(); public void reg() { userMapper.insert();    }    }

但是,在实际项目中,除了UserController以外,还会有其它的组件也可能需要使用到UserMapper,如果每个组件中都使用new UserMapper()的语法来创建对象,就不能保证UserMapper对象的唯一性,就违背了设计初衷。

为了更好的创建对象和管理对象,应该使用Spring框架!

2. 创建基于Spring的工程

当某个项目需要使用Spring框架时,推荐使用Maven工程。

创建名为spring01的Maven工程,创建完成后,会自动打开pom.xml文件,首先,应该在此文件中添加节点,此节点是用于添加依赖项的:

<dependencies></dependencies>

然后,Spring框架的依赖项的代码需要编写在以上节点的子级,而依赖项的代码推荐从 https://mvnrepository.com/ 网站查询得到,Spring的依赖项名称是spring-context,则在此网站搜索该名称,以找到依赖项的代码,代码示例:

<dependency>    <groupId>org.springframework</groupId>    <artifactId>spring-context</artifactId>    <version>5.3.14</version></dependency>

将以上依赖项的代码复制到节点之下即可。

然后,点击IDEA中悬浮的刷新Maven的图标,或展开右侧的Maven面板点击刷新按钮,即可自动下载所需要的jar包文件(这些文件会被下载到本机的Maven仓库中,同一个依赖项的同一个版本只会下载一次)。

3. 通过Spring创建对象–通过@Bean方法

演示案例:spring01

操作步骤:

  • 创建cn.tedu.spring.SpringBeanFactory
  • 在类中添加方法,方法的返回值类型就是你希望Spring创建并管理的对象的类型,并在此方法中自行编写返回有效对象的代码
  • 在此类上添加@Configuration注解
  • 在此方法上添加@Bean注解

以上步骤的示例代码:

package cn.tedu.spring;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import java.util.Random;@Configurationpublic class SpringBeanFactory {    @Bean    public Random random() { return new Random();    }}

接下来,创建某个类用于执行:

public class SpringRunner {    public static void main(String[] args) { // 1. 加载Spring AnnotationConfigApplicationContext ac  = new AnnotationConfigApplicationContext(SpringBeanFactory.class); // 2. 从Spring中获取对象 Random random = (Random) ac.getBean("random"); // 3. 测试使用对象,以便于观察是否获取到了有效的对象 System.out.println("random > " + random); System.out.println("random.nextInt() > " + random.nextInt()); // 4. 关闭 ac.close();    }}

接下来,运行SpringRunner类的main()方法即可看到执行效果。

关于以上代码:

  • AnnotationConfigApplicationContext的构造方法中,应该将SpringBeanFactory.class作为参数传入,否则就不会加载SpringBeanFactory类中内容
    • 其实,在以上案例中,SpringBeanFactory类上的@Configuration注解并不是必须的
  • getBean()时,传入的字符串参数"random"SpringBeanFactory类中的方法的名称
  • SpringBeanFactory类中的方法必须添加@Bean注解,其作用是使得Spring框架自动调用此方法,并管理此方法返回的结果
  • 关于getBean()方法,此方法被重载了多次,典型的有:
    • Object getBean(String beanName)
      • 通过此方法,传入的beanName必须是有效的,否则将导致NoSuchBeanDefinitionException
    • T getBean(Class beanClass);
      • 使用此方法时,传入的类型在Spring中必须有且仅有1个对象,如果没有匹配类型的对象,将导致NoSuchBeanDefinitionException,如果有2个,将导致NoUniqueBeanDefinitionException
    • T getBean(String beanName, Class beanClass)
      • 此方法仍是根据传入的beanName获取对象,并且根据传入的beanClass进行类型转换
  • 使用的@Bean注解可以传入String类型的参数,如果传入,则此注解对应的方法的返回结果的beanName就是@Bean注解中传入的String参数值

4. 通过Spring创建对象–组件扫描

演示案例:spring02

先使用同样的步骤创建spring02工程。

操作步骤:

  • pom.xml中添加spring-context的依赖项

  • 自行创建某个类,例如创建cn.tedu.spring.UserMapper类,并在类的声明之前添加@Component注解

  • 与前次案例相似,创建可执行的类,与前次案例的区别在于:

    • AnnotationConfigApplicationContext的构造方法中传入的是UserMapper类的包名,即:

      AnnotationConfigApplicationContext ac    = new AnnotationConfigApplicationContext("cn.tedu.spring");
    • 调用getBean()时,传入的名称是将UserMapper类的名称的首字母改为小写,即:

      UserMapper userMapper = ac.getBean("userMapper", UserMapper.class);

关于以上代码:

  • 在创建AnnotationConfigApplicationContext时传入的参数是一个basePackages,即多个“根包”,它会使得Spring框架扫描这个包及其子孙包中的所有类,并尝试创建这些包中的组件的对象
    • AnnotationConfigApplicationContext的构造方法设计的是String...类型的参数,即可变参数,当需要输入多个包名时,各包名使用逗号隔开即可
    • 推荐传入的包名是更加具体的,但不需要特别精准,只需要保证不会扫描到非自定义的包即可,例如包名肯定不会包含项目的依赖项的包
  • 即使有了组件扫描,Spring也不会直接创建包下所有类的对象,仅当类上添加了组件注解,才会被Spring视为“组件”,Spring才会创建对应的类的对象
  • getBean()时,由Spring创建的组件类的对象,默认的名称都是将首字母改为小写
    • 以上规则仅适用于:类名中的第1个字母是大写,且第2个字母是小写的情况,如果类名不符合这种情况,则getBean()时传入的名称就是类名(与类名完全相同的字符串)

关于组件:

  • 在Spring框架中,可用的组件注解有:
    • @Component:通用组件注解
    • @Controller:应该添加在“控制器类”上
    • @Service:应该添加在“业务类”上
    • @Repository:应该添加在“数据存取类”上
  • 另外,@Configuration是一种特殊的组件,应该添加在“配置类”上,当执行组件扫描时,添加了@Configuration注解的类也会被创建对象

其它:

  • 可以在@Component等组件注解(不包含@Configuration)中配置字符串参数,以显式的指定Bean的名称

  • 可以使用一个配置类,在配置类上通过@ComponentScan来指定组件扫描的包,并在加载Spring时,传入此配置类即可,例如:

    package cn.tedu.spring;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration@ComponentScan("cn.tedu.spring")public class SpringConfig {}
    // 以下是加载Spring的代码片段AnnotationConfigApplicationContext ac= new AnnotationConfigApplicationContext(SpringConfig.class);

    在使用@ComponentScan时,也可以传入多个包名,例如:

    @ComponentScan({"cn.tedu.spring.controller", "cn.tedu.spring.service"})

5. 关于2种通过Spring创建对象的做法

以上分别介绍了使用@Bean方法和使用组件扫描的方式使得Spring创建对象的做法,在实际应用中:

  • 使用@Bean方法可用在所有场景,但是使用过程相对繁琐
  • 使用组件扫描的做法只适用于自定义的类型(这些类是你自己编写出来的),使用过程非常便捷

所以,当需要被Spring创建对象的类型是自定义的,应该使用组件扫描的做法,如果不是自定义的,只能使用@Bean方法。这2种做法在实际的项目开发中都会被使用到

6. Spring管理的对象的作用域

由Spring管理的对象的作用域默认是单例的(并不是单例模式),对于同一个Bean,无论获取多少次,得到的都是同一个对象!如果希望某个被Spring管理的对象不是单例的,可以在类上添加@Scope("prototype")注解。

并且,在单例的情况下,默认不是懒加载的,还可以通过@Lazy注解控制它是否为懒加载模式!所谓的懒加载,就是“不要逼不得已不创建对象”。

7. Spring管理的对象的生命周期

由Spring创建并管理对象,则开发人员就没有了对象的控制权,无法对此对象的历程进行干预,而Spring允许在类中自定义最多2个方法,分别表示初始化方法和销毁方法,并且,Spring会在创建对象之后就执行初始化方法,在销毁对象之前执行销毁方法。

关于这2个方法,你可以按需添加,例如,当你只需要干预销毁过程时,你可以只定义销毁的方法,不需要定义初始化方法。

关于这2个方法的声明:

  • 访问权限:推荐使用public
  • 返回值类型:推荐使用void
  • 方法名称:自定义
  • 参数列表:推荐为空

然后,需要在初始化方法的声明之前添加@PostConstruct注解,在销毁方法的声明之前添加@PreDestroy注解。

例如:

package cn.tedu.spring;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Lazy;import org.springframework.context.annotation.Scope;import org.springframework.stereotype.Component;import org.springframework.stereotype.Controller;import org.springframework.stereotype.Repository;import org.springframework.stereotype.Service;import javax.annotation.PostConstruct;import javax.annotation.PreDestroy;@Componentpublic class UserMapper {    public UserMapper() { System.out.println("\tUserMapper.UserMapper()");    }    @PostConstruct    public void init() { System.out.println("\tUserMapper.init()");    }    @PreDestroy    public void destroy() { System.out.println("\tUserMapper.destroy()");    }}

注意:仅当类的对象被Spring管理且是单例的,才有讨论生命周期的价值,否则,不讨论生命周期。

如果某个类的对象是通过@Bean方法被Spring管理的,并且这个类不是自定义的,可以在@Bean注解中配置initMethoddestroyMethod这2个属性(它们的值就是方法名称,例如destroyMethod="close"),将这个类中的方法指定为生命周期方法。

8. Spring的自动装配机制

Spring的自动装配机制表现为:当你需要某个对象时,可以使用特定的语法,而Spring就会尝试从容器找到合适的值,并赋值到对应的位置!

最典型的表现就是在类的属性上添加@Autowired注解,Spring就会尝试从容器中找到合适的值为这个属性赋值!

例如有如下代码:

SpringConfig.java

package cn.tedu.spring;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration@ComponentScan("cn.tedu.spring")public class SpringConfig {}

UserMapper.java

package cn.tedu.spring;import org.springframework.stereotype.Repository;@Repositorypublic class UserMapper {    public void insert() { System.out.println("UserMapper.insert() >> 将用户数据写入到数据库中……");    }}

UserController.java

package cn.tedu.spring;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;@Controllerpublic class UserController {    @Autowired // 注意:此处使用了自动装配的注解    private UserMapper userMapper;    public void reg() { System.out.println("UserController.reg() >> 控制器即将执行用户注册……"); userMapper.insert();    }}

SpringRunner.java

package cn.tedu.spring;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class SpringRunner {    public static void main(String[] args) { // 1. 加载Spring AnnotationConfigApplicationContext ac  = new AnnotationConfigApplicationContext(SpringConfig.class); // 2. 从Spring中获取对象 UserController userController  = ac.getBean("userController", UserController.class); // 3. 测试使用对象,以便于观察是否获取到了有效的对象 userController.reg(); // 4. 关闭 ac.close();    }}

关于@Autowired的装配机制:

首先,会根据需要装配的数据的类型在Spring容器中查找匹配的Bean(对象)的数量,当数量为:

  • 0个:判断@Autowired注解的required属性的值
    • required=true时:装配失败,启动项目时即报告异常
    • required=false时:放弃自动装配,不会报告异常
      • 后续当使用到此属性时,会出现NullPointerException
  • 1个:直接装配,且装配成功
  • 多个:自动尝试按照名称实现装配(属性的名称与Spring Bean的名称)
    • 存在与属性名称匹配的Spring Bean:装配成功
    • 不存在与属性名称匹配的Spring Bean:装配失败,启动项目时即报告异常

另外,使用@Resource注解也可以实现自动装配(此注解是javax包中的),其装配机制是先尝试根据名称来装配,如果失败,再尝试根据类型装配!

除了对属性装配以外,Spring的自动装配机制还可以表现出:如果某个方法是由Spring框架自动调用的(通常是构造方法,或@Bean方法,其它的方法中,如果参数有限制则专门说明),当这个方法被声明了参数时,Spring框架也会自动的尝试从容器找到匹配的对象,用于调用此方法!

9. 读取properties配置文件中的信息

操作步骤:

  • 创建新的工程spring04,创建步骤参考前序案例

  • src/main/resources文件夹下创建jdbc.properties,内容为:

    spring.jdbc.url=jdbc:mysql://localhost:3306/teduspring.jdbc.driver=com.mysql.jdbc.Driverspring.jdbc.username=rootspring.jdbc.password=1234spring.jdbc.init-size=5spring.jdbc.max-active=20

    注意:自定义的属性名称建议添加一些前缀,避免与系统属性和Java属性冲突。

  • src/main/java下创建Java类,使用@PropertySource注解读取以上配置文件中的信息,则创建cn.tedu.spring.SpringConfig类:

    package cn.tedu.spring;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.PropertySource;@Configuration@ComponentScan("cn.tedu.spring")@PropertySource("classpath:jdbc.properties")public class SpringConfig {}

    提示:当Spring框架读取了配置文件中的信息后,会将这些读取到的数据封装在内置的Environment对象中,后续,任何需要这些配置信息的组件都可以从Environment中读取到配置的数据。

  • 接下来,可以创建某个Java类,从Environment中读取配置的数据,例如创建JdbcConfig类:

    package cn.tedu.spring;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;@Componentpublic class JdbcConfig {    @Value("${spring.jdbc.url}")    private String url;    @Value("${spring.jdbc.driver}")    private String driver;    @Value("${spring.jdbc.username}")    private String username;    @Value("${spring.jdbc.password}")    private String password;    @Value("${spring.jdbc.init-size}")    private int initSize;    @Value("${spring.jdbc.max-active}")    private int maxActive;    public String getUrl() { return url;    }    public void setUrl(String url) { this.url = url;    }    public String getDriver() { return driver;    }    public void setDriver(String driver) { this.driver = driver;    }    public String getUsername() { return username;    }    public void setUsername(String username) { this.username = username;    }    public String getPassword() { return password;    }    public void setPassword(String password) { this.password = password;    }    public int getInitSize() { return initSize;    }    public void setInitSize(int initSize) { this.initSize = initSize;    }    public int getMaxActive() { return maxActive;    }    public void setMaxActive(int maxActive) { this.maxActive = maxActive;    }}

    提示:前序的操作中,在SpringConfig中已经配置了组件扫描,这个JdbcConfig类必须在组件扫描的范围内,并添加组件注解,这样Spring框架才会创建JdbcConfig类的对象,进而根据各@Value注解将Environment中的配置数据注入到属性中

  • 最后,可以执行本案例:

    package cn.tedu.spring;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class SpringRunner {    public static void main(String[] args) { // 1. 加载Spring System.out.println("1. 加载Spring,开始……"); AnnotationConfigApplicationContext ac  = new AnnotationConfigApplicationContext(SpringConfig.class); System.out.println("1. 加载Spring,完成!"); System.out.println(); // 2. 从Spring中获取对象 System.out.println("2. 从Spring中获取对象,开始……"); JdbcConfig jdbcConfig = ac.getBean("jdbcConfig", JdbcConfig.class); System.out.println("2. 从Spring中获取对象,完成!"); System.out.println(); // 3. 测试使用对象,以便于观察是否获取到了有效的对象 System.out.println("3. 测试使用对象,开始……"); System.out.println("\turl >> " + jdbcConfig.getUrl()); System.out.println("\tdriver >> " + jdbcConfig.getDriver()); System.out.println("\tusername >> " + jdbcConfig.getUsername()); System.out.println("\tpassword >> " + jdbcConfig.getPassword()); System.out.println("\tinit-size >> " + jdbcConfig.getInitSize()); System.out.println("\tmax-active >> " + jdbcConfig.getMaxActive()); System.out.println("3. 测试使用对象,完成!"); System.out.println(); // 4. 关闭 System.out.println("4. 关闭,开始……"); ac.close(); System.out.println("4. 关闭,完成!");    }}

另外,还可以直接装配一个Environment对象,并在需要的时候通过Environment对象读取配置的数据,例如:

package cn.tedu.spring;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.core.env.Environment;import org.springframework.stereotype.Component;@Componentpublic class EnvironmentData {    @Autowired    private Environment environment;    public Environment getEnvironment() { return environment;    }}
package cn.tedu.spring;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import org.springframework.core.env.Environment;public class SpringRunner {    public static void main(String[] args) { // 1. 加载Spring System.out.println("1. 加载Spring,开始……"); AnnotationConfigApplicationContext ac  = new AnnotationConfigApplicationContext(SpringConfig.class); System.out.println("1. 加载Spring,完成!"); System.out.println(); // 2. 从Spring中获取对象 System.out.println("2. 从Spring中获取对象,开始……"); EnvironmentData environmentData  = ac.getBean("environmentData", EnvironmentData.class); System.out.println("2. 从Spring中获取对象,完成!"); System.out.println(); // 3. 测试使用对象,以便于观察是否获取到了有效的对象 System.out.println("3. 测试使用对象,开始……"); System.out.println("---------------------------------------"); System.out.println("通过自动装配Environment对象获取的值:"); Environment env = environmentData.getEnvironment(); System.out.println("\tEnvironment >> " + env); System.out.println("\turl >> " + env.getProperty("spring.jdbc.url")); System.out.println("\tdriver >> " + env.getProperty("spring.jdbc.driver")); System.out.println("\tusername >> " + env.getProperty("spring.jdbc.username")); System.out.println("\tpassword >> " + env.getProperty("spring.jdbc.password")); System.out.println("\tinit-size >> " + env.getProperty("spring.jdbc.init-size")); System.out.println("\tmax-active >> " + env.getProperty("spring.jdbc.max-active")); System.out.println("3. 测试使用对象,完成!"); System.out.println(); // 4. 关闭 System.out.println("4. 关闭,开始……"); ac.close(); System.out.println("4. 关闭,完成!");    }}

10. 关于Spring框架的小结

关于Spring框架,你应该:

  • 了解Spring框架的作用:创建对象,管理对象
  • 掌握通过Spring创建对象的2种方式:
    • 在配置类(带@Configuration注解的类)中使用@Bean方法
    • 使用组件扫描,并在类上添加组件注解
      • 组件注解有:@Component@Controller@Service@Repository
    • 如果是自定义的类,应该使用组件扫描+组件注解的方式,如果不是自定义的类,必须使用配置类中的@Bean方法
  • 了解Spring Bean的作用域与生命周期
  • 掌握@Autowired自动装配,理解其装配机制
    • 建议背下来:@Autowired@Resource的区别
  • 掌握读取.properties配置文件中的数据
    • 先使用@PropertySource注解指定需要读取的文件
    • 读取配置的数据时,可以:
      • 使用@Value注解将值注入到属性中
      • 自动装配Environment对象,并调用此对象的getProperty()方法以获取配置值
  • 了解Spring的IoC(Inversion of Controll:控制反转)和DI(Dependency Injection:依赖注入)
    • Spring框架基于DI实现了IoC,DI是一种实现手段,IoC是最终实现的目标/效果
  • Spring AOP后续再讲

二、SpringMVC

1. 关于Spring MVC

Spring MVC是基于Spring框架基础之上的,主要解决了后端服务器接收客户端提交的请求,并给予响应的相关问题。

MVC = Model + View + Controller,它们分别是:

  • Model:数据模型,通常由业务逻辑层(Service Layer)和数据访问层(Data Access Object Layer)共同构成
  • View:视图
  • Controller:控制器

MVC为项目中代码的职责划分提供了参考。

需要注意:Spring MVC框架只关心V - C之间的交互,与M其实没有任何关系。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ojp9GhlC-1649608506966)(images/springmvc.png)]

2. 创建Spring MVC工程

请参考 http://doc.canglaoshi.org/doc/idea_tomcat/index.html 创建项目,首次练习的项目名称请使用springmvc01

3. 使用Spring MVC工程接收客户端的请求

【操作步骤】

  • pom.xml中添加spring-webmvc依赖项:

    <dependency>    <groupId>org.springframework</groupId>    <artifactId>spring-webmvc</artifactId>    <version>5.3.14</version></dependency>

    提示:如果后续运行时提示不可识别Servlet相关类,则补充添加以下依赖项:

    <dependency>    <groupId>javax.servlet</groupId>    <artifactId>javax.servlet-api</artifactId>    <version>3.1.0</version>    <scope>provided</scope></dependency>
  • 接下来,准备2个配置类,一个是Spring框架的配置类,一个是Spring MVC框架的配置类:

    cn.tedu.springmvc.config.SpringConfig.java

    package cn.tedu.springmvc.config;import org.springframework.context.annotation.Configuration;@Configuration // 此注解不是必须的public class SpringConfig {}

    cn.tedu.springmvc.config.SpringMvcConfig.java

    package cn.tedu.springmvc.config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration // 此注解不是必须的@ComponentScan("cn.tedu.springmvc") // 必须配置在当前配置类,不可配置在Spring的配置类public class SpringMvcConfig implements WebMvcConfigurer {}
  • 接下来,需要创建项目的初始化类,此类必须继承自AbstractAnnotationConfigDispatcherServletInitializer,并在此类中重写父类的3个抽象方法,返回正确的值(各方法的意义请参见以下代码中的注释):

    cn.tedu.springmvc.SpringMvcInitializer

    package cn.tedu.springmvc;import cn.tedu.springmvc.config.SpringConfig;import cn.tedu.springmvc.config.SpringMvcConfig;import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;/** * Spring MVC项目的初始化类 */public class SpringMvcInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {    @Override    protected Class<?>[] getRootConfigClasses() { // 返回自行配置的Spring相关内容的类 return new Class[] { SpringConfig.class };    }    @Override    protected Class<?>[] getServletConfigClasses() { // 返回自行配置的Spring MVC相关内容的类 return new Class[] { SpringMvcConfig.class };    }    @Override    protected String[] getServletMappings() { // 返回哪些路径是由Spring MVC框架处理的 return new String[] { "*.do" };    }}
  • 最后,创建控制器类,用于接收客户端的某个请求,并简单的响应结果:

    cn.tedu.springmvc.controller.UserController

    package cn.tedu.springmvc.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;@Controller // 必须是@Controller,不可以是其它组件注解public class UserController {    public UserController() { System.out.println("UserController.UserController()");    }    // http://localhost:8080/springmvc01_war_exploded/login.do    @RequestMapping("/login.do")    @ResponseBody    public String login() { return "UserController.login()";    }}
  • 全部完成后,启动项目,会自动打开浏览器并显示主页,在主页的地址栏URL上补充/login.do即可实现访问,并看到结果。

关于以上案例:

  • 当启动Tomcat时,会自动将项目打包并部署到Tomcat,通过自动打开的浏览器中的URL即可访问主页,在URL中有很长一段是例如 springmvc01_war_explored 这一段是不可以删除的,其它的各路径必须补充在其之后,例如 /login.do 就必须在此之后
  • 当启动Tomcat时,项目一旦部署成功,就会自动创建并加载AbstractAnnotationConfigDispatcherServletInitializer的子类,即当前项目中自定义的SpringMvcInitialier,无论这个类放在哪个包中,都会自动创建并加载,由于会自动调用这个类中所有方法,所以会将Spring MVC框架处理的请求路径设置为 *.do,并执行对 cn.tedu.springmvc 的组件扫描,进而会创建 UserController 的对象,由于在 UserController 中配置的方法使用了 @RequestMapping("/login.do"),则此时还将此方法与/login.do进行了绑定,以至于后续随时访问/login.do时都会执行此方法
  • 注意:组件扫描必须配置在Spring MVC的配置类中
  • 注意:控制器类上的注解必须是@Controller,不可以是@Component@Service@Repository

4. 关于@RequestMapping注解

@RequestMapping注解的主要作用是配置请求路径处理请求的方法的映射关系,例如将此注解添加在控制器中某个方法之前:

// http://localhost:8080/springmvc01_war_exploded/login.do@RequestMapping("/login.do")@ResponseBodypublic String login() {    return "UserController.login()";}

就会将注解中配置的路径与注解所在的方法对应上!

除了方法之前,此注解还可以添加在控制器类之前,例如:

@Controller@RequestMapping("/user")public class UserController {}

一旦在类上添加了此注解并配置路径,则每个方法实际映射到的请求路径都是“类上的@RequestMapping配置的路径 + 方法上的@RequestMapping配置的路径”。

通常,在项目中,推荐为每一个控制器类都配置此注解,以指定某个URL前缀。

在使用@RequestMapping配置路径时,并不要求各路径使用 / 作为第1个字符!

另外,在@RequestMapping还可以配置:

  • 请求方式
  • 请求头
  • 响应头
  • 等等

所以,在@RequestMapping注解中,增加配置method属性,可以限制客户端的请求方式,例如可以配置为:

@RequestMapping(value = "/login.do", method = RequestMethod.POST)@ResponseBodypublic String login() {    return "UserController.login()";}

如果按照以上代码,则/login.do路径只能通过POST方式发起请求才可以被正确的处理,如果使用其它请求方式(例如GET),则会导致HTTP的405错误。

如果没有配置method属性,则表示可以使用任何请求方式,包括:

GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE

另外,Spring MVC框架还提供了@RequestMapping的相关注解,例如:

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • 等等

这些注解就是已经限制了请求方式的注解!以@GetMapping为例,就限制了请求方式必须是GET,除此以外,使用方式与@RequestMapping完全相同!

所以,在实际应用中,在类的上方肯定使用@RequestMapping(其它的@XxxMapping不可以加在类上),方法上一般都使用@GetMapping@PostMapping等注解,除非在极特殊的情况下,某些请求同时允许多种请求方式,才会在方法上使用@RequestMapping

5. 关于@ResponseBody注解

@ResponseBody注解表示:响应正文。

一旦配置为“响应正文”,则处理请求的方法的返回值就会直接响应到客户端去!

如果没有配置为“响应正文”,则处理请求的方法的返回值表示“视图组件的名称”,当方法返回后,服务器端并不会直接响应,而是根据“视图组件的名称”在服务器端找到对应的视图组件,并处理,最后,将处理后的视图响应到客户端去,这不是前后端分离的做法!

可以在需要正文的方法上添加@ResponseBody注解,由于开发模式一般相对统一,所以,一般会将@ResponseBody添加在控制器类上,表示此控制器类中所有处理请求的方法都将响应正文!

在Spring MVC框架中,还提供了@RestController注解,它同时具有@Controller@ResponseBody注解的效果,所以,在响应正文的控制器上,只需要使用@RestController即可,不必再添加@Controller@ResponseBody注解。

关于响应正文,Spring MVC内置了一系列的转换器(Converter),用于将方法的返回值转换为响应到客户端的数据(并根据HTTP协议补充了必要的数据),并且,Spring MVC会根据方法的返回值不同,自动选取某个转换器,例如,当方法的返回值是String时,会自动使用StringHttpMessageConverter这个转换器,这个转换器的特点就是直接将方法返回的字符串作为响应的正文,并且,其默认的响应文档的字符集是ISO-8859-1,所以在默认情况并不支持非ASCII字符(例如中文)。

在实际应用中,不会使用String作为处理请求的方法的返回值类型,主要是因为普通的字符串不足以清楚的表现多项数据,如果自行组织成JSON或其它某种格式的字符串成本太高!

通常,建议向客户端响应JSON格式的字符串,应该在项目中添加jackson-databind的依赖项:

<dependency>    <groupId>com.fasterxml.jackson.core</groupId>    <artifactId>jackson-databind</artifactId>    <version>2.12.3</version></dependency>

以上jackson-databind依赖项中也有一个转换器,当Spring MVC调用的处理请求的方法的返回值是Spring MVC没有匹配的默认转换器时,会自动使用jackson-databind的转换器,而jackson-databind转换器就会解析方法的返回值,并将其处理为JSON格式的字符串,在响应头中将Content-Type设置为application/json

注意:在Spring MVC项目中,还需要在Spring MVC的配置类(SpringMvcConfig)上添加@EnableWebMvc注解,否则响应时将导致出现HTTP的406错误。

【示例代码】

cn.tedu.springmvc.vo.UserVO

package cn.tedu.springmvc.vo;public class UserVO {    private String username;    private String password;    private String email;    // 请自行补充以上3个属性的Setter & Getter}

UserController的代码片段:

// http://localhost:8080/springmvc01_war_exploded/user/info.do@GetMapping("/info.do")public UserVO info() {    UserVO userVO = new UserVO();    userVO.setUsername("chengheng");    userVO.setPassword("1234567890");    userVO.setEmail("chengheng@qq.com");    return userVO;}

SpringMvcConfig(补充@EnableWebMvc注解)

@Configuration // 此注解不是必须的@EnableWebMvc@ComponentScan("cn.tedu.springmvc") // 必须配置在当前配置类,不可配置在Spring的配置类public class SpringMvcConfig implements WebMvcConfigurer {}

6. 接收请求参数

在Spring MVC中,当需要接收客户端的请求参数时,只需要将各参数直接声明为处理请求的方法的参数即可,例如:

// http://localhost:8080/springmvc01_war_exploded/user/reg.do?username=root&password=123456&age=25@RequestMapping("/reg.do")public String reg(String username, String password, Integer age) {    System.out.println("username = " + username     + ", password = " + password     + ", age = " + age);    return "OK";}

需要注意:

  • 如果客户端提交的请求中根本没有匹配名称的参数,则以上获取到的值将是null
    • 例如:http://localhost/user/login.do
  • 如果客户端仅提交了参数名称,却没有值,则以上获取到的值将是""(长度为0的字符串)
    • 例如:http://localhost/user/login.do?username=&password=&age=
  • 如果客户端提交了匹配名称的参数,并且值是有效的,则可以获取到值
    • 例如:http://localhost/user/login.do?username=admin&password=1234&age=27
  • 以上名称应该是由服务器端决定的,客户端需要根据以上名称来提交请求参数
  • 声明参数时,可以按需将参数声明成期望的类型,例如以上将age声明为Integer类型
    • 注意:声明成String以外的类型时,应该考虑是否可以成功转换类型

当有必要的情况下,可以在以上各参数的声明之前添加@RequestParam注解,其作用主要有:

  • 配置name属性:客户端将按照此配置的值提交请求参数,而不再是根据方法的参数名称来提交请求参数
  • 配置required属性:是否要求客户端必须提交此请求参数,默认为true,如果不提交,则出现400错误,当设置为false时,如果不提交,则服务器端将此参数值视为null
  • 配置defaultValue属性:配置此请求参数的默认值,当客户端没有提交此请求参数时,视为此值

另外,如果需要客户端提交的请求参数较多,可以将这些参数封装为自定义的数据类型,并将自定义的数据类型作为处理方法的参数即可,例如:

cn.tedu.springmvc.dto.UserRegDTO

package cn.tedu.springmvc.dto;public class UserRegDTO {    private String username;    private String password;    private Integer age;    // 生成Setters & Getters    // 生成toString()    }

UserController(代码片段)

// http://localhost:8080/springmvc01_war_exploded/user/reg.do?username=root&password=123456&age=25@RequestMapping("/reg.do")public String reg(UserRegDTO userRegDTO) {    System.out.println(userRegDTO);    return "OK";}

需要注意,不要将@RequestParam添加在封装的类型之前。

另外,你也可以将多个请求参数区分开来,一部分直接声明为处理请求的方法的参数,另一部分封装起来。
200成功 301/302重定向
400请求参数错误 404找不到资源 405请求方式错误 406请求头不可接受 500服务器内部错误

7. 关于RESTful

百科资料:RESTFUL是一种网络应用程序的设计风格和开发方式,基于HTTP,可以使用XML格式定义或JSON格式定义。RESTFUL适用于移动互联网厂商作为业务接口的场景,实现第三方OTT调用移动网络资源的功能,动作类型为新增、变更、删除所调用资源。

RESTful的设计风格的典型表现就是:将某些唯一的请求参数的值放在URL中,使之成为URL的一部分,例如https://www.zhihu.com/question/28557115这个URL的最后一部分28557115 应该就是这篇贴子的id值,而不是使用例如?id=28557115这样的方式放在URL参数中。

注意:RESTful只是一种设计风格,并不是一种规定,也没有明确的或统一的执行方式!

如果没有明确的要求,以处理用户数据为例,可以将URL设计为:

  • /users:查看用户列表
  • /users/9527:查询id=9527的用户的数据
  • /users/9527/delete:删除id=9527的用户的数据

在RESTful风格的URL中,大多是包含了某些请求参数的值,在使用Spring MVC框架时,当需要设计这类URL时,可以使用{名称}进行占位,并在处理请求的方法的参数列表中,使用@PathVariable注解请求参数,即可将占位符的实际值注入到请求参数中!

例如:

// http://localhost:8080/springmvc01_war_exploded/user/3/info.do@GetMapping("/{id}/info.do")public UserVO info(@PathVariable Long id) {    System.out.println("即将查询 id = " + id + " 的用户的信息……");    UserVO userVO = new UserVO();    userVO.setUsername("chengheng");    userVO.setPassword("1234567890");    userVO.setEmail("chengheng@qq.com");    return userVO;}

提示:在以上代码中,URL中使用的占位符是{id},则方法的参数名称也应该是id,就可以直接匹配上!如果无法保证这2处的名称一致,则需要在@PathVariable注解中配置占位符中的名称,例如:

@GetMapping("/{userId}/info.do")public UserVO info(@PathVariable("userId") Long id) {    // ...}

在使用{}格式的占位符时,还可以结合正则表达式进行匹配,其基本语法是:

{占位符名称:正则表达式}

例如:

@GetMapping("/{id:[0-9]+}/info.do")

当设计成以上URL时,仅当占位符位置的是纯数字的URL才会被匹配上,如果不是纯数字的刚出现404错误页面。

并且,以上模式的多种不冲突的正则表达式是可以同时存在的,例如:

@GetMapping("/{id:[0-9]+}/info.do")public UserVO info(@PathVariable Long id) {    System.out.println("即将查询 id = " + id + " 的用户的信息……");    // ...}@GetMapping("/{username:[a-zA-Z]+}/info.do")public UserVO info(@PathVariable String username) {    System.out.println("即将查询 用户名 = " + username + " 的用户的信息……");    // ...}

甚至,还可以存在不使用正则表达式,但是URL格式几乎一样的配置:

@GetMapping("/{id:[0-9]+}/info.do")public UserVO info(@PathVariable Long id) {    System.out.println("即将查询 id = " + id + " 的用户的信息……");    // ...}@GetMapping("/{username:[a-zA-Z]+}/info.do")public UserVO info(@PathVariable String username) {    System.out.println("即将查询 用户名 = " + username + " 的用户的信息……");    // ...}// 【以下是新增的】// http://localhost:8080/springmvc01_war_exploded/user/list/info.do@GetMapping("/list/info.do")public UserVO list() {    System.out.println("即将查询 用户的列表 的信息……");    // ...}

最终执行时,如果使用/user/list/info.do,则会匹配到以上代码中的最后一个方法,并不会因为这个URL还能匹配第2个方法配置的{username:[a-zA-Z]+}而产生冲突。所以,使用了占位符的做法并不影响精准匹配的路径。优先级问题.

8. 关于响应正文时的结果类型

当响应正文时,只要方法的返回值是自定义的数据类型,则Spring MVC框架就一定会调用jackson-databind中的转换器,就可以将结果转换为JSON格式的字符串!

通常,在项目开发中,会定义一个“通用”的数据类型,无论是哪个控制器的哪个处理请求的方法,最终都将返回此类型!

显示的通用返回类型如下:

public class JsonResult<T> {    private Integer state; // 业务返回码    private String message; // 消息    private T data; // 数据    private JsonResult() { }    public static JsonResult<Void> ok() { return ok(null);    }    public static <T> JsonResult<T> ok(T data) { JsonResult<T> jsonResult = new JsonResult<>(); jsonResult.state = State.OK.getValue(); jsonResult.data = data; return jsonResult;    }    public static JsonResult<Void> fail(State state, String message) { JsonResult<Void> jsonResult = new JsonResult<>(); jsonResult.state = state.getValue(); jsonResult.message = message; return jsonResult;    }   public enum State {OK(20000),ERR_USERNAME(40400),ERR_PASSWORD(40600);Integer value;State(Integer value) {    this.value = value;}public Integer getValue() {    return value;}   }   // Setters & Getters}

9. 统一处理异常

Spring MVC框架提供了统一处理异常的机制,使得特定种类的异常对应一段特定的代码,后续,当编写代码时,无论在任何位置,都可以将异常直接抛出,由统一处理异常的代码进行处理即可!

关于统一处理异常,需要自定义方法对异常进行处理,关于此方法:

  • 注解:需要添加@ExceptionHandler注解
  • 访问权限:应该是公有的
  • 返回值类型:可参考处理请求的方法的返回值类型
  • 方法名称:自定义
  • 参数列表:必须包含1个异常类型的参数,并且可按需添加HttpServletRequestHttpServletResponse等少量特定的类型的参数,不可以随意添加参数

例如:

@ExceptionHandlerpublic String handleException(NullPointerException e) {    return "Error, NullPointerException!";}

需要注意:以上处理异常的代码,只能作用于当前控制器类中各个处理请求的方法,对其它控制器类的中代码并不产生任何影响,也就无法处理其它控制类中处理请求时出现的异常!

为保证更合理的处理异常,应该:

  • 将处理异常的代码放在专门的类中
  • 在此类上添加@ControllerAdvice注解
    • 由于目前主流的响应方式都是“响应正文”的,则可以将@ControllerAdvice替换为@RestControllerAdvice

所以,可以创建GlobalExceptionHandler类,代码如下:

@RestControllerAdvicepublic class GlobalExceptionHandler {    @ExceptionHandler    public String handleException(NullPointerException e) { return "Error, NullPointerException!";    }}

另外,可以将处理异常的代码放在所有控制器类公共的父类中,则各控制器类都相当于有此代码,则处理异常的代码可以作用于所有控制器中处理请求的方法!但不推荐此做法。

在以上处理异常的过程中,Spring MVC的处理模式大致如下:

try {userController.npe();} catch (NullPointerException e) {globalExceptionHandler.handleException(e);}

关于以上处理的方法的参数中的异常类型,将对应Spring MVC框架能够统一处理的异常类型,例如将其声明为Throwable时,所有异常都可被此方法进行处理!但是,在处理过程中,应该判断当前异常对象所归属的类型,以针对不同类型的异常进行不同的处理!

需要注意:允许存在多个统一处理异常的方法,例如:

@ExceptionHandlerpublic String handleNullPointerException(NullPointerException e) {    return "Error, NullPointerException!";}@ExceptionHandlerpublic String handleNumberFormatException(NumberFormatException e) {    return "Error, NumberFormatException!";}@ExceptionHandlerpublic String handleThrowable(Throwable e) {    e.printStackTrace();    return "Error, Throwable!";}

并且,如果某个异常能够被多个方法处理(异常类型符合多个处理异常的方法的参数类型),则优先执行最能精准匹配的处理异常的方法,例如,当出现NullPointerException时,将执行handleNullPointerException()而不会执行handleThrowable()

在开发实践中,通常都会有handleThrowable()方法,以避免某个异常没有被处理而导致500错误!

关于@ExceptionHandler注解,可用于表示被注解的方法是用于统一处理异常的,而且,可用于配置被注解的方法能够处理的异常的类型,其效力的优先级高于在方法的参数上指定异常类型。

在开发实践中,建议为每一个@ExceptionHandler配置注解参数,在注解参数中指定需要处理异常的类型,而处理异常的方法的参数直接使用Throwable即可。

例如:

@ExceptionHandler({ NullPointerException.class, ClassCastException.class})public String handleNullPointerException(Throwable e) {    return "Error, NullPointerException or ClassCastException!";}@ExceptionHandler(NumberFormatException.class)public String handleNumberFormatException(Throwable e) {    return "Error, NumberFormatException!";}@ExceptionHandler(Throwable.class)public String handleThrowable(Throwable e) {    return "Error, Throwable!";}

10. 拦截器(Interceptor)

在Spring MVC框架中,拦截器是可以运行在所有控制器处理请求之前和之后的一种组件,并且,如果拦截器运行在控制器处理请求之前,还可以选择对当前请求进行阻止或放行。

注意:拦截器的目的并不是“拦截下来后阻止运行”,更多的是“拦截下来后执行某些代码”,其优势在于可作用于若干种不同请求的处理过程,即写一个拦截器,就可以在很多种请求的处理过程中被执行。

只要是若干种不同的请求过程中都需要执行同样的或高度相似的代码,都可以使用拦截器解决,典型的例如验证用户是否已经登录等等。

当需要使用拦截器时,首先,需要自定义类,实现HandlerInterceptor接口,例如:

package cn.tedu.springmvc.interceptor;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class LoginInterceptor implements HandlerInterceptor {    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("LoginInterceptor.preHandle()"); return false;    }    @Override    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("LoginInterceptor.postHandle()");    }    @Override    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("LoginInterceptor.afterCompletion()");    }}

每个拦截器都必须注册才会被启用,注册过程通过重写WebMvcConfigure接口中的addInterceptors()方法即可,例如:

@Configuration // 此注解不是必须的@EnableWebMvc@ComponentScan("cn.tedu.springmvc") // 必须配置在当前配置类,不可配置在Spring的配置类public class SpringMvcConfig implements WebMvcConfigurer {    @Override    public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor())  .addPathPatterns("/user/login.do");    }}

当进行访问时,在浏览器窗口中将看到一片空白,在Tomcat控制台可以看到preHandle()方法已经执行。当把拦截器中preHandle()方法的返回值改为true时,在Tomcat控制台可以看到依次执行了preHandle() > 控制器中处理请求的方法 > postHandle() > afterCompletion()

其实,preHandle()方法的返回值为true时,表示“放行”,为false时,表示“阻止”。

关于注册拦截器时的配置,使用链式语法可以先调用addInterceptor()方法添加拦截器,然后调用addPathPatter()方法添加哪些路径需要被拦截,此方法的参数可以是String...,也可以是List,在编写路径值时,可以使用*作为通配符,例如配置为/user/*,则可以匹配/user/login.do/user/reg.do等所有直接在/user下的路径,但不能匹配/user/1/info.do,如果需要匹配若干层级,必须使用2个连续的星号,例如配置为/user/**。一旦使用通配符,就有可能导致匹配的范围过大,例如配置为/user/**时,还可以匹配到/user/reg.do(注册)和/user/login.do(登录),如果此拦截器是用于“验证用户是否登录”的,则不应该对这2个路径进行处理,那么,配置拦截器时,还可以在链式语法中调用excludePathPattern()方法,以添加“排除路径”(例外)。

配置示例:

@Overridepublic void addInterceptors(InterceptorRegistry registry) {    registry.addInterceptor(new LoginInterceptor())     .addPathPatterns("/user/**")     .excludePathPatterns("/user/reg.do", "/user/login.do");}

在这里插入图片描述

11. 关于Spring MVC的小结

关于Spring MVC框架,你应该:

  • 理解Spring MVC框架的作用
    • 接收请求,响应结果,处理异常……
  • 掌握创建基于Maven的运行在Tomcat的Webapp
  • 认识基础的依赖项
    • spring-webmvcjavax.servlet-apijackson-databind
  • 掌握配置Spring MVC的运行环境(使得控制器能接收到请求)
  • 掌握以下注解的使用:
    • @Controller / @RestController
    • @ResponseBody
    • @RequestMapping / @GetMapping / @PostMapping
    • @RequestParam / @PathVariable
    • @ExceptionHandler / @ControllerAdvice / @RestControllerAdvice
    • @EnableWebMvc
  • 掌握接收请求参数的方式
    • 将请求参数直接声明在处理请求的方法的参数列表中
    • 将若干个请求参数进行封装,并将封装的类型声明在处理请求的方法的参数列表中
    • 如果是URL中的路径,则需要使用@PathVariable
  • 掌握响应JSON格式的正文的做法
    • 处理请求的方法必须添加@ResponseBody,或当前控制器类添加@ResponseBody,或当前控制器类添加@RestController
    • 在Spring MVC配置类上添加@EnableWebMvc
    • 在项目的pom.xml中添加了jackson-databind
    • 处理请求的方法返回自定义的数据类型
  • 掌握响应JSON格式的正文时,统一的响应类型的类的设计
  • 了解RESTful风格
  • 掌握统一处理异常
  • 掌握拦截器的创建与配置

附:关于POJO

所有用于封装属性的类型都可以统称为POJO。

常见的POJO后缀有:BO、DO、VO、DTO等,不同的后缀表示不同的意义,例如:VO = Value Object / View Object,DTO = Data Transfer Object ……

在一个项目中,哪些情景下使用哪种后缀并没有统一的规定,通常是各项目内部决定。

注意:在为封装属性的类进行命名时,以上BO、DO、VO、DTO等这些后缀的每一个字母都应该是大写的!

附:关于数据库

1. 关于创建数据库

创建数据库的语法是:

CREATE DATABASE 数据库名称;

当某个项目规模特别大时,应该根据数据之间的关系,尽可能的拆为多个数据库。

2. 关于使用数据库

使用数据库的语法是:

USE 数据库名称;

提示:以上语法中的分号是可选的。

3. 创建数据表

创建数据表的基本语法是:

CREATE TABLE 数据表名称 (字段设计列表) CHARSET 字符编码 COMMENT 注释;

注意:主流的设计中,数据表的编码强烈建议配置为utf8mb4(过低版本的MySQL不支持)。

在设计字段时,基本语法是:

字段名 字段类型 字段约束 comment 注释

简单示例:

create table ams_admin (    id bigint unsigned auto_increment,    username varchar(50) default null unique comment '用户名',    password char(64) default null comment '密码(密文)',    nickname varchar(50) default null comment '昵称',    avatar varchar(255) default null comment '头像URL',    phone varchar(50) default null unique comment '手机号码',    email varchar(50) default null unique comment '电子邮箱',    description varchar(255) default null comment '描述',    is_enable tinyint unsigned default 0 comment '是否启用,1=启用,0=未启用',    last_login_ip varchar(50) default null comment '最后登录IP地址(冗余)',    login_count int unsigned default 0 comment '累计登录次数(冗余)',    gmt_last_login datetime default null comment '最后登录时间(冗余)',    gmt_create datetime default null comment '数据创建时间',    gmt_modified datetime default null comment '数据最后修改时间',    primary key (id)) comment '管理员表' charset utf8mb4;

关于以上设计:

  • 自动编号的id应该是bigint unsigned类型的,以确保id够用
    • MySQL中的整形类型:tinyintsmallintintbigint
  • 关于varchar的使用,必须设置长度,建议设置为比合理的最大值更大一些的值,例如规则为“用户名最多16字符”,则varchar中设置的字符长度最少20
  • 永远不要使用not null约束,任何你认为必须的字段,以后都可能不是必须的
  • 相对固定长度的字符串类型应该使用char,而不是使用varchar
    • 并不要求每个数据的长度完全一致
    • char的读取效率略高于varchar,占用的空间可能比varchar少1~2个字节
    • 在这里插入图片描述

附:关于密码加密

在基于Maven的项目中添加以下依赖:

<dependency>    <groupId>commons-codec</groupId>    <artifactId>commons-codec</artifactId>    <version>1.15</version></dependency>
public class DigestTests {    @Test    public void digest() { String rawPassword = "123456" + "fdh98geljgdfskj"; String encodedPassword = DigestUtils.md5Hex(rawPassword); System.out.println("原密码:" + rawPassword); System.out.println("加密后的密码:" + encodedPassword);    }}

三、Mybatis

1. 关于Mybatis

Mybatis的主要作用是快速实现对关系型数据库中的数据进行访问的框架。

2. 创建整合了Spring与Mybatis的工程

Mybatis可以不依赖于Spring等框架直接使用的,但是,就需要进行大量的配置,前期配置工作量较大,基于Spring框架目前是业内使用的标准之一,所以,通常会整合Spring与Mybatis,以减少配置。

在创建工程时,创建普通的Maven工程即可(不需要选择特定的骨架)。

pom.xml中,需要添加几个依赖项,分别是:

Mybatis的依赖项:mybatis

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

Mybatis整合Spring的依赖项:mybatis-spring

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

Spring的依赖项:spring-context

<dependency>    <groupId>org.springframework</groupId>    <artifactId>spring-context</artifactId>    <version>5.3.14</version></dependency>

Spring JDBC的依赖项:spring-jdbc

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

MySQL连接的依赖项:mysql-connector-java

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

数据库连接池的依赖项:commons-dbcp2

<dependency>    <groupId>org.apache.commons</groupId>    <artifactId>commons-dbcp2</artifactId>    <version>2.8.0</version></dependency>

JUnit测试的依赖项:junit-jupiter-api

<dependency>    <groupId>org.junit.jupiter</groupId>    <artifactId>junit-jupiter-api</artifactId>    <version>5.7.0</version>    <scope>test</scope></dependency>

创建完成后,可以在src/test/java下创建测试类,并编写测试方法,例如:

package cn.tedu.mybatis;import org.junit.jupiter.api.Test;public class MybatisTests {    @Test    public void contextLoads() { System.out.println("MybatisTests.contextLoads()");    }}

由于目前尚未编写实质的代码,以上测试代码也非常简单,应该是可以成功通过测试的,如果不能通过测试,必然是开发工具、开发环境、依赖项、项目创建步骤等问题。

3. 配置Mybatis的开发环境

配置java中Teminal环境变量
在这里插入图片描述
在这里插入图片描述
此电脑点右键的属性

首先,登录MySQL控制台,创建名为mall_ams的数据库:

CREATE DATABASE mall_ams;

然后,在IntelliJ IDEA中配置数据库视图:http://doc.canglaoshi.org/doc/idea_database/index.html

然后,通过数据库视图的Console面板创建数据表:

create table ams_admin (    id bigint unsigned auto_increment,    username varchar(50) default null unique comment '用户名',    password char(64) default null comment '密码(密文)',    nickname varchar(50) default null comment '昵称',    avatar varchar(255) default null comment '头像URL',    phone varchar(50) default null unique comment '手机号码',    email varchar(50) default null unique comment '电子邮箱',    description varchar(255) default null comment '描述',    is_enable tinyint unsigned default 0 comment '是否启用,1=启用,0=未启用',    last_login_ip varchar(50) default null comment '最后登录IP地址(冗余)',    login_count int unsigned default 0 comment '累计登录次数(冗余)',    gmt_last_login datetime default null comment '最后登录时间(冗余)',    gmt_create datetime default null comment '数据创建时间',    gmt_modified datetime default null comment '数据最后修改时间',    primary key (id)) comment '管理员表' charset utf8mb4;

至此,本案例所需的数据库与数据表已经准备完毕。

src/main/resources下创建datasource.properties配置文件,用于配置连接数据库的参数,例如:

datasource.url=jdbc:mysql://localhost:3306/mall_ams?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaidatasource.driver=com.mysql.cj.jdbc.Driverdatasource.username=rootdatasource.password=root

并且,在cn.tedu.mybatis包下(不存在,则创建)创建SpringConfig类,读取以上配置文件:

@Configuration@PropertySource("classpath:datasource.properties")public class SpringConfig {}

完成后,在测试方法中补充测试代码:

@Testpublic void contextLoads() {    System.out.println("MybatisTests.contextLoads()");    AnnotationConfigApplicationContext ac     = new AnnotationConfigApplicationContext(SpringConfig.class);    ConfigurableEnvironment environment = ac.getEnvironment();    System.out.println(environment.getProperty("datasource.url"));    System.out.println(environment.getProperty("datasource.driver"));    System.out.println(environment.getProperty("datasource.username"));    System.out.println(environment.getProperty("datasource.password"));    ac.close();}

接下来,在SpringConfig中配置一个DataSource对象:

package cn.tedu.mybatis;import org.apache.commons.dbcp.BasicDataSource;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.PropertySource;import org.springframework.core.env.Environment;import javax.sql.DataSource;@Configuration@PropertySource("classpath:datasource.properties")public class SpringConfig {    @Bean    public DataSource dataSource(Environment env) { BasicDataSource dataSource = new BasicDataSource(); dataSource.setUrl(env.getProperty("datasource.url")); dataSource.setDriverClassName(env.getProperty("datasource.driver")); dataSource.setUsername(env.getProperty("datasource.username")); dataSource.setPassword(env.getProperty("datasource.password")); return dataSource;    }}

并在测试类中添加新的测试方法,以尝试获取数据库的连接对象,检测是否可以正确连接到数据库:

@Testpublic void testConnection() throws Exception {    AnnotationConfigApplicationContext ac     = new AnnotationConfigApplicationContext(SpringConfig.class);    DataSource dataSource = ac.getBean("dataSource", DataSource.class);    Connection connection = dataSource.getConnection();    System.out.println(connection);    ac.close();}

至此,项目的数据库编程的准备完毕。

4. Mybatis的基本使用

当使用Mybatis实现数据访问时,主要:

  • 编写数据访问的抽象方法
  • 配置抽象方法对应的SQL语句

关于抽象方法:

  • 必须定义在某个接口中,这样的接口通常使用Mapper作为名称的后缀,例如AdminMapper
    • Mybatis框架底层将通过接口代理模式来实现
  • 方法的返回值类型:如果要执行的数据操作是增、删、改类型的,统一使用int作为返回值类型,表示“受影响的行数”,也可以使用void,但是不推荐;如果要执行的是查询操作,返回值类型只需要能够装载所需的数据即可
  • 方法的名称:自定义,不要重载,建议风格如下:
    • 插入数据使用insert作为方法名称中的前缀或关键字
    • 删除数据使用delete作为方法名称中的前缀或关键字
    • 更新数据使用update作为方法名称中的前缀或关键字
    • 查询数据时:
      • 如果是统计,使用count作为方法名称中的前缀或关键字
      • 如果是单个数据,使用getfind作为方法名称中的前缀或关键字
      • 如果是列表,使用list作为方法名称中的前缀或关键字
    • 如果操作数据时有条件,可在以上前缀或关键字右侧添加by字段名,例如deleteById
  • 方法的参数列表:取决于需要执行的SQL语句中有哪些参数,如果有多个参数,可将这些参数封装到同一个类型中,使用封装的类型作为方法的参数类型

假设当需要实现“插入一条管理员数据”,则需要执行的SQL语句大致是:

insert into ams_admin (username, password, nickname, avatar, phone, email, description, is_enable, last_login_ip, login_count, gmt_last_login, gmt_create, gmt_modified) values (?,?,? ... ?);

由于以上SQL语句中的参数数量较多,则应该将它们封装起来,则在cn.tedu.mybatis包下创建Admin类,声明一系列的属性,对应以上各参数值:

package cn.tedu.mybatis;import java.time.LocalDateTime;public class Admin { private String username;    private String password;    private String nickname;    private String avatar;    private String phone;    private String email;    private String description;    private Integer isEnable;    private String lastLoginIp;    private Integer loginCount;    private LocalDateTime gmtLastLogin;    private LocalDateTime gmtCreate;    private LocalDateTime gmtModified;     // Setters & Getters    // toString()}

接下来,在cn.tedu.mybatis包下创建mapper.AdminMapper接口,并在接口中添加“插入1条管理员数据”的抽象方法:

package cn.tedu.mybatis.mapper;import cn.tedu.mybatis.Admin;public interface AdminMapper {    int insert(Admin admin);}

所有用于Mybatis处理数据的接口都必须被Mybatis识别,有2种做法:

  • 在每个接口上添加@Mapper注解
  • 推荐:在配置类上添加@MapperScan注解,指定接口所在的根包

例如,在SpringConfig上添加配置@MapperScan

@Configuration@PropertySource("classpath:datasource.properties")@MapperScan("cn.tedu.mybatis.mapper")public class SpringConfig {    // ... ...    }

注意:因为Mybatis会扫描以上配置的包,并自动生成包中各接口中的代理对象,所以,千万不要放其它接口文件!

接下来,需要配置抽象方法对应的SQL语句,这些SQL语句推荐配置在XML文件中,可以从 http://doc.canglaoshi.org/config/Mapper.xml.zip 下载到XML文件。在项目的src/main/resources下创建mapper文件夹,并将下载得到的XML文件复制到此文件夹中,重命名为AdminMapper.xml
在这里插入图片描述放到自己找得到的位置.
在这里插入图片描述

打开XML文件夹,进行配置:

<!DOCTYPE mapper  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"  "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="cn.tedu.mybatis.mapper.AdminMapper">                    <insert id="insert"> insert into ams_admin (     username, password, nickname, avatar,      phone, email, description, is_enable,      last_login_ip, login_count, gmt_last_login, gmt_create,      gmt_modified ) values (     #{username}, #{password}, #{nickname}, #{avatar},      #{phone}, #{email}, #{description}, #{isEnable},      #{lastLoginIp}, #{loginCount}, #{gmtLastLogin}, #{gmtCreate},      #{gmtModified} )    </insert></mapper>

最后,还需要将DataSource配置给Mybatis框架,并且,为Mybatis配置这些XML文件的路径,这2项配置都将通过配置SqlSessionFactoryBean来完成。

先在datasource.properties中补充一条配置:

mybatis.mapper-locations=classpath:mapper/AdminMapper.xml

然后在配置类中创建SqlSessionFactoryBean类型的对象:

@Beanpublic SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource, @Value("${mybatis.mapper-locations}") Resource mapperLocations) {    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();    sqlSessionFactoryBean.setDataSource(dataSource);    sqlSessionFactoryBean.setMapperLocations(mapperLocations);    return sqlSessionFactoryBean;}

最后,在测试类中补充测试方法,以检验是否可以通过调用AdminMapperinsert()方法插入数据:

@Testpublic void testInsert() {    AnnotationConfigApplicationContext ac     = new AnnotationConfigApplicationContext(SpringConfig.class);    AdminMapper adminMapper = ac.getBean(AdminMapper.class);    Admin admin = new Admin();    admin.setUsername("admin001");    admin.setPassword("12345678");    adminMapper.insert(admin);    ac.close();}

5. 获取新增的数据的自动编号的id

如果某数据的id是自动编号,当需要获取新增的数据的id时,需要先使得插入的数据类型中有id对应的属性,则在Admin类中添加id属性:

public class Admin {    private Long id;    // 原有其它属性及Setter & Getter // 补充id的Setter & Getter    // 重新生成toString()    }

接下来,在节点配置2个属性,分别是useGeneratedKeyskeyProperty

<insert id="insert" useGeneratedKeys="true"  keyProperty="id">    原有代码</insert>

当配置完成后,Mybatis执行此插入数据的操作后,会将自动编号的id赋值到参数Admin adminid属性中,以上keyProperty指的就是将自动编号的值放回到参数对象的哪个属性中!
在这里插入图片描述

6. 删除数据

目标:根据id删除某一条数据

要实现此目标,需要执行的SQL语句大致是:

delete from ams_admin where id=?

然后,在AdminMapper接口中添加抽象方法:

int deleteById(Long id);

接下来,在AdminMapper.xml中配置以上抽象方法映射的SQL语句:

<delete id="deleteById">    delete from ams_admin where id=#{id}</delete>

最后,编写并执行测试:
在这里插入图片描述

@Testpublic void testDeleteById() {    AnnotationConfigApplicationContext ac     = new AnnotationConfigApplicationContext(SpringConfig.class);    AdminMapper adminMapper = ac.getBean(AdminMapper.class);    Long id = 12L;    int rows = adminMapper.deleteById(id);    // System.out.println("删除完成,受影响的行数=" + rows);    if (rows == 1) { System.out.println("删除成功");    } else { System.out.println("删除失败,尝试删除的数据(id=" + id + ")不存在!");    }    ac.close();}

7. 修改数据

在这里插入图片描述
jvm在把java文件编译成class文件时局部变量会变量名会丢失,变成arg0,arg1等等。如上图的id随意写效果都一样。
在这里插入图片描述
在这里插入图片描述
这里有改进方法就是把id里改为arg0,arg1…或者param1,param2…第三种就是加@Param注解在AdminMapper里面

目标:根据id修改某一条数据的密码

要实现此目标,需要执行的SQL语句大致是:

update ams_admin set password=? where id=?

然后,在AdminMapper接口中添加抽象方法:

int updatePasswordById(@Param("id") Long id,      @Param("password") String password);

接下来,在AdminMapper.xml中配置以上抽象方法映射的SQL语句:

<update id="updatePasswordById">    update ams_admin set password=#{password} where id=#{id}</update>

最后,编写并执行测试:

@Testpublic void testUpdatePasswordById() {    AnnotationConfigApplicationContext ac     = new AnnotationConfigApplicationContext(SpringConfig.class);    AdminMapper adminMapper = ac.getBean(AdminMapper.class);    Long id = 12L;    String password = "000000";    int rows = adminMapper.updatePasswordById(id, password);    if (rows == 1) { System.out.println("修改密码成功");    } else { System.out.println("修改密码失败,尝试访问的数据(id=" + id + ")不存在!");    }    ac.close();}

8. 查询数据-1

目标:统计当前表中有多少条数据

要实现此目标,需要执行的SQL语句大致是:

select count(*) from ams_admin

然后,在AdminMapper接口中添加抽象方法:

int count();

接下来,在AdminMapper.xml中配置以上抽象方法映射的SQL语句:

<select id="count" resultType="int">    select count(*) from ams_admin</select>

最后,编写并执行测试:

@Testpublic void testCount() {    AnnotationConfigApplicationContext ac     = new AnnotationConfigApplicationContext(SpringConfig.class);    AdminMapper adminMapper = ac.getBean(AdminMapper.class);    int count = adminMapper.count();    System.out.println("当前表中有" + count + "条记录");    ac.close();}

9. 查询数据-2

目标:根据id查询管理员信息

要实现此目标,需要执行的SQL语句大致是:

select * from ams_admin where id=?

然后,在AdminMapper接口中添加抽象方法:

Admin getById(Long id);

接下来,在AdminMapper.xml中配置以上抽象方法映射的SQL语句:

<select id="getById" resultType="cn.tedu.mybatis.Admin">    select * from ams_admin where id=#{id}</select>

最后,编写并执行测试:

@Testpublic void testGetById() {    AnnotationConfigApplicationContext ac     = new AnnotationConfigApplicationContext(SpringConfig.class);    AdminMapper adminMapper = ac.getBean(AdminMapper.class);    Long id = 3L;    Admin admin = adminMapper.getById(id);    System.out.println("查询结果:" + admin);    ac.close();}

通过测试可以发现:当存在匹配的数据时,将可以查询到数据,当不存在匹配的数据时,将返回null

需要注意,如果查询结果集中的列名与类的属性名不匹配时,默认将放弃处理这些结果数据,则返回的对象中对应的属性值为null,为了解决此问题,可以在查询时使用自定义的别名,使得名称保持一致,不过,更推荐配置以指导Mybatis封装查询结果,例如:

<select id="getById" resultMap="BaseResultMap">    select * from ams_admin where id=#{id}</select><resultMap id="BaseResultMap" type="cn.tedu.mybatis.Admin">                    <result column="is_enable" property="isEnable" />    <result column="last_login_ip" property="lastLoginIp" />    <result column="login_count" property="loginCount" />    <result column="gmt_last_login" property="gmtLastLogin" />    <result column="gmt_create" property="gmtCreate" />    <result column="gmt_modified" property="gmtModified" /></resultMap>

注意:在后续的应用中,凡是可以通过resultMap处理结果的,都不要使用resultType

10. 查询数据-3

目标:查询所有管理员的信息

要实现此目标,需要执行的SQL语句大致是:

select * from ams_admin order by id

注意:(1) 查询时,结果集中可能超过1条数据时,必须显式的使用ORDER BY子句对结果集进行排序;(2) 查询时,结果集中可能超过1条数据时,应该考虑是否需要分页。

然后,在AdminMapper接口中添加抽象方法:

List<Admin> list();

接下来,在AdminMapper.xml中配置以上抽象方法映射的SQL语句:

<!-- List list(); --><select id="list" resultMap="BaseResultMap">    select * from ams_admin order by id</select>

最后,编写并执行测试:

@Testpublic void testList() {    AnnotationConfigApplicationContext ac     = new AnnotationConfigApplicationContext(SpringConfig.class);    AdminMapper adminMapper = ac.getBean(AdminMapper.class);    List<Admin> list = adminMapper.list();    for (Admin admin : list) { System.out.println(admin);    }    ac.close();}

注:这里有快捷键list.iter可以快速写出增强for循环。

11. 动态SQL – foreach

Mybatis中的动态SQL表现为:根据参数不同,生成不同的SQL语句

目标:根据若干个id一次性删除若干条管理数据

要实现此目标,需要执行的SQL语句大致是:

delete from ams_admin where id in (?,?)

以上SQL语句中,id值的数量(以上?的数量)对于开发人员而言是未知的!

然后,在AdminMapper接口中添加抽象方法:

int deleteByIds(Long... ids);

int deleteByIds(Long[] ids);

int deleteByIds(List<Long> ids);

接下来,在AdminMapper.xml中配置以上抽象方法映射的SQL语句:

<!-- int deleteByIds(List ids); --><delete id="deleteByIds">    delete from ams_admin where id in (    <foreach collection="list" item="id" separator=",">   #{id}    </foreach>    )</delete>

以上代码中:

  • 标签:用于遍历集合或数组类型的参数对象
  • collection属性:被遍历的参数对象,当抽象方法的参数只有1个且没有添加@Param注解时,如果参数是List类型则此属性值为list,如果参数是数组类型(包括可变参数)则此属性值为array;当抽象方法的参数有多个或添加了@Param注解时,则此属性值为@Param注解中配置的值
  • item属性:自定义的名称,表示遍历过程中每个元素的变量名,可在子级使用#{变量名}表示数据
  • separator属性:分隔符号,会自动添加在遍历到的各元素之间

最后,编写并执行测试:

@Testpublic void testDeleteByIds() {    AnnotationConfigApplicationContext ac     = new AnnotationConfigApplicationContext(SpringConfig.class);    AdminMapper adminMapper = ac.getBean(AdminMapper.class);    List<Long> ids = new ArrayList<>();    ids.add(16L);    ids.add(18L);    ids.add(19L);    int rows = adminMapper.deleteByIds(ids);    System.out.println("受影响的行数为:" + rows);    ac.close();}

12. 动态SQL – 其它

在Mybatis中动态SQL还有其它节点,例如: / + + 等,将在项目中补充。

13. 关于查询时的字段列表

在阿里巴巴的《Java开发手册》中指出:

【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。

通常,建议将字段列表使用节点进行封装,例如:

<sql id="BaseQueryFields">    <if test="true"> id, username, password, nickname, avatar, phone, email, description, is_enable, last_login_ip, login_count, gmt_last_login, gmt_create, gmt_modified    </if></sql>

提示:为避免IntelliJ IDEA误以为以上代码片段是错误的而提示红色的波浪线,所以使用框住了字段列表的代码片段,但这个并不是必须的,即使提示了红色的波浪线,也不影响运行。快捷键:alt+shift+向上或向下箭头可以移动一行数据。和效果一样,推荐如果之间如果没数据时用。
注意:以上节点可以用于封装任何SQL语句的任何片段,不仅仅只是字段列表。

封装后,当需要引用以上代码片段时,可以使用节点进行引用,例如:

<select id="getById" resultMap="BaseResultMap">    select <include refid="BaseQueryFields" />    from ams_admin where id=#{id}</select><!-- List list(); --><select id="list" resultMap="BaseResultMap">    select <include refid="BaseQueryFields" />    from ams_admin order by id</select>

14. 基于Spring的测试

pom.xml中添加spring-test依赖项:

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

注意:与其它的spring-????使用完全相同的版本!

接下来,在编写测试时,就可以在测试类上添加@SpringJUnitConfig注解,并在注解中配置Spring的配置类作为参数,则执行此类的任何测试方法之前,都会加载这些Spring配置类,并且,在编写测试时,只要是在Spring容器中存在的对象,都可以自动装配,例如:

@SpringJUnitConfig(SpringConfig.class)public class MybatisSpringTests {    @Autowired    Environment env;    @Test    public void contextLoads() { System.out.println(env.getProperty("datasource.url")); System.out.println(env.getProperty("datasource.driver")); System.out.println(env.getProperty("datasource.username")); System.out.println(env.getProperty("datasource.password"));    }    }

15. 关于@Sql注解

当添加了spring-test依赖后,可以在测试时使用@Sql注解,以加载某些.sql脚本,使得测试之前或之后将执行这些脚本!

使用此注解主要是为了保障可以反复测试,并且得到预期的结果!例如执行删除的测试时,假设数据是存在的,第1次删除可以成功,但是在这之后的测试将不会成功,因为数据在第1次测试时就已经被删除!则可以编写一个.sql脚本,通过脚本向数据表中插入数据,并在每次测试之前执行此脚本,即可保证每次测试都是成功的!

此注解可以添加在测试类上,则对当前测试类的每个测试方法都是有效的。

此注解也可以添加在测试方法上,则只对当前测试方法是有效的。

如果测试类和测试方法上都添加了此注解,则仅测试方法上的注解会生效。

此注解除了配置需要执行的.sql脚本以外,还可以通过executionPhase属性配置其执行阶段,例如取值为Sql.ExecutionPhase.AFTER_TEST_METHOD时将使得.sql脚本会在测试方法之后被执行。

每个测试方法可以添加多个@Sql注解。
例如:

package cn.tedu.mybatis;import cn.tedu.mybatis.mapper.AdminMapper;import org.junit.jupiter.api.Assertions;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.core.env.Environment;import org.springframework.test.context.jdbc.Sql;import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;import javax.annotation.Resource;import javax.sql.DataSource;import java.sql.Connection;import java.sql.SQLException;@SpringJUnitConfig(SpringConfig.class)public class MybatisSpringTests {    @Autowired    Environment env;    @Test    public void contextLoads() { System.out.println(env.getProperty("datasource.url")); System.out.println(env.getProperty("datasource.driver")); System.out.println(env.getProperty("datasource.username")); System.out.println(env.getProperty("datasource.password"));    }    @Autowired    DataSource dataSource;    @Test    public void testGetConnection() throws SQLException { Connection connection = dataSource.getConnection(); System.out.println(connection);    }    @Autowired    AdminMapper adminMapper;    @Test    @Sql("classpath:insert_data.sql")    public void testDeleteByIdSuccessfully(){ Long id = 1L; int rows = adminMapper.deleteById(id); //断言 Assertions.assertEquals(1,rows);    }    @Test    @Sql("classpath:truncate.sql")    public void testDeleteByIdFailBecauseNotExist(){ Long id = 1L; int rows = adminMapper.deleteById(id); Assertions.assertEquals(0,rows);    }}

insert_data.sql脚本示例:

truncate ams_admin;insert into ams_admin (username, password) values ('admin001', '123456');insert into ams_admin (username, password) values ('admin002', '123456');insert into ams_admin (username, password) values ('admin003', '123456');insert into ams_admin (username, password) values ('admin004', '123456');insert into ams_admin (username, password) values ('admin005', '123456');

truncate.sql脚本示例:

truncate ams_admin;

16. 关于测试中的断言

在执行测试时,应该使用断言对测试结果进行预判,而不是使用输出语句结合肉眼观察结果,这样才更符合自动化测试的标准(在自动化测试中,可以一键执行项目中的所有测试方法,并将测试结果汇总到专门的测试报告文件中)。

通过调用Assertions类中的静态方法可以对结果进行断言,常用方法有:

  • assertEquals():断言匹配(相等)
  • assertNotEquals():断言不匹配(不相等)
  • assertTrue():断言为“真”
  • assertFalse():断言为“假”
  • assertNull():断言为null
  • assertNotNull():断言不为null
  • assertThrows():断言将抛出异常
  • assertDoesNotThrow():断言不会抛出异常
  • 其它

17. 关于#{}${}格式的占位符

在Mybatis中,配置SQL语句时,参数可以使用#{}${}格式的占位符。

例如存在需求:分页查询表中的所有数据。

需要执行的SQL语句大致是:

select * from ams_admin order by id limit ?, ?

则此功能的抽象方法应该是:

List<Admin> listPage(@Param("offset") Integer offset, @Param("size") Integer size);

配置SQL语句:

<!-- List listPage(@Param("offset") Integer offset, @Param("size") Integer size); --><select id="listPage" resultMap="BaseResultMap">    select     <include refid="BaseQueryFields" />    from ams_admin     order by id     limit #{offset}, #{size}</select>

最后,执行测试:

@Test@Sql(scripts = {"classpath:truncate.sql", "classpath:insert_data.sql"})@Sql(scripts = {"classpath:truncate.sql"},      executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)public void testListPage() {    // 准备测试数据    Integer offset = 0;    Integer size = 3;    // 断言不会抛出异常    Assertions.assertDoesNotThrow(() -> { // 执行测试    List<Admin> adminList = adminMapper.listPage(offset, size); // 观察结果(通过输出语句) System.out.println("查询到的记录数:" + adminList.size()); for (Admin admin : adminList) {     System.out.println(admin); }    });}

如果确保文件存在且文件路径没有写错却加载不出来,可以用下面的方法。
在这里插入图片描述
或者删除target,再或者rebuild project。
以上代码可以正常通过测试,并且观察结果也都是符合预期的,即使把SQL语句中的#{}换成${}格式,也是完全没有问题的!

例如还存在需求:根据用户名查询此用户的详情。

在“根据用户名查询用户详情”时,如果将username=#{username}换成username=${username}会出现错误!

其实,使用#{}格式的占位符时,Mybatis在处理时会使用预编译的做法,所以,在编写SQL语句时不必关心数据类型的问题(例如字符串值不需要添加单引号),也不存在SQL注入的风险!这种占位符只能用于表示某个值,而不能表示SQL语句片段!

当使用${}格式的占位符时,Mybatis在处理时会先将参数值代入到SQL语句中,然后再执行编译相关过程,所以需要关心某些值的数据类型问题(例如涉及字符串值时,需要在编写SQL语句时添加一对单引号框住字符串),并且,存在SQL注入的风险!其优点是可以表示SQL语句中的任何片段!
在这里插入图片描述
要满足sql注入至少4的倍数以上单引号。

$符非常灵活。
SSM三大框架:Spring,SpringMVC,Mybatis

在一般情况下,应该尽可能的使用#{}格式的占位符,并不推荐使用${}格式的占位符,即使它可以实现“泛用”的效果!在一些特殊的情况下,如果一定要使用${}格式的占位符,必须考虑SQL注入的风险,应该使用正则表达式或其它做法避免出现SQL注入问题!

18. 关于RBAC

RBAC = Role Based Access Control(基于角色的访问控制)

RBAC是经典的用户权限管理的设计思路。在这样的设计中,会存在3种类型:用户、角色、权限,权限将分配到各种角色上,用户可以关联某种角色,进而实现用户与权限相关。使用这样的设计,更加利于统一管理若干个用户的权限。

在RBAC的设计思路中,用户与角色一般是多对多的关系,而在数据库中,仅仅只是使用“用户”和“角色”这2张表是不利于维护多对多关系的,通常会增加一张中间表,专门记录对应关系,同理,角色和权限也是多对多的关系,也需要使用中间表来记录对应关系!

关于这些表的设计参考如下:

ams_admin:管理员表

-- 管理员表:创建数据表drop table if exists ams_admin;create table ams_admin (    id bigint unsigned auto_increment,    username varchar(50) default null unique comment '用户名',    password char(64) default null comment '密码(密文)',    nickname varchar(50) default null comment '昵称',    avatar varchar(255) default null comment '头像URL',    phone varchar(50) default null unique comment '手机号码',    email varchar(50) default null unique comment '电子邮箱',    description varchar(255) default null comment '描述',    is_enable tinyint unsigned default 0 comment '是否启用,1=启用,0=未启用',    last_login_ip varchar(50) default null comment '最后登录IP地址(冗余)',    login_count int unsigned default 0 comment '累计登录次数(冗余)',    gmt_last_login datetime default null comment '最后登录时间(冗余)',    gmt_create datetime default null comment '数据创建时间',    gmt_modified datetime default null comment '数据最后修改时间',    primary key (id)) comment '管理员表' charset utf8mb4;-- 管理员表:插入测试数据insert into ams_admin (username, password, nickname, email, description, is_enable) values    ('root', '1234', 'root', 'root@tedu.cn', '最高管理员', 1),    ('super_admin', '1234', 'administrator', 'admin@tedu.cn', '超级管理员', 1),    ('nobody', '1234', '无名', 'liucs@tedu.cn', null, 0);

ams_role:角色表

-- 角色表:创建数据表drop table if exists ams_role;create table ams_role (    id bigint unsigned auto_increment,    name varchar(50) default null comment '名称',    description varchar(255) default null comment '描述',    sort tinyint unsigned default 0 comment '自定义排序序号',    gmt_create datetime default null comment '数据创建时间',    gmt_modified datetime default null comment '数据最后修改时间',    primary key (id)) comment '角色表' charset utf8mb4;-- 角色表:插入测试数据insert into ams_role (name) values    ('超级管理员'), ('系统管理员'), ('商品管理员'), ('订单管理员');

ams_admin_role:管理员与角色的关联表

-- 管理员角色关联表:创建数据表drop table if exists ams_admin_role;create table ams_admin_role (    id bigint unsigned auto_increment,    admin_id bigint unsigned default null comment '管理员id',    role_id bigint unsigned default null comment '角色id',    gmt_create datetime default null comment '数据创建时间',    gmt_modified datetime default null comment '数据最后修改时间',    primary key (id)) comment '管理员角色关联表' charset utf8mb4;-- 管理员角色关联表:插入测试数据insert into ams_admin_role (admin_id, role_id) values    (1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (2, 4), (3, 3);

ams_permission:权限表

-- 权限表:创建数据表drop table if exists ams_permission;create table ams_permission (    id bigint unsigned auto_increment,    name varchar(50) default null comment '名称',    value varchar(255) default null comment '值',    description varchar(255) default null comment '描述',    sort tinyint unsigned default 0 comment '自定义排序序号',    gmt_create datetime default null comment '数据创建时间',    gmt_modified datetime default null comment '数据最后修改时间',    primary key (id)) comment '权限' charset utf8mb4;-- 权限表:插入测试数据insert into ams_permission (name, value, description) values('商品-商品管理-读取', '/pms/product/read', '读取商品数据,含列表、详情、查询等'),('商品-商品管理-编辑', '/pms/product/update', '修改商品数据'),('商品-商品管理-删除', '/pms/product/delete', '删除商品数据'),('后台管理-管理员-读取', '/ams/admin/read', '读取管理员数据,含列表、详情、查询等'),('后台管理-管理员-编辑', '/ams/admin/update', '编辑管理员数据'),('后台管理-管理员-删除', '/ams/admin/delete', '删除管理员数据');

ams_role_permission:角色与权限的关联表

-- 角色权限关联表:创建数据表drop table if exists ams_role_permission;create table ams_role_permission (    id bigint unsigned auto_increment,    role_id bigint unsigned default null comment '角色id',    permission_id bigint unsigned default null comment '权限id',    gmt_create datetime default null comment '数据创建时间',    gmt_modified datetime default null comment '数据最后修改时间',    primary key (id)) comment '角色权限关联表' charset utf8mb4;-- 角色权限关联表:插入测试数据insert into ams_role_permission (role_id, permission_id) values    (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6),    (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6),    (3, 1), (3, 2), (3, 3);

mall_ams数据库中创建以上数据表,并插入以上测试数据。

练习(单表数据访问):

  • 向权限表中插入新的数据
  • 根据id删除某个权限
  • 查询权限表中的所有权限

提示:

  • 需要新的数据类型,例如Permission类 ctrl+R替换
  • 需要新的接口文件,用于定义以上2个数据访问功能的抽象方法
  • 需要新的XML文件,用于配置抽象方法对应的SQL语句
  • 需要修改配置信息,将此前指定的XML文件由AdminMapper.xml改为*.xml,并把SpringConfig类中sqlSessionFactoryBean()方法的第2个参数由Resource类型改为Resource...类型
  • 当需要测试时,使用新的测试类
    (setting里面搜dialect可以修改方言)

19. 关于关联查询

首先,请准备一些测试数据,使得:存在若干条用户数据,存在若干条角色数据,某个用户存在与角色的关联,最好有些用户有多个关联,又有些用户只有1个关联,还有些用户没有关联。

假设存在需求:根据id查询某用户信息时,也查出该用户归属于哪几种角色。

测试数据参考:

truncate ams_admin;truncate ams_admin_role;truncate ams_role;truncate ams_permission;insert into ams_admin (username, password) values ('admin001', '123456');insert into ams_admin (username, password) values ('admin002', '123456');insert into ams_admin (username, password) values ('admin003', '123456');insert into ams_admin (username, password) values ('admin004', '123456');insert into ams_admin (username, password) values ('admin005', '123456');insert into ams_admin (username, password) values ('admin006', '123456');insert into ams_admin (username, password) values ('admin007', '123456');insert into ams_admin (username, password) values ('admin008', '123456');insert into ams_admin (username, password) values ('admin009', '123456');insert into ams_admin (username, password) values ('admin010', '123456');insert into ams_admin (username, password) values ('admin011', '123456');insert into ams_admin (username, password) values ('admin012', '123456');insert into ams_admin (username, password) values ('admin013', '123456');insert into ams_admin (username, password) values ('admin014', '123456');insert into ams_admin (username, password) values ('admin015', '123456');insert into ams_admin (username, password) values ('admin016', '123456');insert into ams_admin (username, password) values ('admin017', '123456');insert into ams_admin (username, password) values ('admin018', '123456');insert into ams_admin (username, password) values ('admin019', '123456');insert into ams_admin (username, password) values ('admin020', '123456');insert into ams_permission (name, value, description) values('商品-商品管理-读取', '/pms/product/read', '读取商品数据,含列表、详情、查询等'),('商品-商品管理-编辑', '/pms/product/update', '修改商品数据'),('商品-商品管理-删除', '/pms/product/delete', '删除商品数据'),('后台管理-管理员-读取', '/ams/admin/read', '读取管理员数据,含列表、详情、查询等'),('后台管理-管理员-编辑', '/ams/admin/update', '编辑管理员数据'),('后台管理-管理员-删除', '/ams/admin/delete', '删除管理员数据');insert into ams_role (name) values('超级管理员'), ('系统管理员'), ('商品管理员'), ('订单管理员');insert into ams_admin_role (admin_id, role_id) values(1, 1), (1, 2), (1, 3), (1, 4),(2, 1), (2, 2), (2, 3),(3, 1), (3, 2),(4, 1);

本次查询需要执行的SQL语句大致是:

select *from ams_adminleft join ams_admin_role on ams_admin.id=ams_admin_role.admin_idleft join ams_role on ams_admin_role.role_id=ams_role.idwhere ams_admin.id=?

通过测试运行,可以发现(必须基于以上测试数据):

  • 当使用的id值为1时,共查询到4条记录,并且用户的基本信息是相同的,只是与角色关联的数据不同
  • 当使用的id值为2时,共查询到3条记录
  • 当使用的id值为3时,共查询到2条记录
  • 当使用其它有效用户的id时,共查询到1条记录

其实,这种查询期望的结果应该是:

public class xxx {    // 用户基本信息的若干个属性,例如用户名、密码等    // 此用户的若干个角色数据,可以使用 List}

则可以先创建“角色”对应的数据类型:

public class Role {    private Long id;    private String name;    private String description;    private Integer sort;    private LocalDateTime gmtCreate;    private LocalDateTime gmtModified;    // Setters & Getterss    // toString()}

再创建用于封装此次查询结果的类型:

public class AdminDetailsVO {    private Long id;    private String username;    private String password;    private String nickname;    private String avatar;    private String phone;    private String email;    private String description;    private Integer isEnable;    private String lastLoginIp;    private Integer loginCount;    private LocalDateTime gmtLastLogin;    private LocalDateTime gmtCreate;    private LocalDateTime gmtModified;    private List<Role> roles;    // Setters & Getterss    // toString()}

接下来,可以在AdminMapper接口中添加抽象方法:

AdminDetailsVO getDetailsById(Long id);

需要注意,由于此次关联了3张表一起查询,结果集中必然出现某些列的名称是完全相同的,所以,在查询时,不可以使用星号表示字段列表(因为这样的结果集中的列名就是字段名,会出现相同的列名),而是应该至少为其中的一部分相同名称的列定义别名,例如:

select    ams_admin.id,ams_admin.username,ams_admin.password,ams_admin.nickname,ams_admin.avatar,ams_admin.phone,ams_admin.email,ams_admin.description,ams_admin.is_enable,ams_admin.last_login_ip,ams_admin.login_count,ams_admin.gmt_last_login,ams_admin.gmt_create,ams_admin.gmt_modified,ams_role.id AS role_id,ams_role.name AS role_name,ams_role.description AS role_description,ams_role.sort AS role_sort,ams_role.gmt_create AS role_gmt_create,ams_role.gmt_modified AS role_gmt_modifiedfrom ams_adminleft join ams_admin_role on ams_admin.id=ams_admin_role.admin_idleft join ams_role on ams_admin_role.role_id=ams_role.idwhere ams_admin.id=1;

在Mybatis处理中此查询时,并不会那么智能的完成结果集的封装,所以,必须自行配置用于指导Mybatis完成封装!

<resultMap id="DetailsResultMap" type="xx.xx.xx.xx.AdminDetailsVO">            <id column="id" property="id" /><result column="gmt_create" property="gmtCreate" />        <collection property="roles" ofType="xx.xx.xx.Role"> <id column="role_id" property="id" /> <result column="gmt_create" property="gmtCreate" />    </collection></resultMap>

最后,使用以上的查询SQL语句,并使用以上的封装结果即可!

<sql id="DetailsQueryFields">    <if test="true">ams_admin.id,ams_admin.username,ams_admin.password,ams_admin.nickname,ams_admin.avatar,ams_admin.phone,ams_admin.email,ams_admin.description,ams_admin.is_enable,ams_admin.last_login_ip,ams_admin.login_count,ams_admin.gmt_last_login,ams_admin.gmt_create,ams_admin.gmt_modified,ams_role.id AS role_id,ams_role.name AS role_name,ams_role.description AS role_description,ams_role.sort AS role_sort,ams_role.gmt_create AS role_gmt_create,ams_role.gmt_modified AS role_gmt_modified    </if></sql><resultMap id="DetailsResultMap" type="cn.tedu.mybatis.AdminDetailsVO">            <id column="id" property="id" />    <result column="username" property="username" />    <result column="password" property="password" />    <result column="nickname" property="nickname" />    <result column="avatar" property="avatar" />    <result column="phone" property="phone" />    <result column="email" property="email" />    <result column="description" property="description" />    <result column="is_enable" property="isEnable" />    <result column="last_login_ip" property="lastLoginIp" />    <result column="login_count" property="loginCount" />    <result column="gmt_last_login" property="gmtLastLogin" />    <result column="gmt_create" property="gmtCreate" />    <result column="gmt_modified" property="gmtModified" />        <collection property="roles" ofType="cn.tedu.mybatis.Role"> <id column="role_id" property="id" /> <result column="role_name" property="name" /> <result column="role_description" property="description" /> <result column="role_sort" property="sort" /> <result column="role_gmt_create" property="gmtCreate" /> <result column="role_gmt_modified" property="gmtModified" />    </collection></resultMap><select id="getDetailsById" resultMap="DetailsResultMap">    select <include refid="DetailsQueryFields" />    from ams_admin    left join ams_admin_role on ams_admin.id=ams_admin_role.admin_id    left join ams_role on ams_admin_role.role_id=ams_role.id    where ams_admin.id=#{id}</select>

20. Mybatis的缓存机制

缓存:通常是一个临时存储的数据,在未来的某个时间点可能会被删除,通常,存储缓存数据的位置通常是读写效率较高的,相比其它“非缓存”的数据有更高的处理效率。由于缓存的数据通常并不是必须的,则需要额外消耗一定的存储空间,同时由于从缓存获取数据的效率更高,所以是一种牺牲空间、换取时间的做法!另外,你必须知道,从数据库读取数据的效率是非常低下的!

Mybatis有2种缓存机制,分别称之一级缓存和二级缓存,其中,一级缓存是基于SqlSession的缓存,也称之为“会话缓存”,仅当是同一个会话、同一个Mapper、同一个抽象方法(同一个SQL语句)、同样的参数值时有效,一级缓存在集成框架的应用中默认是开启的,且整个过程不由人为控制(如果是自行得到SqlSession后的操作,可自行清理一级缓存),另外,二级缓存默认是全局开启的,它是基于namespace的,所以也称之为“namespace缓存”,需要在配置SQL语句的XML中添加节点,以表示当前XML中的所有查询都允许开通二级缓存,并且,在节点上配置useCache="true",则对应的节点的查询结果将被二级缓存处理,并且,此查询返回的结果的类型必须是实现了Serializable接口的,如果使用了配置如何封装查询结果,则必须使用节点来封装主键的映射,满足以上条件后,二级缓存将可用,只要是当前namespace中查询出来的结果,都会根据所执行的SQL语句及参数进行结果的缓存。无论是一级缓存还是二级缓存,只要数据发生了写操作(增、删、改),缓存数据都将被自动清理。

由于Mybatis的缓存清理机制过于死板,所以,一般在开发实践中并不怎么使用!更多的是使用其它的缓存工具并自行制定缓存策略。

21. 关于Mybatis小结

关于Mybatis框架,你应该:

  • 了解如何创建一个整合了Spring框架的Mybatis工程
  • 了解整合了Spring框架的Mybatis工程的配置
  • 掌握基于spring-test的测试
    • 在测试类上使用@SpringJUnitConfig注解加载Spring的配置文件,则在当前测试中可以自动装配Spring容器中的任何对象
  • 掌握@Sql注解的使用,掌握断言的使用
  • 掌握声明抽象方法的原则:
    • 返回值类型:增删改类型的操作均返回int,表示“受影响的行数”,查询类型操作,根据操作得到的结果集来决定,只要能够放入所有所需的数据即可,通常,统计查询返回int,查询最多1个结果时返回自定义的某个数据类型,查询多个结果时返回List集合类型
    • 方法名称:自定义,不要使用重载,其它命名建议参考此前的笔记
    • 参数列表:根据需要执行的SQL语句中的参数来设计,并且,当需要执行的是插入数据操作时,必须将这些参数进行封装,并在封装的类中添加主键属性,以便于Mybatis能获取自动编号的值回填到此主键属性中,当需要执行的是其它类型的操作时,如果参数数量较多,可以封装,如果只有1个,则直接声明为方法参数,如果超过1个且数量不多,则每个参数之前添加@Param注解
  • 了解使用注解配置SQL语句
  • 掌握使用XML配置SQL语句
    • 这类XML文件需要顶部特殊的声明,所以,通常是从网上下载或通过复制粘贴得到此类文件
    • 根节点必须是,且必须配置namespace,取值为对应的Java接口的全限定名
    • 应该根据要执行的SQL语句不同来选择节点,这些节点都必须配置id属性,取值为对应的抽象方法的名称
      • 其实,节点和节点可以随意替换使用,但不推荐
      • 在不考虑“获取自动生成的主键值”的情况下,也可以混为一谈,但不推荐
      • 当插入数据时,当需要获取自动生成的主键值时,需要在节点上配置useGeneratedKeyskeyProperty属性
      • 节点上,必须配置resultMapresultType属性中的其中1个
  • 掌握使用封装SQL语句片段,并使用进行引用,以实现SQL语句的复用
  • 掌握的配置方式
    • 主键列与属性的映射必须使用节点配置
    • 在1对多、多对多的查询中,集合类型的属性的映射必须使用子节点配置
    • 其它列与属性的映射使用节点配置
    • 在单表查询中,列与属性名一致时,可以不必显式的配置,但是,在关联查询中,即使列与属性名称一致,也必须显式的配置出来
  • 理解resultTyperesultMap使用原则
    • 尽可能的全部使用resultMap,如果查询结果是单一某个数据类型(例如基本数据类型或字符串或某个时间等),则使用resultType
  • 掌握动态SQL中的的使用
  • 大概了解动态SQL中的其它标签
    • 在后续项目中补充
  • 理解#{}${}的区别
  • 了解Mybatis中的一级缓存和二级缓存
    • 有专门的视频讲解