java框架阶段个人总结

框架

一套规范。

实际是他人实现的一系列接口和类的集合。通入导入对应框架的jar文件(maven项目导入对应的依赖),进行适当的配置,就能使用其中的所有内容。

开发者可以省去很多模板代码,如dao中的CRUD,MVC模式下层与层之间的关联。只需要集中精力实现项目中的业务逻辑部分。

Java主流框架

Spring、SpringMVC、MyBatis、MyBatisPlus、Hibernate、JPA等。

SSH:最初是Spring+Stucts2+Hibernate组成,之后Stucts2被SpringMVC取代。

SSM:Spring+SpringMVC+MyBatis

新项目使用SpringBoot,早起的SSH项目由于维护成本高,基本不会推翻重做,但会维护一些SSM项目。

无论是SSH还是SSM,Spring、SpringMVC必不可少。从2004年推出至今,依旧是主流框架中不可获取的一部分。

Spring

概念

一个轻量级开源的Java框架。是一个管理项目中对象的容器,同时也是其他框架的粘合器,目的就是对项目进行解耦。

轻量级:对原有代码的侵入很小。

Spring的核心是IOC控制反转和AOP面向切面编程

组成

名词解释

IOC

Inversion Of Control 控制反转

DI

Dependency Injection 依赖注入

举例说明

用代码描述场景:员工食堂每天提供事物

  • 食物:米饭类

    public class Rice{
        public Rice(){
            sout("今天吃米饭");
        }
    }
  • 食物:面条类

    public class Noodles{
        public Noodles(){
            sout("今天吃面条");
        }
    }
  • 厨师类

    public class Cook{
        //定义一个做饭的方法,做什么饭new什么对象,创建食物对象的权限,是由当前厨师类决定。
        public void cooking(){
            //new Rice();
            new Noodles();
        }
    }
  • 员工:main方法

    psvm(){
        Cook cook = new Cook();
        cook.cooking();
    }

这种方式,Cook类中的cooking()方法创建什么对象,就输出什么内容。(厨师做什么,员工就吃什么)。

如果有人想要吃指定食物,就要修改源代码cooking()方法中创建的对象。

解决方案:定义一个接口:Food

public interface Food{
    void info();
}

让原本的所有食物Rice类和Noodles类实现该接口

public class Rice implements Food{
    @Override
    public void info(){
        sout("今天吃米饭");
    }
}
public class Noodles implements Food{
    @Override
    public void info(){
        sout("今天吃面条");
    }
}

给厨师Cook类中cooking()方法定义一个参数:Food接口

public class Cook{
    public void cooking(Food food){
        food.info();
    }
}

这时Cook对象调用cooking()方法时,需要提供一个Food接口类型的实现类。

psvm(){
    Cook cook = new Cook();
    //传递什么参数,旧调用该参数重写后的方法
    cook.cooking(new Rice());
    cook.cooking(new Noodles());
}

整个过程中,将创建什么食物对象的控制权,由厨师交给用户,这就是控制反转(IOC)。

厨师对象调用cooking()方法的参数,就是对于食物Food对象的依赖,是由用户注入进来的,这就是依赖注入(DI)。

这样一来,各个对象之间互相独立(没有在某个类中new另一个类的对象),降低了代码之间的耦合度。

总结:控制反转(IOC)是一种思想,就是让创建对象的控制权由自身交给第三方,控制反转这种思想,通过依赖注入(DI)的方式实现。

IOC和DI其实都是在描述控制反转,IOC是思想,DI是具体实现方式。

这里的第三方,就是Spring。Spring是一个容器,可以管理所有对象的创建和他们之间的依赖关系。

可以理解为:"Spring就是用来管理对象的,在需要用到某个对象的时候,帮我们自动创建"。

如Servlet+JSP模式写Web项目时,会在控制层Servlet中创建业务逻辑层Service对象,在Service层中创建数据访问层Dao对象。

有了Spring后,就不会出现new这些对象的代码了。

Spring需要导入对应的jar文件后,定义一个配置文件,在该配置文件中配置程序运行过程中所需的对象。

AOP

Aspect Orintend Programming 面向切面编程

Spring控制台应用

1.创建一个普通的Maven项目,不选择模板

2.添加Spring核心依赖

jdk8用5.x版本

<!-- spring-context表示spring核心容器 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.23</version>
</dependency>

3.创建一个Java类

package com.hqyj.spring01;
/*
* 定义一个普通的Java类
* PlainOrdinaryJavaObject pojo  相当于简化的javabean
* entity  vo  dto  pojo  都是在描述实体类
* */
public class PlainOrdinaryJavaObject {
    public void fun(){
        System.out.println("一个PlainOrdinaryJavaObject(普通的Java类)对象");
    }
}

4.创建Spring配置文件

创建一个Spring的配置文件,在其中注入上一步创建的类的对象

在resources目录下,创建一个xml文件,选择spring config,通常命名为application

<?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标签表示,在Spring容器中,注入某个类的对象-->
    <!--class属性表示要注入哪个类,写类的全限定名(包名+类名)-->
    <!--id表示给类的对象的名称-->
    <bean class="com.hqyj.spring01.PlainOrdinaryJavaObject" id="pojo"></bean>
   
</beans>

5.创建main方法所在类

解析Spring配置文件,获取注入的对象。

package com.hqyj.spring01;
​
import org.springframework.context.support.ClassPathXmlApplicationContext;
​
public class Main {
    public static void main(String[] args) {
        //创建一个自定义类的对象
        //传统方式创建对象
        //PlainOrdinaryJavaObject pojo = new PlainOrdinaryJavaObject();
​
        //使用spring容器获取对象
​
        //创建一个用于解析Spring配置文件的对象,参数为配置文件的名称。实际就是获取Spring容器
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
​
        //获取容器中的对象
        /*
        只通过名称获取,返回值为Object类型,需要转换
        Object obj = context.getBean("pojo");
        PlainOrdinaryJavaObject pojo=(PlainOrdinaryJavaObject) obj;
         */
        //使用这种方式,直接用指定类型接收
        PlainOrdinaryJavaObject pojo = context.getBean("pojo", PlainOrdinaryJavaObject.class);
        //能成功调用方法,说明Spring管理了该类的对象
        pojo.fun();
        //关闭Spring容器
        context.close();
    }
}

思考

  • 1.对象在什么时候创建?

    默认情况下,在解析Spring配置文件的时候,自动创建一个对象。

  • 2.如果不想在解析时自动创建怎么办?

    在配置文件的某个<bean>标签中,添加一个"lazy-init=true"属性,表示该对象设置为懒加载,

    在初始化Spring容器时不会创建对象,只有在调用getBean()时才会创建对象

  • 3.如果设置为懒加载,是不是每次调用getBean(),都会创建一个对象呢?

    默认情况下,Spring容器只会创建一个对象。

    在配置文件的某个<bean>标签中,添加一个"scope='prototype'"属性,表示每次调用getBean()方法,就会创建一个对象。

    该属性的值默认为"singleton",表示单例模式,只会创建一个对象。

总结:默认情况下,通过<bean>标签定义的类,在Spring容器初始化时,创建一个对象。

bean标签常用属性

属性作用
class定义类的全限定名
id定义对象的名称
lazy-init是否为懒加载。默认值为false,在解析配置文件时就会创建对象。设置为true表示懒加载,只有在getBean()时才会创建对象。
scope单例/原型模式。默认值为singleton,表示单例模式,只会创建一个对象。设置为prototype,表示原型模式,每调getBean()就创建一个对象。
init-method初始化时触发的方法。在创建完该对象时自动调用的方法。该方法只能是无参方法,该属性的值只需要写方法名即可
destory-method销毁时触发的方法。Spring容器关闭时自动调用的方法,该方法只能是无参方法。只有在单例模式下有效。

属性注入

给某个bean添加属性的方式有两种:构造器注入和setter注入

setter注入

这种方式注入属性时,类中必须要有set方法

在bean标签中,加入<property></property>标签,

该标签的name属性通常表示该对象的某个属性名,但实际是setXXX()方法中的XXX单词。

如有age属性,但get方法为getNianLing(),name属性就需要写成nianLing。

该标签的value属性表示给该类中的某个属性赋值,该属性的类型为原始类型或String

该标签的ref属性表示给该类中除String以外的引用类型属性赋值,值为Spring容器中另一个bean的id。

<!--注入Car类对象并用set方式注入其属性-->
<bean class="com.hqyj.spring01.Car" id="c">
    <!--该属性是字符串或原始类型,使用value赋值-->
    <property name="brand" value="宝马"></property>
    <!--name并不是类中是属性名,而是该属性对应的getXXX()方法中XXX的名称-->
    <!--如Car类中有color属性,但get方法名为getColo(),这里就要写为colo-->
    <property name="colo" value="白色"></property>
</bean>
​
<!--注入Person类对象并用set方式注入其属性-->
<bean class="com.hqyj.spring01.Person" id="p1">
    <property name="name" value="王海"></property>
    <property name="age" value="22"></property>
    <!--属性是引用类型,需要通过ref赋值,值为另外的bean的id ref即references-->
    <property name="car" ref="c"></property>
</bean>

构造方法注入

这种方式注入属性时,类中必须要有相应的构造方法

在bean标签中,加入<constructor-arg></constructor-arg>标签,

该标签的name属性表示构造方法的参数名,index属性表示构造方法的参数索引。

赋值时,原始类型和字符串用value,引用类型用ref。

<!--注入Person类对象并用构造方法注入其属性-->
<bean class="com.hqyj.spring01.Person" id="p2">
    <!--constructor-arg表示构造方法参数  name是参数名 index是参数索引-->
    <constructor-arg name="name" value="张明"></constructor-arg>
    <constructor-arg index="1" value="20"></constructor-arg>
    <constructor-arg name="car" ref="c"></constructor-arg>
</bean>

复杂属性注入

/*
* 定义电影类
* */
public class Movie {
    //电影名
    private String movieName;
    //导演
    private String director;
    //时长
    private int duration;
    //主演
    private List<String> playerList;
    //类型
    private String movieType;
    //放映时间,最终格式为yyyy/MM/dd HH:mm:ss
    private String showTime;
}

List类型的属性

<!--注入Movie对象-->
<bean class="com.hqyj.spring01.Movie" id="movie1">
    <property name="movieName" value="夏洛特烦恼"></property>
    <property name="director" value="闫非、彭大魔"></property>
    <property name="duration" value="87"></property>
    <!--List类型属性赋值-->
    <property name="playerList" >
        <!--使用list标签-->
        <list>
            <!--如果集合中保存的是引用类型,使用ref标签-->
            <!--如果集合中保存的是原始类型或字符串,使用value标签-->
            <value>沈腾</value>
            <value>艾伦</value>
            <value>马丽</value>
        </list>
    </property>
    <property name="movieType" value="喜剧"></property>
    <property name="showTime" value="2010/06/01 0:0:0"></property>
</bean>

Set类型的属性

<!--注入PetShop对象-->
<bean class="com.hqyj.spring01.test3.PetShop" id="petShop">
    <property name="shopName" value="xxx宠物店"></property>
​
    <property name="petSet">
        <!--Set类型的属性-->
        <set>
            <!--如果中保存的是原始类型或字符串,使用value子标签-->
            <!--如果保存的是引用类型,使用ref子标签加bean属性设置对应的id-->
            <ref bean="p1"></ref>
            <ref bean="p2"></ref>
            <ref bean="p3"></ref>
        </set>
    </property>
</bean>

Map类型的属性

<!--注入Cinema对象-->
<bean class="com.hqyj.spring01.Cinema" id="cinema">
    <property name="name" value="万达影城"></property>
    <property name="timeTable">
        <!--Map类型属性赋值,标明键和值的类型-->
        <map key-type="java.lang.Integer" value-type="com.hqyj.spring01.Movie">
            <!--entry标签表示键值对 如果键值对都是原始类型或字符串,使用key和value-->
            <!--如果键值对中有引用类型,使用key-ref或value-ref-->
            <entry key="1" value-ref="movie1"></entry>
            <entry key="2" value-ref="movie2"></entry>
        </map>
    </property>
</bean>

属性值如果通过某个方法调用而来

如使用String保存yyyy/MM/dd格式的日期,需要通过SimpleDateFormat对象调用parse()方法而来

<!--
       SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
   -->
<!--注入SimpleDateFormat对象,设置日期格式-->
<bean class="java.text.SimpleDateFormat" id="sdf">
    <!--如果构造方法注入时,该构造方法只有一个参数,可以不用写name或index-->
    <constructor-arg value="yyyy/MM/dd"></constructor-arg>
</bean>
<!--
        Date now = new Date();
    -->
<!--注入Date对象-->
<bean class="java.util.Date" id="now"></bean>
​
<!--注入Pet对象-->
<bean class="com.hqyj.spring01.test3.Pet" id="p1">
    <property name="petType" value="哈士奇"></property>
    <property name="petNickName" value="小哈"></property>
    <!--使用当前时间作为该属性的值,以yyyy/MM/dd-->
    <!--
            sdf.format(now);
        -->
    <property name="petBirthday">
        <!--如果某个值是通过某个bean调用了某个方法而来-->
        <bean factory-bean="sdf" factory-method="format">
            <!--方法的实际参数-->
            <constructor-arg ref="now"></constructor-arg>
        </bean>
    </property>
</bean>

属性自动注入autowire

以上所有案例中,如果要在A对象中注入一个引用类型的对象B,都是手动将对象B注入到对象A中。

如在Person中注入Car对象,在Cinema中注入Movie等。

这种情况下,如果当某个被注入的bean的id更改后,所有引用了该bean的地方都要进行修改。

所以将手动注入更改为自动注入(自动装配),就无需添加相应的<property>标签,甚至可以无需定义bean的id。

实现自动注入

在某个bean标签中,加入autowire属性,设置值为"byName"或"byType"。通常设置为"byType"。

Car类

package com.hqyj.spring01.test1;
​
import java.util.HashMap;
​
public class Car {
    private String brand;
    private String color;
​
    @Override
    public String toString() {
        return "Car{" +
                "brand='" + brand + '\'' +
                ", color='" + color + '\'' +
                '}';
    }
​
    public String getBrand() {
        return brand;
    }
​
    public void setBrand(String brand) {
        this.brand = brand;
    }
​
    public String getColor() {
        return color;
    }
​
    public void setColor(String color) {
        this.color = color;
    }
}

Person类

package com.hqyj.spring01.test1;
​
public class Person {
    private String name;
    private int age;
    private Car car;
​
    @Override
    public String toString() {
        return "Person{" +
            "name='" + name + '\'' +
            ", age=" + age +
            ", car=" + car +
            '}';
    }
​
    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 Car getCar() {
        return car;
    }
    //自动注入时,必须要有该方法
    //byType方式自动注入,会自动在容器中寻找该方法参数类型
    //byName方式自动注入,会自动在容器中寻找该方法setCar中的car这个id
    public void setCar(Car car) {
        this.car = car;
    }
}

配置文件

<?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">
​
    <!--在容器中注入Car类型的bean-->
    <bean class="com.hqyj.spring01.test1.Car" id="car"> 
        <property name="brand" value="宝马"></property>
        <property name="colo" value="白色"></property>
    </bean>
​
​
​
    <!--注入Person类的bean-->
    <!--autowire="byType"表示自动检测该类中是否需要使用引用类型参数,如果容器中正好有唯一的一个对应类型的bean,就会自动赋值给对应的属性-->
    <bean class="com.hqyj.spring01.test1.Person" id="person" autowire="byType">
        <property name="name" value="赵明"></property>
        <property name="age" value="26"></property>
    </bean>
    
     <!--autowire="byName"表示自动检测该类中的setXXX方法,如果某个setXXX方法的XXX和容器某个对象的id对应,就会自动赋值给对应的属性-->
    <bean class="com.hqyj.spring01.test1.Person" id="person2" autowire="byName">
        <property name="name" value="王海"></property>
        <property name="age" value="26"></property>
    </bean>
</beans>

Main

package com.hqyj.spring01.test1;
​
import org.springframework.context.support.ClassPathXmlApplicationContext;
​
public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
        Person person = context.getBean("person", Person.class);
        //此时打印时,会输出姓名、年龄和Car对象
        System.out.println(person);
        context.close();
    }
}

autowire属性的值

  • byType

    • 类中要有被注入的属性的setter()方法

    • 被自动注入的对象可以没有id

    • Spring容器中,某个对象的类型要与该setter()方法的参数类型一致,且容器中只有一个该类型的对象。

      • 如setCar(Car c),Spring就会自动在容器中寻找类型为Car的对象自动装配

  • byName

    • 类中要有被注入的属性的setter()方法

    • 被自动注入的对象必须要有id

    • 实际是根据setXXX()方法set后的单词XXX关联

      • 如setCar(Car c),Spring就会自动在容器中寻找id为car的对象自动装配

在Web项目中,可以利用自动装配,在控制层中自动装配业务逻辑层的对象,在业务逻辑层中自动装配数据访问层的对象。

属性自动注入练习

在电影院案例的基础上

1.创建一个dao层CinemaDao类,包含Cinema对象,定义一个fun()方法,输出Cinema对象

package com.hqyj.spring01.test2.dao;
​
import com.hqyj.spring01.test2.Cinema;
​
/*
* 定义数据访问层
* 模拟查询数据库得到结果
* */
public class CinemaDao {
​
    private Cinema cinema;
​
    public void setCinema(Cinema cinema) {
        this.cinema = cinema;
    }
​
    public void fun(){
        System.out.println(cinema);
    }
}

2.创建一个controller层CinemaController类,包含CinemaDao对象,定义方法fun()调用CinemaDao中的fun()方法

package com.hqyj.spring01.test2.controller;
​
import com.hqyj.spring01.test2.dao.CinemaDao;
import org.springframework.context.support.ClassPathXmlApplicationContext;
​
/*
* 定义控制层类,该层需要使用下一层dao中的方法
* */
public class CinemaController {
    //这里无需创建对象,让Spring自动注入CinemaDao对象
    private CinemaDao cinemaDao;
​
    public void setCinemaDao(CinemaDao dao) {
        this.cinemaDao = dao;
    }
​
    public void fun(){
        cinemaDao.fun();
    }
}

最终在Main方法中获取Controller对象后,调用fun()

public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("movie.xml");
    CinemaController controller = context.getBean("controller", CinemaController.class);
    controller.fun();
}

配置文件

<!--省略movie对象-->
​
<!--注入Cinema对象-->
<bean class="com.hqyj.spring01.test2.Cinema" id="cinema">
    <property name="name" value="万达影城"></property>
    <property name="timeTable">
        <!--Map类型属性赋值,标明键和值的类型-->
        <map key-type="java.lang.Integer" value-type="com.hqyj.spring01.test2.Movie">
            <!--entry标签表示键值对 如果键值对都是原始类型或字符串,使用key和value-->
            <!--如果键值对中有引用类型,使用key-ref或value-ref-->
            <entry key="1" value-ref="movie1"></entry>
            <entry key="2" value-ref="movie2"></entry>
        </map>
    </property>
</bean>
​
<!--该类中有一个Cinema类型的属性,这里使用了autowire=byType自动装配,会在容器中寻找Cinema类型的对象自动赋值给属性-->
<bean class="com.hqyj.spring01.test2.dao.CinemaDao" autowire="byType"></bean>
​
<!--该类中有一个CinemaDao类型的属性,这里使用了autowire=byType自动装配,会在容器中寻找CinemaDao类型的对象自动赋值给属性-->
<bean class="com.hqyj.spring01.test2.controller.CinemaController" id="controller" autowire="byType"></bean>

Spring核心注解

在Spring配置文件中加入

<!--设置要扫描的包,扫描这个包下所有使用了注解的类-->
<context:component-scan base-package="com.hqyj.spring02.bookSystem"></context:component-scan>

类上加的注解

  • @Component

    • 当一个类不好归纳时,定义为普通组件

  • @Controller

    • 定义一个类为控制层组件

  • @Service

    • 定义一个类为业务层组件

  • @Repository

    • 定义一个类为持久层(数组访问层)组件

  • @Lazy/@Lazy(value=true)

    • 设置该类为懒加载。

  • @Scope(value="singleton/prototype")

    • 设置为单例/原型模式。

说明

以上注解公共特点

  • 都是将对应类的对象注入到Spring容器中,用于替换配置文件中的bean标签

  • 都默认是单例模式非懒加载

  • 默认注入的对象id为当前类的类名首字母小写形式

    • 如在BookDao类上添加,id默认为bookDao

  • 可以通过注解的value属性自定义注入的对象的id名,如@Component(value="key")表示注入的对象id为key

属性上加的注解

  • @Autowired

    • 优先使用byType方式从Spring容器中获取对应类型的对象自动装配。先检索Spring容器中对应类型对象的数量,如果数量为0直接报错;数量为1直接装配

      数量大于1,会再尝试使用byName方式获取对应id的对象,但要配合@Qualifier(value="某个对象的id")一起使用,指定id进行装配

  • @Qualifier(value="某个对象的id")

    • 配合@Autowired注解,使用byName方式获取某个对象id的bean进行装配

  • @Resource(name="某个对象的id")

    • 该注解相当于@Autowired+@Qualifier(value="某个对象的id")

    • 优先使用byName方式,从Spring容器中检索name为指定名的对象进行装配,如果没有则尝试使用byType方式,要求对象有且只有一个,否则也会报错。

说明

  • 如果要在某个类中使用Spring容器中的某个对象时,只需定义成员变量,无需创建对象,通过@Autowired或@Resource注解进行自动装配

  • 实际开发中,绝大部分情况下,需要自动装配对象有且只有一个,并且命名规范,所以@Autowired或@Resource区别不是很大。@Autowired优先使用byType方式,@Resource优先使用byName方式

  • 如果@Resource不能使用,是因为缺少javax.annotation包,需要引入对应依赖

    <dependency>
        <groupId>javax.annotation</groupId>
        <artifactId>javax.annotation-api</artifactId>
        <version>1.3.2</version>
    </dependency>

在Web项目中使用Spring

1.创建基于Maven的web-app项目

2.添加依赖

<!--servlet-->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
</dependency>
<!--spring容器-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.23</version>
</dependency>
<!--web集成spring-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.3.23</version>
</dependency>

3.在main目录下创建java和resources目录,修改web.xml版本为4.0

4.在resources目录下创建Spring配置文件application.xml,扫描使用了注解的根包

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
​
    <!--扫描使用了Spring注解的根包-->
    <context:component-scan base-package="com.hqyj.springweb"></context:component-scan>
</beans>

5.创建一个类,使用@Componet注解将其注入到Spring容器中

package com.hqyj.springweb.entity;
​
import org.springframework.stereotype.Component;
​
@Component
public class Pojo {
    public void fun(){
        System.out.println("hello springweb!");
    }
}

如何初始化Spring容器(解析Spring配置文件)

在控制台应用程序中,可以在main方法中通过ClassPathXmlApplicationContext来解析Spring配置文件,初始化Spring容器。

在web项目中没有main方法,只有servlet中的service方法,如果在service方法中创建ClassPathXmlApplicationContext对象,会每次访问都执行。

而Spring容器只需初始化一次,在项目启动时就解析Spring配置文件,全局监听器就是一个很好的选择。

spring-web包中提供了一个ContextLoaderListener类,它实现了ServletContextListener,属于项目级别的全局监听器。

这个类需要一个contextConfigLocation参数,表示要解析的Spring配置文件的路径。

这个监听器会在项目启动时,读取指定的Spring配置文件路径,并且创建WebApplicationContext对象,即Spring容器。

6.在web.xml中配置监听器用于初始化Spring容器

<?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_4_0.xsd"
         version="4.0">
​
    <!--配置监听器ContextLoaderListener-->
    <listener>
        <!--监听器全限定名-->
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <!--定义全局参数contextConfigLocation用于读取Spring配置文件-->
    <context-param>
        <!--参数名固定contextConfigLocation-->
        <param-name>contextConfigLocation</param-name>
        <!--只是Spring配置文件的路径 classpath:表示从根目录出发-->
        <param-value>classpath:application.xml</param-value>
    </context-param>
