框架
一套规范。
实际是他人实现的一系列接口和类的集合。通入导入对应框架的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">«</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">»</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创建
通过官网模板创建
点击生成,会下载一个压缩文件,解压后通过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插件,无需安装)
-
IDEA插件官网Versions: Lombok - IntelliJ IDEs Plugin | Marketplace (jetbrains.com)
-
IDEA内置插件市场搜索
在某个实体类上添加注解
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">«</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">»</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/And | findByBookNameOrBookAuthor(String name,String author) | book_name = name or book_author = author |
After/Befor | findByBookPriceAfter(double price) | book_price > price |
GreaterThanEqual/LessThanEqual | findByBookNumLessThanEqual(int num) | book_num <= num |
Between | findByBookNumBetween(int min,int max) | book_num between min and max |
Is[Not]Null | findByPublisherDateIsNull() | publish_date is null |
[Not]Like | findByBookNameLike(String condition) | book_name like 'condition' |
[Not]Contains | findByBookNameContains(String keyword) | book_name like '%keyword%' |
StartsWith/EndsWith | findByBookNameStartsWith(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对象
##