</web-app>

7.创建一个Servlet,访问该Servlet,获取Spring容器,从容器中获取注入的对象

package com.hqyj.springweb.controller;
​
import com.hqyj.springweb.entity.Pojo;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
​
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
​
@WebServlet("/hello")
public class MyServlet extends HttpServlet {
​
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //获取Spring容器
        WebApplicationContext app = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        //从容器中获取某个bean
        Pojo pojo = app.getBean("pojo", Pojo.class);
        pojo.fun();
    }
}

在Spring-web项目中使用Spring-jdbc

Spring提供了一套自己的JDBC解决方案,相较于自己写的JDBC,省去了繁琐的过程(获取连接、关闭),简化了对数据库的操作。

Spring-jdbc后续会用其他持久层框架代替,如MyBatis、JPA。

1.引入相关依赖

当前案例中核心的依赖是 spring-jdbc

<!--servlet-->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
</dependency>
<!--mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.31</version>
</dependency>
<!--jstl-->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>
<!--spring容器-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.23</version>
</dependency>
<!--web集成spring-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.3.23</version>
</dependency>
<!-- spring-jdbc -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.3.23</version>
</dependency>

2.创建包结构(controller、service、dao、entity),创建实体类

public class Hero {
    private int id;
    private String name;
    private String position;
    private String sex;
    private int price;
    private String shelfDate;
    //省略get/set、构造方法、toString等
}

3.在Spring配置文件中配置数据源和JDBC模板

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
​
    <!--扫描使用了Spring注解的根包-->
    <context:component-scan base-package="com.hqyj.springweb"></context:component-scan>
​
    <!--配置数据源-->
    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
        <!--配置MySQL驱动-->
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
        <!--配置MySQL地址-->
        <property name="url" value="jdbc:mysql://localhost:3306/gamedb?serverTimezone=Asia/Shanghai"></property>
        <property name="username" value="root"></property>
        <property name="password" value="root"></property>
    </bean>
​
    <!--配置JDBC模板-->
    <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
        <!--设置要操作的数据源-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
</beans>

4.数据访问层(dao)的写法

package com.hqyj.springweb.dao;
​
import com.hqyj.springweb.entity.Hero;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
​
import java.util.List;
​
@Repository
public class HeroDao {
​
    @Autowired
    //通过jdbc模板对象实现CRUD
    private JdbcTemplate jdbcTemplate;
​
    /*
     * 查询所有
     * */
    public List<Hero> queryAll() {
        //定义sql语句
        String sql = "select * from hero";
        //BeanPropertyRowMapper对象表示将查询到的字段与指定的类中的属性进行映射
        BeanPropertyRowMapper<Hero> mapper = new BeanPropertyRowMapper<>(Hero.class);
        //query()方法执行查询
        List<Hero> list = jdbcTemplate.query(sql, mapper);
        return list;
    }
​
    /*
     * 删除
     * */
    public boolean delete(int id) {
        String sql = "delete from hero where id=?";
        int i = jdbcTemplate.update(sql, id);
        return i > 0;
    }
​
    /*
     * 修改
     * */
    public boolean update(Hero hero) {
        String sql = "update hero set name=?,sex=?,position=?,price=?,shelf_date=? where id=?";
        int i = jdbcTemplate.update(sql, hero.getName(), hero.getSex(), hero.getPosition(), hero.getPrice(), hero.getShelfDate(), hero.getId());
        return i > 0;
    }
​
    /*
     * 添加
     * */
    public boolean insert(Hero hero) {
        String sql = "insert  into hero values(null,?,?,?,?,?)";
        int i = jdbcTemplate.update(sql, hero.getName(), hero.getSex(), hero.getPosition(), hero.getPrice(), hero.getShelfDate());
        return i > 0;
    }
​
    /*
     * 根据id查询
     * */
    public Hero findById(int id) {
        String sql = "select * from hero where id=?";
        BeanPropertyRowMapper<Hero> mapper = new BeanPropertyRowMapper<>(Hero.class);
        Hero hero = jdbcTemplate.queryForObject(sql, mapper, id);
        return hero;
    }
}

忽略后续service、servlet、jsp等;

JDBCTemplate常用方法

方法作用说明
query(String sql,RowMapper mapper)无条件查询返回值为List集合
update(String sql)无条件更新(删除、修改)返回值为受影响的行数
query(String sql,RowMapper mapper,Object... objs)条件查询可变参数为?的值
update(String sql,Object... objs)条件更新(增加、删除、修改)可变参数为?的值
queryForObject(String sql,RowMapper mapper)无条件查询单个对象返回值为指定对象
queryForObject(String sql,RowMapper mapper,Object... objs)条件查询单个对象返回值为指定对象
execute(String sql)执行指定的sql无返回值

AOP

概念

Process Oriented Programming 面向过程编程POP

Object Oriented Programming 面向对象编程OOP

Aspect Oriented Programming 面向切面编程AOP

以上都是编程思想,但AOP不是OOP和POP的替代,而是增强、拓展和延伸。主流编程思想依然是OOP。

作用

在传统的OOP思想中,我们将程序分解为不同层次的对象,通过封装、继承、多态等特性,

将对象组织成一个整体来完成功能。但在某些场景下,OOP会暴露出一些问题。

如在处理业务中,除了核心的业务代码外,通常还会添加一些如果参数验证、异常处理、事务、记录日志等操作。

这些内容会分散在各个业务逻辑中,依旧会出现大量重复操作。如果将这些重复的代码提取出来,在程序编译运行时,

再将提出来的内容应用到需要执行的地方,就可以减少很多代码量。方便统一管理,更专注于核心业务。

简单来说,就是将不同位置中重复出现的一些事情拦截到一处进行统一处理。

如图,这是用户模块的不同操作,其中除了核心业务之外,其余都是重复代码

将相同的位置进行切分

切分后

使用

一个普通业务层service类

package com.hqyj.springweb.service;
​
import com.hqyj.springweb.dao.HeroDao;
import com.hqyj.springweb.entity.Hero;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
​
import java.util.List;
​
/*
 * 一个普通业务层service类
 * */
@Service
public class HeroService {
    @Autowired
    private HeroDao dao;
​
​
    public List<Hero> queryAll() {
        //在没有使用AOP时,比如要在执行前记录日志
        //System.out.println("============");
        //System.out.println("操作开始执行"+new Date());
​
        System.out.println("xxx进行了查询操作");
        //核心业务代码
        List<Hero> list=dao.queryAll();
​
        //在没有使用AOP时,比如要在执行后记录日志
        //System.out.println("操作执行结束"+new Date());
        //System.out.println("============");
        return list;
    }
​
    public void delete(int id) {
​
        //在没有使用AOP时,比如要在执行前记录日志
        //System.out.println("============");
        //System.out.println("操作开始执行"+new Date());
​
        System.out.println("xxx删除了"+id);
        //核心业务代码
        dao.delete(id);
​
        //在没有使用AOP时,比如要在执行后记录日志
        //System.out.println("操作执行结束"+new Date());
        //System.out.println("============");
​
    }
​
}
​

提取在执行某个业务方法前后要执行的公共代码

package com.hqyj.springweb;
​
import org.springframework.stereotype.Component;
​
import java.util.Date;
​
/*
* 模拟各个业务中都要执行的记录日志的操作,单独定义在一个类中
* 将其注入到Spring容器中
* */
@Component
public class LogOperate {
​
    public void start(){
        System.out.println("============");
        System.out.println("操作开始执行"+new Date());
    }
​
​
    public void end(){
        System.out.println("操作执行结束"+new Date());
        System.out.println("============");
    }
}
​

在项目中添加AOP所需依赖

<!-- 切面织入包 -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.19</version>
    <scope>runtime</scope>
</dependency>

在Spring配置文件中配置AOP

<!--配置AOP-->
<aop:config>
    <!--配置切面(配置拦截后要做的事情)-->
    <!--ref是提取出的公共代码所在类LogOperate在Spring容器中的id。由于使用了@Componet注解,默认id名为类名首字母小写-->
    <aop:aspect ref="logOperate">
        <!--配置切点(对哪个包中的哪个类、哪个方法执行的时候进行拦截)-->
        <!--id是切点名,自定义-->
        <aop:pointcut id="logPointcut" expression="execution(* com.hqyj.springweb.service..*.*(..))"/>
        <!--配置前置增强(切点之前执行的方法)-->
        <aop:before method="start" pointcut-ref="logPointcut"></aop:before>
        <!--配置后置增强(切点之后执行的方法)-->
        <aop:after method="end" pointcut-ref="logPointcut"></aop:after>
    </aop:aspect>
</aop:config>

上面的配置代码中execution是切点指示符,小括号中是一个切点表达式,用于配置需要切入后增强处理的方法的特征

execution(* com.hqyj.springweb.service..*.*(..))
第一个*表示匹配所有方法的返回值
com.hqyj.springweb.service  表示匹配指定包
..* 表示匹配该包下的所有子包及所有类
.* 表示匹配该类下的所有方法
(..) 表示匹配所有参数类型和个数
​
整体表示在com.hqyj.springweb.service包中的一切类中的一切方法在执行前后,都会进入切入增强处理。

MVC

MVC设计思想并不是某个语言特有的设计思想,而是一种通用的模式。

是将一个应用分为三个组成部分:Model模型,View视图,Controller控制器

这样会降低系统的耦合度,提高它的可扩展性和维护性。

SpringMVC

在Web阶段中,控制层是由Servlet实现,传统的Servlet,需要创建、重写方法、配置映射。使用时极不方便,SpringMVC可以替换Servlet

SpringMVC是Spring框架中位于Web开发中的一个模块,是Spring基于MVC设计模式设计的轻量级Web框架。

SpringMVC提供了一个DispatcherServlet的类,是一个Servlet。它在指定映射(通常设置为/或*.do)接收某个请求后,调用相应的模型处理得到结果,再通过视图解析器,跳转到指定页面,将结果进行渲染。

原理大致为:配置SpringMVC中的DispatcherServlet将其映射设置为/或*.do。

如果是/表示一切请求先经过它,如果是*.do表示以.do结尾的请求先经过它,

它对该请求进行解析,指定某个Controller中的某个方法,这些方法通常返回一个字符串,

这个字符串是一个页面的名称,再通过视图解析器,将该字符串解析为某个视图的名称,跳转到该视图页面。

详细流程

使用SpringMVC

1.创建webapp项目

  • 修改web.xml版本为4.0

  • 创建java和resources目录

  • 创建包结构

2.添加依赖

<!-- spring-webmvc -->
<!-- 这个依赖会包含spring-web和spring-context -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.3.23</version>
</dependency>

3.配置初始化Spring

  • 在resources目录下创建Spring配置文件application.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    ​
        <!--设置扫描使用了注解的根包-->
        <context:component-scan base-package="com.hqyj.springmvc"></context:component-scan>
    </beans>
  • 在web.xml中使用监听器ContextLoaderListener初始化Spring

    <?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_4_0.xsd"
             version="4.0">
    ​
        <!--配置全局监听器初始化Spring-->
        <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
        </listener>
        <!--定义全局参数读取Spring配置文件-->
        <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:application.xml</param-value>
        </context-param>
    </web-app>

4.配置SpringMVC

  • 在resources目录下创建配置SpringMVC的配置文件springmvc.xml

    • 配置要扫描的控制层类所在的包名

    • 配置内部资源视图解析器以及视图路径的前后缀

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    ​
    ​
        <!--扫描控制层所在的包-->
        <context:component-scan base-package="com.hqyj.springmvc.controller"></context:component-scan>
    ​
        <!--配置内部资源视图解析器-->
        <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <!--最终控制层跳转的页面所在的路径及页面自身后缀名-->
            <!--jsp页面不建议直接通过浏览器访问。在WEB-INF目录下在资源,无法通过浏览器直接方法,所以将jsp保存在WEB-INF目录下,最好创建一个pages-->
            <property name="prefix" value="/WEB-INF/pages/"></property>
            <!--现阶段使用jsp输出数据,所以后缀为.jsp-->
            <property name="suffix" value=".jsp"></property>
        </bean>
    </beans>
  • 在web.xml中配置DispatcherServlet

    • 将该Servlet的请求映射设置为/,表示所有请求都会访问该Servlet,由该Servlet再进行分发

    <!--配置DispatcherServlet-->
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--设置该Servlet的初始化参数,用于读取SpringMVC配置文件-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <!--设置该servlet的映射为/或*.do-->
        <url-pattern>/</url-pattern>
    </servlet-mapping>

5.在WEB-INF目录下创建一个pages目录,在其中创建一个welcome.jsp页面

  • 通常jsp页面不允许被浏览器直接访问,需要保存在WEB-INF目录下

6.编写控制层代码

  • 在controller包下创建一个类,加上@Controller注解

  • 该类中定义的方法方法加入@RequestMapping()注解表示访问该方法的映射

  • 该类中定义的方法返回值通常为字符串,表示某个页面的名称,也可以是另一个controller中的方法映射名

    package com.hqyj.springmvc.controller;
    ​
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    ​
    //加入@Controller注解表示该类是一个控制层类,替换之前的servlet
    @Controller
    //该注解如果只设置请求映射,直接填字符串
    @RequestMapping("/first")
    public class HelloController {
    ​
        //该注解如果还有其他参数要设置,路径用path赋值
        @RequestMapping(path="/hello")
        public String hello(){
            //返回的字符串是某个页面的名称或另一个控制层中方法的请求映射
            return "welcome";
        }
    }

将项目部署到tomcat后,访问http://localhost:8080/SpringMVC_war_exploded/first/hello,即可跳转到指定页面

SpringMVC相关注解

  • @Controller

    • 只能写在类上,表示该类属于一个控制器

  • @RequestMapping("/请求映射名")/@RequestMapping(value="/请求映射名")/@RequestMapping(path="/请求映射名")

    • 该注解可以写在类或方法上。写在类上用于区分功能模块,写在类上用于区分具体功能

    • 默认写一个属性或value或path后的值,都表示访问该类或该方法时的请求映射

  • @RequestMapping(value="/请求映射名",method=RequestMethod.GET/POST/PUT/DELETE)

    • method属性表示使用哪种请求方式访问该类或该方法

    • 如果注解中不止一个属性,每个属性都需要指定属性名

  • @GetMapping("/请求映射名")相当于@RequestMapping(value="/请求映射名",method=RequestMethod.GET)

    • post、put、delete同理

    • @GetMapping只能写在方法上

  • @PathVariable

    • 该注解写在某个方法的某个形参上

    • 通常配合@RequestMapping("/{path}")获取请求时传递的参数

      @RequestMapping("/{path}")
      public String fun(@PathVariable("path") String pageName){
          return pageName;
      }
      //当前方法如果通过"localhost:8080/项目名/hello"访问,就会跳转到hello.jsp
      //当前方法如果通过"localhost:8080/项目名/error"访问,就会跳转到error.jsp
      //映射中的/{path}就是获取路径中的hello或error,将其赋值给形参
      //通常用于跳转指定页面
  • @RequestParam(value="传递的参数名",defaultValue ="没有传递参数时的默认值")

    • 该注解写在某个方法的某个参数上

    • 用于获取提交的数据,可以设置默认值在没有提交数据时使用

控制层中获取请求时传递的参数

  • controller中方法的形参名和表单的name或?后的参数名一致

    表单或a标签

    <form action="${pageContext.request.contextPath}/first/sub">
        <input type="text" name="username">
        <input type="text" name="password">
        <input type="submit" >
    </form>
    ​
    <a href="${pageContext.request.contextPath}/first/sub?username=admin">xxx</a>

    controller

    public String login(String username,int password){
        //此时可以正常获取
        //无关数据类型,但是提交数据时必须是对应的类型,否则会有400错误
    }
  • controller中方法的形参名和表单的name或?后的参数名不一致

    controller

    public String login(@RequestParam(value="username",defaultValue="admin")String name,@RequestParam("username")int pwd){
        //如果没有提交数据时,会使用设置的默认值
    }
  • 如果传递的参数都是某个实体类中的属性时

    User类

    package com.hqyj.springmvc.entity;
    ​
    public class User {
        private String username;
        private String password;
    ​
        @Override
        public String toString() {
            return "User{" +
                    "username='" + username + '\'' +
                    ", password='" + password + '\'' +
                    '}';
        }
    ​
        public User() {
        }
    ​
        public User(String username, String password) {
            this.username = username;
            this.password = password;
        }
    ​
        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;
        }
    }

    controller

    @RequestMapping("/login")
    public String login(User user){
        //某个实体类的属性和提交的参数名一致时,可以直接将对象作为形参,会自动将对应参数赋值给对应属性
        System.out.println(user);
        return "";
    }

解决提交数据时的中文乱码

使用过滤器解决中文乱码。

在web.xml中配置spring-web提供的CharaterEncodingFilter过滤器

<!--定义解决中文乱码的过滤器CharacterEncodingFilter-->
<filter>
    <filter-name>encodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <!--设置编码格式-->
    <init-param>
        <!--该过滤器需要设置一个encoding属性,用于设置编码格式-->
        <param-name>encoding</param-name>
        <param-value>utf-8</param-value>
    </init-param>
</filter>
<!--设置将什么请求经过该过滤器,通常设置为/*表示一切请求先经过该过滤器-->
<filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

将数据保存到作用域中

  • 在controller中的某个方法的参数列表里添加Model参数

    • 调用Model对象的addAttribute(String str,Object obj)方法,将某个obj对象保存在request作用域中,命名为str

    @RequestMapping("/queryAll")
    public String queryAll(Model model){
        ArrayList list = new ArrayList<>();
        list.add("qwe");
        list.add(123);
        list.add(true);
        list.add("哈哈");
        //将list保存到request作用域中
        model.addAttribute("list",list);
        //这种跳转属于请求转发
        return "welcome";
    }
  • 在controller中的某个方法的参数列表里添加ModelAndView参数,同时将该方法的返回值设置为ModelAndView类型

    • 使用ModelAndView对象调用addObject(String str,Object obj),将某个obj对象保存在request作用域中,命名为str

    • 使用ModelAndView对象调用setViewName(String viewName),跳转到viewName页面

    package com.hqyj.springmvc.controller;
    ​
    import com.hqyj.springmvc.service.HeroService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.SessionAttributes;
    import org.springframework.web.servlet.ModelAndView;
    ​
    ​
    @Controller
    @RequestMapping("/hero")
    //如果要将数据保存到session中,使用该注解定义session中对象的名称
    @SessionAttributes({"str1","str2"})
    public class HeroController {
        @Autowired
        private HeroService heroService;
    ​
        @RequestMapping("/queryAll")
        public String queryAll(Model model){
            model.addAttribute("list", heroService.queryAll());
            return "heroList";
        }
    ​
        @RequestMapping("/showAll")
        public ModelAndView queryAll(ModelAndView mav){
            //默认保存查询出的对象到request中,命名为list
            mav.addObject("list",heroService.queryAll());
            //也可以保存到session
            //mav.addObject("str1",heroService.queryAll());
            //设置要跳转的页面
            mav.setViewName("heroList");
            return mav;
        }
    }
    ​

数据都是保存在request作用域中,在页面中使用jsp内置对象或EL获取。

如果要保存到session中,需要在类上加入@SessionAttributes({"str1","str2"})注解,str1,str2表示保存到session中的对象的名称,再在方法中,使用

Model对象的addAttribute(String str,Object obj)将其保存在session中。

SpringMVC中的跳转

  • 控制层跳转到某个jsp页面

    • 在控制层中定义方法,这种方式跳转,属于请求转发

    • 如果要使用重定向跳转,在页面名之前添加"redirect:"

      @RequestMapping("/hello")
      public String hello(){
          //返回页面名称
          return "hello";//请求转发
      }
      ​
      @RequestMapping("/hello")
      public ModelAndView hello(ModelAndView mav){
          //设置页面名称
          mav.setViewName("hello");
          return mav;
      }
    • 在springmvc配置文件中

      <mvc:view-controller path="请求映射" view-name="页面名称"></mvc:view-controller>
      <!-- 访问项目根目录,跳转到welcome.jsp页面 -->
      <mvc:view-controller path="/" view-name="welcome"></mvc:view-controller>
      <!-- 这个标签使用时,会让@RequesMapping失效,如果要共存,添加以下标签 -->
      <!--来自于xmlns:mvc="http://www.springframework.org/schema/mvc" -->
      <mvc:annotation-driven></mvc:annotation-driven>
  • 控制层跳转到另一个控制层中的方法

    • 方法的返回值为"redirect/forward:另一个控制层中方法的映射名"

      @RequestMapping("/hello")
      public String hello(){
          return "redirect:hero";//使用重定向的方式,跳转到映射名为hero的控制层
          return "forward:hero"//使用请求转发的方式,跳转到映射名为hero的控制层
      }
  • jsp页面跳转另一个jsp页面

    • 当前项目中jsp页面都在WEB-INF下,无法直接访问,a标签同样如此,只能通过控制层跳转页面

    • 定义用于跳转页面控制层类

      package com.hqyj.springmvc.controller;
      ​
      import org.springframework.stereotype.Controller;
      import org.springframework.web.bind.annotation.PathVariable;
      import org.springframework.web.bind.annotation.RequestMapping;
      ​
      /*
       * 定义一个用于跳转指定页面或controller的控制层
       * */
      @Controller
      public class ToPageController {
      ​
          /*
              项目启动时或直接访问根目录,跳转到指定的controller
          */
          @RequestMapping("/")
          public String toIndex() {
              return "redirect:/hero/queryAll";
          }
      ​
          /*
          * 这个方法的作用:会将请求中第一个/后的单词截取出来命名为path赋值给参数page
          * 如 localhost:8080/web/hero,就会识别出hero,return "hero";
          * 就会跳转到 /WEB-INF/pages/hero.jsp页面
          * */
          @RequestMapping("/{path}")
          public String toPage(@PathVariable("path") String page) {
              return page;
          }
      }
    • 这时在页面中这样跳转

      <%--这个路径实际是/项目名/addHero,会截取addHero,跳转到/项目名/WEB-INF/pages/addHero.jsp--%>
      <a href="${pageContext.request.contextPath}/addHero">添加</a>

文件上传

使用apche提供的通用文件上传组件实现。

1.导入所需依赖

<!--文件上传-->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>

2.向Spring容器中注入上传文件的核心类

在application.xml中注入通用多部件解析器CommonsMultipartResolver

<!--注入上传文件的核心类:通用多部件解析器CommonsMultipartResolver-->
    <bean class="org.springframework.web.multipart.commons.CommonsMultipartResolver" id="multipartResolver">
        <!--设置上传的单个文件最大字节 10M:1024*1024*10-->
        <property name="maxUploadSizePerFile" value="10485760"></property>
    </bean>

3.上传页面的表单

<!--上传的表单控件使用file-->
<!--提交方式为post-->
<!--添加enctype="multipart/form-data"属性-->
<form action="/upload" method="post" enctype="multipart/form-data">
    请选择图片<input type="file" name="uploadFile">
    <input type="submit" value="上传">
</form>

4.控制层获取上传的文件

package com.hqyj.springmvc.controller;
​
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
​
import java.io.File;
import java.io.IOException;
import java.util.UUID;
​
@Controller
@RequestMapping("/admin")
public class UploadController {
​
    @RequestMapping("/upload")
    public String upload(@RequestParam("uploadFile") MultipartFile uploadFile) throws IOException {//uploadFile就是上传的文件对象
        //获取上传的文件名
        String oldName = uploadFile.getOriginalFilename();
        //得到源文件的后缀名
        String prefix = oldName.substring(oldName.lastIndexOf("."));
        //有可能不同的人上传的文件名相同,所以获取源文件的后缀后,生成一个随机文件名,拼接新文件名。
        String newName = UUID.randomUUID()+prefix;
        //创建一个文件File对象,
        File file = new File("d:\\上传文件夹", newName);
        //将文件写入硬盘中
        uploadFile.transferTo(file);
        return "welcome";
    }
}

如果要在某个页面中显示上传的文件,要配置一个虚拟目录

配置Spring+SpringMVC时用到的关键类

  • 在web.xml中配置Spring全局监听器

    • ContextLoaderListener

      <listener>
          <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
      </listener>
      ​
      <context-param>
          <param-name>contextConfigLocation</param-name>
          <param-value>classpath:application.xml</param-value>
      </context-param>
  • 在Springmvc配置文件中配置SpringMVC内部资源视图解析器

    • InternalResourceViewResolver

      <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
          <property name="prefix" value="/WEB-INF/pages/"></property>
          <property name="suffix" value=".jsp"></property>
      </bean>
  • 在web.xml中配置SpringMVC请求分发Servlet

    • DispatcherServlet

      <servlet>
          <servlet-name>dispatcherServlet</servlet-name>
          <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
          <init-param>
              <param-name>contextConfigLocation</param-name>
              <param-value>classpath:springmvc.xml</param-value>
          </init-param>
      </servlet>
      ​
      <servlet-mapping>
          <servlet-name>dispatcherServlet</servlet-name>
          <url-pattern>/</url-pattern>
      </servlet-mapping>
  • 在web.xml中配置字符编码过滤器

    • CharacterEncodingFilter

      <filter>
          <filter-name>encodingFilter</filter-name>
          <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
          <init-param>
              <param-name>encoding</param-name>
              <param-value>utf-8</param-value>
          </init-param>
      </filter>
      ​
      <filter-mapping>
          <filter-name>encodingFilter</filter-name>
          <url-pattern>/*</url-pattern>
      </filter-mapping>

SSM项目搭建补充

通过db.properties文件保存连接数据库的信息

.properties文件称为属性文件,其中的数据以键值对(key=value)的形式保存

  • 在resources目录下新建db.properties文件

    DB_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver
    DB_URL=jdbc:mysql://localhost:3306/bookdb?serverTimezone=Asia/Shanghai
    DB_USERNAME=root
    DB_PASSWORD=root
  • 在application.xml中读取properties文件

    <!--读取properties文件-->
    <context:property-placeholder location="classpath:db.properties"></context:property-placeholder>
  • 读取时使用EL表达式访问其中的键

    <!--Druid数据源DruidDataSource-->
    <bean class="com.alibaba.druid.pool.DruidDataSource" id="druidDataSource">
        <property name="driverClassName" value="${DB_DRIVER_CLASS_NAME}"></property>
        <property name="url" value="${DB_URL}"></property>
        <property name="username" value="${DB_USERNAME}"></property>
        <property name="password" value="${DB_PASSWORD}"></property>
    </bean>

MyBatis配置文件常用设置

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
​
    <!--设置-->
    <settings>
        <!--开启驼峰命名映射-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!--开启SQL日志-->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
​
</configuration>

MyBatis基本增删改查

dao层

package com.hqyj.ssm02.dao;
​
import com.hqyj.ssm02.entity.BookInfo;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
​
import java.util.List;
​
@Repository
public interface BookInfoDao {
​
    //查询所有
    List<BookInfo> queryAll();
    //根据id删除
    int delete(int id);
    //添加
    int insert(BookInfo bookInfo);
    //根据id查询
    BookInfo findById(int no);
    //修改时,参数通常为一个完整的修改对象
    int update(BookInfo bookInfo);
​
    //批量删除
    //分页查询
    //关键字分页
​
    //如果dao层中某个方法不止1个参数,需要给每个参数添加@Param("参数名")注解,给该参数命名
    //命名后,才能在mybatis的sql映射文件中使用该参数,即#{参数名}
    //如这里的newPrice,在sql中用#{newPrice}获取
    int update(@Param("newPrice") int bookPrice,@Param("newNum") int bookNum,@Param("updateId") int bookId);
}
​

service层

package com.hqyj.ssm02.service;
​
import com.hqyj.ssm02.dao.BookInfoDao;
import com.hqyj.ssm02.entity.BookInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
​
import java.util.List;
​
@Service
public class BookInfoService {
​
    @Autowired
    private BookInfoDao bookInfoDao;
​
    public List<BookInfo> queryAll() {
        return bookInfoDao.queryAll();
    }
​
    public boolean delete(int id) {
        return bookInfoDao.delete(id) > 0;
    }
​
    public void insert(BookInfo bookInfo) {
        bookInfoDao.insert(bookInfo);
    }
​
    public BookInfo findById(int no) {
        return bookInfoDao.findById(no);
    }
​
    public void update(BookInfo bookInfo) {
        bookInfoDao.update(bookInfo);
    }
}
​

controller层

package com.hqyj.ssm02.controller;
​
import com.hqyj.ssm02.entity.BookInfo;
import com.hqyj.ssm02.entity.BookType;
import com.hqyj.ssm02.service.BookInfoService;
import com.hqyj.ssm02.service.BookTypeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
​
import java.util.List;
​
@Controller
@RequestMapping("/bookInfo")
public class BookInfoController {
​
    @Autowired
    private BookInfoService bookInfoService;
    //注入BookTypeService,用于获取所有图书类型
    @Autowired
    private BookTypeService bookTypeService;
​
​
    @RequestMapping("/queryAll")
    public String queryAll(Model model) {
        List<BookInfo> list = bookInfoService.queryAll();
        model.addAttribute("list", list);
        return "bookInfoList";
    }
​
    @RequestMapping("/delete")
    public String delete(int id) {//如果在页面中传递的参数名和方法的形参名一致,会自动获取数据赋值
        if (bookInfoService.delete(id)) {
            //增删改执行后,使用重定向跳转
            return "redirect:queryAll";
        }
        return "error";
    }
​
​
    @RequestMapping("/insert")
    //如果表单提交的参数和方法的形参名一致,自动获取并赋值
    //如果表单提交的所有参数正好是一个实体类对象,可以用对应的实体类对象获取
    public String insert(BookInfo bookInfo){
        bookInfoService.insert(bookInfo);
        return "redirect:queryAll";
    }
​
    @RequestMapping("/findById")
    //如果表单提交的参数名和方法的参数名不一致,使用@RequestParam("提交的参数名")
    public String findById(@RequestParam("id") int no,Model model){
        //查询对应的图书信息
        BookInfo byId = bookInfoService.findById(no);
        model.addAttribute("book",byId);
        //查询所有的图书类型,保存到请求中
        List<BookType> bookTypeList = bookTypeService.queryAll();
        model.addAttribute("btList",bookTypeList);
        return "bookEdit";
    }
​
​
    @RequestMapping("/update")
    public String update(BookInfo bookInfo){
        bookInfoService.update(bookInfo);
        return "redirect:queryAll";
    }
​
​
}
​

sql映射文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hqyj.ssm02.dao.BookInfoDao">
​
    <!--查询所有-->
    <select id="queryAll" resultType="com.hqyj.ssm02.entity.BookInfo">
        select *
        from book_info
    </select>
​
    <!--根据id查询-->
    <select id="findById" resultType="com.hqyj.ssm02.entity.BookInfo">
        select *
        from book_info
        where book_id = #{no}
    </select>
​
    <!--根据id删除-->
    <delete id="delete">
        delete
        from book_info
        where book_id = #{id}
    </delete>
    <!--添加-->
    <insert id="insert">
        insert into book_info
        values (null, #{typeId}, #{bookName}, #{bookAuthor}, #{bookPrice}, #{bookNum}, #{publisherDate}, #{bookImg})
    </insert>
    <!--修改-->
    <update id="update">
        update book_info
        set book_price=#{newPrice},
            book_num=#{newNum},
            type_id=#{typeId}
        where book_id = #{updateId}
    </update>
​
​
</mapper>

使用BootStrap渲染数据

下载bootstrap的文件夹和jquery,保存到webapp根目录下。

由于在web.xml中,SpringMVC的核心类DispatcherServlet(请求分发器)的映射设置成了"/",表示所有请求,包含静态资源的请求都会交给SpringMVC处理。

解决无法引入静态资源的问题

  • 在webapp目录下,新建一个目录,通常命名为static。将项目中的静态资源文件都保存于此。

  • 在springmvc.xml中

    <!--映射静态资源目录-->
    <!--location表示要映射的静态资源目录-->
    <!--mapping表示最终通过哪种方式进行访问。这里表示只要以/static开头的请求,都可以访问静态资源目录-->
    <mvc:resources mapping="/static/**" location="/static/"></mvc:resources>
    <!--开启注解驱动,有这个标签,SpringMVC就能区分哪个是资源文件,哪个是Controller-->
    <mvc:annotation-driven></mvc:annotation-driven>

导入BootStrap的样式和JS文件

<%--导入bootstrap的css文件--%>
<link href="${pageContext.request.contextPath}/static/bootstrap-3.4.1-dist/css/bootstrap.min.css" rel="stylesheet"
      type="text/css">
<%--导入jquery--%>
<script src="${pageContext.request.contextPath}/static/bootstrap-3.4.1-dist/js/jquery-3.6.2.min.js"></script>
<%--导入boostrap的js文件--%>
<script src="${pageContext.request.contextPath}/static/bootstrap-3.4.1-dist/js/bootstrap.min.js"></script>

创建顶部导航页面

每个页面都需要这三句话,为了方便起见,给每个页面添加顶部导航页面top.jsp

这样其他页面只需要通过<jsp:include>导入该页面的同时,使用BootStrap的样式和JS文件

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>Title</title>
        <%--导入bootstrap的css文件--%>
        <link href="${pageContext.request.contextPath}/static/bootstrap-3.4.1-dist/css/bootstrap.min.css" rel="stylesheet"
              type="text/css">
        <%--导入jquery--%>
        <script src="${pageContext.request.contextPath}/static/bootstrap-3.4.1-dist/js/jquery-3.6.2.min.js"></script>
        <%--导入boostrap的js文件--%>
        <script src="${pageContext.request.contextPath}/static/bootstrap-3.4.1-dist/js/bootstrap.min.js"></script>
    </head>
    <body>
        <nav class="navbar navbar-default">
            <div class="container-fluid">
                <!-- Brand and toggle get grouped for better mobile display -->
                <div class="navbar-header">
                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                        <span class="sr-only">Toggle navigation</span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
                    <a class="navbar-brand" href="#">Brand</a>
                </div>
​
                <!-- Collect the nav links, forms, and other content for toggling -->
                <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                    <ul class="nav navbar-nav">
                        <li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li>
                        <li><a href="#">Link</a></li>
                        <li class="dropdown">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
                            <ul class="dropdown-menu">
                                <li><a href="#">Action</a></li>
                                <li><a href="#">Another action</a></li>
                                <li><a href="#">Something else here</a></li>
                                <li role="separator" class="divider"></li>
                                <li><a href="#">Separated link</a></li>
                                <li role="separator" class="divider"></li>
                                <li><a href="#">One more separated link</a></li>
                            </ul>
                        </li>
                    </ul>
                    <form class="navbar-form navbar-left">
                        <div class="form-group">
                            <input type="text" class="form-control" placeholder="Search">
                        </div>
                        <button type="submit" class="btn btn-default">Submit</button>
                    </form>
                    <ul class="nav navbar-nav navbar-right">
                        <li><a href="#">Link</a></li>
                        <li class="dropdown">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
                            <ul class="dropdown-menu">
                                <li><a href="#">Action</a></li>
                                <li><a href="#">Another action</a></li>
                                <li><a href="#">Something else here</a></li>
                                <li role="separator" class="divider"></li>
                                <li><a href="#">Separated link</a></li>
                            </ul>
                        </li>
                    </ul>
                </div><!-- /.navbar-collapse -->
            </div><!-- /.container-fluid -->
        </nav>
    </body>
</html>

列表页面

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
​
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<%--使用jsp动作包含一个子页面,也能使用其中的css和js文件--%>
<jsp:include page="top.jsp"></jsp:include>
<div class="row" style="width: 100%">
    <div class="col-md-2"></div>
    <div class="col-md-8">
        <table class="table-striped table">
            <tr>
                <th>图书编号</th>
                <th>类型编号</th>
                <th>图书名称</th>
                <th>图书作者</th>
                <th>图书价格</th>
                <th>图书库存</th>
                <th>出版时间</th>
                <th >操作</th>
                <th><a href="${pageContext.request.contextPath}/bookAdd" class="btn btn-default btn-sm">添加</a></th>
            </tr>
            <c:forEach var="book" items="${list}">
                <tr>
                    <td>${book.bookId}</td>
                    <td>${book.typeId}</td>
                    <td>${book.bookName}</td>
                    <td>${book.bookAuthor}</td>
                    <td>${book.bookPrice}</td>
                    <td>${book.bookNum}</td>
                    <td>${book.publisherDate}</td>
                    <td><a href="${pageContext.request.contextPath}/bookInfo/findById?id=${book.bookId}" class="btn btn-primary btn-sm">编辑</a></td>
                    <td><a href="${pageContext.request.contextPath}/bookInfo/delete?id=${book.bookId}" class="btn btn-danger btn-sm">删除</a></td>
                </tr>
            </c:forEach>
        </table>
    </div>
    <div class="col-md-2"></div>
</div>
</body>
</html>
​

详情页面

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>Title</title>
    </head>
    <body>
        <jsp:include page="top.jsp"></jsp:include>
​
        <div class="row">
            <div class="col-md-3"></div>
            <div class="col-md-6">
                <form class="form-horizontal" action="${pageContext.request.contextPath}/bookInfo/update" method="post">
                    <%--隐藏域提交id--%>
                    <input type="hidden" name="bookId" value="${book.bookId}">
                    <div class="form-group">
                        <label  class="col-sm-2 control-label">图书名称</label>
                        <div class="col-sm-10">
                            <input type="text" readonly class="form-control" value="${book.bookName}" name="bookName" placeholder="请输入图书名称" required>
                        </div>
                    </div>
                    <div class="form-group">
                        <label  class="col-sm-2 control-label">图书作者</label>
                        <div class="col-sm-10">
                            <input type="text" readonly class="form-control" value="${book.bookAuthor}" name="bookAuthor" placeholder="请输入图书作者" required>
                        </div>
                    </div>
                    <div class="form-group">
                        <label  class="col-sm-2 control-label">图书类型</label>
                        <div class="col-sm-10">
                            <select class="form-control" name="typeId" >
                                <%--遍历所有的图书类型--%>
                                <c:forEach items="${btList}" var="bt">
                                    <option value="${bt.typeId}" ${bt.typeId==book.typeId?"selected":""}>${bt.typeName}</option>
                                </c:forEach>
                            </select>
                        </div>
                    </div>
                    <div class="form-group">
                        <label  class="col-sm-2 control-label">图书价格</label>
                        <div class="col-sm-10">
                            <input type="number" min="1" class="form-control" value="${book.bookPrice}" name="bookPrice" placeholder="请输入图书价格" required>
                        </div>
                    </div>
                    <div class="form-group">
                        <label  class="col-sm-2 control-label">图书库存</label>
                        <div class="col-sm-10">
                            <input type="number" min="1" class="form-control" value="${book.bookNum}" name="bookNum" placeholder="请输入图书库存" required>
                        </div>
                    </div>
                    <div class="form-group">
                        <label  class="col-sm-2 control-label">出版时间</label>
                        <div class="col-sm-10">
                            <input type="date" readonly class="form-control" value="${book.publisherDate}"   name="publisherDate" required>
                        </div>
                    </div>
                    <div class="form-group">
                        <div class="col-sm-offset-2 col-sm-10">
                            <button type="submit" class="btn btn-default">修改</button>
                        </div>
                    </div>
                </form>
            </div>
            <div class="col-md-3"></div>
        </div>
​
​
    </body>
</html>
​

添加页面

<%--
    Created by IntelliJ IDEA.
    User: Administrator
        Date: 2023/1/30
            Time: 14:17
                To change this template use File | Settings | File Templates.
                --%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>Title</title>
    </head>
    <body>
        <jsp:include page="top.jsp"></jsp:include>
​
        <div class="row">
            <div class="col-md-3"></div>
            <div class="col-md-6">
                <form class="form-horizontal" action="${pageContext.request.contextPath}/bookInfo/insert" method="post">
                    <div class="form-group">
                        <label class="col-sm-2 control-label">图书名称</label>
                        <div class="col-sm-10">
                            <input type="text" class="form-control" name="bookName" placeholder="请输入图书名称" required>
                        </div>
                    </div>
                    <div class="form-group">
                        <label class="col-sm-2 control-label">图书作者</label>
                        <div class="col-sm-10">
                            <input type="text" class="form-control" name="bookAuthor" placeholder="请输入图书作者" required>
                        </div>
                    </div>
                    <div class="form-group">
                        <label class="col-sm-2 control-label">图书类型</label>
                        <div class="col-sm-10">
                            <select class="form-control" name="typeId">
​
                            </select>
                        </div>
                    </div>
                    <div class="form-group">
                        <label class="col-sm-2 control-label">图书价格</label>
                        <div class="col-sm-10">
                            <input type="number" min="1" class="form-control" name="bookPrice" placeholder="请输入图书价格" required>
                        </div>
                    </div>
                    <div class="form-group">
                        <label class="col-sm-2 control-label">图书库存</label>
                        <div class="col-sm-10">
                            <input type="number" min="1" class="form-control" name="bookNum" placeholder="请输入图书库存" required>
                        </div>
                    </div>
                    <div class="form-group">
                        <label class="col-sm-2 control-label">出版时间</label>
                        <div class="col-sm-10">
                            <input type="date" class="form-control" name="publisherDate" required>
                        </div>
                    </div>
                    <div class="form-group">
                        <div class="col-sm-offset-2 col-sm-10">
                            <button type="submit" class="btn btn-default">添加</button>
                        </div>
                    </div>
                </form>
            </div>
            <div class="col-md-3"></div>
        </div>
​
​
    </body>
    <script>
        /*
    使用ajax查询所有图书类型
    在页面中使用ajax访问后端如果要得到数据,该数据必须是JSON格式
    */
        $.ajax({
            //请求地址
            url: "${pageContext.request.contextPath}/bookType/queryAllToJson",
            //访问成功后的回调函数
            success: function (res) {//这里的res是所有类型对象的集合
                for (var i = 0; i < res.length; i++) {
                    var $opt = $("<option value='" + res[i].typeId + "'>" + res[i].typeName + "</option>")
                    $("select[name=typeId]").append($opt);
                }
            }
        });
​
    </script>
​
</html>
​

在SpringMVC中,让某个控制层中的方法返回JSON格式的数据

  • 添加依赖

    <!--jackson:将数据转换为JSON格式-->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.14.2</version>
    </dependency>
  • @ResponseBody注解

    该注解可以加在类或方法上

    • 如果加在方法上,表示该方法的返回值类型为JSON格式

    • 如果加在类上,表示该类中的所有方法的返回值类型为JSON格式

    package com.hqyj.ssm02.controller;
    ​
    import com.hqyj.ssm02.entity.BookType;
    import com.hqyj.ssm02.service.BookTypeService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    ​
    import java.util.List;
    ​
    @Controller
    @RequestMapping("/bookType")
    public class BookTypeController {
    ​
        @Autowired
        private BookTypeService bookTypeService;
    ​
    ​
        //如果controller的某个方法返回一个JSON字符串,需要使用@ResponseBody
        @RequestMapping("/queryAllToJson")
        @ResponseBody//表示该方法无论返回值是什么,都返回JSON格式字符串
        public List<BookType> queryAllToJson(){
            List<BookType> list = bookTypeService.queryAll();
            return list;
        }
    }
    ​

SSM项目中使用Ajax

ajax依赖于jquery,所以先保证页面中存在jquery.js。

$.ajax({
    url:"访问地址",
    data:{
        "提交的参数名":"实际值",
        "提交的参数名":"实际值"
    },
    type:"get/post",
    success:function(res){
        //成功后的回调函数,res为访问后的结果,必须是json格式
    }
});

在前端页面中使用ajax访问controller时,controller的返回值必须是一个JSON格式的字符串。

所以controller中的方法上要加入@ResponseBody注解

使用Aajx实现注册和登录

当前表为customer,包含字段如下

使用该表中的phone和password作为登录时的账户和密码

实体类

package com.hqyj.ssm02.entity;
/*
* 对应customer表
* */
public class SysAdmin {
    //用户名adminName属性对应phone字段
    private String adminName;
    //密码adminPwd属性对应password字段
    private String adminPwd;
    
    //省略get/set/toString
}
​

注册

注册时先判断用户名是否存在,如果存在则不能注册

  • dao

    /*查询用户名是否存在,返回查询到的数量*/
    int findByAdminName(String adminName);
    /*添加*/
    int insert(SysAdmin sysAdmin);
  • mapper.xml

    <!--查询用户名(phone)是否存在-->
    <select id="findByAdminName" resultType="java.lang.Integer">
        select count(*)
        from customer
        where phone = #{adminName}
    </select>
    ​
    <!--添加用户-->
    <insert id="insert">
        insert into customer
        values (null, #{adminName}, #{adminPwd}, 0, null)
    </insert>

  • service

    /*
         * 检查注册的用户名是否存在
         * */
    public boolean findAdminName(SysAdmin sysAdmin) {
        //查询要注册的用户名是否存在
        int i = sysAdminDao.findByAdminName(sysAdmin.getAdminName());
        if (i != 0) {
            return false;
        }
        return true;
    }
    ​
    /*
        * 注册
        * */
    public boolean register(SysAdmin sysAdmin) {
        return sysAdminDao.insert(sysAdmin) > 0;
    }

  • controller

    /*
         * 查询名称是否存在,返回boolean类型的json字符串
         * */
    @RequestMapping("/findAdminName")
    @ResponseBody
    public boolean findAdminName(SysAdmin sysAdmin) {
        return sysAdminService.findAdminName(sysAdmin);
    }
    ​
    /*
         * 注册成功后跳转到登录页面
         * */
    @RequestMapping("/register")
    public String register(SysAdmin sysAdmin) {
        sysAdminService.register(sysAdmin);
        return "login";
    }

  • 页面

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
        <head>
            <title>Title</title>
            <style>
                .cus-portrait {
                    margin: 0 auto;
                    width: 100px;
                    height: 100px;
                    border: 1px solid #337ab7;
                    text-align: center;
                    line-height: 100px;
                    border-radius: 50px;
                }
            </style>
        </head>
        <body>
            <jsp:include page="top.jsp"></jsp:include>
            <div class="col-md-4"></div>
            <div class="col-md-4">
                <div class="panel panel-primary">
                    <div class="panel-heading">用户注册</div>
                    <div class="panel-body">
                        <form action="${pageContext.request.contextPath}/sysAdmin/register" method="post">
                            <div class="form-group">
                                <label class="warning">用户名</label>
                                <input type="text" class="form-control" required name="adminName" placeholder="请输入用户名">
                            </div>
                            <div class="form-group">
                                <label>密码</label>
                                <input type="password" class="form-control" required name="adminPwd" placeholder="请输入密码">
                            </div>
                            <button  type="submit" class="btn btn-default">注册</button>
                        </form>
                    </div>
                </div>
            </div>
            <div class="col-md-4"></div>
    ​
            <script>
                $(function () {
                    //用户名文本框失去焦点触发
                    $("input[name=adminName]").blur(function () {
                        $.ajax({
                            //访问controller
                            url:"${pageContext.request.contextPath}/sysAdmin/findAdminName",
                            data:{
                                //提交数据
                                "adminName":$(this).val()
                            },
                            success:function(res){
                                //res是true、false。true表示该用户名不存在
                                if(res){
                                    $(".warning").text("√").css("color","green");
                                    $("button[type=submit]").removeAttr("disabled");
                                }else{
                                    $(".warning").text("该用户名已存在").css("color","red");
                                    $("button[type=submit]").attr("disabled","disabled");
                                }
                            }
                        });
                    });
                });
    ​
            </script>
        </body>
    </html>

登录

  • dao

    /*登录*/
    SysAdmin login(SysAdmin sysAdmin);
  • mapper.xml

    <!--登录-->
    <!--如果查询的字段名和实体的属性名不一致,需要自定义查询结果集映射-->
    <select id="login" resultMap="loginMap">
        select *
        from customer
        where phone = #{adminName}
        and password = #{adminPwd}
    </select>
    <!--自定义返回结果集映射-->
    <resultMap id="loginMap" type="com.hqyj.ssm02.entity.SysAdmin">
        <!--表中的phone字段对应SysAdmin对象的adminName字段-->
        <result property="adminName" column="phone"></result>
        <result property="adminPwd" column="password"></result>
    </resultMap>
  • service

    /*
        * 登录
        * */
    public SysAdmin login(SysAdmin sysAdmin){
        return sysAdminDao.login(sysAdmin);
    }
  • controller

    @RequestMapping("/login")
    @ResponseBody
    public SysAdmin login(SysAdmin sysAdmin,Model model) {
        SysAdmin login = sysAdminService.login(sysAdmin);
        //将登录成功的对象保存到session中
        model.addAttribute("sysAdmin",login);
        return login;
    }
  • 页面

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
        <head>
            <title>Title</title>
        </head>
        <body>
            <jsp:include page="top.jsp"></jsp:include>
            <div class="col-md-4"></div>
            <div class="col-md-4">
                <div class="panel panel-primary">
                    <div class="panel-heading">用户登录</div>
                    <div class="panel-body">
                        <div class="form-group">
                            <label>用户名</label>
                            <input type="text" class="form-control" required id="name"  placeholder="请输入用户名">
                        </div>
                        <div class="form-group">
                            <label>密码</label>
                            <input type="password" class="form-control" required id="pwd" placeholder="请输入密码">
                        </div>
                        <div class="form-group">
                            <label class="warning">验证码</label>
                            <span class="vcode"></span>
                            <input type="text" class="form-control inpVcode" required placeholder="请输入验证码">
                        </div>
                        <a class="btn btn-default loginBtn">登录</a>
                        <a class="btn btn-default" href="">注册</a>
                    </div>
                </div>
            </div>
            <div class="col-md-4"></div>
    ​
            <script>
                //1000~9999
                var vcode = Math.floor(Math.random() * 8999 + 1000);
                //显示验证码
                $(".vcode").text(vcode).css("font-weight", "bolder");
                //定义一个boolean值用于提交数据时判断验证码是否有误,默认false
                var goon=false;
                //bind("绑定事件名",满足时触发的函数)   input propertychange表示监听文本框输入事件(只要内容有变化就触发)
                $(".inpVcode").bind("input propertychange", function () {
                    if (vcode != $(this).val()) {//如果验证码有误,改变警告文字
                        $(".warning").text("验证码输入错误").css("color","#f00");
                    }else{
                        //如果输入正确,改变boolean值为true
                        $(".warning").text("√").css("color","#0f0");
                        goon=true;
                    }
                });
                //登录按钮单击
                $(".loginBtn").click(function () {
                    //判断验证码
                    if (!goon) {
                        return;
                    }
                    //使用ajax提交数据进行登录
                    $.ajax({
                        url:"${pageContext.request.contextPath}/sysAdmin/login",
                        data:{
                            "adminName":$("#name").val(),
                            "adminPwd":$("#pwd").val()
                        },
                        success:function (res){
                            if(res!=""){
                                location.href="${pageContext.request.contextPath}/bookInfo/queryAll";
                            }else{
                                alert("用户名或密码错误");
                            }
                        }
                    });
                });
            </script>
        </body>
    </html>

在SpringMVC中使用Session

方式一:@SessionAttributes注解

由于SSM项目中,没有使用servlet,所以不能通过request.getSession()方法来获取session对象。

在控制器Controller中,在类上加入@SessionAttributes注解

@SessionAttributes({"参数1","参数2"})表示在session中保存两个参数

再在某个方法中,通过Model对象调用addAttribute("参数1",对象)方法将指定对象保存到session中

使用和销毁

@Controller
@RequestMapping("/sysAdmin")
//如果要将数据保存到session中,先使用该注解定义session中的参数名
@SessionAttributes({"sysAdmin"})
public class SysAdminController {
​
    @Autowired
    SysAdminService sysAdminService;
​
​
    @RequestMapping("/login")
    @ResponseBody
    public SysAdmin login(SysAdmin sysAdmin,Model model) {
        SysAdmin login = sysAdminService.login(sysAdmin);
        //将登录成功的对象保存到session中
        model.addAttribute("sysAdmin",login);
        return login;
    }
​
    /*登出时销毁session*/
    @RequestMapping("/logout")
    public String logout(SessionStatus sessionStatus) {
        //在方法中使用SessionStatus参数表示session状态对象
        //调用setComplete()方法,将session设置为完成状态
        sessionStatus.setComplete();
        return "redirect:/login";
    }
}

方式二:HttpSession参数

给项目添加javax.servlet.api依赖,给controller中某个方法添加HttpSession参数后,获取session使用

<!--如果要使用servlet相关内容-->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
</dependency>

使用和销毁

@Controller
@RequestMapping("/sysAdmin")
//如果要将数据保存到session中,先使用该注解定义session中的参数名
@SessionAttributes({"sysAdmin"})
public class SysAdminController {
​
    @Autowired
    SysAdminService sysAdminService;
​
​
    @RequestMapping("/login")
    @ResponseBody
    public SysAdmin login(SysAdmin sysAdmin,HttpSession session) {
        SysAdmin login = sysAdminService.login(sysAdmin);
        //将登录成功的对象保存到session中
        session.setAttribute("sysAdmin",login);
        return login;
    }
​
    /*登出时销毁session*/
    @RequestMapping("/logout")
    public String logout(HttpSession session) {
        session.invalidate();
        return "redirect:/login";
    }
}

拦截器

每次请求controller时,都要经过的一个类。

当一个项目中有过滤器、拦截器和controller时的执行流程

实现过程

1.导入servlet依赖

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
</dependency>

2.自定义一个类,实现拦截器HandlerInterceptor接口

其中有三个default方法可以重写

  • preHandle

    • 在发送请求后,DispatcherServlet解析控制器中某个RequestMapping之前执行的方法

    • 该方法返回true时,请求才能继续

  • postHandle

    • 在preHandle方法返回值为true后执行

    • 在DispatcherServlet解析控制器中某个RequestMapping之后执行

  • afterCompletion

    • 在preHandle方法返回true后执行

    • 在解析视图后执行的方法

这里只需重写preHandle方法即可

package com.hqyj.ssm02.interceptor;
​
import org.springframework.web.servlet.HandlerInterceptor;
​
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
​
/*
* 自定义拦截器,用户拦截未登录时的请求
* */
public class MyInterceptor implements HandlerInterceptor {
​
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        //登录成功后,会在session中保存一个名为sysAdmin的对象
        Object sysAdmin = request.getSession().getAttribute("sysAdmin");
        //如果有对象,说明登录成功,可以放行return true
        if(sysAdmin!=null){
            return true;
        }else{
            response.sendRedirect(request.getContextPath()+"/login");
        }
        System.out.println(requestURI+"试图访问,拦截成功");
        return false;
    }
}

3.在springmvc.xml中配置拦截器

<!--配置拦截器们-->
<mvc:interceptors>
    <!--配置某个拦截器-->
    <mvc:interceptor>
        <!--设置要拦截的请求,这里的/**表示拦截一切请求-->
        <mvc:mapping path="/**"/>
        <!--设置不拦截的请求-->
        <!--放行登录和注册页-->
        <mvc:exclude-mapping path="/login"/>
        <mvc:exclude-mapping path="/register"/>
        <!--放行静态资源-->
        <mvc:exclude-mapping path="/static/**"/>
        <!--放行用户模块-->
        <mvc:exclude-mapping path="/sysAdmin/**"/>
        <!--注入指定的拦截器-->
        <bean class="com.hqyj.ssm02.interceptor.MyInterceptor"></bean>
    </mvc:interceptor>
</mvc:interceptors>

单元测试

1.导入依赖

如果只在普通的Maven项目中使用,只需导入该依赖

<!--单元测试-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>

如果要在Spring环境下使用,还需

<!--spring集成JUnit-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.3.23</version>
    <scope>test</scope>
</dependency>

2.创建单元测试类所在目录

在项目的src目录下新建test目录,会自动在其中创建java目录

3.使用

  • 在test/java目录下,新建一个类

  • 在类上加入

    • @ContextConfiguration("classpath:application.xml")

    • @RunWith(SpringJUnit4ClassRunner.class)

  • 在类中创建公开的无返回值的无参数方法,加入@Test注解

  • 在该方法上右键运行,运行的是当前方法,在类中空白处右键运行,运行的是当前类中的所有方法。

import com.hqyj.ssm02.dao.BookInfoDao;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
​
//解析Spring配置文件
@ContextConfiguration("classpath:application.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class Test {
​
    /*
    * 单元测试的方法必须是无参数无返回值的公共方法
    * */
    @org.junit.Test
        public void fun(){
        System.out.println("单元测试");
    }
​
    @Autowired
    private BookInfoDao bookInfoDao;
​
    @org.junit.Test
        public void test1(){
        System.out.println(bookInfoDao.queryAll());
    }
}
​

MyBatis整理

使用前提

  • 搭建好MyBatis的环境

    • mybatis配置文件模板

      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE configuration
              PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
              "http://mybatis.org/dtd/mybatis-3-config.dtd">
      <configuration>
      ​
      </configuration>
  • 根据表创建实体类

    • 默认属性名要和字段名一致

    • 如果表中的字段名使用下划线分割单词,实体属性使用驼峰命名法。在mybatis的配置文件中加入开启驼峰命名方式映射

      <!--设置-->
      <settings>
          <!--开启驼峰命名映射-->
          <setting name="mapUnderscoreToCamelCase" value="true"/>
          <!--开启SQL日志-->
          <setting name="logImpl" value="STDOUT_LOGGING"/>
      </settings>
    • 如果表中字段名和实体的属性名既不一致,也不是驼峰命名,就要在写sql语句时,指定字段名和属性名的映射关系

    • 实体类必须是公共的,其中必须有无参数的构造方法和get/set方法

  • 数据访问层

    • 数据访问层通常是一个接口,可以称为dao层或mapper层,命名时可以是xxDao或xxMapper

  • 写接口的sql映射文件

    • 命名通常为xxMapper.xml

    • sql映射文件模板

      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE mapper
              PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
              "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="数据访问层接口的全限定名">
      </mapper>
    • sql映射文件可以放在resources目录下,spring的配置文件application.xml中通过"classpath:具体路径"读取

      如保存在resources/mapper下

      <!--SqlSessionFactoryBean-->
      <bean class="org.mybatis.spring.SqlSessionFactoryBean" id="sqlSessionFactory">
          <!--指定数据源-->
          <property name="dataSource" ref="druidDataSource"></property>
          <!--读取mybatis配置文件-->
          <property name="configLocation" value="classpath:mybatis-config.xml"></property>
          <!--读取sql映射文件-->
          <property name="mapperLocations" value="classpath:mapper/*.xml"></property>
      </bean>
    • sql映射文件也可能放在数据访问层目录下,即dao包下

      IDEA中,如果将xml文件保存在java包下时,默认在target目录(真正运行时的目录)不会编译加载这些文件,要进行设置

      在pom.xml中的build标签中加入以下内容,重新加载

      <!--解决xml如果放在java包下时无法编译的问题-->
      <resources>
          <resource>
              <directory>src/main/java</directory>
              <includes>
                  <include>**/*.xml</include>
              </includes>
          </resource>
      </resources>

      spring的配置文件application.xml中读取路径正确

      <bean class="org.mybatis.spring.SqlSessionFactoryBean" id="sqlSessionFactory">
          <!--指定数据源-->
          <property name="dataSource" ref="druidDataSource"></property>
          <!--读取mybatis配置文件-->
          <property name="configLocation" value="classpath:mybatis-config.xml"></property>
          <!--读取sql映射文件-->
          <property name="mapperLocations" value="classpath:com/hqyj/ssm02/dao/mapper/*.xml"></property>
      </bean>

案例

数据表

book_type图书类型表(主表)

book_info图书详情表(从表)

实体类

开启驼峰命名映射,忽略get/set

BookInfo

public class BookInfo {
    private int bookId;
    private int typeId;
    private String bookName;
    private String bookAuthor;
    private int bookPrice;
    private int bookNum;
    private String publisherDate;
    private String bookImg;
}

BookType

public class BookType {
    private int typeId;
    private String typeName;
}

查询<select></select>

单表查询

dao中的方法

List<BookType> queryAll();

sql映射文件

  • 当数据库表中的字段名和实体属性一致或开启了驼峰命名映射,使用resultType

    <select id="queryAll" resultType="com.xxx.entity.BookType">
        select * from book_type
    </select>
  • 当数据库表中的字段名和实体属性不一致,使用resultMap

    <select id="queryAll" resultMap="map">
        select * from book_type
    </select>
    <!--自定义结果集映射-->
    <resultMap id="map" type="com.xxx.entity.BookType">
        <!--主键使用id标签,其他字段使用result标签-->
        <!--column对应字段名 property对应属性名-->
        <id column="type_id" property="typeId"></id>
        <result column="type_name" property="typeName"></result>
    </resultMap>

多表查询

多对一查询

多对一表示多个从表实体对应一个主表实体。如多本图书对应一个图书类型。

在从表实体中,额外添加一个属性:对应的主表实体对象

public class BookInfo {
    private int bookId;
    private int typeId;
    private String bookName;
    private String bookAuthor;
    private int bookPrice;
    private int bookNum;
    private String publisherDate;
    private String bookImg;
    //多对一查询,额外添加外键对应的实体对象
    private BookType bt;
}

dao中的方法

List<BookInfo> queryAll();

sql映射文件

  • 关联查询:构建特殊的sql语句

    • 适合外键对应的表中字段比较少的情况下使用

    <!--sql语句除了查询自身表之外,还要查询关联的表中的字段,将其命名为"实体对象.属性"-->
    <select id="queryAll" resultType="com.xxx.entity.BookInfo">
        select bi.*,type_name as 'bt.typeName' from book_info bi,book_type bt where bi.type_id=bt.type_id
    </select>
  • 连接查询(不建议)

    <select id="queryAll" resultMap="booksMap">
        select *
        from book_info bi,
        book_type bt
        where bi.type_id = bt.type_id
    </select>
    <resultMap id="booksMap" type="com.hqyj.ssm02.entity.BookInfo">
        <!--主键用id标签,其他字段用result标签赋值-->
        <id property="bookId" column="book_id"></id>
        <result property="bookName" column="book_name"></result>
        <result property="typeId" column="type_id"></result>
        <result property="bookAuthor" column="book_author"></result>
        <result property="bookPrice" column="book_price"></result>
        <result property="bookNum" column="book_num"></result>
        <result property="bookImg" column="book_img"></result>
        <result property="publisherDate" column="publisher_date"></result>
        <!--外键对应的实体对象使用association标签,通过property属性对应实体对象名-->
        <association property="bt" >
            <!--给外键对象属性赋值-->
            <id column="type_id" property="typeId"></id>
            <result column="type_name" property="typeName"></result>
        </association>
    </resultMap>
  • 子查询

    • 适合外键对应的表中字段比较多的情况

    <!--1.查询从表-->
    <select id="queryAll" resultMap="booksMap">
        select * from book_info
    </select>
    <!--2.自定义结果集映射,使用type_id进行子查询,下方的sql-->
    <resultMap id="booksMap" type="com.hqyj.ssm02.entity.BookInfo">
        <!--由于使用type_id进行了子查询,所以如果要给type_id赋值,需要再次进行映射-->
        <result column="type_id" property="typeId"></result>
        <!--外键对应的实体对象,使用association赋值-->
        <!--如果这里的子查询来自当前映射文件-->
        <association property="bt" column="type_id" select="findTypeByTypeId"></association>
        <!--如果这里的子查询来自于其他dao中的方法-->
        <!--<association property="bookType" column="type_id" select="com.hqyj.ssm02.dao.BookTypeDao.findById"></association>-->
    </resultMap>
    ​
    <!--3.根据type_id查询完整对象-->
    <select id="findTypeByTypeId" resultType="com.hqyj.ssm02.entity.BookType">
        select * from book_type where type_id=#{typeId}
    </select>

一对多查询

一对多表示一个主表实体对应多个从表实体。如一个图书类型对应多本图书。

在主表对应的实体类中,额外添加一个属性:多个从表对象的集合

public class BookType {
    private int typeId;
    private String typeName;
    private List<BookInfo> books;
}

dao中的方法

BookType queryBooksByType(int typeId);

sql映射文件

  • 连接查询

    <!--一对多查询,方式一:连接查询-->
    <select id="queryBooksByType" resultMap="testMap">
        select *
        from book_info bi
        inner join book_type bt on bi.type_id = bt.type_id
        where bi.type_id = #{typeId}
    </select>
    <!--自定义结果集映射-->
    <resultMap id="testMap" type="com.hqyj.ssm02.entity.BookType">
        <id property="typeId" column="type_id"></id>
        <result property="typeName" column="type_name"></result>
        <!--集合类型的属性,使用collection标签,使用ofType设置集合中的对象类型-->
        <collection property="books" ofType="com.hqyj.ssm02.entity.BookInfo">
            <id column="book_id" property="bookId"></id>
            <result column="book_name" property="bookName"></result>
            <result column="type_id" property="typeId"></result>
            <result column="book_author" property="bookAuthor"></result>
            <result column="book_num" property="bookNum"></result>
            <result column="book_price" property="bookPrice"></result>
            <result column="book_img" property="bookImg"></result>
            <result column="publisher_date" property="publisherDate"></result>
        </collection>
    </resultMap>
  • 子查询

    <!--一对多查询,方式二:子查询-->
    <!--1.根据类型编号查询自身表-->
    <select id="queryBooksByType" resultMap="testMap">
        select *
        from book_type
        where type_id = #{typeId}
    </select>
    <!--2.设置结果集映射-->
    <resultMap id="testMap" type="com.hqyj.ssm02.entity.BookType">
        <id column="type_id" property="typeId"></id>
        <result column="type_name" property="typeName"></result>
        <!--集合对象,使用collection标签。使用type_id字段执行子查询getBooksByTypeId,将结果映射到books属性-->
        <collection property="books" column="type_id" select="getBooksByTypeId"></collection>
    </resultMap>
    <!--3.子查询,根据类型编号查询所有对应的图书集合-->
    <select id="getBooksByTypeId" resultType="com.hqyj.ssm02.entity.BookInfo">
        select *
        from book_info
        where type_id = #{typeId}
    </select>
    ​

总结

  • 多对一

    如果查询要以从表数据为主,关联主表相应的数据时,属于多对一查询。如查询所有图书的同时,显示其类型。

    建议使用子查询或自定义特殊的sql语句。都需要在从表实体中添加一个主表对象属性。

    • 主表字段比较少,建议使用自定义特殊的sql语句,保证字段重命名为"主表对象.属性"

    • 主表字段比较多,建议使用子查询,使用<association>标签映射主表对象属性

  • 一对多

    如果查询要以主表数据为主,关联该主键对应的从表实体对象的集合时,属于一对多查询。如查询某个类型,同时显示该类型下的所有图书。

    建议使用子查询。在主表实体中添加一个从表集合属性。

    使用<collection>标签映射从表集合属性

多条件查询

参考#{}的用法

#{}和${}

在mybatis的sql映射文件中,可以使用#{}和${}表示sql语句中的参数。

#{}相当于预处理,会将参数用?占位后传值

${}相当于拼接,会将参数原样拼接

通常使用#{},防止sql注入

-- #{}会自动用''将参数引起来,如参数为admin
select * from user where username=#{username}
select * from user where username='admin'
-- ${}会将参数原样拼接,如参数为admin
select * from user where username=${username}
select * from user where username=admin

#{}的使用

  • 如果dao层接口中的方法参数为一个原始类型或字符串时

    • sql语句#{}中的参数是接口中方法的形参名

    public interface UserDao{
        User findById(int userId);
    }
    <select id="findById" resultType="com.xxx.entity.User">
        select * from user where id=#{userId}
    </select>
  • 如果dao层接口中的方法参数为一个实体对象时

    • sql语句#{}中的参数必须是接口中方法参数的某个属性

    public interface UserDao{
        User login(User user);
    }
    <select id="login" resultType="com.xxx.entity.User">
        select * from user where username=#{username} and password=#{password}
    </select>
  • 如果dao层接口中的方法参数不止一个时

    • 给每个参数添加@Param("自定义参数名")

    • sql语句#{}中的参数必须是@Param注解中自定义的名称

    public interface UserDao{
        User update(@Param("bianhao")int id,@Param("mima")String password);
    }
    <update id="update">
        update user set password=#{mima} where id=#{bianhao}
    </update>

${}的使用

当需要拼接的参数不能带引号时,必须使用${},如动态表名、排序条件等

动态表名

select * from ${表名}

排序条件

select * from 表名 order by ${字段}

删除<delete></delete>

删除单个

delete from 表 where 字段 = 值

只需一个值即可,通常为主键id

dao层接口

int delete(int id);

sql映射文件

<delete id="delete">
    delete from book_info where book_id=#{id}
</delete>

删除多个

delete from 表 where 字段 in(数据集合)

如delete from book_info where book_id in (1001,1002,1005)

in后面的内容通常是一个数组

前端页面通过复选框选中要删除的数据,获取选中的数据的id,保存到一个数组中

dao层接口

int deleteByIds(@Param("idList")List<Integer> idList)

sql映射文件

<delete id="deleteByIds">
    delete from book_info where book_id in 
    <!-- 
        foreach标签用户遍历dao层传递的集合对象
        collection表示要遍历的集合
        open和close表示将遍历出的数据使用什么开头和结尾
        separator表示将遍历出的数据用什么分隔
    -->
    <foreach collection="idList" item="id" separator="," open="(" close=")">
        #{id}
    </foreach>
</delete>

foreach标签可以用于sql语句中条件是集合的情况,配合where条件使用,dao层接口中方法参数为集合。

如where 字段 in/not in (值1,值2,值3)

添加<insert></insert>

添加时的参数虽多,但通常为一个完整的实体对象,所以dao层接口中方法的参数要定义成一个实体对象。

dao层

int insert(BookType bookType);

sql映射文件

  • 通常加上parameterType参数指定添加的对象实体类全限定名

  • #{}中的参数一定来自于dao层接口方法实体参数对象的属性

  • 如果要在添加成功的同时,获取自动增长的主键值时,要添加

    • useGeneratedKeys="true" 表示自动获取自增的值

    • keyColumn="type_id" 表示自增字段

    • keyProperty="typeId" 表示自增字段对应的属性名

<insert id="testInsert" useGeneratedKeys="true" keyColumn="type_id" keyProperty="typeId"
        parameterType="com.hqyj.ssm02.entity.BookType">
    insert into book_type
    values (null, #{typeName})
</insert>

修改<update></update>

修改可以分为修改所有字段和修改部分字段

修改所有字段

dao层

int update(BookInfo bookInfo);

sql映射文件

<update id="update">
    update book_info set
    book_name = #{bookName},
    book_author = #{bookAuthor},
    book_num = #{bookNum},
    book_price = #{bookPrice},
    publisher_date = #{publisherDate}
    where book_id=#{bookId}
</update>

这种方式会修改所有字段,如果实体参数的某个属性没有赋值,就会用该属性的默认值进行修改。

如String类型的属性没有赋值,就会用null修改,int类型用0修改,如果表中该字段非空,就会导致sql执行异常。

所以修改所有字段,参数为一个完整对象时,保证其中的属性都有值。

修改部分字段

方式一:dao层只写要修改的字段

dao

int updateSth(int bookId,int bookPrice,int bookNum);

sql映射文件

<update id="updateSth">
    update book_info set
    book_num = #{bookNum},
    book_price = #{bookPrice}
    where book_id = #{bookId}
</update>

方式二:dao层写完整对象,sql语句中判断字段是否有值

dao

int updateSth(BookInfo bookInfo);

sql映射文件

<update id="updateSth">
    update book_info set
    <if test="bookPrice!=0">
        book_price = #{bookPrice},
    </if>
    <if test="bookName!=null">
        book_name=#{bookName}
    </if>
    where book_id = #{bookId}
</update>

这样写,有以下几个问题

1.如果只有第一个条件满足,后续条件都不满足,最终拼接的sql语句,会在where关键字之前多出一个逗号,导致语法错误

解决方式:将set替换成<set>标签,mybatis会自动去除最后的逗号

2.替换为<set>标签后,如果所有条件都不满足,mybatis会自动去除set部分,sql语句就会变为update book_info where book_id=?,导致语法错误

解决方式:在<set>标签中,添加一个不影响原始数据的条件,如 book_id = #{bookId},只需要一个book_id参数,sql语句没有语法错误,对原始数据没有任何影响

动态SQL

  • <set>搭配<if>用于修改

    <!--动态SQL:set-if用于修改-->
    <update id="testUpdate">
        update book_info
        <set>
            book_id=#{bookId},
            <if test="bookName!=null">
                book_name = #{bookName},
            </if>
            <if test="bookPrice!=null">
                book_price = #{bookPrice}
            </if>
        </set>
        where book_id=#{bookId}
    </update>
  • <where>搭配<if>用于查询、修改、删除时的条件

    <select id="queryByCondition" resultType="com.xxx.entity.BookInfo">
        select * from book_info
        <where>
            <if test="bookName!=null">
                book_name = #{bookName}
            </if>
            <if test="bookAuthor!=null">
                and book_author = #{bookAuthor}
            </if>
            <if test="typeId!=null">
                and type_id = #{typeId}
            </if>
        </where>
    </select>
  • <trim>搭配<if>可以替换set-if和where-if

    该标签有四个属性

    prefix 表示如果trim标签中有if条件满足,就在整个trim部分添加指定前缀

    suffix 表示如果trim标签中有if条件满足,就在整个trim部分添加指定后缀

    prefixOverrides 表示去除整个trim部分多余的前缀

    suffixOverrides 表示去除整个trim部分多余的后缀

使用trim实现修改

可以修改所有字段,也可以修改部分字段

<update id="testUpdate">
    update book_info
    <!--prefix="set"表示在所有内容前加入set关键字-->
    <!--suffixOverrides=","表示所有内容之后如果有多余的逗号,去掉逗号-->
    <trim prefix="set" suffixOverrides=",">
        book_id=#{bookId},
        <if test="bookName!=null">
            book_name = #{bookName},
        </if>
        <if test="bookAuthor!=null">
            book_author = #{bookAuthor},
        </if>
        <if test="bookNum!=null">
            book_num = #{bookNum},
        </if>
        <if test="bookPrice!=null">
            book_price = #{bookPrice},
        </if>
        <if test="publisherDate!=null">
            publisher_date = #{publisherDate}
        </if>
    </trim>
    where book_id=#{bookId}
</update>

使用trim标签实现多条件查询

<select id="queryByCondition" resultType="com.hqyj.ssm02.entity.BookInfo">
    SELECT bi.*,type_name as 'bookType.typeName' FROM book_info bi,book_type bt
    <!--prefix="where"表示在所有内容前加入where关键字-->
    <!--prefixOverrides="and"表示所有内容之前如果有多余的and,去掉and-->
    <!--suffix="order by book_id desc"表示在所有内容之后加入order by book_id desc-->
    <trim prefix="where" prefixOverrides="and" suffix="order by book_id desc">
        bi.type_id=bt.type_id
        <if test="bookName!=null">
            and book_name like concat ('%',#{bookName},'%')
        </if>
        <if test="bookAuthor!=null">
            and book_author like concat ('%',#{bookAuthor},'%')
        </if>
        <if test="typeId!=0">
            and bt.type_id =#{typeId}
        </if>
    </trim>
</select>

多选删除具体实现

页面核心js

$(function () {
    //一键全选按钮
    $("#checkAll").click(function () {
        var state = this.checked;
        $(".checkDel").each(function () {
            this.checked = state;
        });
    });
    //删除所选按钮
    $("#deleteAll").click(function () {
        //定义保存id的数组
        var ids = [];
        //获取当前被选中的复选框所在的tr
        let $tr = $(".checkDel:checked").parent().parent();
        //遍历所选的tr
        $tr.each(function () {
            //获取id对应的td的值
            var id = $(this).children("td:eq(1)").text();
            //保存到数组中
            ids.push(id);
        });
        //至少选中一项
        if (ids.length == 0) {
            alert("请至少选中一项");
            return;
        }
        if (!confirm("确认要删除这" + ids.length + "条数据吗")) {
            return;
        }
        //使用ajax访问controller删除所选
        $.ajax({
            url: "${pageContext.request.contextPath}/bookInfo/deleteByChecked",
            data: {
                "ids": ids
            },
            type: "post",
            //ajax提交数组,需要添加一个参数
            traditional: true,
            success: function () {
                location.reload();
            }
        });
    });
});

dao

//批量删除
int deleteByIds(@Param("idList") List<Integer> idList);

mapper.xml

<!--批量删除-->
<delete id="deleteByIds">
    delete from book_info where book_id in
    <foreach collection="idList" item="id" open="(" close=")" separator=",">
        #{id}
    </foreach>
</delete>

service

public void deleteByChecked(Integer[] ids) {
    //将数组转换为集合
    List<Integer> list = Arrays.asList(ids);
    bookInfoDao.deleteByIds(list);
}

controller

@RequestMapping("/deleteByChecked")
public String deleteByChecked(Integer[] ids){
    bookInfoService.deleteByChecked(ids);
    return "redirect:queryAll";
}

多条件查询具体实现

搜索表单

<form class="navbar-form navbar-left"
      action="${pageContext.request.contextPath}/bookInfo/queryByCondition">
    <div class="form-group">
        <input type="text" class="form-control" name="bookName" placeholder="请输入书名关键字">
        <input type="text" class="form-control" name="bookAuthor" placeholder="请输入作者关键字">
        <select id="topSelect" class="form-control" name="typeId">
            <option value="0">全部</option>
        </select>
    </div>
    <button type="submit" class="btn btn-default">搜索</button>
</form>

dao

List<BookInfo> queryByCondition(BookInfo bookInfo);

mapper.xml

<select id="queryByCondition" resultType="com.hqyj.ssm02.entity.BookInfo">
    SELECT bi.*,type_name as 'bookType.typeName' FROM book_info bi,book_type bt
    <trim prefix="where" prefixOverrides="and" suffix="order by book_id desc">
        bi.type_id=bt.type_id
        <if test="bookName!=null">
            and book_name like concat ('%',#{bookName},'%')
        </if>
        <if test="bookAuthor!=null">
            and book_author like concat ('%',#{bookAuthor},'%')
        </if>
        <if test="typeId!=0">
            and bt.type_id =#{typeId}
        </if>
    </trim>
</select>

service

public List<BookInfo> queryByCondition(BookInfo bookInfo) {
    return bookInfoDao.queryByCondition(bookInfo);
}

controller

@RequestMapping("/queryByCondition")
public String queryByCondition(BookInfo bookInfo,Model model) {
    List<BookInfo> list= bookInfoService.queryByCondition(bookInfo);
    model.addAttribute("list",list);
    return "bookInfoList";
}

分页

使用分页组件PageHelper

1.导入依赖

<!--分页组件-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.3.2</version>
</dependency>

2.在mybatis的配置文件中

<!--设置分页插件-->
<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!--保证翻页不会超出范围-->
        <property name="reasonable" value="true"/>
    </plugin>
</plugins>

3.使用

  • 通过PageHelper类调用静态方法startPage(当前页数,每页显示的记录数)开启分页

  • 查询所有,返回集合

  • 创建PageInfo分页模型对象,构造方法的参数为查询出的集合,设置泛型,

//定义当前页和每页显示的数量
int pageNum=1;
int size=10;
//开启分页
PageHelper.startPage(pageNum,size);
//正常调用查询,返回查询到的数据集合
BookInfo bookInfo = new BookInfo();
bookInfo.setTypeId(1);
List<BookInfo> list = bookInfoDao.queryByCondition(bookInfo);
​
//创建分页模型对象,构造方法的参数为查询出的结果,设置泛型,
PageInfo<BookInfo> pageInfo = new PageInfo<>(list);
​
//此时分页相关数据都保存在pageInfo对象中
System.out.println("总记录数"+pageInfo.getTotal());
System.out.println("最大页数"+pageInfo.getPages());
System.out.println("当前页"+pageInfo.getPageNum());
System.out.println("当前容量"+pageInfo.getSize());
System.out.println("当前分页数据"+pageInfo.getList());
System.out.println("是否有上一页"+pageInfo.isHasPreviousPage());
System.out.println("是否有下一页"+pageInfo.isHasNextPage();
System.out.println("是否是首页"+pageInfo.isIsFirstPage());
System.out.println("是否是尾页"+pageInfo.isIsLastPage());

PageInfo对象常用属性和方法作用
total/getTotal()得到总记录数
pages/getPages()得到最大页数
pageNum/getPageNum()得到当前页
size/getSize()得到每页显示的记录数
list/getList()得到按当前page和size查询到的数据集合
isFirstPage/isIsFirstPage()是否是首页
isLastPage/isIsLastPage()是否是尾页

多条件分页具体实现

controller

@RequestMapping("/queryByCondition")
public String queryByCondition(
    @RequestParam(defaultValue = "1") int pageNum,
    @RequestParam(defaultValue = "8") int size,
    BookInfo bookInfo,
    Model model) {
    //1.PageHelper.startPage()
    PageHelper.startPage(pageNum, size);
    //2.查询集合
    List<BookInfo> list = bookInfoService.queryByCondition(bookInfo);
    //3.PageInfo(集合)
    PageInfo<BookInfo> pageInfo = new PageInfo<>(list);
    //将查询到的分页模型对象保存到model中
    model.addAttribute("pageInfo",pageInfo);
​
    //构造页数的集合
    ArrayList<Integer> pageList = new ArrayList<>();
    for (int i = 1; i <= pageInfo.getPages(); i++) {
        pageList.add(i);
    }
    model.addAttribute("pageList",pageList);
​
    return "bookInfoList";
}

页面分页组件

<nav aria-label="Page navigation">
    <ul class="pagination">
        <c:if test="${!pageInfo.isFirstPage}">
            <li>
                <a href="${pageContext.request.contextPath}/bookInfo/queryByCondition?bookName=${param.bookName}&bookAuthor=${param.bookAuthor}&typeId=${param.typeId}&pageNum=${pageInfo.pageNum-1}"
                   aria-label="Previous">
                    <span aria-hidden="true">&laquo;</span>
                </a>
            </li>
        </c:if>
        <c:forEach items="${pageList}" var="pno">
            <li class="pno">
                <a href="${pageContext.request.contextPath}/bookInfo/queryByCondition?bookName=${param.bookName}&bookAuthor=${param.bookAuthor}&typeId=${param.typeId}&pageNum=${pno}">${pno}</a>
            </li>
        </c:forEach>
        <c:if test="${!pageInfo.isLastPage}">
            <li>
                <a href="${pageContext.request.contextPath}/bookInfo/queryByCondition?bookName=${param.bookName}&bookAuthor=${param.bookAuthor}&typeId=${param.typeId}&pageNum=${pageInfo.pageNum+1}"
                   aria-label="Next">
                    <span aria-hidden="true">&raquo;</span>
                </a>
            </li>
        </c:if>
    </ul>
</nav>

SpringBoot

Spring推出的一个Spring框架的脚手架。

不是一个新的框架,而是搭建Spring相关内容框架的平台。

它省去了Spring、SpringMVC项目繁琐的配置过程,让开发变得更加简单。

本质还是Spring+SpringMVC,可以搭配其他的ORM框架,如MyBatis、MyBatisPlus、JPA、Hibernate等。

特点

  • 内置了Tomcat服务器,不需要部署项目到Tomcat中

  • 内置了数据源Hikari

  • 减少了jar文件依赖的配置

  • SpringBoot中的配置文件可以使用yml格式文件,代替properties或xml

创建SpringBoot项目

通过IDEA创建

通过官网模板创建

官网模板https://start.spring.io/

点击生成,会下载一个压缩文件,解压后通过IDEA打开。

无论哪种方式,都需要重写设置Maven配置文件

创建成功后的目录结构

第一个springboot项目的helloworld

热部署

项目在开发过程中,可以不需要每次都重启,等待一段时间后会自动更新编译运行

使用

添加依赖,可以在创建的项目的时候选择,也可以中途添加

<!--热部署-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <version>2.7.8</version>
</dependency>

开启热部署

Lombok

用于简化实体类中模板代码的工具

使用

添加依赖,可以在创建的项目的时候选择,也可以中途添加

<!--Lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
    <scope>provided</scope>
</dependency>

安装插件(IDEA2020.2之后的版本会内置Lombok插件,无需安装)

在某个实体类上添加注解

lombok常用注解作用
@AllArgsConstructor自动生成全参构造方法
@Data以下注解之和
@Setter自动生成set方法
@Getter自动生成get方法
@NoArgsConstructor自动生成无参构造方法
@ToString自动生成toString方法
@EqualsAndHashcode自动生成equals和hashcode方法

SpringBoot+MyBatis实现单表查询

1.创建好SpringBoot项目

最好在创建的时候选择以下依赖

  • spring-web(必选)

  • lombok

  • spring-devtools

  • springboot集成mybatis

  • mysql驱动

都可以后续添加

<!--web-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
​
<!--热部署-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <version>2.7.8</version>
</dependency>
​
<!--Lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
    <scope>provided</scope>
</dependency>

2.在pom.xml中添加mybatis集成SpringBoot依赖和数据库驱动

<!--mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.31</version>
</dependency>
<!--springboot集成MyBatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.0</version>
</dependency>

3.在springboot配置文件application.properties中

#注释
#.properties文件称为属性文件,数据以键值对"键=值"的形式保存
#设置项目启动端口号
#server.port=9090
#设置项目上下文路径
#server.servlet.context-path=/springbootday1
​
#mybatis相关配置
#开启驼峰命名映射
mybatis.configuration.map-underscore-to-camel-case=true
#打印sql语句
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#扫描mybatis的sql映射文件(将mapper.xml文件保存在resources目录下的mapper目录下)
mybatis.mapper-locations=classpath:mapper/*.xml
​
#数据库信息
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/gamedb?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root

mybatis的sql映射文件模板

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--设置该文件对应的dao层接口全限定名-->
<mapper namespace="">
​
</mapper>

4.根据数据表创建实体类、dao层接口、service、controller

Hero

@Data
public class Hero{
    private Integer id;
    private String name;
    private String position;
    private String sex;
    private Double price;
    private String shelfDate;
}

dao

@Repository
public interface HeroDao{
    List<Hero> queryAll();
}

mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--设置该文件对应的dao层接口全限定名-->
<mapper namespace="com.xxx.dao.HeroDao">
    <select id="queryAll" resultType="com.xxx.entity.Hero">
        select * from hero
    </select>
</mapper>

service

@Service
public class HeroService{
    @Autowired
    private HeroDao heroDao;
    
    public List<Hero> queryAll(){
        return heroDao.queryAll();   
    }
}

controller

@Controller
@RequestMapping("/hero")
public class HeroController{
    
    @Autowired
    private HeroService heroService;
    
    @RequestMapping("/queryAll")
    @ResponseBody
    public List<Hero> queryAll(){
        return heroDao.queryAll();   
    }
}

5.在SpringBoot的启动类上,加入@MapperScan注解,扫描dao层所在根包

@SpringBootApplication
//扫描dao层所在的根包
@MapperScan("com.hqyj.first.dao")
public class FirstApplication {
​
    public static void main(String[] args) {
        SpringApplication.run(FirstApplication.class, args);
    }
​
}

启动项目,按自定的项目上下文路径和端口号访问某个controller中的方法

SpringBoot+LayUI实现酒店客房管理

核心知识点

  • SpringBoot项目搭建

    • 核心依赖

      <dependency>
        <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
    • 集成MyBatis

      <dependency>
         <groupId>org.mybatis.spring.boot</groupId>
          <artifactId>mybatis-spring-boot-starter</artifactId>
          <version>2.3.0</version>
      </dependency>
  • 新注解

    • @RestController

      如果某个控制器类中的所有方法都要加@ResponseBody,可以在类上加@ResponseBody,也可以用@RestController代替@ResponseBody和@Controller

  • LayUI

    • 数据表格

      • 数据接口格式

      • 真假分页

      • 头工具栏事件

      • 行内事件

      • 单元格编辑事件

    • 弹出层

      • layer.msg()

      • layer.confirm()

      • layer.open()

  • ajax

    $.ajax({
        url:"",
        data:xxx,
        type:"get/post",
        success:function(){
            
        }
    })
  • 前后端分离

    • 该案例可以将页面独立出来,成为一个前后端分离项目,也可以将页面作为静态资源保存在static目录下

  • 如果设计为前后端分离,要在控制器类上加入@CrossOrigin,表示该类中的所有方法允许跨域请求

  • 打包SpringBoot项目

    保证项目中无错误,包含单元测试中

打包后是一个.jar文件,位于target目录中

在安装有java环境的机器中,控制台运行jar文件

java -jar 文件名.jar

组织结构图

部分功能流程图

管理员

入住

数据库部分设计

ER图

数据库表详细设计

管理员表sys_admin

客房表room

客户表customer

订单表orders

核心Java代码

实体类

订单表

/*
* 订单
* */
@Data
public class Orders {
    private Integer id;
    private Integer roomNo;
    private Integer cusId;
    //@JsonFormat是springboot集成的jackson包中的注解,用于格式化日期
    //pattern指定格式日期 timezone指定时区,这里和数据库的时区保持一致
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "Asia/Shanghai")
    private Date leaveTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "Asia/Shanghai")
    private Date stayTime;
    private Integer cost;
​
    //显示订单的同时,要显示房间的相关信息
    private Room room;
    //显示订单的同时,要显示客户的相关信息
    private Customer customer;
}

数据访问层

  • 实现随意修改某个字段的值

    int update(@Param("field") String field,
               @Param("value") String value,
               @Param("id") int id);

    sql映射文件

    <update id="update">
        update room
        <set>
            <if test="field=='roomType'">
                room_type= #{value}
            </if>
            <if test="field=='roomPrice'">
                room_price= #{value}
            </if>
            <if test="field=='roomUse'">
                room_use=#{value}
            </if>
        </set>
        where room_no=#{id}
    </update>

  • 添加时获取自增的值

    int insert(Customer customer);

    sql映射文件

    <!--添加顾客的同时,获取自增的id-->
    <insert id="insert" useGeneratedKeys="true" keyColumn="cus_id" keyProperty="cusId">
        insert into customer values(null,#{cusName},#{cusPhone},#{cusIdcard})
    </insert>
  • 多表连接查询/条件查询

    List<Orders> queryAll();
    ​
    List<Orders> search(String keyword);
    ​
    //根据客户编号查询是否已入住
    Orders isStay(Integer cusId);
    ​
    //根据房间号查询正在入住的订单信息
    Orders findCheckInByRoomNo(Integer roomNo);
    ​
    //计算费用
    int checkOut(@Param("roomNo") Integer roomNo,@Param("cost") Integer cost);

    sql映射文件

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <!--设置该文件对应的dao层接口全限定名-->
    <mapper namespace="com.hqyj.hotel_sys.dao.OrdersDao">
    ​
        <insert id="insert">
            insert into orders
            values (null, #{roomNo}, #{cusId}, now(), null, 0)
        </insert>
    ​
    ​
        <!--关联查询方式一:特殊的SQL语句-->
        <!--
         <select id="queryAll" resultType="com.hqyj.hotel_sys.entity.Orders">
             select o.*,
                    cus_name as 'customer.cusName',
                     cus_phone as 'customer.cusPhone',
                     cus_idcard as 'customer.cusIdcard',
                     room_type as 'room.roomType',
                     room_price as 'room.roomPrice',
                     room_use as 'room.roomUse'
             from customer c,
                  room r,
                  orders o
             where c.cus_id = o.cus_id
               and r.room_no = o.room_no
         </select>
         -->
    ​
        <!--多表连接条件查询-->
        <select id="search" resultMap="ordersMap">
            SELECT * FROM orders o,customer c
            <trim prefix="where" prefixOverrides="and">
                o.cus_id = c.cus_id
                <if test="keyword!=null">
                    and cus_name like concat('%',#{keyword},'%')
                </if>
            </trim>
        </select>
    ​
        <!--关联查询方式二:子查询-->
        <!--1.查询自身表-->
        <select id="queryAll" resultMap="ordersMap">
            select *
            from orders
        </select>
        <!--自定义结果集映射,用cus_id和room_no做子查询,重新映射一次到orders对象-->
        <resultMap id="ordersMap" type="com.hqyj.hotel_sys.entity.Orders">
            <result property="roomNo" column="room_no"></result>
            <result property="cusId" column="cus_id"></result>
            <association property="room" column="room_no" select="findRoomByNo"></association>
            <association property="customer" column="cus_id" select="findCustomerById"></association>
        </resultMap>
        <!--子查询一:根据房号查房间-->
        <select id="findRoomByNo" resultType="com.hqyj.hotel_sys.entity.Room">
            select *
            from room
            where room_no = #{roomNo}
        </select>
        <!--子查询二:根据编号查客户-->
        <select id="findCustomerById" resultType="com.hqyj.hotel_sys.entity.Customer">
            select *
            from customer
            where cus_id = #{cusId}
        </select>
    ​
    ​
        <select id="isStay" resultType="com.hqyj.hotel_sys.entity.Orders">
            select *
            from orders
            where cus_id = #{cusId}
              and leave_time is null
        </select>
    ​
        <!--
            根据房号查询正在入住的订单信息,包含房间价格
        -->
        <select id="findCheckInByRoomNo" resultType="com.hqyj.hotel_sys.entity.Orders">
            select o.*, room_price as 'room.roomPrice'
            from orders o,
                 room r
            where o.room_no = r.room_no
              and r.room_no = #{roomNo}
              and leave_time is null
        </select>
    ​
        <update id="checkOut">
            update orders
            set leave_time=now(),
                cost=#{cost}
            where room_no = #{roomNo}
              and leave_time is null
        </update>
    </mapper>

业务流程层

package com.hqyj.hotel_sys.service;
​
import com.hqyj.hotel_sys.dao.CustomerDao;
import com.hqyj.hotel_sys.dao.OrdersDao;
import com.hqyj.hotel_sys.dao.RoomDao;
import com.hqyj.hotel_sys.entity.Customer;
import com.hqyj.hotel_sys.entity.Orders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
​
import java.util.Date;
import java.util.List;
​
@Service
public class CustomerService {
​
​
    @Autowired
    private CustomerDao customerDao;
    @Autowired
    private RoomDao roomDao;
    @Autowired
    private OrdersDao ordersDao;
​
    /*
     * 入住
     * 1.获取顾客信息:姓名、电话、身份证号,
     *   根据身份证号判断是否存在,如果存在,用存在的对象
     *   如果不存在,向customer表中添加一条记录 insert into customer values(null,#{cusName},#{cusPhone},#{cusIdcard})
     *   添加成功时,要获取自增的id,用于订单表中使用
     *   判断该顾客是否已入住:查询订单表中对应顾客编号的记录,如果退房时间为空,说明已入住
     *   select * from orders where cus_id=#{cusId} and leave_time is null
     * 2.将对应房间的状态改为1,对room表中修改 update room set room_use=1 where room_no=#{roomNo}
     * 3.向订单表中添加一条记录 insert into orders values (null,#{roomNo},#{cusId},now(),null,0)
     *
     * 以上3个步骤属于一个事务,要在该方法上加事务注解
     * */
    @Transactional//让该方法成为一个事务,如果执行中途出错,会自动回滚
    public boolean checkIn(Customer customer, int roomNo) {
        //1.添加客户,根据身份证号判断是否存在
        Customer byIdcard = customerDao.findByIdcard(customer.getCusIdcard());
        boolean b1;
        if (byIdcard == null) {//不存在,调用添加
            b1 = customerDao.insert(customer) > 0;
        } else {
            customer = byIdcard;
            b1 = true;
        }
        //查看是否已入住
        if (ordersDao.isStay(customer.getCusId()) != null) {
            return false;
        }
        //2.修改房间状态
        boolean b2 = roomDao.update("roomUse", "1", roomNo) > 0;
        //3.添加订单信息
        //创建订单对象
        Orders orders = new Orders();
        //客户编号在添加客户成功后,会自动获取
        orders.setCusId(customer.getCusId());
        orders.setRoomNo(roomNo);
        boolean b3 = ordersDao.insert(orders) > 0;
​
        return b1 & b2 & b3;
    }
​
​
    /*
     * 退房
     * 1.根据房间号查询对应正在入住的订单记录(同时查询出房价)
     * select o.* from orders o,room r where o.room_no=r.room_no and r.room_no =#{roomNo} and leave_time is null
     * 2.结算
     * 添加退房时间、添加花费
     * update orders set leave_time =now() ,cost=#{cost} where room_no =#{roomNo} and leave_time is null
     * 3.修改房间状态为空闲
     * update room set room_use = 0 where room_no=#{roomNo}
     * */
    @Transactional
    public boolean checkOut(Integer roomNo) {
        //1.根据房间号查询对应正在入住的订单记录
        Orders orders = ordersDao.findCheckInByRoomNo(roomNo);
        //2.计算费用
        Date stayTime = orders.getStayTime();
        Date now = new Date();
        double l = now.getTime() - stayTime.getTime();
        //转换为天数
        double day = Math.ceil(l / 1000 / 3600 / 24);
        double cost = orders.getRoom().getRoomPrice() * day;
        //修改订单中的花费和退房时间
        //update orders set leave_time=now(),cost=#{cost} where room_no=#{roomNo} and leave_time is null
        boolean b1 = ordersDao.checkOut(roomNo, (int) cost) > 0;
        //3.修改房间状态为空闲0
        //update room set room_use where room_no = #{roomNo}
        boolean b2 = roomDao.update("roomUse", "0", roomNo) > 0;
        return b1 & b2;
    }
}

控制层

如果某个控制器中的所有方法都需要返回JSON格式字符串,在类上加@ResponseBody

@Controller
@RequestMapping("/room")
public class RoomController {
​
    @Autowired
    private RoomService roomService;
​
    @ResponseBody
    @RequestMapping("/queryAll")
    public ResultData<Room> queryAll(Integer page, Integer limit) {
        //使用PageHelper分页
        PageHelper.startPage(page, limit);
        //正常查询所有
        List<Room> rooms = roomService.queryAll();
        //创建分页模型对象
        PageInfo<Room> pageInfo = new PageInfo<>(rooms);
        //返回的数据为分页后的集合,数量为总记录数
        return new ResultData<>((int)pageInfo.getTotal(),pageInfo.getList());
    }
}
​

LayUI数据表格

数据表格所需的数据接口格式为

{
    code:0,
    msg:"",
    count:1000,
    data:[{},{}]
}

构造满足LayUI数据表格的数据模型类ResultData

/*
* 定义一个用于LayUI数据表格对应格式的模板类
* code  状态码         0成功
* msg   提示文字
* count 数据总量
* data  数据集合
* */
@Data
public class ResultData<T> {
    private Integer code;
    private String msg;
    private Integer count;
    private List<T> data;
​
    /*
    * 定义带count和data的构造方法,用于初始化code和msg
    * */
    public ResultData(Integer count, List<T> data) {
        code=0;
        msg="";
        this.count = count;
        this.data = data;
    }
}
​

最后在控制层中,将查询的方法的返回值更改为ResultData类型

@RequestMapping("/queryAll")
public ResultData<Room> queryAll() {
    List<Room> rooms = roomService.queryAll();
    return new ResultData<>(rooms.size(), rooms);
}

最终页面

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>LayUI</title>
    <meta name="renderer" content="webkit">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <link rel="stylesheet" href="../LayUI/css/LayUI.css" media="all">
    <!-- 注意:如果你直接复制所有代码到本地,上述css路径需要改成你本地的 -->
</head>
<body>
<table class="LayUI-hide" id="test" lay-filter="test"></table>
<script type="text/html" id="toolbarDemo">
    <div class="LayUI-btn-container">
        <button class="LayUI-btn LayUI-btn-sm" lay-event="getCheckData">获取选中行数据</button>
        <button class="LayUI-btn LayUI-btn-sm" lay-event="getCheckLength">获取选中数目</button>
        <button class="LayUI-btn LayUI-btn-sm" lay-event="isAll">验证是否全选</button>
        <button class="LayUI-btn LayUI-btn-sm" lay-event="addRoom">添加客房</button>
    </div>
</script>
<script type="text/html" id="barDemo">
    <!--插值表达式-->
    {{# if(d.roomUse==0){ }}
    <a class="LayUI-btn LayUI-btn-normal LayUI-btn-sm" lay-event="check-in">入住</a>
    <a class="LayUI-btn LayUI-btn-danger LayUI-btn-sm" lay-event="del">删除</a>
    {{# }else{ }}
    <a class="LayUI-btn LayUI-btn-warm LayUI-btn-sm" lay-event="check-out">退房</a>
    {{# } }}
</script>
​
<script src="../LayUI/LayUI.js" charset="utf-8"></script>
<!-- 注意:如果你直接复制所有代码到本地,上述 JS 路径需要改成你本地的 -->
<script>
    LayUI.use('table', function () {
        var table = LayUI.table;
​
        //引入Jquery
        var $ = LayUI.$;
​
​
        /*
        * 渲染表格数据
        * url:数据访问接口
        * cols:表格列
        * field:数据字段名
        * title:表头
        * cellminwidth:100  自适应宽度
        * fixed:left    固定在某侧
        * unresize:true 不可改变尺寸
        * sort:true 排序
        * edit:true 行内编辑
        * */
        table.render({
            elem: '#test'
            //设置数据接口
            , url: 'http://localhost:9090/hotel/room/queryAll'
            , toolbar: '#toolbarDemo' //开启头部工具栏,并为其绑定左侧模板
            , defaultToolbar: ['filter', 'exports', 'print', { //自定义头部工具栏右侧图标。如无需自定义,去除该参数即可
                title: '提示'
                , layEvent: 'LAYTABLE_TIPS'
                , icon: 'LayUI-icon-tips'
            }]
            , title: '用户数据表'
            , cols: [[
                {type: 'checkbox', fixed: 'left'}
                , {field: 'roomNo', title: '房间号', cellminwidth: 100, fixed: 'left', unresize: true, sort: true}
                , {field: 'roomType', title: '房间类型', cellminwidth: 100, edit: 'text'}
                , {field: 'roomPrice', title: '房间单价', cellminwidth: 100, edit: 'text', sort: true}
                , {
                    field: 'roomUse', title: '使用状态', cellminwidth: 100,
                    templet: function (res) {
                        return res.roomUse == 0 ? "<span style='color:green'>空闲中</span>" : "<span style='color:red'>使用中</span>";
                    }
                }
                , {fixed: 'right', title: '操作', toolbar: '#barDemo', cellminwidth: 100,}
            ]]
            //开启分页组件
            , page: true
            //设置每页显示记录数的下拉选项
            , limits: [5, 10, 20]
            //默认每页显示的条数,没有设置默认为10
            , limit: 10
            //解析数据表格url请求后的数据
            , parseData: function (res) {//res就是url对应的数据
               /*
                //假分页
                var result;
                if (this.page.curr) {
                    result = res.data.slice(this.limit * (this.page.curr - 1), this.limit * this.page.curr);
                } else {
                    result = res.data.slice(0, this.limit);
                }
                */
                return {
                    "code": res.code,
                    "msg": res.msg,
                    "count": res.count,
                    "data": res.data
                }
            }
        });
​
        //头工具栏事件
        table.on('toolbar(test)', function (obj) {
            var checkStatus = table.checkStatus(obj.config.id);
            switch (obj.event) {
                case 'addRoom':
                    layer.open({
                        title: '添加客房',
                        type: 2,
                        content: 'addRoom.html',
                        area: ['350px', '250px'],
                        resize: false,
                        anim: 2,
                        /* success: function(layero, index){
                             console.log(layero, index);
                         }*/
                    })
                    break;
                case 'getCheckData':
                    //data是所选数据的集合
                    var data = checkStatus.data;
                    for (var i = 0; i < data.length; i++) {
                        console.log(data[i].roomNo)
                    }
                    // layer.alert(JSON.stringify(data));
                    break;
                case 'getCheckLength':
                    var data = checkStatus.data;
                    layer.msg('选中了:' + data.length + ' 个');
                    break;
                case 'isAll':
                    layer.msg(checkStatus.isAll ? '全选' : '未全选');
                    break;
​
                //自定义头工具栏右侧图标 - 提示
                case 'LAYTABLE_TIPS':
                    layer.alert('这是工具栏右侧自定义的一个图标按钮');
                    break;
            }
            ;
        });
​
        //监听单元格编辑事件
        table.on('edit(test)', function (obj) {
            // console.log(obj);
            //layer.confirm("提示文件",function(){确认触发},function(){取消触发})
            layer.confirm('确认要修改吗', function (index) {
                //使用ajax提交要修改的字段名、修改后的值、要修改的编号
                $.ajax({
                    url: "http://localhost:9090/hotel/room/update",
                    data: {
                        "field": obj.field,//要修改的字段
                        "value": obj.value,//修改后的值
                        "id": obj.data.roomNo//要修改的id
                    },
                    success: function () {
                        //修改成功,关闭确认框
                        layer.close(index);
                    }
                });
            }, function () {
                //修改失败,重新加载
                location.reload()
            })
        })
​
​
        //监听行工具事件
        table.on('tool(test)', function (obj) {
            //data就是当前行中的数据
            var data = obj.data;
            // console.log(obj)
            if (obj.event === 'del') {
                //弹出确认框
                layer.confirm('真的删除行么', function (index) {
                    //使用ajax提交要删除的id
                    $.ajax({
                        url: "http://localhost:9090/hotel/room/delete",
                        data: {
                            "delId": data.roomNo
                        },
                        success: function (res) {
                            if (res) {
                                //前端假删除
                                obj.del();
                                //关闭确认框
                                layer.close(index);
                            }
                        }
                    });
                });
            } else if (obj.event === 'check-in') {
                //弹出输入顾客信息表单
                layer.open({
                    title: '输入顾客信息',
                    type: 2,
                    content: 'addCustomer.html',
                    area: ['350px', '400px'],
                    resize: false,
                    anim: 2,
                    //弹出窗口后的回调函数
                    success: function (layero, index) {
                        //弹出成功后,在弹出页面中加入当前点击行的roomNo
                        //获取弹出层的body部分
                        var body = layer.getChildFrame('body', index);
                        //获取弹出层body中的隐藏域,给其赋值
                        body.find("input[name=roomNo]").val(data.roomNo);
                    }
                })
            } else if (obj.event === 'check-out') {
                layer.confirm("确定要退房吗", function () {
                    $.ajax({
                        url: "http://localhost:9090/hotel/customer/checkOut",
                        data: {
                            "roomNo": data.roomNo
                        },
                        success: function (res) {
                            if (res) {
                                location.reload();
                            }
                        }
                    });
                })
            }
        });
    });
</script>
​
</body>
</html>

LayUI添加页面

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <link rel="stylesheet" href="../LayUI/css/LayUI.css" media="all">
    </head>
    <body>
        <form class="LayUI-form" action="">
            <div class="LayUI-form-item">
                <label class="LayUI-form-label">房间类型</label>
                <div class="LayUI-input-inline">
                    <input type="text" name="roomType" required  lay-verify="required" placeholder="请输入房间类型" autocomplete="off" class="LayUI-input">
                </div>
            </div>
            <div class="LayUI-form-item">
                <label class="LayUI-form-label">房间单价</label>
                <div class="LayUI-input-inline">
                    <input type="text" name="roomPrice" required lay-verify="required" placeholder="请输入房间单价" autocomplete="off" class="LayUI-input">
                </div>
            </div>
            <div class="LayUI-form-item">
                <div class="LayUI-input-block">
                    <button class="LayUI-btn" lay-submit lay-filter="formDemo">立即提交</button>
                    <button type="reset" class="LayUI-btn LayUI-btn-primary">重置</button>
                </div>
            </div>
        </form>
        <script src="../LayUI/LayUI.js" charset="utf-8"></script>
        <script>
            //Demo
            LayUI.use('form', function(){
                var form = LayUI.form;
                //使用LayUI内置jquery
                var $=LayUI.$;
                //监听提交
                form.on('submit(formDemo)', function(data){
                    // data.field是当前表单中的所有数据
                    // layer.msg(JSON.stringify(data.field));
                    $.ajax({
                        url:'http://localhost:9090/hotel/room/addRoom',
                        //将表单中的所有数据一起提交,实际提交的是name=value,如roomType=值&roomPrice=值
                        data:data.field,
                        //相当于
                        /* data:{
                    roomType:"",
                    roomPrice:""
                }*/
                        success:function(res){
                            if(res){
                                //刷新父页面
                                parent.location.reload()
                            }
                        }
                    })
                    //return false时不提交表单
                    return false;
                });
            });
        </script>
    </body>
</html>

LayUI分页

  • 假分页

    查询所有,在页面中分页,适合记录比较少的情况

    table.render({
        //省略url等
    ​
        //开启分页组件
        , page: true
        //设置每页显示记录数的下拉选项
        , limits: [5, 10, 20]
        //默认每页显示的条数,没有设置默认为10
        , limit: 10
        //解析数据表格url请求后的数据
        , parseData: function (res) {//res就是url对应的数据
    ​
            //假分页
            var result;
            if (this.page.curr) {
                result = res.data.slice(this.limit * (this.page.curr - 1), this.limit * this.page.curr);
            } else {
                result = res.data.slice(0, this.limit);
            }
    ​
            return {
                "code": res.code,
                "msg": res.msg,
                "count": res.count,
                "data": result
            }
        }
    })
  • 真分页

    可以使用PageHelper组件

    依赖

    <!--分页组件SpringBoot集成PageHelper-->
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper-spring-boot-starter</artifactId>
        <version>1.4.6</version>
    </dependency>

    在application.properties中配置

    # 防止不合理分页
    pagehelper.reasonable=true

    在控制层中使用

    @ResponseBody
    @RequestMapping("/queryAll")
    //这里的page和limit参数是layui数据表格自动传递
    public ResultData<Room> queryAll(Integer page, Integer limit) {
        //使用PageHelper分页
        PageHelper.startPage(page, limit);
        //正常查询所有
        List<Room> rooms = roomService.queryAll();
        //创建分页模型对象
        PageInfo<Room> pageInfo = new PageInfo<>(rooms);
        //返回的数据为分页后的集合,数量为总记录数
        return new ResultData<>((int)pageInfo.getTotal(),pageInfo.getList());
    }

LayUI条件查询

页面头部工具栏中加入搜索框

<script type="text/html" id="toolbarDemo">
    <div class="layui-form-item">
        <div class="layui-input-inline">
            <input type="text" name="keyword" required lay-verify="required" placeholder="请输入姓名关键字" autocomplete="off"
                   class="layui-input">
    </div>
        <button class="layui-btn layui-btn-primary" lay-event="search">搜索</button>
    </div>
</script>

js部分

//头工具栏事件
table.on('toolbar(test)', function (obj) {
    switch (obj.event) {
        case 'search':
            //获取输入的内容
            let keyword = $("input[name=keyword]").val();
            if(keyword==""){
                layer.msg("输入不能为空");
                return;
            }
            //如果要修改数据表格中的数据,只能更改url的地址后,重新加载数据表格
            table.reload('test',{
                url:"http://localhost:9090/hotel/orders/search?keyword="+keyword
            });
            break;
    }
});

dao层参考上方数据访问层条件查询部分

MyBatisPlus

官网简介 | MyBatis-Plus (baomidou.com)

MyBatis-Plus (简称 MP)是一个MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

只需简单的配置,就能实现对单表的CURD。

其核心有两个接口:BaseMapper和IService

BaseMapper中封装了大量数据访问层的方法

IServcie中封装了大量业务流程层的方法

SpringBoot+MyBatisPlus

1.创建SpringBoot项目

创建时勾选依赖

  • devtools

  • lombok

  • spring-web

  • mysql-driver

2.导入SpringBoot集成MyBatisPlus依赖

<!-- SpringBoot集成MyBatisPlus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>

3.配置application.properties文件

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/gamedb?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
​
# 开启sql日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# 无需加入开启驼峰命名映射,MyBatisPlus默认使用驼峰命名进行属性-字段映射
#mybatis-plus.configuration.map-underscore-to-camel-case=true

4.根据数据表创建实体类

实体类的属性名命名方式:

  • MyBatisPlus默认使用驼峰命名对字段和属性进行映射。如将字段stu_name对应的属性写为stuName

  • 如果字段名和属性名不一致,在属性名上加入@TableField(value = "字段名")

  • 主键字段对应的属性,需要加入@TableId注解,其type属性表示主键生成策略

    • @TableId(type = IdType.AUTO)表示主键自增,在数据库也要将主键设置为自增

    • @TableId(type = IdType.ASSIGN_ID)//IdType.ASSIGN_ID表示使用"雪花算法"(根据时间和机器特征码)生成一个id

    • @TableId(type = IdType.ASSIGN_UUID)//IdType.ASSIGN_UUID表示使用UUID生成一个随机字符串id

@Data
public class Hero {
    //type表示主键生成策略,
    @TableId(type = IdType.AUTO)// IdType.AUTO表示主键自增,在数据库也要将主键设置为自增
    //@TableId(type = IdType.ASSIGN_ID)//IdType.ASSIGN_ID表示使用"雪花算法"(根据时间和机器特征码)生成一个id
    //@TableId(type = IdType.ASSIGN_UUID)//IdType.ASSIGN_UUID表示使用UUID生成一个随机字符串id
    private Integer id;
    //如果属性名和字段名不一致
    @TableField(value = "name")
    private String heroName;
    private String position;
    private String sex;
    private Integer price;
    private String shelfDate;
}
​

5.编写数据访问层接口

可以不用写@Repository,继承BaseMapper接口,设置泛型

/*
* 数据访问层可以称为dao或mapper层
* 可以不用加@Repository注解
* */
public interface HeroMapper extends BaseMapper<Hero> {
​
}

6.在SpringBoot的启动类中,扫描数据访问层所在包

@SpringBootApplication
@MapperScan("com.hqyj.sbmp01.mapper")
public class Sbmp01Application {
​
    public static void main(String[] args) {
        SpringApplication.run(Sbmp01Application.class, args);
    }
}

测试

在SpringBoot自带的单元测试类中,注入HeroMapper对象,调用BaseMapper中定义的方法即可实现CURD。

BaseMapper接口

BaseMapper接口中定义了常用的增删改查方法,

在数据访问层接口中继承该接口

方法列表

使用

public interface HeroMapper extends BaseMapper<Hero> {
​
}

BaseMapper接口中的常用方法

方法名参数作用
selectList(Wrapper wrapper)条件构造器根据条件查询集合,如果实参为null表示查询所有,返回List集合
selectById(Serializable id)主键根据主键查询单个对象,返回单个对象
selectOne(Wrapper wrapper)条件构造器条件查询单个对象,返回单个对象
insert(T entity)实体对象添加单个实体
updateById(T entity)实体对象根据实体对象单个修改,对象必须至少有一个属性和主键
update(T entity,Wrapper wrapper)实体对象和条件构造器根据条件修改全部,对象必须至少有一个属性
deleteById(Serializable id/T entity)主键/实体对象根据主键删除单个对象
deleteBatchIds(Collection ids)主键集合根据集合删除
delete(Wrapper wrapper)条件构造器根据条件删除,如果实参为null表示无条件删除所有

测试

package com.hqyj.sbmp01;
​
import com.hqyj.sbmp01.entity.Hero;
import com.hqyj.sbmp01.mapper.HeroMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
​
@SpringBootTest
class Sbmp01ApplicationTests {
​
​
    @Autowired
    //@Resource
    private HeroMapper mapper;
​
    @Test
    void contextLoads() {
        //查询所有
        List<Hero> list = mapper.selectList(null);
        for (Hero hero : list) {
            System.out.println(hero);
        }
    }
​
    @Test
    void test1() {
        //根据主键查询单个对象
        System.out.println(mapper.selectById(20));
    }
​
    @Test
    void test2() {
        //根据id删除,参数为id
        //System.out.println(mapper.deleteById(20));
​
        //根据id删除,参数为带有id的对象
        /*Hero hero = new Hero();
        hero.setId(100);
        System.out.println(mapper.deleteById(hero));*/
​
        //删除所有
        //mapper.delete(null);
​
        //根据id集合删除
        ArrayList<Integer> list = new ArrayList<>();
        list.add(99);
        list.add(100);
        list.add(88);
        list.add(69);
        System.out.println(mapper.deleteBatchIds(list));
    }
​
​
    @Test
    void test3() {
        //根据id修改
        //参数为一个对象,这个对象至少要有一个非主键属性有值
        Hero hero = new Hero();
        hero.setHeroName("a");
        hero.setPosition("b");
        hero.setSex("c");
        hero.setShelfDate("d");
        hero.setPrice(123);
        hero.setId(999);
        //System.out.println(mapper.updateById(hero));
​
        // 修改全部数据
        // mapper.update(修改后的对象,条件构造器)
        mapper.update(hero,null);
    }
​
    @Test
    void test4() {
        Hero hero = new Hero();
        hero.setHeroName("asdfsdfsdf");
        hero.setPosition("b");
        hero.setSex("c");
        hero.setShelfDate("1999-9-9");
        hero.setPrice(123);
        mapper.insert(hero);
    }
}

IService接口

IService接口减少业务逻辑层的代码,并对BaseMapper进行了拓展

在业务流程类中继承该接口

部分方法列表

使用

  • 1.创建一个业务层接口,继承IService接口,设置泛型

    /*
    * 创建业务逻辑层接口,继承IService<T>接口,设置泛型
    * */
    public interface HeroService extends IService<Hero> {
    }
    ​
  • 2.创建一个业务层接口的实现类,添加@Service注解,继承 ServiceImpl<M, T>,实现上一步创建的接口

    • M是数据访问层接口

    • T是实体类

    /*
    * 1.添加@Service
    * 2.继承ServiceImpl<M, T> M是Mapper类 T是实体类
    * 3.实现自定义Service接口
    * */
    @Service
    public class ImpHeroService extends ServiceImpl<HeroMapper, Hero> implements HeroService {
    ​
    }

IService接口中的常用方法

方法作用
list()无条件查询所有
list(Wrapper wrapper)条件查询素有
page(Page page)无条件分页查询,Page是分页模型对象
page(Page page,Wrapper wrapper)条件分页查询,Page是分页模型对象
getById(Serializable id)根据主键查询单个对象
getOne(Wrapper wrapper)条件查询单个对象
save(T entity)添加单个对象
save(Collection col)批量添加对象的集合
updateById(T entity)修改,参数至少有一个属性值和主键
saveOrUpdate(T entity)添加或修改。如果实参对象的主键值不存在则添加,存在则修改
update(T entity,Wrapper wrapper)条件修改,条件为null则修改全部数据
removeById(Serializable id/T entity)根据主键或包含主键的对象删除
removeBatchByIds(Collection ids)根据集合删除
remove(Wrapper wrapper)根据条件删除,条件为null则删除全部

测试

package com.hqyj.sbmp01;
​
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hqyj.sbmp01.entity.Hero;
import com.hqyj.sbmp01.mapper.HeroMapper;
import com.hqyj.sbmp01.service.HeroService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
​
@SpringBootTest
class IServiceTest {
​
​
    @Autowired
    private HeroService heroService;
​
​
    @Test
    void test1() {
        //无条件查询所有
        //heroService.list().forEach(System.out::println);
        //条件查询,如果条件为null表示查询所有
        //heroService.list(null).forEach(System.out::println);
        //根据主键查询
        //Hero hero = heroService.getById(22);
        //System.out.println(hero);
        //根据条件查询,如果条件为null表示查询所有,会导致异常
        //System.out.println(heroService.getOne(null));
    }
​
    @Test
    void test2() {
        //添加save(T entity);参数对象至少要有一个属性值
        //添加后,主键自增的值会自动赋值给主键属性
        /*
        Hero hero = new Hero();
        hero.setHeroName("cvb");
        hero.setSex("男");
        hero.setPrice(2000);
        hero.setPosition("战士");
        heroService.save(hero);
        System.out.println(hero);
        */
        //批量添加,参数为要添加的对象集合
        /* Hero h1 = new Hero(0, "1", "1", "1", 100, "1999-9-9");
        Hero h2 = new Hero(0, "2", "1", "1", 100, "1999-9-9");
        Hero h3 = new Hero(0, "3", "1", "1", 100, "1999-9-9");
        List<Hero> heroes = Arrays.asList(h1, h2, h3);
        heroService.saveBatch(heroes);*/
​
        Hero h1 = new Hero(0, "666", "1", "1", 100, "1999-9-9");
        //如果添加的对象主键值已存在执行修改,不存在则添加
        heroService.saveOrUpdate(h1);
    }
​
​
    @Test
    void test3(){
        //根据对象修改
        Hero h1 = new Hero(999, "修改后", "修改后", "女", 100, "1999-9-9");
        //如果对象主键值已存在执行修改,不存在则添加
        //heroService.saveOrUpdate(h1);
        //根据主键修改
        heroService.updateById(h1);
​
        //根据条件修改
        //heroService.update(null);
​
        //根据实体和条件修改,如果条件为空,会将所有数据修改为指定实体的数据
        //heroService.update(h1,null);
    }
​
    @Test
    void test4(){
        //根据主键或带有主键值的对象删除
        //heroService.removeById(1103302663);
        /*Hero hero = new Hero();
        hero.setId(1103302662);
        heroService.removeById(hero);*/
​
        //批量删除
        //heroService.removeBatchByIds(Arrays.asList(77,88,99));
​
        //根据条件删除,条件为null则删除全部
        heroService.remove(null);
    }
​
}

条件构造器Wrapper

BaseMapper和IService接口中有很多方法都有这个参数,表示一个条件构造器对象。

如果该参数实际传递的值为null,表示没有任何条件,

这个Wrapper是一个抽象类,如果想要带条件,就要创建一个该类的子类对象。

常用子类为QueryWrapper和UpdateWrapper,

查询是创建QueryWrapper对象,更新时创建UpdateWrapper,实际使用无区别。

Wrapper对象带参数

Wrapper<T> wrapper =  new QueryWrapper(T entity);
QueryWrapper<T> wrapper =  new QueryWrapper(T entity);

Wrapper构造方法的参数如果是一个实体对象,只要该对象的属性不为空,就会将所有属性用and拼接起来作为条件。

这种适合已知某个字段为某个值时使用。

@Test
void test1() {
    //创建实体对象,只带两个属性值
    Hero hero = new Hero();
    hero.setHeroName("瑞兹");
    hero.setSex("女");
    //创建一个带实体参数的条件构造器对象
    Wrapper<Hero> wrapper = new QueryWrapper<>(hero);
    System.out.println(heroService.getOne(wrapper));
}

Wrapper对象不带参数

Wrapper<T> wrapper =  new QueryWrapper();
QueryWrapper<T> wrapper =  new QueryWrapper();

当条件构造器不带参数时,就需要自定义条件。

这种适用于自定义查询条件。

默认多个条件时,用and连接,如果条件之间要使用or,调用or()方法

指定或范围

@Test
void test2() {
    /*
    * eq(String column,Object val)      equals          column = val
    * ne(String column,Object val)      not equals      column <> val
    * lt(String column,Object val)      less then       column < val
    * gt(String column,Object val)      great then      column > val
    * le(String column,Object val)      less equals     column <= val
    * ge(String column,Object val)      great equals    column >= val
    * between(String column Object val1,Object val2)    between and          column between val1 and bal2
    * notBetween(String column Object val1,Object val2) not between and      column not between val1 and bal2
    * */
​
​
    //创建一个不带实体参数的条件构造器对象
    QueryWrapper<Hero> wrapper = new QueryWrapper<>();
    //自定义条件:指定值
    //查询性别为男的数据  eq equals    =
    //wrapper.eq("sex","男");
    //查询性别不为男的数据 ne not equals   <>
    //wrapper.ne("sex","男");
​
    //wrapper.between("price",1000,4000);
​
    //查询价格小于5000的战士
    //多个条件默认用and拼接
    /*wrapper.lt("price",5000);
    wrapper.eq("position","战士");*/
​
    //查询性别为女或价格大于3000
    //or()方法会将前后的条件使用or拼接
    /*wrapper.gt("price",3000);
    wrapper.or();
    wrapper.eq("sex","女");*/
​
    //查询性别为男的战士或价格大于3000的法师
    //支持链式写法
    wrapper.eq("sex","男")
            .eq("position","战士")
            .or()
            .eq("position","法师")
            .gt("price","3000");
​
    heroService.list(wrapper).forEach(System.out::println);
}

空值和模糊查询

@Test
void test3() {
    /*
         * isNull(String column)                            column is null
         * isNotNull(String column)                         column is not null
         * like(String column,Object val)                   column like '%val%'
         * likeLeft(String column,Object val)               column like '%val'
         * likeRight(String column,Object val)              column like 'val%'
         * notLike(String column,Object val)                column not like '%val%'
         * notLikeLeft(String column,Object val)            column not like '%val'
         * notLikeRight(String column,Object val)           column not like 'val%'
         * */
    QueryWrapper<Hero> wrapper = new QueryWrapper<>();
​
    //字段为空
    //wrapper.isNull("shelf_date");
    //带有指定关键字
    //wrapper.like("name","琳");
    //以指定关键字结尾
    //wrapper.likeLeft("name","琳");
    //以指定关键字开头
    //wrapper.likeRight("name","伊");
​
    //查询名字中带有琳字且上架时间为空
    wrapper.like("name","琳").isNull("shelf_date");
​
    heroService.list(wrapper).forEach(System.out::println);
}

集合和排序

@Test
    void test4() {
        /*
        *   集合
        *   in(String column,Object... vals)            column in (val1,val2...)
        *   in(String column,Collection vals)           column in (val1,val2...)
        *   notIn(String column,Object... vals)         column not in (val1,val2...)
        *   notIn(String column,Collection vals)        column not in (val1,val2...)
        *
        *   排序
        *   orderByAsc(String column)                   order by column asc
        *   orderByDesc(String column)                  order by column desc
        *
        * */
​
​
        QueryWrapper<Hero> wrapper = new QueryWrapper<>();
        //职业为战士或法师
        //wrapper.eq("position","战士").or().eq("position","法师");
        //可变参数
        //wrapper.in("position","战士","法师");
        //集合
        //wrapper.in("position", Arrays.asList("战士","法师"));
​
        //所有男性角色,按价格降序
        wrapper.eq("sex","男").orderByDesc("price");
​
        heroService.list(wrapper).forEach(System.out::println);
    }

分组和聚合函数(自定义查询)

分组通常会和聚合函数(count、sum、min、max、avg)一起使用,但MyBatisPlus中没有现成的聚合函数。

需要使用select(String... fields)自定义查询字段。

如查询每个每个位置的总数

条件构造器对象.select("position","count(id)"),对应的sql查询部分为select "position","count(id)" from hero

@Test
void test() {
    /*
    * select(String... fields)          select field1,field2...
    * groupBy(String column)            group by column
    * */
​
    QueryWrapper<Hero> wrapper = new QueryWrapper<>();
    //无参数时,查询所有字段
    //wrapper.select();
    //查询指定字段
    //wrapper.select("name");
    //使用聚合函数
    wrapper.select("count(id)");
​
    //查询不同性别的人数
    //select sex,count(id) from hero group by sex
    //wrapper.select("sex","count(id)");
    //根据指定字段分组
    //wrapper.groupBy("sex");
​
    //当前的sql语句查询的结果是一个订制数据,如查询总数只会得到一个数据,所以使用listMaps()方法
    //List<Map<String, Object>> maps = heroService.listMaps(wrapper);
​
    //根据定位分组,查询值为法师、战士的人数,按人数降序
    wrapper.select("position","count(id) as count")
            .in("position","战士","法师")
            .groupBy("position")
            .orderByDesc("count");
​
    List<Map<String, Object>> maps = heroService.listMaps(wrapper);
    System.out.println(maps);
}

分页查询

MyBatisPlus中集成了分页功能,只需在项目的启动类中注入一个分页拦截器对象

/*
        注入一个MyBatisPlus提供的分页拦截器对象
        定义一个方法,在方法上加入@Bean注解,将该方法的返回值注入到Spring容器中
    */
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
    //创建一个MybatisPlus拦截器对象
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    //通过拦截器对象添加分页拦截器对象,设置数据库类型
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
}
@Test
void test1(){
    //无条件分页查询page(Page pageInfo)
    //1.创建一个分页模型对象,设置泛型,参数为当前页和每页显示的记录数
    Page<Hero> pageInfo = new Page<>(1, 5);
    //2.调用IService中的page方法,参数为分页模型对象,返回值也是该对象
    pageInfo = heroService.page(pageInfo);
    //分页的相关信息都保存在分页模型对象pageInfo中
    System.out.println("总页数"+pageInfo.getPages());
    System.out.println("当前页"+pageInfo.getCurrent());
    System.out.println("每页显示的记录数"+pageInfo.getSize());
    System.out.println("总记录数"+pageInfo.getTotal());
    System.out.println("分页后的集合"+pageInfo.getRecords());
    System.out.println("是否有下一页"+pageInfo.hasNext());
    System.out.println("是否有上一页"+pageInfo.hasPrevious());
​
​
    //条件分页page(Page pageInfo,Wrapper wrapper)
    //分页查询所有法师
    pageInfo= heroService.page(pageInfo,new QueryWrapper<Hero>().eq("position","法师"));
    pageInfo.getRecords().forEach(System.out::println);
}

代码生成器

MyBatisPlus中用于自动生成entity、mapper、service、controller这些类和接口的工具

1.添加依赖

<!-- 代码生成器 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.5.3</version>
</dependency>
<!--代码生成器所需引擎-->
<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.3</version>
</dependency>

2.代码生成器类

修改以下几处

  • 数据库信息

  • 输出目录为本地硬盘目录

  • 父包名

  • 要生成的表名

package com.hqyj.question_sys.util;
​
​
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
​
public class CodeGenerator {
    public static void main(String[] args) {
        //***数据库的url, 用户名和密码
        FastAutoGenerator.create("jdbc:mysql://localhost:3306/question_sys?serverTimezone=Asia/Shanghai",
                                 "root", "root")
            .globalConfig(builder -> {
                builder.author("HQYJ") // 设置作者,生成文件的注释里的作者
                    .outputDir("F:\\框架\\question_sys\\src\\main\\java"); //***指定输出目录
            }).packageConfig(builder -> {
            builder.parent("com.hqyj.question_sys"); //***设置父包名
            // .controller("controller") //控制层包名
            // .service("service") //service接口包名
            // .serviceImpl("service.impl")  //service接口实现包名
            // .mapper("mapper")  //mapper接口包名
            // .xml("mapper.xml") //映射文件的包名(如果要生成的话,加上这句,去掉下面的.xml方法)
            // .entity("entity"); //实体类的包名
        }).strategyConfig(builder -> {
            builder.addInclude("question_catalog","question")  //***设置需要生成的表名
                .controllerBuilder();//只生成Controller
            //.enableRestStyle(); //生成的Controller类添加@RestController;
        }).templateConfig(builder -> {
            builder.xml(null); //禁止生成xml映射文件
        }).execute();
    }
}

运行该类即可自动生成

MyBatisPlus中的逻辑删除和物理删除

物理删除:将数据真正删除。delete from 表 where id=值

逻辑删除:数据不删除,而是打个"被删除"的标记。额外添加一个是否被删除的字段"deleted",其值为0或1,0表示未删除,1表示已删除。

当删除某条数据时,执行修改 update 表 set deleted=1 where id=值

添加了"deleted"字段后,查询要更改为select * from 表 where deleted=0

实现过程

1.在表中添加一个新字段

deleted:默认为0表示未删除,1表示已删除

2.在实体类中对应属性,添加@TableLogic注解

@TableLogic//该注解表示该字段是逻辑删除字段
private Integer deleted;

此时调用IService接口中的list()方法查询所有时,会在条件中加入deleted=0

此时调用IService接口中的删除时,会执行修改

Thymeleaf

SpringBoot官方建议使用的页面模板引擎,代替之前的JSP。

它是以HTML为基础,可以展示静态数据,方便前端人员开发静态页面,

也可以通过Thymeleaf的语法,配合EL输出由控制器跳转而来的动态数据。

Thymeleaf从入门到精通 - 知乎 (zhihu.com)

用法

1.添加依赖

<!--thymeleaf页面模板引擎-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>2.7.3</version>
</dependency>

2.在temlates目录下创建一个HTML页面

在页面的HTML标签中,加入xmlns:th="http://www.thymeleaf.org"属性。

可以通过修改IDEA中的HTML页面模板,之后每次创建HTML页面都会有该属性

3.thymeleaf具体使用

  • 双标签中的输出变量: th:text

    <h1 th:text="${作用域中某个对象名}"></h1>
    ​
    <h1 th:text="${username}"></h1>
  • 单标签中设置value的值:th:value

    <input type="text" th:value="${作用域中某个对象名}">
    ​
    <input type="text" th:value="${变量.属性名}">
  • 遍历:th:each

    <table>
        <tr th:each="变量:${作用域中的集合名}">
            <td th:text="${变量.属性名}"></td>
        </tr>
    </table>
    ​
    <table>
        <tr th:each="hero:${heroList}">
            <td th:text="${hero.id}"></td>
            <td th:text="${hero.name}"></td>
            <td th:text="${hero.sex}"></td>
        </tr>
    </table>
  • 超链接获取全局路径:th:href="@{/}"

    • 不带参数

      <link th:href="@{/bootstrap-3.4.1-dist/css/bootstrap.css}" rel="stylesheet">
    • 带参数

      <a th:href="@{'/hero/delete?id='+${hero.id}">删除</a>
      ​
      <a th:href="@{/question/findById(id=${question.quesCode})}">编辑</a>
  • 表单提交路径:th:action="@{/}"

    <form methos="post" th:action="@{/hero/insert}">
        
    </form>
  • 判断:th:if

    <h1 th:if="判断逻辑">用户信息</h1>
    ​
    <h1 th:if="${userinfo.username==null}">请登录</h1>
  • 选中单选按钮或复选框:th:checked

    <input type="radio" value="男" name="sex" th:checked="逻辑判断">
    ​
    <input type="radio" value="男" name="sex" th:checked="${user.sex=='男'}">男
    <input type="radio" value="女" name="sex" th:checked="${user.sex eq '女'}">女
  • 选中下拉菜单:th:selected

    <select>
        <option th:selected="逻辑判断"></option>
    </select>
    ​
    <select>
        <option th:selected="${book.typeId==bookType.typeId}" th:each="bookType:${list}" th:value="${bookType.typeId}" th:text="${bookType.typeName}"></option>
    </select>
  • src属性:th:src

    <script th:src="@{/bootstrap-3.4.1-dist/js/jquery-3.6.2.min.js}"></script>
  • 内联表达式

    Thymeleaf页面中使用[[]]可以在script标签中使用EL表达式,这两对中括号拼接的内容称为内联表达式。

    在给script标签加入th:inline="javascript"后使用

    <script th:inline="javascript">
        $(function () {
            //遍历页码
            $(".pno").each(function (){
                //如果页码和当前页一致,高亮显示
                if($(this).text()==[[${pageInfo.current}]]){
                    $(this).addClass("active");
                }
            });
        });
    </script>
    ​
    <script th:inline="javascript">
        $(function () {
            //使用ajax读取所有的题目类型对象,遍历成option
            $.ajax({
                url:[[@{/qc/queryAll}]],
                success:function (res){
                    for(var i=0;i<res.length;i++){
                        let qcId = res[i].qcId;
                        let qcName = res[i].qcName;
                        $("<option></option>").text(qcName).val(qcId).appendTo($("select[name=qcId]"));
                    }
                }
            });
        });
    </script>

    单表条件分页

    原理:调用IService接口中的page(Page page,Wrapper wrapper)方法,创建分页模型对象和条件构造器对象

    在Springboot启动类中加入mybatis分页拦截器

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

    搜索框

    <form class="navbar-form navbar-left" th:action="@{/question/queryAll}">
        <div class="form-group">
            <input type="text" class="form-control" name="keyword" placeholder="请输入题目关键字">
        </div>
        <button type="submit" class="btn btn-default">搜索</button>
    </form>

    控制层

    @RequestMapping("/queryAll")
    public String queryAll(@RequestParam(defaultValue = "1") Integer page,
                           @RequestParam(defaultValue = "5") Integer size,
                           @RequestParam(defaultValue = "") String keyword,
                           Model model) {
        //1.创建分页模型对象
        Page<Question> pageInfo = new Page<>(page, size);
    ​
        //创建条件构造器对象
        QueryWrapper<Question> wrapper = new QueryWrapper<>();
        wrapper.like("ques_title",keyword);
        //2.调用条件分页查询的方法
        questionService.page(pageInfo,wrapper);
        // 将分页模型对象保存到请求中
        model.addAttribute("pageInfo", pageInfo);
        //构造页数
        List<Long> pageList = new ArrayList<>();
        //每次最多显示5页
        //定义最大数字
        long maxPage = page + 4 > pageInfo.getPages() ? pageInfo.getPages() : page + 4;
        //定义最小数字
        long minPage = page - 4 < 1 ? page : maxPage - 4;
    ​
        for (long i = minPage; i <= maxPage; i++) {
            pageList.add(i);
        }
    ​
        model.addAttribute("pageList", pageList);
    ​
        return "questionList";
    }

    分页组件

    <nav aria-label="Page navigation">
        <ul class="pagination">
            <li>
                <a th:if="${pageInfo.hasPrevious}" th:href="@{/question/queryAll(keyword=${param.keyword},page=${pageInfo.current-1})}"
                   aria-label="Previous">
                    <span aria-hidden="true">&laquo;</span>
                </a>
            </li>
            <li class="pno" th:each="pno:${pageList}">
                <a th:href="@{/question/queryAll(keyword=${param.keyword},page=${pno})}" th:text="${pno}"></a>
            </li>
            <li>
                <a th:if="${pageInfo.hasNext}" th:href="@{/question/queryAll(keyword=${param.keyword},page=${pageInfo.current+1})}"
                   aria-label="Next">
                    <span aria-hidden="true">&raquo;</span>
                </a>
            </li>
        </ul>
    </nav>

    MyBatisPlus关联查询

    可以通过创建xml文件来实现,过程同MyBatis关联查询。

    这里使用注解实现。

    主表question_catalog

    从表question

    多对一

    查询时以从表数据为主体,关联对应的主表信息。

    多个question对象对应一个question_catalog对象。

    在查询question表的同时,显示关联question_catalog表中的数据。

    实体类

    在从表的实体类中,额外添加一个外键对应的主表实体对象

    @Data
    public class Question implements Serializable {
    ​
        private static final long serialVersionUID = 1L;
    ​
        @TableId(type = IdType.ASSIGN_UUID)
        private String quesCode;
        private Integer qcId;
        private String quesTitle;
        private String quesOptA;
        private String quesOptB;
        private String quesOptC;
        private String quesAns;
        @TableLogic//该注解表示该字段是逻辑删除字段
        private Integer deleted;
    ​
        /*
        * 如果在实体类中添加了一个不属于该表中字段的属性
        * 要在该属性上加@TableField(exist = false)表示该属性没有对应的字段
        * */
        @TableField(exist = false)
        private QuestionCatalog qc;
    }

    原理

    如果直接通过IService接口中的list()方法查询,实际调用的是BaseMapper接口中的selectList()方法,默认查询自身表。

    如果重写业务层中的list()方法,或在业务层中自定义一个方法,让其调用数据访问层中自定义的某个方法,重新定制sql语句,就能得到想要的数据

    实现过程

    1.在Mapper(QuestionMapper)接口中自定义一个方法,使用注解编写sql语句

    有两种方式:

    • 方式一:构造自定义的sql语句

      • 建议外键关联字段比较少的情况下使用

      • sql语句需要连接查询,将外键关联的表的字段重命名为"外键实体对象名.属性名"

      @Select("select q.*,qc_name as 'qc.qcName' from question q,question_catalog qc where q.qc_id=qc.qc_id ")
      List<Question> mySelect();
    • 方式二:使用子查询,自定义结果集映射

      • 建议外键关联字段比较多的情况下使用

      • 先查询从表,再使用外键字段查询主表

      • 外键字段需要重新映射一次

      //1.查询自身表
      @Select("select * from question")
      //2.自定义结果集映射
      @Results({
          //qc_id字段在下面的子查询中已经使用了,将外键字段qc_id重新映射
          @Result(property = "qcId",column = "qc_id"),
          //遇到qc属性,使用qc_id进行子查询
          //执行QuestionCatalogMapper中的selectById方法
          //该方法是BaseMapper接口中默认就存在的方法
          @Result(property ="qc" ,column ="qc_id",one =@One(select = "com.hqyj.question_sys.mapper.QuestionCatalogMapper.selectById"))
      })
      List<Question> mySelect();

    2.在Service层中调用Mapper层中自定义的方法

    @Service
    public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {
    ​
        /*
        * 如果直接调用该方法查询,会最终调用BaseMapper中的selectList()方法
        * 这里重写该方法,让其调用BaseMapper中的自定义方法
        * */
        @Override
        public List<Question> list() {
            return baseMapper.mySelect();
        }
    }

    一对多

    以主表为主体,查询时显示关联的从表对象集合。

    查询question_catalog表,同时显示qc_id对应的question表中的数据集合。

    实体类

    在主表实体类中,额外添加一个从表实体对象集合。

    @TableName("question_catalog")
    @Data
    public class QuestionCatalog implements Serializable {
    ​
        private static final long serialVersionUID = 1L;
    ​
        @TableId(value = "qc_id", type = IdType.AUTO)
        private Integer qcId;
    ​
        private String qcName;
    ​
        /*
        * 一对多查询,定义关联的从表对象集合
        * @TableField(exist = false)标明该属性没有对应的字段
        * */
        @TableField(exist = false)
        private List<Question> questionList;
    }
    ​

    原理同多对一查询,重写service层中的list()方法,改变原本方法中调用的内容

    实现过程

    1.在Mapper(QuestionCatalogMapper)接口中自定义一个方法,使用注解编写sql语句

    /*
         * 自定义查询
         * 使用子查询实现一对多
         * */
    //1.查询question_catalog表
    @Select("select * from question_catalog")
    //2.自定义结果集映射
    @Results({
        //由于在下面使用qc_id进行了子查询,对其重新映射
        @Result(property = "qcId",column = "qc_id"),
        //3.使用qc_id进行子查询,查询question表,调用根据qc_id查询对应数据集合的方法,没有现成的方法,需要自定义
        @Result(property = "questionList", column = "qc_id", many = @Many(select = "com.hqyj.question_sys.mapper.QuestionMapper.queryByQcId"))
    })
    List<QuestionCatalog> mySelect();

    2.在Mapper(QuestionMapper)接口中定义方法

    /*
        * 根据qc_id查询对应数据集合
        * */
    @Select("select * from question where qc_id=#{qcId}")
    List<Question> queryByQcId();

    3.在Service层中调用Mapper层中自定义的方法

    /*
            * 重写原本的查询所有,执行自定义的查询
            * */
    @Override
    public List<QuestionCatalog> list() {
        return baseMapper.mySelect();
    }

    多对一关联查询条件分页

    原理

    参考原本的条件分页查询方法selectPage(P page, Wrapper<Question> queryWrapper)

    page用于分页,queryWrapper用于构造条件。通过自定义的sql语句关联两张表实现。 在自定义的sql语句中,

    通过${ew.sqlSegment}或${ew.customSqlSegment}使用条件构造器Wrapper对象

${ew.sqlSegment} 将条件原样拼接

${ew.customSqlSegment} 在所有条件前自动加入where关键字

实现过程

1.在Mapper(QuestionMapper)中自定义关联查询的sql

  • ${ew.sqlSegment} 将条件原样拼接

  • ${ew.customSqlSegment} 在所有条件前加入where关键字

  //${ew.sqlSegment}   将条件原样拼接
  //@Select("SELECT q.*,qc_name AS 'qc.qcName' FROM question q,question_catalog qc WHERE q.qc_id = qc.qc_id and ${ew.sqlSegment}")
  
  //${ew.customSqlSegment}  在所有条件前加入where关键字
  @Select("SELECT q.*,qc_name AS 'qc.qcName' FROM question q inner join question_catalog qc on q.qc_id = qc.qc_id ${ew.customSqlSegment}")
  <P extends IPage<Question>> P queryByCondition(P page, @Param("ew") Wrapper<Question> queryWrapper);

2.在service层中重写或自定义方法,调用mapper层中自定义的方法

  @Override
  public <E extends IPage<Question>> E page(E page, Wrapper<Question> queryWrapper) {
      return baseMapper.queryByCondition(page,queryWrapper);
  }

3.controller层

  • 接收所需参数

  • 创建分页模型对象

  • 创建条件构造器对象

  @RequestMapping("/queryAll")
  public String queryAll(@RequestParam(defaultValue = "1") Integer page,
                         @RequestParam(defaultValue = "5") Integer size,
                         @RequestParam(defaultValue = "") String keyword,
                         @RequestParam(defaultValue = "0") Integer qcId,
                         Model model) {
      //1.创建分页模型对象
      Page<Question> pageInfo = new Page<>(page, size);
      //创建条件构造器对象
      QueryWrapper<Question> wrapper = new QueryWrapper<>();
      wrapper.like("ques_title", keyword);
      if (qcId != 0) {
          wrapper.eq("q.qc_id", qcId);
      }
      //2.调用条件分页查询的方法
      questionService.page(pageInfo, wrapper);
  
      model.addAttribute("pageInfo", pageInfo);
      //构造页数
      List<Long> pageList = new ArrayList<>();
      //每次最多显示5页
      //定义最大数字
      long maxPage = page + 4 > pageInfo.getPages() ? pageInfo.getPages() : page + 4;
      //定义最小数字
      long minPage = page - 4 < 1 ? page : maxPage - 4;
  
      for (long i = minPage; i <= maxPage; i++) {
          pageList.add(i);
      }
  
      model.addAttribute("pageList", pageList);
  
      return "questionList";
  }

4.页面表单

  <form class="navbar-form navbar-left" th:action="@{/question/queryAll}">
      <div class="form-group">
          <input type="text" class="form-control" name="keyword" placeholder="请输入题目关键字">
          <select class="form-control" name="qcId">
  
          </select>
      </div>
      <button type="submit" class="btn btn-default">搜索</button>
  </form>
  <script th:inline="javascript">
      $(function () {
          $.ajax({
              url:[[@{/qc/queryAll}]],
              success:function (res){
                  for(var i=0;i<res.length;i++){
                      let qcId = res[i].qcId;
                      let qcName = res[i].qcName;
                      $("<option></option>").text(qcName).val(qcId).appendTo($("select[name=qcId]"));
                  }
              }
          });
      });
  </script>

答题功能核心内容

随机出题

  /*
      * 随机5条记录
      * */
  @Select("select  * from  question order by rand() limit 5 ")
  List<Question> queryByRand();

答题页面

  <!DOCTYPE html>
  <html xmlns:th="http://www.thymeleaf.org">
      <head>
          <title>Title</title>
          <link rel="shortcut icon" href="https://www.baidu.com/favicon.ico" type="image/x-icon">
          <!--bootstrap的css文件路径-->
          <link th:href="@{/bootstrap-3.4.1-dist/css/bootstrap.css}" rel="stylesheet">
      </head>
      <body>
          <div class="col-md-4"></div>
          <div class="col-md-4">
              <form id="result">
                  <div class="panel panel-primary" th:each="question:${list}">
                      <div class="panel-heading" th:text="${question.quesTitle}"></div>
                      <div class="panel-body">
                          <div class="form-group">
                              <label>
                                  <input type="radio" value="a" th:name="${question.quesCode}">
                                  <span th:text="${question.quesOptA}"></span>
                              </label>
  
                              <label>
                                  <input type="radio" value="b" th:name="${question.quesCode}">
                                  <span th:text="${question.quesOptB}"></span>
                              </label>
                              <label>
                                  <input type="radio" value="c" th:name="${question.quesCode}">
                                  <span th:text="${question.quesOptC}"></span>
                              </label>
                          </div>
                      </div>
                  </div>
                  <button class="btn btn-default loginBtn" type="submit">提交</button>
              </form>
          </div>
          <div class="col-md-4"></div>
  
          <script th:src="@{/bootstrap-3.4.1-dist/js/jquery-3.6.2.min.js}"></script>
          <script th:inline="javascript">
              $(function () {
                  $("#result").submit(function () {
                      //创建数组保存答题结果
                      var ansList = [];
                      //获取表单的数据 表单对象.serialize()可以获取表单中每个name-value,将其拼接为字符串
                      var data = $(this).serialize();//name=value&name=value
                      //切分答题结果
                      let list = data.split("&");
                      //遍历答题结果,将其构造为一个答题结果对象
                      for (var i = 0; i < list.length; i++) {
                          //创建一个对象
                          var ans = {};
                          //构造对象的属性
                          ans.code = list[i].split("=")[0];
                          ans.opt = list[i].split("=")[1];
                          ansList.push(ans);
                      }
                      //提交ansList
                      $.ajax({
                          url: [[@{/question/result}]],
                          data: {
                              ansList: JSON.stringify(ansList)
                          },
                          traditional: true,
                          success: function (res) {
                              alert("你答对了"+res+"道题");
                          }
                      });
                      return false;
                  })
              });
          </script>
      </body>
  </html>

答题

答题对象

  package com.hqyj.question_sys.entity;
  
  import lombok.Data;
  
  /*
  * 创建答题结果对象
  * */
  @Data
  public class AnswerResult {
      private String code;
      private String opt;
  }
  

controller

  @RequestMapping("/result")
  @ResponseBody
  public Integer result(String ansList) throws JsonProcessingException {
      //将JSON格式的字符串转换为对象
      //SpringBoot中集成了jackson,可以用于对象和JSON之间的转换
      //创建ObjectMapper对象
      ObjectMapper jsonTool = new ObjectMapper();
      //读取接收到的JSON字符串,转换为Result对象数组
      AnswerResult[] list = jsonTool.readValue(ansList, AnswerResult[].class);
      //遍历答题结果
      Integer count=0;
      for (AnswerResult res : list) {
          /*
              //答题题目
              String code = res.getCode();
              //回答结果
              String opt = res.getOpt();
              //原本题目
              Question question = questionService.getById(code);
              //真正答案
              String quesAns = question.getQuesAns();
              */
          if (questionService.getById(res.getCode()).getQuesAns().equals(res.getOpt())) {
              count++;
          }
      }
      return count;
  }

Spring Data JPA

2001年推出了Hibernate,是一个全自动ORM框架。可以不用编写SQL语句,就能实现对数据库的持久化操作。

SUN公司在Hibernate的基础上,制定了JPA,全称 Java Persisitence API,中文名Java持久化API,

是一套Java访问数据库的规范,由一系列接口和抽象类构成。

后来Spring团队在SUN公司制定的JPA这套规范下,推出了Spring Data JPA,是JPA的具体实现。

如今常说的JPA,通常指Spring Data JPA。

SpringBoot集成Spring Data JPA

1.创建SpringBoot项目,选择依赖

2.编辑配置文件,设置要连接的数据库信息

  spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
  spring.datasource.url=jdbc:mysql://localhost:3306/bookdb?serverTimezone=Asia/Shanghai
  spring.datasource.username=root
  spring.datasource.password=root
  
  # 设置数据库类型
  spring.jpa.database=mysql
  # 打印SQL语句
  spring.jpa.show-sql=true

3.创建实体类

  • 类上加@Entity注解

  • 主键属性上加

    • @Id注解标明主键

    • @GeneratedValue(strategy = GenerationType.IDENTITY)设置MySQL数据库主键生成策略,数据库设置为自增

  • 其他属性名与字段名一致或驼峰命名法

    • 如果字段名多个单词之间用_,使用驼峰命名法

    • 如果不相同,使用@Column(name="字段名")注解指定该属性对应的字段名

  @Data
  @Entity
  /*
  * 实体类的属性名建议使用驼峰命名法
  * */
  public class BookInfo {
      @Id//主键字段
      //主键生成策略,GenerationType.IDENTITY表示MySQL自增
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Integer bookId;
      private Integer typeId;
      private String bookName;
      private String bookAuthor;
      //如果字段名和属性名不一致,使用@Column指定字段名
      @Column(name = "book_price")
      private Integer price;
      private Integer bookNum;
      private String publisher_date;
  }

4.数据访问层接口

  • 类上加@Repository注解

  • 继承JpaRepository<实体类型,主键类型>接口

  @Repository
  public interface BookInfoDao extends JpaRepository<BookInfo,Integer> {
  
  }

5.测试常用方法

方法名返回值作用
findAll()List<T>查询所有数据。
save(T entity)T entity添加或修改。如果不存在主键属性或主键值不存在,执行添加;如果存在主键属性且有该主键值,执行修改。
delete(T entity)void根据对象删除。如果该对象中有主键属性且有该主键值,根据该主键值删除。
findById(主键)Optional<T>根据主键值查询。返回的对象调用isPresent()结果为true,表示查询到了数据,继续调用get()得到查询到的对象。
  @SpringBootTest
  class Day16SpringBootJpaApplicationTests {
  
  
      @Autowired
      private BookInfoDao bookInfoDao;
  
      @Test
      void contextLoads() {
          List<BookInfo> all = bookInfoDao.findAll();
          all.forEach(System.out::println);
      }
  
      @Test
      void insert() {
          BookInfo bookInfo = new BookInfo();
          bookInfo.setBookName("测试");
          bookInfo.setBookAuthor("测试");
          bookInfo.setBookNum(156);
          bookInfo.setPrice(20);
          bookInfo.setTypeId(1);
          //save()方法调用时,如果对象中没有主键或主键值不存在,作为添加使用
          BookInfo save = bookInfoDao.save(bookInfo);
          //添加成功后,会自动获取主键自增的值
          System.out.println(save);
      }
  
      @Test
      void update() {
          BookInfo bookInfo = new BookInfo();
          bookInfo.setBookName("xxxxxxxxxxx");
          bookInfo.setBookAuthor("测试");
          bookInfo.setBookNum(356);
          bookInfo.setPrice(23);
          bookInfo.setTypeId(1);
          //save()方法调用时,如果对象中有主键且存在,作为修改使用
          BookInfo save = bookInfoDao.save(bookInfo);
          //修改成功后,返回修改后的对象
          System.out.println(save);
      }
      @Test
      void delete() {
          //根据主键值删除,如果值不存在,会报错
          //bookInfoDao.deleteById(36);
  
          //根据对象删除,如果对象中包含主键值则删除,如果没有值或不存在,不会报错
          BookInfo bookInfo = new BookInfo();
          //bookInfo.setBookId(330);
          bookInfoDao.delete(bookInfo);
      }
  
  
      @Test
      void findOne() {
          //根据主键查询,返回值Optional类型
          Optional<BookInfo> byId = bookInfoDao.findById(60);
          //isPresent()如果为true表示查询到了数据
          if (byId.isPresent()) {
              //get()将查询到的数据转换为对应的实体类
              BookInfo bookInfo=byId.get();
              System.out.println(bookInfo);
          }else{
              System.out.println("未查询到数据");
          }
      }
  }

JPA进阶

分页查询

调用数据访问层中的findAll(Pageable pageable)方法,即可实现分页。

参数Pageable是org.springframework.data.domain包中的一个接口,通过其实现类

PageRequest,调用静态方法of(int page,int size),当做Pageable对象使用。

这里的page从0开始为第一页。

  @Test
  void queryByPage(){
      
      //PageRequest是Pageable的实现类,调用静态方法of(int page,int size)
      //这里的page的值0表示第一页
      //调用findAll(Pageable pageable)方法,返回分页模型对象
      Page<BookInfo> pageInfo = bookInfoDao.findAll(PageRequest.of(0,5));
      //分页相关数据
      System.out.println("总记录数"+pageInfo.getTotalElements());
      System.out.println("最大页数"+pageInfo.getTotalPages());
      System.out.println("分页后的数据集合"+pageInfo.getContent());
      System.out.println("当前页数"+pageInfo.getNumber());
      System.out.println("每页显示的记录数"+pageInfo.getSize());
      System.out.println("是否还有下一页"+pageInfo.hasNext());
      System.out.println("是否还有上一页"+pageInfo.hasPrevious());
  }

条件查询

在JPA中,使用自定义方法名自动生成对应的SQL语句,实现条件查询。

如在dao中定义了queryById(int id)方法,就表示根据id查询,自动生成sql语句。

方法命名格式

[xxx] [By] [字段对应的属性名] [规则] [Or/And] [字段对应的属性名] [规则] ...

  • **xxx可以是find、get、query、search

  • 方法如果有参数,参数的顺序和方法名中的参数顺序一致

如findByBookNameAndBookAuthor(String bookName,String bookAuthor),

对应的sql语句为 select * from book where book_name =? and book_author=?

常用规则

规则方法名SQL中的条件
指定值findByBookName(String name)book_name = name
Or/AndfindByBookNameOrBookAuthor(String name,String author)book_name = name or book_author = author
After/BeforfindByBookPriceAfter(double price)book_price > price
GreaterThanEqual/LessThanEqualfindByBookNumLessThanEqual(int num)book_num <= num
BetweenfindByBookNumBetween(int min,int max)book_num between min and max
Is[Not]NullfindByPublisherDateIsNull()publish_date is null
[Not]LikefindByBookNameLike(String condition)book_name like 'condition'
[Not]ContainsfindByBookNameContains(String keyword)book_name like '%keyword%'
StartsWith/EndsWithfindByBookNameStartsWith(String firstName)book_name like 'firstName%'
无条件排序:findAllByOrderBy字段[Desc/Asc]findAllByOrderByBookId()order by book_id asc
有条件排序:findAllBy条件OrderBy字段[Desc/Asc]findAllByTypeIdOrderByBookIdDesc()type_id = ? order by book_id desc
  @Repository
  public interface BookInfoDao extends JpaRepository<BookInfo,Integer> {
  
  
      //指定值查询
      //根据书名查询
      List<BookInfo> getAllByBookName(String x);
  
      //查询价格大于指定值   字段对应的属性名  After/GreaterThan
      List<BookInfo> findAllByPriceAfter(int price);
  
      //查询价格小于于指定值    字段对应的属性名  Before/LessThan
      List<BookInfo> findAllByPriceLessThan(int price);
  
  
      //查询库存大于等于指定值 GreaterThanEqual
      List<BookInfo> queryAllByBookNumGreaterThanEqual(int num);
  
      //查询库存在指定闭区间内 Between(int min,int max)
      List<BookInfo> findAllByBookNumBetween(int min,int max);
  
  
      //空值查询 null
      //查询出版日期为空  IsNull/IsNotNull
      List<BookInfo> findAllByPublisherDateIsNull();
  
      //书名中带有关键字 Like/NotLike 实参一定要使用%或_
      List<BookInfo> getAllByBookNameLike(String keyword);
  
      //作者名中带有关键字  Contains/NotContains 实参只需要关键字
      List<BookInfo> getAllByBookAuthorContains(String keyword);
  
      //指定作者的姓   指定开头/结尾  StartsWith/EndsWith
      List<BookInfo> getAllByBookAuthorStartsWith(String keyword);
  
  
      //查询所有数据,按价格降序    无条件排序 OrderBy字段[Desc/Asc]
      List<BookInfo> getAllByOrderByPriceDesc();
  
      //查询指定类型,按id降序
      List<BookInfo> getAllByTypeIdOrderByBookIdDesc(Integer typeId);
  
  }

条件分页查询

只需在定义方法时,将方法的返回值设置为Page类型,在参数中就如Pageable对象

  //根据作者和书名的关键字分页查询
  Page<BookInfo> getAllByBookNameContainsOrBookAuthorContains(
      String nameKeyword,
      String authorKeyword,
      Pageable pageable);
  Page<BookInfo> pageInfo = bookInfoDao.getAllByBookNameContainsOrBookAuthorContains("龙","山",PageRequest.of(0,5));
  //分页相关数据保存在pageInfo对象中

聚合函数分组查询

自定义SQL

在数据访问层接口中的方法上,可以加入@Query注解,默认要使用HQL(Hibernate专用)格式的语句。

如果要使用原生的SQL语句,需要添加nativeQuery=true属性,用value属性定义SQL语句

  /*
       * 在JPA中,如果要使用自定义的SQL语句
       * nativeQuery = true 开启原生SQL语句
       * value="sql语句"
       * */
  @Query(nativeQuery = true, value = "select book_author,count(book_id) from book_info group by book_author")
  List testQuery();
  @Test
  void test(){
      List list = bookInfoDao.testQuery();
      //查询的结果为集合,集合中保存的是每一行数据
      for (Object row : list) {
          //每一行页数一个对象数组
          Object[] obj= (Object[])row;
          //根据索引得到查询出的内容
          System.out.println(obj[0]+"---"+obj[1]);
      }
  }

自定义SQL中带参数

SQL语句中的":XXX"表示参数

如果方法的形参名和xxx一致时直接使用,如果不一致,在形参上加入@Param注解设置形参名

  /*
      * 根据作者查询其图书总库存
      * 使用":形参名"在SQL语句中带参数
      * 在方法中通过@Prama定义形参
      * */
  @Query(nativeQuery = true, value = " select book_author,sum(book_num) from book_info where book_author=:zuozhe")
  List testQuery3(@Param("zuozhe") String xxx);
  @Test
  void test(){
      List list = bookInfoDao.testQuery3("金庸");
      //查询的结果为集合,集合中保存的是每一行数据
      for (Object row : list) {
          //每一行页数一个对象数组
          Object[] obj= (Object[])row;
          //根据索引得到查询出的内容
          System.out.println(obj[0]+"---"+obj[1]);
      }
  }

关联查询

主表book_type

从表book_info

实体类

主表实体BookType

  @Entity
  @Data
  public class BookType {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Integer typeId;
      private String typeName;
  }

从表实体BookInfo

  • 无需写出外键字段属性

  • 额外添加外键字段对应的实体类对象属性

  @Data
  @Entity
  public class BookInfo {
      @Id//主键字段
      //主键生成策略,GenerationType.IDENTITY表示MySQL自增
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Integer bookId;
      //private Integer typeId;
      private String bookName;
      private String bookAuthor;
      //如果字段名和属性名不一致,使用@Column指定字段名
      @Column(name = "book_price")
      private Integer price;
      private Integer bookNum;
      private String publisherDate;
  
      //多对一查询,以当前从表信息为主体,关联相应的主表信息
      @JoinColumn(name = "type_id")//使用type_id字段进行多对一查询
      @ManyToOne//多对一
      private BookType bt;
  }

使用

  • 多对一查询,调用dao中的查询方法,就会自动给外键字段对应的对象赋值

  • 添加时,参数所需外键字段,使用外键字段对应的对象名代替

    //添加
    @PostMapping("/book")
    public RestResult<BookInfo> insert(BookInfo bookInfo) {
        return RestResult.ok("添加成功", bookInfoDao.save(bookInfo));
    }

  • 如果根据外键字段查询,要在从表dao中定义方法findBy外键对象名_外键对象属性名(数据类型 外键字段)

    //如果根据外键字段查询,方法名写为 By外键实体类属性名_属性名
    List<BookInfo> getAllByBt_TypeId(Integer typeId);

前后端分离项目

前后端分离,就是将web应用中的前端页面和后端代码分开完成、部署。

  • 前后端的开发者只需要完成各自的事情,最终以文档的形式约定数据接口(URL、参数、返回值、请求方式)

  • 前后端分别用独立的服务器

  • 后端只需处理数据并提供访问接口(路径),以RESTFul风格的JSON格式传输数据

  • 前端只需负责渲染页面和展示数据

传统项目和前后端分离项目对比

传统项目

前端和后端的代码运行在一个服务器上,页面经由控制器跳转

SSM项目、图书管理系统、答题系统

前后端分离项目

前后端的代码分别运行在各自的服务器上

后端提供JSON格式字符串的数据接口

前端负责跳转、解析JSON数据。

酒店客房管理系统

前后端分离项目后端控制层设计

请求方式设计:RESTFul风格

风格,不是标准,可以不用强制遵循。

RESTFul风格:用不同的请求方式去访问同一个URL地址时,执行不同的操作。

特点

  • 通过URL就能知道当前在哪个模块

  • 通过不同的请求方式决定执行什么操作

  • 通过返回的状态码得到操作结果

使用RESTFul风格和普通方式对比

普通方式

  localhost:8080/user/queryAll                          查询所有
  localhost:8080/user/queryById?id=1001                 条件查询
  localhost:8080/user/insert?name=ez&sex=男&age=20           添加
  localhost:8080/user/update?name=ez&id=1001                修改
  localhost:8080/user/delete?id=1001                        删除

RESTFul风格

  localhost:8080/user                           查询所有get请求
  localhost:8080/user/1001                  条件查询get请求
  localhost:8080/user                           添加post请求
  localhost:8080/user                           修改put请求
  localhost:8080/user/1001                  删除delete请求

RESTFul风格具体使用

  • 在请求映射的命名上,统一用小写字母的名词形式表示当前位于哪个模块。如/user、/book_info

  • 访问时如果要传参,使用"/模块名/参数"方式,配合controller中的@PathVariable获取

    @GetMapping("/book/{id}")
    public BookInfo queryById(@PathVariable("id")Integer id){
        return service.findById(id);
    }
  • 在controller的方法上,使用@XXXMapping()设置访问该方法的请求方式

    • @GetMapping("路径") 查询

    • @PostMapping("路径") 添加

    • @PutMapping("路径") 修改

    • @DeleteMapping("路径") 删除

    • @RequestMapping(value="路径",method=RequestMethod.GET/POST/PUT/DELETE))

  • 如果请求方式不匹配,会报405异常

  • 在同一个controller中,不能出现两个请求方式和路径都一致的方法

返回值设计

前后端分离项目的控制层方法的返回值也需要进行统一。

返回值通常包含以下信息

  • 传递状态,用状态码表示Integer code

  • 传递消息,用字符串表示String msg

  • 传递集合,用集合表示List list

  • 传递对象,用对象表示Object obj

将这些信息封装到一个对象中,这个对象称为返回结果类RestResult对象

##

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值