spring入门 spring简介 spring是分层的Java SE/EE应用 轻量级开源框架。提供了表现层 SpringMVC和持久层 Spring JDBC Template以及 业务层 事务管理等众多的企业级应用技术,还能整合开源世界众多著名的第三方框架和类库,逐渐成为使用最多的Java EE 企业应用开源框架。
spring有两大核心:
IOC Inverse Of Control:控制反转
AOP Aspect Oriented Programming:面向切面编程
IOC IOC概念 IOC 控制反转,是一种思想,实际上是指对一个对象的控制权的反转。
控制:在java中指对象的控制权限(创建、销毁)
反转:指对象控制权由原来 程序员在类中手动控制 反转到 由spring容器控制
比如这样一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Book { private Integer id; private String name; private Double price; } ...... public class User { private Integer id; private String name; private Integer age; public void read () { Book book = new Book (); book.setId(1 ); book.setName("聊斋志异" ); book.setPrice(20.56 ); } }
上述代码中,Book的对象控制权在User对象里面,你要使用User对象的方法就必须要创建一个Book对象,这样User和Book就高度耦合。你在其他地方使用Book对象,必须自己手动创建、初始化、销毁等。
有了spring,我们就可以把对象的创建等操作交给spring容器来管理。在项目启动时,所有的Bean对象都注册到了spring容器中,如果使用到了哪个Bean就去spring容器中要,而不需要自己手动创建,这样就可以专注于业务,可以从对象的创建中解脱出来。
自己实现IOC 需求:实现service层与dao层代码解耦合
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependencies > <dependency > <groupId > dom4j</groupId > <artifactId > dom4j</artifactId > <version > 1.6.1</version > </dependency > <dependency > <groupId > jaxen</groupId > <artifactId > jaxen</artifactId > <version > 1.1.6</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.12</version > </dependency > </dependencies >
Dao接口:
1 2 3 4 5 6 7 package com.cwz.dao;public interface IUserDao { public void save () ; }
实现类:
1 2 3 4 5 6 7 8 9 10 11 12 package com.cwz.dao.impl;import com.cwz.dao.IUserDao;public class UserDaoImpl implements IUserDao { @Override public void save () { System.out.println("dao被调用了..." ); } }
service接口:
1 2 3 4 5 6 7 package com.cwz.service;public interface IUserService { public void save () ; }
实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.cwz.service.impl;import com.cwz.dao.IUserDao;import com.cwz.service.IUserService;import com.cwz.utils.BeanFactory;public class UserServiceImpl implements IUserService { @Override public void save () { IUserDao userDao = (IUserDao) BeanFactory.getBean("userDao" ); userDao.save(); } }
把所有需要创建对象的信息定义在配置文件中
1 2 3 <beans > <bean id ="userDao" class ="com.cwz.dao.impl.UserDaoImpl" > </bean > </beans >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package com.cwz.utils;import org.dom4j.Document;import org.dom4j.DocumentException;import org.dom4j.Element;import org.dom4j.io.SAXReader;import java.io.InputStream;import java.util.HashMap;import java.util.List;import java.util.Map;public class BeanFactory { private static Map<String, Object> iocmap = new HashMap <>(); static { InputStream resourceAsStream = BeanFactory.class.getClassLoader().getResourceAsStream("beans.xml" ); SAXReader saxReader = new SAXReader (); try { Document document = saxReader.read(resourceAsStream); String xpath = "//bean" ; List<Element> list = document.selectNodes(xpath); for (Element element : list) { String id = element.attributeValue("id" ); String className = element.attributeValue("class" ); Object o = Class.forName(className).newInstance(); iocmap.put(id, o); } } catch (DocumentException | ClassNotFoundException | InstantiationException | IllegalAccessException e) { e.printStackTrace(); } } public static Object getBean (String beanId) { Object o = iocmap.get(beanId); return o; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.cwz.test;import com.cwz.service.impl.UserServiceImpl;import org.junit.Test;public class SpringTest { @Test public void test () { UserServiceImpl userService = new UserServiceImpl (); userService.save(); } }
这样就手动实现了类似与IOC容器的功能,实际上BeanFactory就是一个简单的Spring的IOC容器所具备的功能。
IOC体验 首先创建一个普通的Maven项目,引入spring-context依赖
1 2 3 4 5 6 7 <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.6</version> </dependency> </dependencies>
然后在resources目录下创建一个spring配置文件:
1 2 3 4 5 <?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:p ="http://www.springframework.org/schema/p" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" > </beans >
在这个文件中,我们可以配置所有需要注册到spring容器的bean:
1 2 3 4 5 6 7 8 <?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:p ="http://www.springframework.org/schema/p" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" > <bean class ="org.javaboy.User" id ="user" /> </beans >
class 属性表示需要注册的bean的 全路径,id表示bean的唯一标识,也可以用name属性作为bean的标志。
接下来,写一个main方法去加载这个配置文件,从容器中获取对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package org.javaboy.ioc;import org.javaboy.ioc.model.User;import org.springframework.context.support.ClassPathXmlApplicationContext;public class Main { public static void main (string[] args) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext ("applicationContext.xml" ); User user = ctx.getBean("user" , User.class); System.out.println("user = " + user); } }
上述代码中获取bean对象是通过id和class组合来获取一个bean的,一般这样不会出错。嫌麻烦可以直接使用id或者name来获取,但一般不建议直接使用class来获取(如果xml文件中存在两个相同的bean就不好办了)
属性的注入 构造方法注入 通过 Bean 的构造方法给 Bean 的属性注入值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class User { private String username; private String address; private Integer id; public User (String username, String address, Integer id) { this .username = username; this .address = address; this .id = id; } }
在 xml 文件中注入 Bean
1 2 3 4 5 6 7 8 9 10 11 12 <?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:p ="http://www.springframework.org/schema/p" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" > <bean class ="org.javaboy.ioc.model.User" id ="user" > <constructor-arg name ="id" value ="1" /> <constructor-arg name ="username" value ="javaboy" /> <constructor-arg name ="address" value ="javaboy.org" /> </bean > </beans >
如果有多个构造方法,则会根据给出参数个数以及参数类型,自动匹配到对应的构造方法上,进而初始化一个对象。
set方法注入 1 2 3 4 5 <bean class ="org.javaboy.ioc.model.User" id ="user2" > <property name ="id" value ="2" /> <property name ="username" value ="javaboy2" /> <property name ="address" value ="www.javaboy.org" /> </bean >
set 方法注入,有一个很重要的问题,就是属性名。很多人会有一种错觉,觉得属性名就是你定义的属性名,这个是不对的。在所有的框架中,凡是涉及到反射注入值的,属性名统统都不是 Bean 中定义的属性名,而是通过 Java 中的内省机制分析出来的属性名,简单说,就是根据 get/set 方法分析出来的属性名。
p 名称空间注入 1 <bean class ="org.javaboy.ioc.model.User" id ="user3" p:username ="javaboy" p:id ="3" p:address ="itboy.com" > </bean >
p 名称空间注入,使用的比较少,它本质上也是调用了 set 方法。
外部 Bean 的注入 有时候,我们使用一些外部 Bean,这些 Bean 可能没有构造方法,而是通过 Builder 来构造的,这个时候,就无法使用上面的方式来给它注入值了。
例如在 OkHttp 的网络请求中,原生的写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package org.javaboy.ioc;import okhttp3.*;import org.jetbrains.annotations.NotNull;import java.io.IOException;public class OkHttpTest { public static void main (String[] args) { OkHttpClient okHttpClient = new OkHttpClient .Builder().build(); OkHttpClient okHttpClient = ctx.getBean("okHttpClient" , OkHttpClient.class); Request request = new Request .Builder() .get() .url("https://www.baidu.com" ) .build(); Call call = okHttpClient.newCall(request); call.enqueue(new Callback () { @Override public void onFailure (@NotNull Call call, @NotNull IOException e) { System.out.println("e.getMessage() = " + e.getMessage()); } @Override public void onResponse (@NotNull Call call, @NotNull Response response) throws IOException { System.out.println("response.body() = " + response.body().string()); } }); } }
这个 Bean 有一个特点,OkHttpClient 和 Request 两个实例都不是直接 new 出来的,在调用 Builder 方法的过程中,都会给它配置一些默认的参数。这种情况,我们可以使用 静态工厂注入或者实例工厂注入来给 OkHttpClient 提供一个实例。
静态工厂注入
提供一个 OkHttpClient 的静态工厂:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package org.javaboy.ioc;import okhttp3.OkHttpClient;public class OkHttpStaticFactory { private static OkHttpClient okHttpClient; public static OkHttpClient getInstance () { if (okHttpClient == null ) { okHttpClient = new OkHttpClient .Builder().build(); } return okHttpClient; } }
在 xml 文件中,配置该静态工厂:
1 <bean class ="org.javaboy.ioc.OkHttpStaticFactory" factory-method ="getInstance" id ="okHttpClient" />
这个配置表示 OkHttpStaticFactory类中的 getInstance 是我们需要的实例,实例的名字就叫 okHttpClient。然后,在 Java 代码中,获取到这个实例,就可以直接使用了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package org.javaboy.ioc;import okhttp3.*;import org.jetbrains.annotations.NotNull;import org.springframework.context.support.ClassPathXmlApplicationContext;import java.io.IOException;public class OkHttpTest { public static void main (String[] args) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext ("applicationContext.xml" ); OkHttpClient okHttpClient = ctx.getBean("okHttpClient" , OkHttpClient.class); Request request = new Request .Builder() .get() .url("https://www.baidu.com" ) .build(); Call call = okHttpClient.newCall(request); call.enqueue(new Callback () { @Override public void onFailure (@NotNull Call call, @NotNull IOException e) { System.out.println("e.getMessage() = " + e.getMessage()); } @Override public void onResponse (@NotNull Call call, @NotNull Response response) throws IOException { System.out.println("response.body() = " + response.body().string()); } }); } }
实例工厂注入
实例工厂就是工厂方法是一个实例方法,这样,工厂类必须实例化之后才可以调用工厂方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package org.javaboy.ioc;import okhttp3.OkHttpClient;public class OkHttpFactory { private OkHttpClient okHttpClient; public OkHttpClient getInstance () { if (okHttpClient == null ) { okHttpClient = new OkHttpClient .Builder().build(); } return okHttpClient; } }
此时,在 xml 文件中,需要首先提供工厂方法的实例,然后才可以调用工厂方法:
1 2 <bean class ="org.javaboy.ioc.OkHttpFactory" id ="okHttpFactory" /> <bean class ="okhttp3.OkHttpClient" factory-bean ="okHttpFactory" factory-method ="getInstance" id ="okHttpClient" />
自己写的 Bean 一般不会使用这两种方式注入,但是,如果需要引入外部 jar,外部 jar 的类的初始化,有可能需要使用这两种方式。
复杂属性的注入 1 2 3 4 5 6 7 8 9 public class User { private String username; private String address; private Integer id; private Cat cat; private Cat[] cats; private Map<String, Object> detail; private Properties info; }
1 2 3 4 public class Cat { private String name; private Integer age; }
对象注入 1 2 3 4 5 6 7 8 <bean class ="org.javaboy.User" id ="user" > <property name ="cat" ref ="cat" /> </bean > <bean class ="org.javaboy.Cat" id ="cat" > <property name ="name" value ="小白" /> <property name ="color" value ="白色" /> </bean >
可以通过 xml 注入对象,通过 ref 来引用一个对象
数组注入 数组注入和集合注入在 xml 中的配置是一样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <bean class ="org.javaboy.User" id ="user" > <property name ="cat" ref ="cat" /> <property name ="favorites" > <array > <value > 足球</value > <value > 篮球</value > <value > 乒乓球</value > </array > </property > </bean > <bean class ="org.javaboy.Cat" id ="cat" > <property name ="name" value ="小白" /> <property name ="color" value ="白色" /> </bean >
array节点也可使用list节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <bean class ="org.javaboy.User" id ="user" > <property name ="cat" ref ="cat" /> <property name ="favorites" > <list > <value > 足球</value > <value > 篮球</value > <value > 乒乓球</value > </list > </property > <property name ="cats" > <list > <ref bean ="cat" /> <ref bean ="cat2" /> <bean class ="org.javaboy.Cat" id ="cat3" > <property name ="name" value ="小花" /> <property name ="color" value ="花色" /> </bean > </list > </property > </bean > <bean class ="org.javaboy.Cat" id ="cat" > <property name ="name" value ="小白" /> <property name ="color" value ="白色" /> </bean > <bean class ="org.javaboy.Cat" id ="cat2" > <property name ="name" value ="小黑" /> <property name ="color" value ="黑色" /> </bean >
注意,既可以通过 ref 使用外部定义好的 Bean,也可以直接在 list 或者 array 节点中定义 bean。
Map注入 1 2 3 4 5 6 <property name ="map" > <map > <entry key ="age" value ="99" /> <entry key ="name" value ="javaboy" /> </map > </property >
properties注入 1 2 3 4 5 6 <property name ="info" > <props > <prop key ="age" > 99</prop > <prop key ="name" > javaboy</prop > </props > </property >
Java配置 在 Spring 中,想要将一个 Bean 注册到 Spring 容器中,整体上来说,有三种不同的方式:
xml注入
Java配置,通过 Java 代码将 Bean 注册到 Spring 容器中
自动化扫描
Java 配置这种方式在 Spring Boot 出现之前,其实很少使用,自从有了 Spring Boot,Java 配置开发被广泛使用,因为在 Spring Boot 中,不使用一行 XML 配置
例如我有如下一个 Bean:
1 2 3 4 5 public class SayHello { public String sayHello (String name) { return "hello " + name; } }
在 Java 配置中,我们用一个 Java 配置类去代替之前的 applicationContext.xml 文件:
1 2 3 4 5 6 7 @Configuration public class JavaConfig { @Bean SayHello sayHello () { return new SayHello (); } }
首先在配置类上有一个 @Configuration 注解,这个注解表示这个类不是一个普通类,而是一个配置类,它的作用相当于 applicationContext.xml。 然后,定义方法,方法返回对象,方法上添加 @Bean 注解,表示将这个方法的返回值注入的 Spring 容器中去。也就是说,@Bean 所对应的方法,就相当于applicationContext.xml 中的 bean 节点。
既然是配置类,我们需要在项目启动时加载配置类。
1 2 3 4 5 6 7 public class Main { public static void main (String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext (JavaConfig.class); SayHello hello = ctx.getBean(SayHello.class); System.out.println(hello.sayHello("哈哈哈哈哈" )); } }
注意,配置的加载,是使用 AnnotationConfigApplicationContext 来实现。
Bean 的默认名称是方法名。以上面的案例为例,Bean 的名字是 sayHello。 如果开发者想自定义方法名,也是可以的,直接在 @Bean 注解中进行过配置。如下配置表示修改 Bean 的名字为 javaboy:
1 2 3 4 5 6 7 @Configuration public class JavaConfig { @Bean("javaboy") SayHello sayHello () { return new SayHello (); } }
自动化配置 在我们实际开发中,大量的使用自动配置。
自动化配置既可以通过 Java 配置来实现,也可以通过 xml 配置来实现。
准备工作 例如我有一个 UserService,我希望在自动化扫描时,这个类能够自动注册到 Spring 容器中去,那么可以给该类添加一个 @Service,作为一个标记。
和 @Service 注解功能类似的注解,一共有四个:
@Component
@Repository
@Controller
@Controller
这四个中,另外三个都是基于 @Component 做出来的,而且从目前的源码来看,功能也是一致的,那么为什么要搞三个呢?主要是为了在不同的类上面添加时方便。
在 Service 层上,添加注解时,使用 @Service
在 Dao 层,添加注解时,使用 @Repository
在 Controller 层,添加注解时,使用 @Controller
在其他组件上添加注解时,使用 @Component
1 2 3 4 5 6 7 8 9 10 @Service public class UserService { public List<String> getAllUser () { List<String> users = new ArrayList <>(); for (int i = 0 ; i < 10 ; i++) { users.add("javaboy:" + i); } return users; } }
添加完成后,自动化扫描有两种方式,一种就是通过 Java 代码配置自动化扫描,另一种则是通过 xml 文件来配置自动化扫描。
Java 代码配置自动扫描 1 2 3 4 5 @Configuration @ComponentScan(basePackages = "org.javaboy.javaconfig.service") public class JavaConfig { }
然后,在项目启动中加载配置类,在配置类中,通过 @ComponentScan 注解指定要扫描的包(如果不指定,默认情况下扫描的是配置类所在的包下载的 Bean 以及配置类所在的包下的子包下的类),然后就可以获取 UserService 的实例了:
1 2 3 4 5 6 7 public class Main { public static void main (String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext (JavaConfig.class); UserService userService = ctx.getBean(UserService.class); System.out.println(userService.getAllUser()); } }
注意:
默认情况下,Bean 的名字是类名首字母小写。例如上面的 UserService,它的实例名,默认就是 userService。如果开发者想要自定义名字,就直接在 @Service 注解中添加即可。
上面的配置,我们是按照包的位置来扫描的。也就是说,Bean 必须放在指定的扫描位置,否则,即使你有 @Service 注解,也扫描不到。
除了按照包的位置来扫描,还有另外一种方式,就是根据注解来扫描。例如如下配置:
1 2 3 @Configuration @ComponentScan(basePackages = "org.javaboy.javaconfig",useDefaultFilters = true,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Controller.class)}) public class JavaConfig {}
这个配置表示扫描 org.javaboy.javaconfig 下的 除了 Controller之外 所有 Bean
XML 配置自动化扫描 下面这行配置表示扫描 org.javaboy.javaconfig 下的所有 Bean。当然也可以按照类来扫描。
1 <context:component-scan base-package ="org.javaboy.javaconfig" />
XML 配置完成后,在 Java 代码中加载 XML 配置即可。
1 2 3 4 5 6 7 8 public class XMLTest { public static void main (String[] args) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext ("applicationContext.xml" ); UserService userService = ctx.getBean(UserService.class); List<String> list = userService.getAllUser(); System.out.println(list); } }
也可以在 XML 配置中按照注解的类型进行扫描:
1 2 3 <context:component-scan base-package ="org.javaboy.javaconfig" use-default-filters ="true" > <context:exclude-filter type ="annotation" expression ="org.springframework.stereotype.Controller" /> </context:component-scan >
对象注入问题 自动扫描时的对象注入有三种方式:
@Autowired, @Autowired 是根据类型去查找,然后赋值,这就有一个要求,这个类型只可以有一个对象,否则就会报错
@Resources, @Resources 是根据名称去查找,默认情况下,定义的变量名,就是查找的名称,当然开发者也可以在 @Resources 注解中手动指定
@Injected
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Service public class UserService { @Autowired UserDao userDao; public String hello () { return userDao.hello(); } public List<String> getAllUser () { List<String> users = new ArrayList <>(); for (int i = 0 ; i < 10 ; i++) { users.add("javaboy:" + i); } return users; } }
如果一个类存在多个实例,那么就应该使用 @Resources 去注入,如果非得使用 @Autowired,也是可以的,此时需要配合另外一个注解,@Qualifier,在 @Qualifier 中可以指定变量名,两个一起用(@Qualifier 和 @Autowired)就可以实现通过变量名查找到变量。
混合配置 混合配置就是 Java 配置+XML 配置。混用的话,可以在 Java 配置中引入 XML 配置
1 2 3 @Configuration @ImportResource("classpath:applicationContext.xml") public class JavaConfig {}
在 Java 配置中,通过 @ImportResource 注解可以导入一个 XML 配置。
条件注解 条件注解的示例 在 Windows 中如何获取操作系统信息?Windows 中查看文件夹目录的命令是 dir,Linux 中查看文件夹目录的命令是 ls,我现在希望当系统运行在 Windows 上时,自动打印出 Windows 上的目录展示命令,Linux 运行时,则自动展示 Linux 上的目录展示命令。
首先定义一个显示文件夹目录的接口:
1 2 3 public interface ShowCmd { String showCmd () ; }
然后,分别实现 Windows 下的实例和 Linux 下的实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class WinShowCmd implements ShowCmd { @Override public String showCmd () { return "dir" ; } } public class LinuxShowCmd implements ShowCmd { @Override public String showCmd () { return "ls" ; } }
接下来,定义两个条件,一个是 Windows 下的条件,另一个是 Linux 下的条件。
1 2 3 4 5 6 7 8 9 10 11 12 13 public class WindowsCondition implements Condition { @Override public boolean matches (ConditionContext context, AnnotatedTypeMetadata metadata) { return context.getEnvironment().getProperty("os.name" ).toLowerCase().contains("windows" ); } } public class LinuxCondition implements Condition { @Override public boolean matches (ConditionContext context, AnnotatedTypeMetadata metadata) { return context.getEnvironment().getProperty("os.name" ).toLowerCase().contains("linux" ); } }
接下来,在定义 Bean 的时候,就可以去配置条件注解了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration public class JavaConfig { @Bean("showCmd") @Conditional(WindowsCondition.class) ShowCmd winCmd () { return new WinShowCmd (); } @Bean("showCmd") @Conditional(LinuxCondition.class) ShowCmd linuxCmd () { return new LinuxShowCmd (); } }
这里,一定要给两个 Bean 取相同的名字,这样在调用时,才可以自动匹配。然后,给每一个 Bean 加上条件注解,当条件中的 matches 方法返回 true 的时候,这个 Bean 的定义就会生效。
1 2 3 4 5 6 7 public class JavaMain { public static void main (String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext (JavaConfig.class); ShowCmd showCmd = (ShowCmd) ctx.getBean("showCmd" ); System.out.println(showCmd.showCmd()); } }
条件注解有一个非常典型的使用场景,就是多环境切换。
多环境切换 开发中,如何在 开发/生产/测试 环境之间进行快速切换?Spring 中提供了 Profile 来解决这个问题,Profile 的底层就是条件注解。这个从 @Profile 注解的定义就可以看出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(ProfileCondition.class) public @interface Profile { String[] value(); } class ProfileCondition implements Condition { @Override public boolean matches (ConditionContext context, AnnotatedTypeMetadata metadata) { MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); if (attrs != null ) { for (Object value : attrs.get("value" )) { if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) { return true ; } } return false ; } return true ; } }
我们定义一个 DataSource:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class DataSource { private String url; private String username; private String password; @Override public String toString () { return "DataSource{" + "url='" + url + '\'' + ", username='" + username + '\'' + ", password='" + password + '\'' + '}' ; } public String getUrl () { return url; } public void setUrl (String url) { this .url = url; } 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; } }
然后,在配置 Bean 时,通过 @Profile 注解指定不同的环境:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Bean("ds") @Profile("dev") DataSource devDataSource () { DataSource dataSource = new DataSource (); dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/dev" ); dataSource.setUsername("root" ); dataSource.setPassword("123" ); return dataSource; } @Bean("ds") @Profile("prod") DataSource prodDataSource () { DataSource dataSource = new DataSource (); dataSource.setUrl("jdbc:mysql://192.168.33.113:3306/dev" ); dataSource.setUsername("jkldasjfkl" ); dataSource.setPassword("jfsdjflkajkld" ); return dataSource; }
最后,在加载配置类,注意,需要先设置当前环境,然后再去加载配置类:
1 2 3 4 5 6 7 8 9 10 public class JavaMain { public static void main (String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext (); ctx.getEnvironment().setActiveProfiles("dev" ); ctx.register(JavaConfig.class); ctx.refresh(); DataSource ds = (DataSource) ctx.getBean("ds" ); System.out.println(ds); } }
这个是在 Java 代码中配置的。
环境的切换,也可以在 XML 文件中配置,如下配置在 XML 文件中,必须放在其他节点后面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <beans profile ="dev" > <bean class ="org.javaboy.DataSource" id ="dataSource" > <property name ="url" value ="jdbc:mysql:///devdb" /> <property name ="password" value ="root" /> <property name ="username" value ="root" /> </bean > </beans > <beans profile ="prod" > <bean class ="org.javaboy.DataSource" id ="dataSource" > <property name ="url" value ="jdbc:mysql://192.168.33.113:3306/devdb" /> <property name ="password" value ="jsdfaklfj789345fjsd" /> <property name ="username" value ="root" /> </bean > </beans >
启动类中设置当前环境并加载配置:
1 2 3 4 5 6 7 8 9 10 public class Main { public static void main (String[] args) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext (); ctx.getEnvironment().setActiveProfiles("prod" ); ctx.setConfigLocation("applicationContext.xml" ); ctx.refresh(); DataSource dataSource = (DataSource) ctx.getBean("dataSource" ); System.out.println(dataSource); } }
Bean 的作用域 在 XML 配置中注册的 Bean,或者用 Java 配置注册的 Bean,如果我多次获取,获取到的对象是否是同一个?
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext ("applicationContext.xml" ); User user = ctx.getBean("user" , User.class); User user2 = ctx.getBean("user" , User.class); System.out.println(user==user2); } }
如上,从 Spring 容器中多次获取同一个 Bean,默认情况下,获取到的实际上是同一个实例。
通过在 XML 节点中,设置 scope 属性,我们可以调整默认的实例个数。scope 的值为 singleton(默认),表示这个 Bean 在 Spring 容器中,是以单例的形式存在,如果 scope 的值为 prototype,表示这个 Bean 在 Spring 容器中不是单例,多次获取将拿到多个不同的实例。
1 <bean class ="org.javaboy.User" id ="user" scope ="prototype" />
除了 singleton 和 prototype 之外,还有两个取值,request 和 session。这两个取值在 web 环境下有效。
上面的是在 XML 中的配置,我们也可以在 Java 中配置。
1 2 3 4 5 6 7 8 @Configuration public class JavaConfig { @Bean @Scope("prototype") SayHello sayHello () { return new SayHello (); } }
在 Java 代码中,我们可以通过 @Scope 注解指定 Bean 的作用域。
在自动扫描配置中,也可以指定bean的作用域
1 2 3 4 5 6 7 @Repository @Scope("prototype") public class UserDao { public String hello () { return "userdao" ; } }
id 和 name 的区别 在 XML 配置中,我们可以看到,即可以通过 id 给 Bean 指定一个唯一标识符,也可以通过 name 来指定,大部分情况下这两个作用是一样的,有一个小小区别:
name 支持取多个。多个 name 之间,用,
隔开
1 <bean class ="org.javaboy.User" name ="user,user1,user2,user3" scope ="prototype" />
此时,通过 user、user1、user2、user3 都可以获取到当前对象:
1 2 3 4 5 6 7 8 9 public class Main { public static void main (String[] args) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext ("applicationContext.xml" ); User user = ctx.getBean("user" , User.class); User user2 = ctx.getBean("user2" , User.class); System.out.println(user); System.out.println(user2); } }
而 id 不支持有多个值。如果强行用 , 隔开,它还是一个值。例如如下配置:
1 <bean class ="org.javaboy.User" id ="user,user1,user2,user3" scope ="prototype" />
这个配置表示 Bean 的名字为 user,user1,user2,user3
,具体调用如下:
1 2 3 4 5 6 7 8 9 public class Main { public static void main (String[] args) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext ("applicationContext.xml" ); User user = ctx.getBean("user,user1,user2,user3" , User.class); User user2 = ctx.getBean("user,user1,user2,user3" , User.class); System.out.println(user); System.out.println(user2); } }
Aware接口 Aware 接口,从字面上理解就是感知捕获。单纯的一个 Bean 是没有知觉的。
在实际开发中,我们可能会遇到一些类,需要获取到容器的详细信息,那就可以通过 Aware 接口来实现。
Aware 是一个空接口,有很多实现类:
这些实现的接口,有一些公共特性:
都是以 Aware 结尾
都继承Aware
接口内均定义了一个 set 方法
每一个子接口均提供了一个 set 方法,方法的参数就是当前 Bean 需要感知的内容,因此我们需要在 Bean 中声明相关的成员变量来接受这个参数。接收到这个参数后,就可以通过这个参数获取到容器的详细信息了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package org.javaboy.aware;import org.springframework.beans.BeansException;import org.springframework.beans.factory.BeanFactory;import org.springframework.beans.factory.BeanFactoryAware;import org.springframework.beans.factory.BeanNameAware;import org.springframework.context.EnvironmentAware;import org.springframework.context.ResourceLoaderAware;import org.springframework.context.annotation.PropertySource;import org.springframework.core.env.Environment;import org.springframework.core.io.Resource;import org.springframework.core.io.ResourceLoader;import org.springframework.stereotype.Service;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;@Service @PropertySource(value = "javaboy.properties") public class AwareService implements BeanNameAware , BeanFactoryAware, ResourceLoaderAware, EnvironmentAware { private String beanName; private ResourceLoader resourceLoader; private Environment environment; public void output () throws IOException { System.out.println("beanName = " + beanName); Resource resource = resourceLoader.getResource("javaboy.txt" ); BufferedReader br = new BufferedReader (new InputStreamReader (resource.getInputStream())); String s = br.readLine(); System.out.println("s = " + s); br.close(); String address = environment.getProperty("javaboy.address" ); System.out.println("address = " + address); } @Override public void setBeanFactory (BeanFactory beanFactory) throws BeansException { } @Override public void setBeanName (String s) { this .beanName = s; } @Override public void setEnvironment (Environment environment) { this .environment = environment; } @Override public void setResourceLoader (ResourceLoader resourceLoader) { this .resourceLoader = resourceLoader; } }
AOP aop简介 AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程。
AOP 是 OOP(面向对象编程) 的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
面向切面编程,就是在程序运行时,不改变程序源码的情况下,动态的增强方法的功能,常见的使用场景非常多:日志、事务、数据库操作等。
这些操作中,无一例外,都有很多模板化的代码,而解决模板化代码,消除臃肿就是 Aop 的强项。
在 Aop 中,有几个常见的概念:
概念
说明
Target(目标对象)
代理的目标对象
Proxy (代理)
一个类被 AOP 织入增强后,就产生一个结果代理类
Joinpoint(连接点)
指那些可以被拦截到的点。在spring中,这些点指的是方法,因为 spring只支持方法类型的连接点
Pointcut(切入点)
指我们要对哪些 Joinpoint 进行拦截
Advice(通知/ 增强)
通知是指拦截到 Joinpoint 之后所要做的事情,分为前置通知、后置通知、异常通知、最终通知、环绕通知
Aspect(切面)
是切入点和通知(引介)的结合
Weaving(织入)
是指把增强应用到目标对象来创建新的代理对象的程。spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入
在 Aop 实际上集基于 Java 动态代理来实现的,Java 中的动态代理有两种实现方式:
基于JDK的动态代理 不使用spring的aop来实现
1 2 3 4 5 public interface MyCalculator { int add (int a, int b) ; }
1 2 3 4 5 6 public class MyCalculatorImpl implements MyCalculator { @Override public int add (int a, int b) { return a + b; } }
定义代理类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;public class CalculatorProxy { public static Object getInstance (MyCalculatorImpl myCalculator) { return Proxy.newProxyInstance(CalculatorProxy.class.getClassLoader(), myCalculator.getClass().getInterfaces(), new InvocationHandler () { @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { System.out.println(method.getName() + "方法开始执行了" ); Object invoke = method.invoke(myCalculator, args); System.out.println(method.getName() + "方法执行结束了" ); return invoke; } }); } }
Proxy.newProxyInstance 方法接收三个参数,第一个是一个 classloader,第二个是代理多项实现的接口,第三个是代理对象方法的处理器,所有要额外添加的行为都在 invoke 方法中实现。
测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Main { public static void main (String[] args) { MyCalculatorImpl myCalculator = new MyCalculatorImpl (); MyCalculator calculator = (MyCalculator) CalculatorProxy.getInstance(myCalculator); int add = calculator.add(3 , 4 ); System.out.println("add = " + add); test("a" , "e" , "aaa" ); } public static void test (String... x) { for (String s : x) { System.out.println("s = " + s); } } }
AOP的五种通知 Spring 中的 Aop 的通知类型有 5 种:
首先添加依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 5.3.6</version > </dependency > <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjweaver</artifactId > <version > 1.9.6</version > </dependency > <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjrt</artifactId > <version > 1.9.6</version > </dependency > </dependencies >
接下来,定义切点,这里有两种切点的定义方式:
其中,使用自定义注解标记切点,是侵入式的,所以这种方式在实际开发中不推荐,仅作为了解,另一种使用规则来定义切点的方式,无侵入,一般推荐使用这种方式。
自定义注解 首先自定义一个注解:
1 2 3 4 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Action {}
计算器的接口:
1 2 3 4 5 6 7 public interface MyCalculator { int add (int a, int b) ; void min (int a, int b) ; }
然后在需要拦截的方法上,添加该注解,在 add 方法上添加了 @Action 注解,表示该方法将会被 Aop 拦截,而其他未添加该注解的方法则不受影响。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import org.springframework.stereotype.Component;@Component public class MyCalculatorImpl implements MyCalculator { @Action @Override public int add (int a, int b) { System.out.println(a + "+" + b + "=" + (a + b)); return a + b; } @Override public void min (int a, int b) { System.out.println(a + "-" + b + "=" + (a - b)); } }
接下来,定义增强(通知、Advice):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;@Component @Aspect public class LogAspect { @Before("@annotation(Action)") public void before (JoinPoint joinPoint) { String name = joinPoint.getSignature().getName(); System.out.println(name + "方法开始执行了。。。" ); } @After("@annotation(Action)") public void after (JoinPoint joinPoint) { String name = joinPoint.getSignature().getName(); System.out.println(name + "方法执行结束了。。。" ); } @AfterReturning(value = "@annotation(Action)", returning = "r") public void returning (JoinPoint joinPoint, Integer r) { String name = joinPoint.getSignature().getName(); System.out.println(name + "方法返回通知:" + r); } @AfterThrowing(value = "@annotation(Action)", throwing = "e") public void afterThrowing (JoinPoint joinPoint, Exception e) { String name = joinPoint.getSignature().getName(); System.out.println(name + "方法返回通知:" + e.getMessage()); } @Around("@annotation(Action)") public Object around (ProceedingJoinPoint pjp) throws Throwable { Object proceed = pjp.proceed(); return proceed; } }
通知定义完成后,接下来在配置类中,开启包扫描和自动代理:
1 2 3 4 5 6 7 8 9 import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;@Configuration @ComponentScan @EnableAspectJAutoProxy public class JavaConfig {}
在 Main 方法中,开启调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class Main { public static void main (String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext (JavaConfig.class); MyCalculator calculator = ctx.getBean(MyCalculator.class); calculator.add(3 , 4 ); calculator.min(3 , 4 ); } }
方法中统一定义切点 再来回顾 LogAspect 切面,我们发现,切点的定义不够灵活,之前的切点是直接写在注解里边的,这样,如果要修改切点,每个方法上都要修改,因此,我们可以将切点统一定义,然后统一调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Component @Aspect public class LogAspect { @Pointcut("@annotation(Action)") public void pointcut () { } @Before(value = "pointcut()") public void before (JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); String name = signature.getName(); System.out.println(name + "方法开始执行了..." ); } }
非侵入定义切点 上面使用注解是侵入式的,我们还可以继续优化,改为非侵入式的。重新定义切点,新切点的定义就不在需要 @Action 注解了,要拦截的目标方法上也不用添加 @Action 注解。下面这种方式是更为通用的拦截方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Component @Aspect public class LogAspect { @Pointcut("execution(* com.cwz.aop.commons.*.*(..))") public void pointcut () { } @Before(value = "pointcut()") public void before (JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); String name = signature.getName(); System.out.println(name + "方法开始执行了..." ); } }
XML配置AOP 定义通知/增强,但是单纯定义自己的行为即可,不再需要注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class LogAspect { public void before (JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); String name = signature.getName(); System.out.println(name + "方法开始执行了..." ); } public void after (JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); String name = signature.getName(); System.out.println(name + "方法执行结束了..." ); } public void returing (JoinPoint joinPoint,Integer r) { Signature signature = joinPoint.getSignature(); String name = signature.getName(); System.out.println(name + "方法返回:" +r); } public void afterThrowing (JoinPoint joinPoint,Exception e) { Signature signature = joinPoint.getSignature(); String name = signature.getName(); System.out.println(name + "方法抛异常了:" +e.getMessage()); } public Object around (ProceedingJoinPoint pjp) { Object proceed = null ; try { proceed = pjp.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } return proceed; } }
在spring中添加xml配置:
1 2 3 4 5 6 7 8 9 10 11 <bean class="org.javaboy.aop.LogAspect" id="logAspect" /> <aop:config> <aop:pointcut id="pc1" expression="execution(* org.javaboy.aop.commons.*.*(..))" /> <aop:aspect ref="logAspect" > <aop:before method="before" pointcut-ref="pc1" /> <aop:after method="after" pointcut-ref="pc1" /> <aop:after-returning method="returing" pointcut-ref="pc1" returning="r" /> <aop:after-throwing method="afterThrowing" pointcut-ref="pc1" throwing="e" /> <aop:around method="around" pointcut-ref="pc1" /> </aop:aspect> </aop:config>
在 Main 方法中加载配置文件:
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext ("applicationContext.xml" ); MyCalculatorImpl myCalculator = ctx.getBean(MyCalculatorImpl.class); myCalculator.add(3 , 4 ); myCalculator.min(5 , 6 ); } }
JdbcTemplate JdbcTemplate 是 Spring 利用 Aop 思想封装的 JDBC 操作工具。
比原生的jdbc方便,但不如mybatis这种。
准备工作 添加依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-jdbc</artifactId > <version > 5.3.6E</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 5.3.6</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.17</version > </dependency > </dependencies >
准备数据:
1 2 3 4 5 6 CREATE TABLE `user ` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `username` varchar (255 ) COLLATE utf8mb4_general_ci DEFAULT NULL , `address` varchar (255 ) COLLATE utf8mb4_general_ci DEFAULT NULL , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_general_ci;
实体类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class User { private Integer id; private String username; private String address; @Override public String toString () { return "User{" + "id=" + id + ", username='" + username + '\'' + ", address='" + address + '\'' + '}' ; } public Integer getId () { return id; } public void setId (Integer id) { this .id = id; } public String getUsername () { return username; } public void setUsername (String username) { this .username = username; } public String getAddress () { return address; } public void setAddress (String address) { this .address = address; } }
Java配置 提供一个配置类,在配置类中配置 JdbcTemplate:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration public class JdbcConfig { @Bean DataSource dataSource () { DriverManagerDataSource dataSource = new DriverManagerDataSource (); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver" ); dataSource.setUsername("root" ); dataSource.setPassword("123456" ); dataSource.setUrl("jdbc:mysql:///test01" ); return dataSource; } @Bean JdbcTemplate jdbcTemplate () { return new JdbcTemplate (dataSource()); } }
提供两个 Bean,一个是 DataSource 的 Bean,另一个是 JdbcTemplate 的 Bean,JdbcTemplate 的配置非常容易,只需要 new 一个 Bean 出来,然后配置一下 DataSource 就可以。
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class Main { private JdbcTemplate jdbcTemplate; @Before public void before () { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext (JdbcConfig.class); jdbcTemplate = ctx.getBean(JdbcTemplate.class); } @Test public void insert () { jdbcTemplate.update("insert into user (username,address) values (?,?);" , "javaboy" , "www.javaboy.org" ); } @Test public void update () { jdbcTemplate.update("update user set username=? where id=?" , "javaboy123" , 1 ); } @Test public void delete () { jdbcTemplate.update("delete from user where id=?" , 2 ); } @Test public void select () { User user = jdbcTemplate.queryForObject("select * from user where id=?" , new BeanPropertyRowMapper <User>(User.class), 1 ); System.out.println(user); } }
在查询时,如果使用了 BeanPropertyRowMapper,要求查出来的字段必须和 Bean 的属性名一一对应。如果不一样,则不要使用 BeanPropertyRowMapper,此时需要自定义 RowMapper 或者给查询的字段取别名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void select2 () { User user = jdbcTemplate.queryForObject("select * from user where id=?" , new RowMapper <User>() { public User mapRow (ResultSet resultSet, int i) throws SQLException { int id = resultSet.getInt("id" ); String username = resultSet.getString("username" ); String address = resultSet.getString("address" ); User u = new User (); u.setId(id); u.setName(username); u.setAddress(address); return u; } }, 1 ); System.out.println(user); }
1 2 3 4 5 @Test public void select3 () { User user = jdbcTemplate.queryForObject("select id,username as name,address from user where id=?" , new BeanPropertyRowMapper <User>(User.class), 1 ); System.out.println(user); }
XML配置 1 2 3 4 5 6 7 8 9 <bean class ="org.springframework.jdbc.datasource.DriverManagerDataSource" id ="dataSource" > <property name ="username" value ="root" /> <property name ="password" value ="123456" /> <property name ="url" value ="jdbc:mysql:///test01?serverTimezone=Asia/Shanghai" /> <property name ="driverClassName" value ="com.mysql.cj.jdbc.Driver" /> </bean > <bean class ="org.springframework.jdbc.core.JdbcTemplate" id ="jdbcTemplate" > <property name ="dataSource" ref ="dataSource" /> </bean >
事务 Spring 中的事务主要是利用 Aop 思想,简化事务的配置,可以通过 Java 配置也可以通过 XML 配置。
我们通过一个转账操作来看下 Spring 中的事务配置。
配置 JdbcTemplate:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration public class JdbcConfig { @Bean DataSource dataSource () { DriverManagerDataSource dataSource = new DriverManagerDataSource (); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver" ); dataSource.setUsername("root" ); dataSource.setPassword("123456" ); dataSource.setUrl("jdbc:mysql:///test02" ); return dataSource; } @Bean JdbcTemplate jdbcTemplate () { return new JdbcTemplate (dataSource()); } }
提供转账操作的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Repository public class UserDao { @Autowired JdbcTemplate jdbcTemplate; public void addMoney (String username, Integer money) { jdbcTemplate.update("update account set money=money+? where username=?" , money, username); } public void minMoney (String username, Integer money) { jdbcTemplate.update("update account set money=money-? where username=?" , money, username); } } @Service public class UserService { @Autowired UserDao userDao; public void updateMoney () { userDao.addMoney("zhangsan" , 200 ); int i = 1 / 0 ; userDao.minMoney("lisi" , 200 ); } }
最后,在 XML 文件中,开启自动化扫描:
1 2 3 4 5 6 7 8 9 10 <context:component-scan base-package ="org.javaboy" /> <bean class ="org.springframework.jdbc.datasource.DriverManagerDataSource" id ="dataSource" > <property name ="username" value ="root" /> <property name ="password" value ="123456" /> <property name ="url" value ="jdbc:mysql:///test02?serverTimezone=Asia/Shanghai" /> <property name ="driverClassName" value ="com.mysql.cj.jdbc.Driver" /> </bean > <bean class ="org.springframework.jdbc.core.JdbcTemplate" id ="jdbcTemplate" > <property name ="dataSource" ref ="dataSource" /> </bean >
XML配置 配置 TransactionManager
1 2 3 <bean class ="org.springframework.jdbc.datasource.DataSourceTransactionManager" id ="transactionManager" > <property name ="dataSource" ref ="dataSource" /> </bean >
配置事务要处理的方法
1 2 3 4 5 6 7 8 <tx:advice id ="txAdvice" transaction-manager ="transactionManager" > <tx:attributes > <tx:method name ="update*" /> <tx:method name ="insert*" /> <tx:method name ="add*" /> <tx:method name ="delete*" /> </tx:attributes > </tx:advice >
配置aop
1 2 3 4 <aop:config > <aop:pointcut id ="pc1" expression ="execution(* org.javaboy.service.*.*(..))" /> <aop:advisor advice-ref ="txAdvice" pointcut-ref ="pc1" /> </aop:config >
测试:
1 2 3 4 5 6 7 8 9 10 @Before public void before () { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext ("applicationContext.xml" ); jdbcTemplate = ctx.getBean(JdbcTemplate.class); userService = ctx.getBean(UserService.class); } @Test public void test1 () { userService.updateMoney(); }
Java配置 如果要开启 Java 注解配置,在 XML 配置中添加如下配置:
1 <tx:annotation-driven transaction-manager ="transactionManager" />
上面的配置相当于:
1 2 3 4 5 6 7 8 9 10 11 12 <tx:advice id ="txAdvice" transaction-manager ="transactionManager" > <tx:attributes > <tx:method name ="update*" /> <tx:method name ="insert*" /> <tx:method name ="add*" /> <tx:method name ="delete*" /> </tx:attributes > </tx:advice > <aop:config > <aop:pointcut id ="pc1" expression ="execution(* org.javaboy.service.*.*(..))" /> <aop:advisor advice-ref ="txAdvice" pointcut-ref ="pc1" /> </aop:config >
在需要添加事务的方法上,添加 @Transactional 注解,表示该方法开启事务,当然,这个注解也可以放在类上,表示这个类中的所有方法都开启事务。
1 2 3 4 5 6 7 8 9 10 11 @Service public class UserService { @Autowired UserDao userDao; @Transactional public void updateMoney () { userDao.addMoney("zhangsan" , 200 ); int i = 1 / 0 ; userDao.minMoney("lisi" , 200 ); } }
Spring MVC MVC模式 MVC是软件工程中的一种软件架构模式,它是一种分离业务逻辑与显示界面的开发思想。
1 2 3 * M(model)模型:处理业务逻辑,封装实体 * V(view) 视图:展示内容 * C(controller)控制器:负责调度分发(1.接收请求、2.调用模型、3.转发到视图)
SpringMVC简介 Spring Web MVC 是一种基于 Java 的实现了 Web MVC 设计模式的轻量级 Web 框架,使用了 MVC 架构模式的思想,将 web 层进行职责解耦,基于请求驱动指的就是使用请求-响应模型。
在 传统的 Jsp/Servlet 技术体系中,如果要开发接口,一个接口对应一个 Servlet,会导致我们开发出许多 Servlet,使用 SpringMVC 可以有效的简化这一步骤。
SpringMVC的优势:
能非常简单的设计出干净的 Web 层
天生与 Spring 框架集成(如 IoC 容器、AOP 等)
非常容易与其他视图技术集成,如 Velocity、FreeMarker 等等,因为模型数据不放在特定的 API 里,而是放在一个 Model 里。
支持灵活的本地化、主题等解析
支持 RESTful 风格
SpringMVC的框架就是封装了原来Servlet中的共有行为;例如:参数封装,视图转发等
SpringMVC实现一个helloword 创建一个Maven工程,添加依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-webmvc</artifactId > <version > RELEASE</version > </dependency > <dependency > <groupId > javax.servlet</groupId > <artifactId > javax.servlet-api</artifactId > <version > 4.0.1</version > </dependency > <dependency > <groupId > javax.servlet.jsp</groupId > <artifactId > javax.servlet.jsp-api</artifactId > <version > 2.3.3</version > </dependency >
添加了 spring-webmvc 依赖之后,其他的 spring-web、spring-aop、spring-context 等等就全部都加入进来了。
这是一个Java SE工程,现在要把它改造成web工程
依赖加入:
1 <packaging > war</packaging >
右键打开Open Module Settings
:
这样就把webapp创建出来了
编写Controller类和视图页面 准备一个 Controller,即一个处理浏览器请求的接口
MyController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.cwz.springmvc01.controller;import org.springframework.web.servlet.ModelAndView;import org.springframework.web.servlet.mvc.Controller;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class MyController implements Controller { @Override public ModelAndView handleRequest (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception { ModelAndView mv = new ModelAndView ("hello" ); mv.addObject("name" , "cwz" ); return mv; } }
采用 jsp 作为视图,在 webapp 目录下创建 hello.jsp 文件,内容如下:
/WEB-INF/jsp/ hello.jsp
1 2 3 4 5 6 7 8 9 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> hello ${name} ! </body> </html>
配置springmvc核心配置文件 spring-servlet.xml 在 resources 目录下,创建一个名为 spring-servlet.xml 的 springmvc 的配置文件,这里,我们先写一个简单的 demo ,因此可以先不用添加 spring 的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" > <bean class ="com.cwz.springmvc01.controller.MyController" name ="/hello" /> <bean class ="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" id ="beanNameUrlHandlerMapping" > <property name ="beanName" value ="/hello" /> </bean > <bean class ="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" id ="handlerAdapter" /> <bean class ="org.springframework.web.servlet.view.InternalResourceViewResolver" id ="viewResolver" > <property name ="prefix" value ="/jsp/" /> <property name ="suffix" value =".jsp" /> </bean > </beans >
加载 springmvc 配置文件 在 web 项目启动时,加载 springmvc 配置文件,这个配置是在 web.xml 中完成的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?xml version="1.0" encoding="UTF-8" ?> <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" > <servlet > <servlet-name > springmvc</servlet-name > <servlet-class > org.springframework.web.servlet.DispatcherServlet</servlet-class > <init-param > <param-name > contextConfigLocation</param-name > <param-value > classpath:spring-servlet.xml</param-value > </init-param > </servlet > <servlet-mapping > <servlet-name > springmvc</servlet-name > <url-pattern > /</url-pattern > </servlet-mapping > </web-app >
所有请求都将自动拦截下来,拦截下来后,请求交给 DispatcherServlet 去处理,在加载 DispatcherServlet 时,还需要指定配置文件路径。
这里有一个默认的规则,如果配置文件放在 webapp/WEB-INF/ 目录下,并且配置文件的名字等于 DispatcherServlet 的名字+ -servlet
(即这里的配置文件路径是 webapp/WEB-INF/springmvc-servlet.xml),如果是这样的话,可以不用添加 init-param 参数,即不用手动配置 springmvc 的配置文件,框架会自动加载。
最后项目启动,进行检验
SpringMVC的执行流程
用户发送请求至前端控制器DispatcherServlet
DispatcherServlet收到请求调用HandlerMapping处理器映射器
处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet
DispatcherServlet调用HandlerAdapter处理器适配器
HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)
Controller执行完成返回ModelAndView
HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet
DispatcherServlet将ModelAndView传给ViewReslover视图解析器
ViewReslover解析后返回具体View
DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)
DispatcherServlet将渲染后的视图响应响应用户
SpringMVC中组件
用户请求到达前端控制器,它就相当于 mvc 模式中的c,DispatcherServlet 是整个流程控制的中心,相当于是 SpringMVC 的大脑,由它调用其它组件处理用户的请求,DispatcherServlet 的存在降低了组件之间的耦合性。
HandlerMapping 负责根据用户请求找到 Handler 即处理器(也就是我们所说的 Controller),SpringMVC 提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等,在实际开发中,我们常用的方式是注解方式。
Handler 是继 DispatcherServlet 前端控制器的后端控制器,在DispatcherServlet 的控制下 Handler 对具体的用户请求进行处理。由于 Handler 涉及到具体的用户业务请求,所以一般情况需要程序员根据业务需求开发 Handler。(这里所说的 Handler 就是指我们的 Controller)
通过 HandlerAdapter 对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行
ViewResolver 负责将处理结果生成 View 视图,ViewResolver 首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成 View 视图对象,最后对 View 进行渲染将处理结果通过页面展示给用户
SpringMVC 中央控制器DispatcherServlet DispatcherServlet 是前端控制器设计模式的实现,提供 Spring Web MVC 的集中访问点,而且负责职责的分派,而且与 Spring IOC 容器无缝集成,从而可以获得 Spring 的所有好处。DispatcherServlet 主要用作职责调度工作,本身主要用于控制流程,主要职责如下:
通过 HandlerMapping,将请求映射到处理器(返回一个 HandlerExecutionChain,它包括一个处理器、多个 HandlerInterceptor 拦截器)
通过 HandlerAdapter 支持多种类型的处理器(HandlerExecutionChain 中的处理器)
通过 ViewResolver 解析逻辑视图名到具体视图实现
本地化解析,渲染具体的试图等
如果执行过程中遇到异常将交给 HandlerExceptionResolver 来解析
DispathcherServlet配置
1 2 3 4 5 6 7 8 9 10 11 12 13 <servlet > <servlet-name > springmvc</servlet-name > <servlet-class > org.springframework.web.servlet.DispatcherServlet</servlet-class > <init-param > <param-name > contextConfigLocation</param-name > <param-value > classpath:spring-servlet.xml</param-value > </init-param > <load-on-startup > 1</load-on-startup > </servlet > <servlet-mapping > <servlet-name > springmvc</servlet-name > <url-pattern > /</url-pattern > </servlet-mapping >
load-on-startup:表示启动容器时初始化该 Servlet
contextConfigLocation:表示 SpringMVC 配置文件的路径
url-pattern:表示哪些请求交给 Spring Web MVC 处理, /
是用来定义默认 servlet 映射的。也可以如 *.html
表示拦截所有以 html 为扩展名的请求
contextClass:实现WebApplicationContext接口的类,当前的servlet用它来创建上下文。如果这个参数没有指定, 默认使用XmlWebApplicationContext
contextConfigLocation:传给上下文实例(由contextClass指定)的字符串,用来指定上下文的位置。这个字符串可以被分成多个字符串(使用逗号作为分隔符) 来支持多个上下文(在多上下文的情况下,如果同一个bean被定义两次,后面一个优先)
namespace:WebApplicationContext命名空间。默认值是[server-name]-servlet
Spring和SpringMVC分开配置 之前的hello案例中,只有 SpringMVC,没有 Spring,Web 项目也是可以运行的。在实际开发中,Spring 和 SpringMVC 是分开配置的,所以对上面的项目继续进行完善,添加 Spring 相关配置。
项目添加一个service包,提供HelloService类:
1 2 3 4 5 6 7 8 9 10 11 package com.cwz.springmvc01.service;import org.springframework.stereotype.Service;@Service public class HelloService { public String hello (String name) { return "hello" + name; } }
将 HelloService 注入到 Spring 容器中并使用它,这个是属于 Spring 层的 Bean,所以我们一般将除了 Controller 之外的所有 Bean 注册到 Spring 容器中,而将 Controller 注册到 SpringMVC 容器中,在 resources 目录下添加 applicationContext.xml 作为 spring 的配置:
1 2 3 4 5 6 7 8 9 10 11 <?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.cwz.springmvc01" use-default-filters ="true" > <context:exclude-filter type ="annotation" expression ="org.springframework.stereotype.Controller" /> </context:component-scan > </beans >
这个配置文件默认情况下不会被加载,需要在 web.xml 中对其进行配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <?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" > <context-param > <param-name > contextConfigLocation</param-name > <param-value > classpath:applicationContext.xml</param-value > </context-param > <listener > <listener-class > org.springframework.web.context.ContextLoaderListener</listener-class > </listener > <servlet > <servlet-name > springmvc</servlet-name > <servlet-class > org.springframework.web.servlet.DispatcherServlet</servlet-class > <init-param > <param-name > contextConfigLocation</param-name > <param-value > classpath:spring-servlet.xml</param-value > </init-param > </servlet > <servlet-mapping > <servlet-name > springmvc</servlet-name > <url-pattern > /</url-pattern > </servlet-mapping > </web-app >
通过 context-param 指定 Spring 配置文件的位置,这个配置文件也有一些默认规则,它的配置文件名默认就叫 applicationContext.xml 。
如果你将这个配置文件放在 WEB-INF 目录下,那么这里就可以不用指定配置文件位置了,只需要指定监听器就可以了。这段配置是 Spring 集成 Web 环境的通用配置;一般用于加载除 Web 层的 Bean(如DAO、Service 等),以便于与其他任何Web框架集成。
配置完成之后,还需要修改 MyController,在 MyController 中注入 HelloSerivce:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.cwz.springmvc01.controller;import com.cwz.springmvc01.service.HelloService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.servlet.ModelAndView;import org.springframework.web.servlet.mvc.Controller;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;@org .springframework.stereotype.Controller("/hello" )public class MyController implements Controller { @Autowired HelloService helloService; @Override public ModelAndView handleRequest (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception { ModelAndView mv = new ModelAndView ("hello" ); mv.addObject("name" , "cwz" ); System.out.println("helloService = " + helloService); return mv; } }
为了在 SpringMVC 容器中能够扫描到 MyController ,这里给 MyController 添加了 @Controller
注解,同时,由于我们目前采用的 HandlerMapping 是 BeanNameUrlHandlerMapping(意味着请求地址就是处理器 Bean 的名字),所以,还需要手动指定 MyController 的名字。
最后,修改 SpringMVC 的配置文件,将 Bean 配置为扫描形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <?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.cwz.springmvc01" use-default-filters ="false" > <context:include-filter type ="annotation" expression ="org.springframework.stereotype.Controller" /> </context:component-scan > <bean class ="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" id ="beanNameUrlHandlerMapping" > <property name ="beanName" value ="/hello" /> </bean > <bean class ="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" id ="handlerAdapter" /> <bean class ="org.springframework.web.servlet.view.InternalResourceViewResolver" id ="viewResolver" > <property name ="prefix" value ="/jsp/" /> <property name ="suffix" value =".jsp" /> </bean > </beans >
Spring容器和SpringMVC容器 当 Spring 和 SpringMVC 同时出现,我们的项目中将存在两个容器,一个是 Spring 容器,另一个是 SpringMVC 容器,Spring 容器通过 ContextLoaderListener 来加载,SpringMVC 容器则通过 DispatcherServlet 来加载,这两个容器不一样:
可以看出:
ContextLoaderListener 初始化的上下文加载的 Bean 是对于整个应用程序共享的,一般如 DAO 层、Service 层 Bean;
DispatcherServlet 初始化的上下文加载的 Bean 是只对 Spring Web MVC 有效的 Bean,如 Controller、HandlerMapping、HandlerAdapter 等等,该初始化上下文应该只加载 Web相关组件。
子容器可以访问父容器,但父容器不能访问子容器,Spring是父容器,SpringMVC是子容器。
Spring容器中不扫描所有Bean:
因为请求达到服务端后,找 DispatcherServlet 去处理,只会去 SpringMVC 容器中找,这就意味着 Controller 必须在 SpringMVC 容器中扫描
为什么不在 SpringMVC 容器中扫描所有 Bean:
这个是可以的,可以在 SpringMVC 容器中扫描所有 Bean。不写在一起,是为了方便配置文件的管理
SpringMVC处理器详解 HandlerMapping 在 SpringMVC 中,系统提供了很多 HandlerMapping:
HandlerMapping 是负责根据 request 请求找到对应的 Handler 处理器及 Interceptor 拦截器,将它们封装在 HandlerExecutionChain 对象中返回给前端控制器。
BeanNameUrlHandlerMapping
BeanNameUrl 处理器映射器,根据请求的 url 与 Spring 容器中定义的 bean 的 name 进行匹配,从而从 Spring 容器中找到 bean 实例,就是说,请求的 url 地址就是处理器 Bean 的名字。但是不能配置多个请求路径。
配置如下:
1 2 3 4 <bean class ="com.cwz.springmvc01.controller.MyController" name ="/hello" /> <bean class ="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" id ="beanNameUrlHandlerMapping" > <property name ="beanName" value ="/hello" /> </bean >
SimpleUrlHandlerMapping
SimpleUrlHandlerMapping 是 BeanNameUrlHandlerMapping 的增强版本,它可以将 url 和处理器 bean 的 id 进行统一映射配置:
1 2 3 4 5 6 7 8 9 10 11 12 <bean class ="com.cwz.springmvc01.controller.MyController" name ="myController" /> <bean class ="com.cwz.springmvc01.controller.MyController2" name ="myController2" /> <bean class ="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping" id ="handlerMapping" > <property name ="mappings" > <props > <prop key ="/hello" > myController</prop > <prop key ="/hello2" > myController2</prop > </props > </property > </bean >
注意,在 props 中,可以配置多个请求路径和处理器实例的映射关系。
HandlerAdapter controller实现的方式很多,每一种执行方式肯定是不一样的。处理器适配器会根据执行的方式执行不同的代码。
HandlerAdapter 会根据适配器接口对后端控制器进行包装(适配),包装后即可对处理器进行执行,通过扩展处理器适配器可以执行多种类型的处理器,这里使用了适配器设计模式。
在 SpringMVC 中,HandlerAdapter 也有诸多实现类:
SimpleControllerHandlerAdapter
SimpleControllerHandlerAdapter简单控制器处理器适配器,所有实现了org.springframework.web.servlet.mvc.Controller
接口的Bean通过此适配器进行适配。即 如果我们开发的接口是通过实现 Controller 接口来完成的(不是通过注解开发的接口),那么 HandlerAdapter 必须是 SimpleControllerHandlerAdapter。
1 <bean class ="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />
HttpRequestHandlerAdapter
http请求处理器适配器,所有实现了org.springframework.web.HttpRequestHandler
接口的 Bean 通过此适配器进行适配
例如:
1 2 3 4 5 6 @Controller public class MyController3 implements HttpRequestHandler { public void handleRequest (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("-----MyController3-----" ); } }
1 2 3 4 5 6 7 8 <bean class ="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping" id ="handlerMapping" > <property name ="mappings" > <props > <prop key ="/hello3" > myController3</prop > </props > </property > </bean > <bean class ="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter" id ="handlerAdapter" />
处理器适配器与处理器映射器的最佳实践
web 开发中,我们基本上不再通过 XML 或者 Java 配置来创建一个 Bean 的实例,而是直接通过组件扫描来实现 Bean 的配置,如果要扫描多个包,多个包之间用,
隔开即可:
spring配置:
1 2 3 <context:component-scan base-package ="com.cwz.springmvc02" use-default-filters ="true" > <context:exclude-filter type ="annotation" expression ="org.springframework.stereotype.Controller" /> </context:component-scan >
springmvc配置:
1 2 3 <context:component-scan base-package ="com.cwz.springmvc02" use-default-filters ="false" > <context:include-filter type ="annotation" expression ="org.springframework.stereotype.Controller" /> </context:component-scan >
正常情况下,我们在项目中使用的是 RequestMappingHandlerMapping,这个是根据处理器中的注解,来匹配请求(即 @RequestMapping 注解中的 url 属性)。因为在上面我们都是通过实现类来开发接口的,相当于还是一个类一个接口,所以,我们可以通过 RequestMappingHandlerMapping 来做处理器映射器,这样我们可以在一个类中开发出多个接口。
对于上面提到的通过 @RequestMapping 注解所定义出来的接口方法,这些方法的调用都是要通过 RequestMappingHandlerAdapter 这个适配器来实现
举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.servlet.ModelAndView;@Controller public class MyController { @RequestMapping("/hello") public ModelAndView hello () { ModelAndView mv = new ModelAndView ("hello" ); mv.addObject("name" , "cwz" ); return mv; } }
要能够访问到这个接口,我们需要 RequestMappingHandlerMapping 才能定位到需要执行的方法,需要 RequestMappingHandlerAdapter,才能执行定位到的方法,修改 springmvc 的配置文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?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" xmlns:mvc ="http://www.springframework.org/schema/mvc" 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 http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd" > <context:component-scan base-package ="com.cwz.springmvc02" use-default-filters ="false" > <context:include-filter type ="annotation" expression ="org.springframework.stereotype.Controller" /> </context:component-scan > <mvc:annotation-driven /> <bean class ="org.springframework.web.servlet.view.InternalResourceViewResolver" id ="viewResolver" > <property name ="prefix" value ="/" /> <property name ="suffix" value =".jsp" /> </bean > </beans >
SpringMVC中Controller的细节问题 @RequestMapping 这个注解用来标记一个接口,在接口开发中,使用最多的注解之一
标记请求 URL 很简单,只需要在相应的方法上添加该注解即可:
1 2 3 4 5 6 7 @Controller public class HelloController { @RequestMapping("/hello") public ModelAndView hello () { return new ModelAndView ("hello" ); } }
这里 @RequestMapping(“/hello”)
表示当请求地址为 /hello 的时候,这个方法会被触发。其中,地址可以是多个,就是可以多个地址映射到同一个方法。
1 2 3 4 5 6 7 @Controller public class HelloController { @RequestMapping({"/hello","/hello2"}) public ModelAndView hello () { return new ModelAndView ("hello" ); } }
这个配置,表示 /hello 和 /hello2 都可以访问到该方法。
请求窄化 同一个项目中,会存在多个接口,例如订单相关的接口都是 /order/xxx
格式的,用户相关的接口都是 /user/xxx
格式的。为了方便处理,这里的前缀(就是 /order、/user)可以统一在 Controller 上面处理。
1 2 3 4 5 6 7 8 @Controller @RequestMapping("/user") public class HelloController { @RequestMapping("/hello") public ModelAndView hello () { return new ModelAndView ("hello" ); } }
当类上加了 @RequestMapping 注解之后,此时,要想访问到 hello ,地址就应该是/user/hello
请求方法限定 默认情况下,使用 @RequestMapping 注解定义好的方法,可以被 GET 请求访问到,也可以被 POST 请求访问到,但是 DELETE 请求以及 PUT 请求不可以访问到。
1 2 3 4 5 6 7 8 @Controller @RequestMapping("/user") public class HelloController { @RequestMapping(value = "/hello",method = RequestMethod.GET) public ModelAndView hello () { return new ModelAndView ("hello" ); } }
通过 @RequestMapping 注解,指定了该接口只能被 GET 请求访问到,此时,该接口就不可以被 POST 以及请求请求访问到了。强行访问会报405错误
Controller的返回值 返回ModelAndView 如果是前后端不分的开发,大部分情况下,我们返回 ModelAndView,即数据模型+视图:
1 2 3 4 5 6 7 8 9 10 @Controller @RequestMapping("/user") public class HelloController { @RequestMapping("/hello") public ModelAndView hello () { ModelAndView mv = new ModelAndView ("hello" ); mv.addObject("name" , "cwz" ); return mv; } }
Model 中,放我们的数据,然后在 ModelAndView 中指定视图名称。
hello.jsp
1 2 3 4 5 6 7 8 9 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h1>hello ${name}</h1> </body> </html>
返回void 没有返回值,并不一定真的没有返回值,只是方法的返回值为 void,我们可以通过其他方式给前端返回。
这种方式也可以理解为 Servlet 中的那一套方案
通过 HttpServletRequest 做服务端跳转
1 2 3 4 @RequestMapping("/hello2") public void hello2 (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.getRequestDispatcher("/hello.jsp" ).forward(req,resp); }
通过 HttpServletResponse 做重定向
1 2 3 4 @RequestMapping("/hello3") public void hello3 (HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.sendRedirect("/hello.jsp" ); }
也可以自己手动指定响应头去实现重定向:
1 2 3 4 5 @RequestMapping("/hello3") public void hello3 (HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setStatus(302 ); resp.addHeader("Location" , "/hello.jsp" ); }
通过 HttpServletResponse 给出响应
1 2 3 4 5 @RequestMapping("/hello4") public void hello4 (HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType("text/html;charset=utf-8" ); resp.getWriter().write("hello world" ); }
这种方式,既可以返回 JSON,也可以返回普通字符串。
返回字符串
前面的 ModelAndView 可以拆分为两部分,Model 和 View,在 SpringMVC 中,Model 我们可以直接在参数中指定,然后返回值是逻辑视图名
1 2 3 4 5 @RequestMapping("/hello5") public String hello5 (Model model) { model.addAttribute("name" , "cwz" ); return "hello" ; }
1 2 3 4 @RequestMapping("/hello5") public String hello5 () { return "forward:/hello.jsp" ; }
1 2 3 4 @RequestMapping("/hello5") public String hello5 () { return "redirect:/user/hello" ; }
本质上就是浏览器重定向
上面几种情况返回的字符串是有特殊的条件的,如需要forward
等跳转。
如果一定要返回一个字符串,需要额外添加一个注意:@ResponseBody ,这个注解表示当前方法的返回值就是要展示出来返回值,没有特殊含义
1 2 3 4 5 @RequestMapping(value = "/hello5",produces = "text/html;charset=utf-8") @ResponseBody public String hello5 () { return "hello world!" ; }
参数绑定 默认支持的参数类型 就是可以直接写在 @RequestMapping 所注解的方法中的参数类型,一共有四类:
HttpServletRequest
HttpServletResponse
HttpSession
Model/ModelMap
1 2 public void hello2 (HttpServletRequest req, HttpServletResponse resp, HttpSession session, Model model, ModelMap modelMap) throws ServletException, IOException {}
在请求的方法中,默认的参数就是这几个,如果在方法中,刚好需要这几个参数,那么就可以把这几个参数加入到方法中。
简单数据类型 Integer、Boolean、Double 等等简单数据类型也都是支持的
在 /webapp/ 目录下创建 addbook.jsp 作为图书添加页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h1>添加图书</h1> <form action="/book/addbook2" method="post" > <table> <tr> <td>图书名称</td> <td><input type="text" name="name" ></td> </tr> <tr> <td>图书作者</td> <td><input type="text" name="author" ></td> </tr> <tr> <td>图书价格</td> <td><input type="text" name="price" ></td> </tr> <tr> <td><input type="submit" value="添加" ></td> </tr> </table> </form> </body> </html>
创建控制器,控制器提供两个功能,一个是访问 jsp 页面,另一个是提供添加接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Controller @RequestMapping("book") public class BookController { @GetMapping("/book") public String book () { return "addbook" ; } @PostMapping(value = "/addbook", produces = "text/html;charset=utf-8") @ResponseBody public String addBook (String name, String author, Double price) { System.out.println("name = " + name); return name + ">>>" + author + ">>>" + price; }
由于 addBook方法只想返回字符串,所以需要给该方法添加 @ResponseBody 注解,表示这个方法到此为止,不用再去查找相关视图了。另外, POST 请求传上来的中文会乱码,所以,我们在 web.xml 中再额外添加一个编码过滤器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <filter > <filter-name > encoding</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 > <init-param > <param-name > forceRequestEncoding</param-name > <param-value > true</param-value > </init-param > <init-param > <param-name > forceResponseEncoding</param-name > <param-value > true</param-value > </init-param > </filter > <filter-mapping > <filter-name > encoding</filter-name > <url-pattern > /*</url-pattern > </filter-mapping >
最后,浏览器中输入 http://localhost:8080/book/book ,就可以执行添加操作,服务端会打印出来相应的日志。
在上面的绑定中,有一个要求,表单中字段的 name 属性要和接口中的变量名一一对应,才能映射成功,否则服务端接收不到前端传来的数据。有一些特殊情况,我们的服务端的接口变量名可能和前端不一致,这个时候我们可以通过 @RequestParam 注解来解决。
1 2 3 4 5 6 @PostMapping(value = "/addbook", produces = "text/html;charset=utf-8") @ResponseBody public String addBook (@RequestParam("bookname") String name, String author, @RequestParam(defaultValue = "99.0") Double price) { System.out.println("name = " + name); return name + ">>>" + author + ">>>" + price; }
注解中的bookname
表示给 name这个变量取的别名,也就是说,name 将接收前端传来的 bookname 这个变量的值。
实体类 参数除了是简单数据类型之外,也可以是实体类。实际上,在开发中,大部分情况下,都是实体类。
当然对象中可能还有对象,图书作者是一个对象,有name、age属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Book { private String name; private Double price; private Author author; public String getName () { return name; } public void setName (String name) { this .name = name; } public Double getPrice () { return price; } public void setPrice (Double price) { this .price = price; } public Author getAuthor () { return author; } public void setAuthor (Author author) { this .author = author; } }
服务端接收数据方式如下:
1 2 3 4 5 @RequestMapping(value = "/addbook2",method = RequestMethod.POST) @ResponseBody public String addBook2 (Book book) { return book.toString(); }
Book 对象中,有一个 Author 属性,如何给 Author 属性传值呢?前端写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h1>添加图书</h1> <form action="/book/addbook2" method="post" > <table> <tr> <td>图书名称</td> <td><input type="text" name="name" ></td> </tr> <tr> <td>图书作者</td> <td><input type="text" name="author.name" ></td> </tr> <tr> <td>作者年龄</td> <td><input type="text" name="author.age" ></td> </tr> <tr> <td>图书价格</td> <td><input type="text" name="price" ></td> </tr> <tr> <td><input type="submit" value="添加" ></td> </tr> </table> </form> </body> </html>
自定义参数绑定 前面的转换,都是系统自动转换的,这种转换仅限于基本数据类型
特殊的数据类型,系统无法自动转换,例如日期是需要自己手动转换。
前端传一个日期到后端,后端不是用字符串接收,而是使用一个 Date 对象接收,这个时候就会出现参数类型转换失败。这个时候,需要我们手动定义参数类型转换器,将日期字符串手动转为一个 Date 对象。
自定义日期转换器:
1 2 3 4 5 6 7 8 9 10 11 12 @Component public class MyDateConverter implements Converter <String, Date> { SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd" ); public Date convert (String source) { try { return sdf.parse(source); } catch (ParseException e) { e.printStackTrace(); } return null ; } }
在自定义的参数类型转换器中,将一个 String 转为 Date 对象,同时,将这个转换器注册为一个 Bean。
在SpringMVC中配置:
1 2 3 4 5 6 7 8 <mvc:annotation-driven conversion-service ="conversionService" /> <bean class ="org.springframework.format.support.FormattingConversionServiceFactoryBean" id ="conversionService" > <property name ="converters" > <set > <ref bean ="myDateConverter" /> </set > </property > </bean >
集合类的参数
String 数组可以直接用数组去接收,前端传递的时候,数组的传递其实就多相同的 key,这种一般用在 checkbox 中较多
例如添加爱好:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h1>添加图书</h1> <form action="/book/addbook2" method="post" > <table> <tr> <td>图书名称</td> <td><input type="text" name="name" ></td> </tr> <tr> <td>图书作者</td> <td><input type="text" name="author.name" ></td> </tr> <tr> <td>作者年龄</td> <td><input type="text" name="author.age" ></td> </tr> <tr> <td>作者兴趣爱好</td> <td> <input type="checkbox" value="足球" name="author.favorites" >足球 <input type="checkbox" value="篮球" name="author.favorites" >篮球 <input type="checkbox" value="乒乓球" name="author.favorites" >乒乓球 </td> </tr> <tr> <td>出版时间</td> <td><input type="date" name="publishDate" ></td> </tr> <tr> <td>图书价格</td> <td><input type="text" name="price" ></td> </tr> <tr> <td><input type="submit" value="添加" ></td> </tr> </table> </form> </body> </html>
在服务端用一个数组去接收 favorites 对象:
1 2 3 4 5 @PostMapping(value = "/addbook2", produces = "text/html;charset=utf-8") @ResponseBody public String addBook2 (Book book, String[] favorites) { return book.toString(); }
注意,前端传来的数组对象,服务端不可以使用 List 集合去接收
如果需要使用 List 集合接收前端传来的数据,List 集合本身需要放在一个封装对象中,这个时候,List 中,可以是基本数据类型,也可以是对象
例如作者对象中有角色属性:
1 2 3 4 5 6 7 public class Author { private String name; private Integer age; private List<String> favorites; private List<Role> roles; }
添加作者时可以有多个角色:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h1>添加图书</h1> <form action="/book/addbook2" method="post" > <table> <tr> <td>图书名称</td> <td><input type="text" name="name" ></td> </tr> <tr> <td>图书作者</td> <td><input type="text" name="author.name" ></td> </tr> <tr> <td>作者年龄</td> <td><input type="text" name="author.age" ></td> </tr> <tr> <td>作者兴趣爱好</td> <td> <input type="checkbox" value="足球" name="author.favorites" >足球 <input type="checkbox" value="篮球" name="author.favorites" >篮球 <input type="checkbox" value="乒乓球" name="author.favorites" >乒乓球 </td> </tr> <tr> <td>作者角色</td> <td> <input type="checkbox" value="管理员" name="author.roles[0].name" >管理员 <input type="hidden" value="1" name="author.roles[0].id" > <input type="checkbox" value="用户" name="author.roles[1].name" >用户 <input type="hidden" value="2" name="author.roles[1].id" > </td> </tr> <tr> <td>出版时间</td> <td><input type="date" name="publishDate" ></td> </tr> <tr> <td>图书价格</td> <td><input type="text" name="price" ></td> </tr> <tr> <td><input type="submit" value="添加" ></td> </tr> </table> </form> </body> </html>
服务端直接接收数据即可:
1 2 3 4 5 @PostMapping(value = "/addbook2", produces = "text/html;charset=utf-8") @ResponseBody public String addBook2 (Book book, String[] favorites) { return book.toString(); }
相对于实体类而言,Map 是一种比较灵活的方案,但是,Map 可维护性比较差,因此一般不推荐使用。
给上述的Book实体类添加信息:
1 2 3 4 5 6 7 public class Book { private String name; private Double price; private Author author; private Date publishDate; private Map<String,Object> info; }
在前端,通过如下方式给 info 这个 Map 赋值:添加出版社和责任编辑属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h1>添加图书</h1> <form action="/book/addbook2" method="post" > <table> <tr> <td>图书名称</td> <td><input type="text" name="name" ></td> </tr> <tr> <td>图书作者</td> <td><input type="text" name="author.name" ></td> </tr> <tr> <td>作者年龄</td> <td><input type="text" name="author.age" ></td> </tr> <tr> <td>作者兴趣爱好</td> <td> <input type="checkbox" value="足球" name="author.favorites" >足球 <input type="checkbox" value="篮球" name="author.favorites" >篮球 <input type="checkbox" value="乒乓球" name="author.favorites" >乒乓球 </td> </tr> <tr> <td>作者角色</td> <td> <input type="checkbox" value="管理员" name="author.roles[0].name" >管理员 <input type="hidden" value="1" name="author.roles[0].id" > <input type="checkbox" value="用户" name="author.roles[1].name" >用户 <input type="hidden" value="2" name="author.roles[1].id" > </td> </tr> <tr> <td>出版时间</td> <td><input type="date" name="publishDate" ></td> </tr> <tr> <td>出版社</td> <td><input type="text" name="info['publish']" ></td> </tr> <tr> <td>责任编辑</td> <td><input type="text" name="info['editor']" ></td> </tr> <tr> <td>图书价格</td> <td><input type="text" name="price" ></td> </tr> <tr> <td><input type="submit" value="添加" ></td> </tr> </table> </form> </body> </html>
文件上传 SpringMVC 中对文件上传做了封装,我们可以更加方便的实现文件上传。从 Spring3.1 开始,对于文件上传,提供了两个处理器:
CommonsMultipartResolver
StandardServletMultipartResolver
第一种处理器兼容性好,可以兼容 Servlet3.0 之前的版本,但是它依赖了 commons-fileupload 这个第三方工具,所以如果使用这个,一定要添加 commons-fileupload 依赖。
第二个处理器兼容性较差,它适用于 Servlet3.0 之后的版本,它不依赖第三方工具,使用它,可以直接做文件上传。
CommonsMultipartResolver 首先添加依赖:
1 2 3 4 5 <dependency > <groupId > commons-fileupload</groupId > <artifactId > commons-fileupload</artifactId > <version > 1.4</version > </dependency >
在 SpringMVC 的配置文件中,配置 MultipartResolver:
1 <bean class ="org.springframework.web.multipart.commons.CommonsMultipartResolver" id ="multipartResolver" />
创建 jsp 页面:
1 2 3 4 5 6 7 8 9 10 11 12 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <form action="/upload" method="post" enctype="multipart/form-data" > <input type="file" name="file" > <input type="submit" value="上传" > </form> </body> </html>
注意文件上传请求是 POST 请求,enctype 一定是 multipart/form-data
文件上传接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Controller public class FileUploadController { SimpleDateFormat sdf = new SimpleDateFormat ("/yyyy/MM/dd/" ); @GetMapping("/hello") @ResponseBody public String hello () { return "hello" ; } @PostMapping("/upload") @ResponseBody public String fileUpload (MultipartFile file, HttpServletRequest req) { String format = sdf.format(new Date ()); String realPath = req.getServletContext().getRealPath("/" ) + format; File folder = new File (realPath); if (!folder.exists()){ folder.mkdirs(); } String oldName = file.getOriginalFilename(); String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("." )); try { file.transferTo(new File (folder, newName)); String url = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + format + newName; return url; } catch (IOException e) { e.printStackTrace(); } return "error" ; } }
这里还有一个小问题,在 SpringMVC 中,静态资源默认都是被自动拦截的,无法访问,意味着上传成功的图片无法访问,因此,还需要我们在 SpringMVC 的配置文件中,再添加如下配置:
1 <mvc:resources mapping ="/**" location ="/" />
我们还可以自己手动配置文件上传大小等:
1 2 3 4 5 6 7 8 9 10 11 12 <bean class ="org.springframework.web.multipart.commons.CommonsMultipartResolver" id ="multipartResolver" > <property name ="defaultEncoding" value ="UTF-8" /> <property name ="maxUploadSize" value ="1048576" /> <property name ="maxUploadSizePerFile" value ="1048576" /> <property name ="maxInMemorySize" value ="4096" /> <property name ="uploadTempDir" value ="file:///E:\\tmp" /> </bean >
StandardServletMultipartResolver 这种方式不需要其他jar依赖
在SpringMVC 的配置文件中,配置这个 Bean:
1 <bean class ="org.springframework.web.multipart.support.StandardServletMultipartResolver" id ="multipartResolver" > </bean >
配置完成后,注意,这个 Bean 无法直接配置上传文件大小等限制。需要在 web.xml 中进行配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <servlet > <servlet-name > springmvc</servlet-name > <servlet-class > org.springframework.web.servlet.DispatcherServlet</servlet-class > <init-param > <param-name > contextConfigLocation</param-name > <param-value > classpath:spring-servlet.xml</param-value > </init-param > <multipart-config > <location > E:\\temp</location > <max-file-size > 1048576</max-file-size > <max-request-size > 1048576</max-request-size > <file-size-threshold > 4096</file-size-threshold > </multipart-config > </servlet > <servlet-mapping > <servlet-name > springmvc</servlet-name > <url-pattern > /</url-pattern > </servlet-mapping >
多文件上传 多文件上传分为两种,一种是 key 相同的文件,另一种是 key 不同的文件
key相同的文件 这种上传,前端页面一般如下:
1 2 3 4 <form action="/upload2" method="post" enctype="multipart/form-data" > <input type="file" name="files" multiple> <input type="submit" value="上传" > </form>
主要是 input 节点中多了 multiple 属性。后端用一个数组来接收文件即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @RequestMapping("/upload2") @ResponseBody public void upload2 (MultipartFile[] files, HttpServletRequest req) { String format = sdf.format(new Date ()); String realPath = req.getServletContext().getRealPath("/" ) + format; File folder = new File (realPath); if (!folder.exists()) { folder.mkdirs(); } try { for (MultipartFile file : files) { String oldName = file.getOriginalFilename(); String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("." )); file.transferTo(new File (folder, newName)); String url = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + format + newName; System.out.println(url); } } catch (IOException e) { e.printStackTrace(); } }
key不同的文件 key 不同的,一般前端定义如下:
1 2 3 4 5 <form action="/upload3" method="post" enctype="multipart/form-data" > <input type="file" name="file1" > <input type="file" name="file2" > <input type="submit" value="上传" > </form>
在后端用不同的变量来接收就行了:
1 2 3 @RequestMapping("/upload3") @ResponseBody public void upload3 (MultipartFile file1, MultipartFile file2, HttpServletRequest req) {}
异常处理 项目中,可能会抛出多个异常,我们不可以直接将异常的堆栈信息展示给用户,主要是用户体验不好、不安全。
所以,针对异常,我们可以自定义异常处理,SpringMVC 中,针对全局异常也提供了相应的解决方案,主要是通过 @ControllerAdvice 和 @ExceptionHandler 两个注解来处理的。
自定义异常,只需要提供一个异常处理类即可:
1 2 3 4 5 6 7 8 9 @ControllerAdvice public class MyException { @ExceptionHandler(Exception.class) public ModelAndView fileuploadException (Exception e) { ModelAndView error = new ModelAndView ("error" ); error.addObject("error" , e.getMessage()); return error; } }
@ControllerAdvice 表示这是一个增强版的 Controller,主要用来做全局数据处理
@ExceptionHandler 表示这是一个异常处理方法,这个注解的参数,表示需要拦截的异常,参数为 Exception 表示拦截所有异常,这里也可以具体到某一个异常,如果具体到某一个异常,那么发生了其他异常则不会被拦截到。
异常方法的定义,和 Controller 中方法的定义一样,可以返回 ModelAndview,也可以返回 String 或者 void
服务端数据校验 普通校验 需要加入校验需要的依赖:
1 2 3 4 5 <dependency > <groupId > org.hibernate</groupId > <artifactId > hibernate-validator</artifactId > <version > 6.1.0.Final</version > </dependency >
在 SpringMVC 的配置文件中配置校验的 Bean:
1 2 3 4 <bean class ="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" id ="validatorFactoryBean" > <property name ="providerClass" value ="org.hibernate.validator.HibernateValidator" /> </bean > <mvc:annotation-driven validator ="validatorFactoryBean" />
配置时,提供一个 LocalValidatorFactoryBean 的实例,然后 Bean 的校验使用 HibernateValidator
提供一个添加学生的页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <form action="/addstudent" method="post" > <table> <tr> <td>学生编号:</td> <td><input type="text" name="id" ></td> </tr> <tr> <td>学生姓名:</td> <td><input type="text" name="name" ></td> </tr> <tr> <td>学生邮箱:</td> <td><input type="text" name="email" ></td> </tr> <tr> <td>学生年龄:</td> <td><input type="text" name="age" ></td> </tr> <tr> <td colspan="2" > <input type="submit" value="提交" > </td> </tr> </table> </form>
在这里需要提交的数据中,假设学生编号不能为空,学生姓名长度不能超过 10 且不能为空,邮箱地址要合法,年龄不能超过 150。那么在定义实体类的时候,就可以加入这个判断条件了。
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Student { @NotNull private Integer id; @NotNull @Size(min = 2,max = 10) private String name; @Email private String email; @Max(150) private Integer age; }
@NotNull 表示这个字段不能为空
@Size 中描述了这个字符串长度的限制
@Email 表示这个字段的值必须是一个邮箱地址
@Max 表示这个字段的最大值
接下来,在 Controller 中定义接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Controller public class StudentController { @RequestMapping("/addstudent") @ResponseBody public void addStudent (@Validated Student student, BindingResult result) { if (result != null ) { List<ObjectError> allErrors = result.getAllErrors(); for (ObjectError allError : allErrors) { System.out.println(allError.getObjectName()+":" +allError.getDefaultMessage()); } } } }
@Validated 表示 Student 中定义的校验规则将会生效
BindingResult 表示出错信息,如果这个变量不为空,表示有错误,否则校验通过
默认情况下,打印出来的错误信息时系统默认的错误信息,这个错误信息,我们也可以自定义
在 resources 目录下新建一个 MyMessage.properties 文件:
1 2 3 4 5 student.id.notnull =id 不能为空 student.name.notnull =name 不能为空 student.name.length =name 最小长度为 2 ,最大长度为 10 student.email.error =email 地址非法 student.age.error =年龄不能超过 150
在 SpringMVC 配置中,加载这个配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <bean class ="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" id ="validatorFactoryBean" > <property name ="providerClass" value ="org.hibernate.validator.HibernateValidator" /> <property name ="validationMessageSource" ref ="bundleMessageSource" /> </bean > <bean class ="org.springframework.context.support.ReloadableResourceBundleMessageSource" id ="bundleMessageSource" > <property name ="basenames" > <list > <value > classpath:MyMessage</value > </list > </property > <property name ="defaultEncoding" value ="UTF-8" /> <property name ="cacheSeconds" value ="300" /> </bean > <mvc:annotation-driven validator ="validatorFactoryBean" />
最后,在实体类上的注解中,加上校验出错时的信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Student { @NotNull(message = "{student.id.notnull}") private Integer id; @NotNull(message = "{student.name.notnull}") @Size(min = 2,max = 10,message = "{student.name.length}") private String name; @Email(message = "{student.email.error}") private String email; @Max(value = 150,message = "{student.age.error}") private Integer age; }
配置完成后,如果校验再出错,就会展示我们自己的出错信息了。
分组校验 校验规则都是定义在实体类上面的,但是在不同的数据提交环境下,校验规则可能不一样。
例如,用户的 id 是自增长的,添加的时候,可以不用传递用户 id,但是修改的时候则必须传递用户 id,这种情况下,就需要使用分组校验。
分组校验,首先需要定义校验组,所谓的校验组,其实就是空接口:
1 2 3 4 5 public interface ValidationGroup1 {} public interface ValidationGroup2 {}
在实体类中,指定每一个校验规则所属的组:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Student { @NotNull(message = "{student.id.notnull}",groups = ValidationGroup1.class) private Integer id; @NotNull(message = "{student.name.notnull}",groups = {ValidationGroup1.class, ValidationGroup2.class}) @Size(min = 2,max = 10,message = "{student.name.length}",groups = {ValidationGroup1.class, ValidationGroup2.class}) private String name; @Email(message = "{student.email.error}",groups = {ValidationGroup1.class, ValidationGroup2.class}) private String email; @Max(value = 150,message = "{student.age.error}",groups = {ValidationGroup2.class}) private Integer age; }
在 group 中指定每一个校验规则所属的组,一个规则可以属于一个组,也可以属于多个组
最后在接收参数的地方,指定校验组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Controller public class StudentController { @RequestMapping("/addstudent") @ResponseBody public void addStudent (@Validated(ValidationGroup2.class) Student student, BindingResult result) { if (result != null ) { List<ObjectError> allErrors = result.getAllErrors(); for (ObjectError allError : allErrors) { System.out.println(allError.getObjectName()+":" +allError.getDefaultMessage()); } } } }
配置完成后,属于 ValidationGroup2 这个组的校验规则,才会生效。
数据回显 数据回显就是当用户数据提交失败时,自动填充好已经输入的数据。一般来说,如果使用 Ajax 来做数据提交,基本上是没有数据回显这个需求的,但是如果是通过表单做数据提交,那么数据回显就非常有必要了。
简单数据类型 看一下student.jsp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <form action="/addstudent" method="post" > <table> <tr> <td>学生编号:</td> <td><input type="text" name="id" value="${id}" ></td> </tr> <tr> <td>学生姓名:</td> <td><input type="text" name="name" value="${name}" ></td> </tr> <tr> <td>学生邮箱:</td> <td><input type="text" name="email" value="${email}" ></td> </tr> <tr> <td>学生年龄:</td> <td><input type="text" name="age" value="${age}" ></td> </tr> <tr> <td colspan="2" > <input type="submit" value="提交" > </td> </tr> </table> </form>
在接收数据时,使用简单数据类型去接收:
1 2 3 4 5 6 7 8 @RequestMapping("/addstudent") public String addStudent2 (Integer id, String name, String email, Integer age, Model model) { model.addAttribute("id" , id); model.addAttribute("name" , name); model.addAttribute("email" , email); model.addAttribute("age" , age); return "student" ; }
这种方式,相当于框架没有做任何工作,就是我们手动做数据回显的。此时访问页面,服务端会再次定位到该页面,而且数据已经预填好。
实体类 SpringMVC 在页面跳转时,会自动将对象填充进返回的数据中
student.jsp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <form action="/addstudent" method="post" > <table> <tr> <td>学生编号:</td> <td><input type="text" name="id" value="${student.id}" ></td> </tr> <tr> <td>学生姓名:</td> <td><input type="text" name="name" value="${student.name}" ></td> </tr> <tr> <td>学生邮箱:</td> <td><input type="text" name="email" value="${student.email}" ></td> </tr> <tr> <td>学生年龄:</td> <td><input type="text" name="age" value="${student.age}" ></td> </tr> <tr> <td colspan="2" > <input type="submit" value="提交" > </td> </tr> </table> </form>
这 student 就是服务端接收数据的变量名,服务端的变量名和这里的 student 要保持一直。服务端定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 @RequestMapping("/addstudent") public String addStudent (@Validated(ValidationGroup2.class) Student student, BindingResult result) { if (result != null ) { List<ObjectError> allErrors = result.getAllErrors(); for (ObjectError allError : allErrors) { System.out.println(allError.getObjectName()+":" +allError.getDefaultMessage()); } return "student" ; } return "hello" ; }
student 这个变量会被自动填充到返回的 Model 中。变量名就是填充时候的 key。如果想自定义这个 key,可以在参数中写出来 Model,然后手动加入 Student 对象,就像简单数据类型回显那样。
@ModelAttribute @ModelAttribute 这个注解,主要有两方面的功能:
在数据回显时,给变量定义别名
定义全局数据
定义别名
在数据回显时,给变量定义别名,非常容易,直接加这个注解即可:
1 2 3 4 5 6 7 8 9 10 11 12 @RequestMapping("/addstudent") public String addStudent (@ModelAttribute("s") @Validated(ValidationGroup2.class) Student student, BindingResult result) { if (result != null ) { List<ObjectError> allErrors = result.getAllErrors(); for (ObjectError allError : allErrors) { System.out.println(allError.getObjectName()+":" +allError.getDefaultMessage()); } return "student" ; } return "hello" ; }
这样定义完成后,在前端再次访问回显的变量时,变量名称就不是 student 了,而是 s:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <form action="/addstudent" method="post" > <table> <tr> <td>学生编号:</td> <td><input type="text" name="id" value="${s.id}" ></td> </tr> <tr> <td>学生姓名:</td> <td><input type="text" name="name" value="${s.name}" ></td> </tr> <tr> <td>学生邮箱:</td> <td><input type="text" name="email" value="${s.email}" ></td> </tr> <tr> <td>学生年龄:</td> <td><input type="text" name="age" value="${s.age}" ></td> </tr> <tr> <td colspan="2" > <input type="submit" value="提交" > </td> </tr> </table> </form>
定义全局数据
假设有一个 Controller 中有很多方法,每个方法都会返回数据给前端,但是每个方法返回给前端的数据又不太一样,虽然不太一样,但是没有方法的返回值又有一些公共的部分。可以将这些公共的部分提取出来单独封装成一个方法,用 @ModelAttribute 注解来标记。
在一个controller中加入:
1 2 3 4 5 6 7 @ModelAttribute("info") public Map<String,Object> info () { Map<String, Object> map = new HashMap <>(); map.put("username" , "setcreed" ); map.put("address" , "https://setcree.gitee.io" ); return map; }
当用户访问当前 Controller 中的任意一个方法,在返回数据时,都会将添加了 @ModelAttribute 注解的方法的返回值,一起返回给前端。@ModelAttribute 注解中的 info 表示返回数据的 key。
json处理 返回json 目前主流的 JSON 处理工具主要有三种:jackson、gson、fastjson
在 SpringMVC 中,对 jackson 和 gson 都提供了相应的支持,就是如果使用这两个作为 JSON 转换器,只需要添加对应的依赖就可以了,返回的对象和返回的集合、Map 等都会自动转为 JSON。
如果使用 fastjson,除了添加相应的依赖之外,还需要自己手动配置 HttpMessageConverter 转换器。
jackson、gson也是使用 HttpMessageConverter 转换器,是SpringMVC 自动提供的,但SpringMVC 没有给 fastjson 提供相应的转换器。
jackson 在 SpringMVC 中使用 jackson ,只需要添加 jackson 的依赖即可:
1 2 3 4 5 <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12 .3 </version> </dependency>
依赖添加成功后,凡是在接口中直接返回的对象,集合等等,都会自动转为 JSON。如下:
1 2 3 4 5 6 7 public class Book { private String name; private String author; private Integer id; }
controller:
1 2 3 4 5 6 7 8 9 @RequestMapping("/book") @ResponseBody public Book getBookById () { Book book = new Book (); book.setId(1 ); book.setName("三国演义" ); book.setAuthor("罗贯中" ); return book; }
这里返回一个对象,但是在前端接收到的则是一个 JSON 字符串,这个对象会通过 HttpMessageConverter 自动转为 JSON 字符串。
如果想返回一个 JSON 数组,写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 @RequestMapping("/books") @ResponseBody public List<Book> getAllBooks () { List<Book> list = new ArrayList <Book>(); for (int i = 0 ; i < 10 ; i++) { Book book = new Book (); book.setId(i); book.setName("三国演义:" + i); book.setAuthor("罗贯中:" + i); list.add(book); } return list; }
添加了 jackson ,就能够自动返回 JSON,这个依赖于一个名为 HttpMessageConverter 的类,这本身是一个接口,从名字上就可以看出,它的作用是 Http 消息转换器,既然是消息转换器,它提供了两方面的功能:
将返回的对象转为json
将前端提交上来的json转为对象
但是,HttpMessageConverter 只是一个接口,由各个 JSON 工具提供相应的实现,在 jackson 中,实现的名字叫做MappingJackson2HttpMessageConverter
,而这个东西的初始化,则由 SpringMVC 来完成。除非自己有一些自定义配置的需求,否则一般来说不需要自己提供MappingJackson2HttpMessageConverter
。
在上面的例子中,book对象有一个出版日期:
1 2 3 4 5 6 7 8 public class Book { private Integer id; private String name; private String author; private Date publish; }
然后在构造 Book 时添加日期属性:
1 2 3 4 5 6 7 8 9 10 @RequestMapping("/book") @ResponseBody public Book getBookById () { Book book = new Book (); book.setId(1 ); book.setName("三国演义" ); book.setAuthor("罗贯中" ); book.setPublish(new Date ()); return book; }
访问 /book 接口,返回的 json 格式如下:
如果我们想自己定制返回日期的格式,简单的办法,可以通过添加注解来实现:
1 2 3 4 5 6 7 public class Book { private Integer id; private String name; private String author; @JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai") private Date publish; }
这样,就可以定制返回的日期格式了。
但是,这种方式有一个弊端,这个注解可以加在属性上,也可以加在类上,也就说,最大可以作用到一个类中的所有日期属性上。如果项目中有很多实体类都需要做日期格式化,使用这种方式就比较麻烦了,这个时候,我们可以自己提供一个 jackson 的 HttpMesageConverter 实例,在这个实例中,自己去配置相关属性,这里的配置将是一个全局配置。
在SpringMVC配置文件中配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <mvc:annotation-driven > <mvc:message-converters > <ref bean ="httpMessageConverter" /> </mvc:message-converters > </mvc:annotation-driven > <bean class ="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" id ="httpMessageConverter" > <property name ="objectMapper" > <bean class ="com.fasterxml.jackson.databind.ObjectMapper" > <property name ="dateFormat" > <bean class ="java.text.SimpleDateFormat" > <constructor-arg name ="pattern" value ="yyyy-MM-dd HH:mm:ss" /> </bean > </property > <property name ="timeZone" value ="Asia/Shanghai" /> </bean > </property > </bean >
添加完成后,去掉 Book 实体类中日期格式化的注解,再进行测试,结果如下:
gson gson 是 Google 推出的一个 JSON 解析器,主要在 Android 开发中使用较多,不过,Web 开发中也是支持这个的,而且 SpringMVC 还针对 Gson 提供了相关的自动化配置,我们只要添加依赖就可以使用了
1 2 3 4 5 <dependency > <groupId > com.google.code.gson</groupId > <artifactId > gson</artifactId > <version > 2.8.6</version > </dependency >
如果项目中,同时存在 jackson 和 gson 的话,那么默认使用的是 jackson
在 org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter
类的构造方法中,加载顺序就是先加载 jackson 的 HttpMessageConverter,后加载 gson 的 HttpMessageConverter
加完依赖之后,就可以直接返回 JSON 字符串了。使用 Gson 时,如果想做自定义配置,则需要自定义 HttpMessageConverter。
1 2 3 4 5 6 7 8 9 10 11 12 13 <mvc:annotation-driven > <mvc:message-converters > <ref bean ="httpMessageConverter" /> </mvc:message-converters > </mvc:annotation-driven > <bean class ="org.springframework.http.converter.json.GsonHttpMessageConverter" id ="httpMessageConverter" > <property name ="gson" > <bean class ="com.google.gson.Gson" factory-bean ="gsonBuilder" factory-method ="create" /> </property > </bean > <bean class ="com.google.gson.GsonBuilder" id ="gsonBuilder" > <property name ="dateFormat" value ="yyyy-MM-dd" /> </bean >
fastjson fastjson 号称最快的 JSON 解析器,但是也是这三个中 BUG 最多的一个。
SpringMVC 并没针对 fastjson 提供相应的 HttpMessageConverter,所以,fastjson 在使用时,一定要自己手动配置 HttpMessageConverter。
添加依赖:
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.60</version > </dependency >
然后在 SpringMVC 的配置文件中配置 HttpMessageConverter:
1 2 3 4 5 6 7 8 9 10 11 12 <mvc:annotation-driven > <mvc:message-converters > <ref bean ="httpMessageConverter" /> </mvc:message-converters > </mvc:annotation-driven > <bean class ="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter" id ="httpMessageConverter" > <property name ="fastJsonConfig" > <bean class ="com.alibaba.fastjson.support.config.FastJsonConfig" > <property name ="dateFormat" value ="yyyy-MM-dd" /> </bean > </property > </bean >
fastjson 默认中文乱码,添加如下配置解决:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <mvc:annotation-driven > <mvc:message-converters > <ref bean ="httpMessageConverter" /> </mvc:message-converters > </mvc:annotation-driven > <bean class ="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter" id ="httpMessageConverter" > <property name ="fastJsonConfig" > <bean class ="com.alibaba.fastjson.support.config.FastJsonConfig" > <property name ="dateFormat" value ="yyyy-MM-dd" /> </bean > </property > <property name ="supportedMediaTypes" > <list > <value > application/json;charset=utf-8</value > </list > </property > </bean >
接收json 浏览器传来的参数,可以是 key/value 形式的,也可以是一个 JSON 字符串。在 Jsp/Servlet 中,我们接收 key/value 形式的参数,一般是通过 getParameter 方法。如果客户端是 JSON 数据,我们可以通过如下格式进行解析:
1 2 3 4 5 6 7 @RequestMapping("/addbook2") @ResponseBody public void addBook2 (HttpServletRequest req) throws IOException { ObjectMapper om = new ObjectMapper (); Book book = om.readValue(req.getInputStream(), Book.class); System.out.println(book); }
但是这种解析方式有点麻烦,在 SpringMVC 中,我们可以通过一个注解来快速的将一个 JSON 字符串转为一个对象:
1 2 3 4 5 @RequestMapping("/addbook3") @ResponseBody public void addBook3 (@RequestBody Book book) { System.out.println(book); }
这样就可以直接收到前端传来的 JSON 字符串了
SpringMVC对RESTful的支持 SpringMVC 对 RESTful 提供了非常全面的支持,主要有如下几个注解:
这个注解是一个组合注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Controller @ResponseBody public @interface RestController { @AliasFor(annotation = Controller.class) String value () default "" ; }
一般,直接用 @RestController 来标记 Controller,可以不使用 @Controller。
请求方法中,提供了常见的请求方法:
@PostMapping
@GetMapping
@PutMapping
@DeleteMapping
另外还有一个提取请求地址中的参数的注解 @PathVariable:
1 2 3 4 5 6 @GetMapping("/book/{id}") public Book getBookById (@PathVariable Integer id) { Book book = new Book (); book.setId(id); return book; }
参数 2 将被传递到 id 这个变量上
静态资源访问 在 SpringMVC 中,静态资源,默认都是被拦截的,例如 html、js、css、jpg等等,都是无法直接访问的。因为所有请求都被拦截了,所以,针对静态资源,我们要做额外处理,处理方式很简单,直接在 SpringMVC 的配置文件中,添加如下内容:
1 <mvc:resources mapping ="/static/html/**" location ="/static/html/" />
mapping 表示映射规则,也是拦截规则,就是说,如果请求地址是 /static/html 这样的格式的话,那么对应的资源就去 /static/html/ 这个目录下查找。
**
通配符表示匹配多层路径
为了省事,一般这样配:
1 <mvc:resources mapping ="/**" location ="/" />
拦截器 SpringMVC 中的拦截器,相当于 Jsp/Servlet 中的过滤器,只不过拦截器的功能更为强大。
拦截器的定义非常容易:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @Component public class MyInterceptor1 implements HandlerInterceptor { public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("MyInterceptor1:preHandle" ); return true ; } public void postHandle (HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("MyInterceptor1:postHandle" ); } public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("MyInterceptor1:afterCompletion" ); } } @Component public class MyInterceptor2 implements HandlerInterceptor { public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("MyInterceptor2:preHandle" ); return true ; } public void postHandle (HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("MyInterceptor2:postHandle" ); } public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("MyInterceptor2:afterCompletion" ); } }
拦截器定义好之后,需要在 SpringMVC 的配置文件中进行配置:
1 2 3 4 5 6 7 8 9 10 <mvc:interceptors > <mvc:interceptor > <mvc:mapping path ="/**" /> <ref bean ="myInterceptor1" /> </mvc:interceptor > <mvc:interceptor > <mvc:mapping path ="/**" /> <ref bean ="myInterceptor2" /> </mvc:interceptor > </mvc:interceptors >
如果存在多个拦截器,拦截规则如下:
preHandle 按拦截器定义顺序调用
postHandler 按拦截器定义逆序调用
afterCompletion 按拦截器定义逆序调用
postHandler 在拦截器链内所有拦截器返成功调用
afterCompletion 只有 preHandle 返回 true 才调用
MyBastis MyBatis简介 JDBC存在的问题
数据库连接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题
Sql 语句在代码中硬编码,造成代码不易维护,实际应用 sql 变化的可能较大,sql 变动需要改变 java 代码
……
Sql 语句在代码中硬编码,造成代码不易维护,实际应用 sql 变化的可能较大,sql 变动需要改变 java 代码
MyBatis介绍
MyBatis 官网:https://mybatis.org/mybatis-3/zh/index.html
MyBatis 是一个优秀的持久层框架,它对 jdbc 的操作数据库的过程进行封装,使开发者只需要关注 SQL 本身,而不需要花费精力去处理例如注册驱动、创建 connection、创建 statement、手动设置参数、结果集检索等 jdbc 繁杂的过程代码。Mybatis 通过 xml 或注解的方式将要执行的各种 statement(statement、preparedStatemnt、CallableStatement)配置起来,并通过 java 对象和 statement 中的 sql 进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射成 java 对象并返回。
与其他的对象关系映射框架不同,MyBatis 并没有将 Java 对象与数据库表关联起来,而是将 Java 方法与 SQL 语句关联。MyBatis 允许用户充分利用数据库的各种功能,例如存储过程、视图、各种复杂的查询以及某数据库的专有特性。如果要对遗留数据库、不规范的数据库进行操作,或者要完全控制 SQL 的执行,MyBatis 是一个不错的选择。
Mybatis简单使用 首先来准备一个数据库:
1 2 3 4 5 6 7 8 CREATE TABLE `user ` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `username` varchar (255 ) COLLATE utf8mb4_general_ci DEFAULT NULL , `address` varchar (255 ) COLLATE utf8mb4_general_ci DEFAULT NULL , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 8 DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_general_ci; insert into `user `(`id`,`username`,`address`) values (1 ,'cwz' ,'上海' ),(4 ,'张三' ,'深圳' ),(5 ,'李四' ,'广州' ),(6 ,'王五' ,'北京' );
创建Maven工程,添加MyBatis依赖:
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > 3.5.6</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 5.1.47</version > </dependency >
接下来,准备一个 Mapper 文件,Mapper 是用来在 MyBatis 中定义 SQL 的 XML 配置文件,由于在实际开发中,我们经常需要使用到 Mapper,经常需要自己创建 Mapper 文件,因此,我们可以将 Mapper 文件做成一个模板。具体操作如下:
在 IDEA 中,选择 resources 目录,右键单击,New–>Edit File Templates:
然后添加一个新的模板进来,给模板取名,同时设置扩展名,并将如下内容拷贝到模板中:
1 2 3 4 5 6 <?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 ="#[[$namespace$]]#" > </mapper >
这样创建Mapper文件就很方便了
1 2 3 4 5 6 7 <?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.cwz.mybatis01.mapper.UserMapper" > </mapper >
创建一个新的 mapper ,需要首先给它取一个 namespace,这相当于是一个分隔符,因为我们在项目中,会存在很多个 Mapper,每一个 Mapper 中都会定义相应的增删改查方法,为了避免方法冲突,也为了便于管理,每一个 Mapper 都有自己的 namespace,而且这个 namespace 不可以重复。
接下来,在 Mapper 中,定义一个简单的查询方法,根据 所有用户:
1 2 3 4 5 6 7 8 9 <?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.cwz.mybatis01.mapper.UserMapper" > <select id ="getAllUser" resultType ="com.cwz.mybatis01.model.User" > select * from user; </select > </mapper >
id 表示查询方法的唯一标识符,resultType 定义了返回值的类型
定义的User实体类:
1 2 3 4 5 6 7 public class User { private Integer id; private String username; private String address; }
接下来,创建 MyBatis 配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" > <configuration > <environments default ="development" > <environment id ="development" > <transactionManager type ="JDBC" /> <dataSource type ="POOLED" > <property name ="driver" value ="com.mysql.jdbc.Driver" /> <property name ="url" value ="jdbc:mysql:///test1?useSSL=false& serverTimezone=Asia/Shanghai" /> <property name ="username" value ="root" /> <property name ="password" value ="123456" /> </dataSource > </environment > </environments > <mappers > <mapper resource ="com/cwz/mybatis01/mapper/UserMapper.xml" /> </mappers > </configuration >
在这个配置文件中,我们只需要配置 environments 和 mapper 即可,environment 就是 MyBatis 所连接的数据库的环境信息,它放在一个 environments 节点中,意味着 environments 中可以有多个 environment,为社么需要多个呢?开发、测试、生产,不同环境各一个 environment,每一个 environment 都有一个 id,也就是它的名字,然后,在 environments 中,通过 default 属性,指定你需要的 environment。每一个 environment 中,定义一个数据的基本连接信息。
在 mappers 节点中,定义 Mapper,也就是指定我们上一步所写的 Mapper 的路径。
注意: 一般情况下我们把mapper文件与接口放在一起,但如果mapper文件不放在资源文件夹resources下,可能会报错,这是maven的问题。解决方案就是在pom.xml中加入:
1 2 3 4 5 6 7 8 9 10 11 12 13 <build > <resources > <resource > <directory > src/main/java</directory > <includes > <include > **/*.xml</include > </includes > </resource > <resource > <directory > src/main/resources</directory > </resource > </resources > </build >
最后测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Main { public static void main (String[] args) throws IOException { SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder ().build(Resources.getResourceAsStream("mybatis-config.xml" )); SqlSession sqlSession = sqlSessionFactory.openSession(); List<User> list = sqlSession.selectList("com.cwz.mybatis01.mapper.UserMapper.getAllUser" ); for (User user : list) { System.out.println("user = " + user); } sqlSession.close(); } }
首先,我们加载主配置文件,生成一个 SqlSessionFactory,再由 SqlSessionFactory 生成一个 SqlSession,一个 SqlSession 就相当于是我们的一个会话,类似于 JDBC 中的一个连接,在 SQL 操作完成后,这个会话是可以关闭的。
在这里,SqlSessionFactoryBuilder 用于创建 SqlSessionFacoty,SqlSessionFacoty 一旦创建完成就不需要 SqlSessionFactoryBuilder 了,因为 SqlSession 是通过 SqlSessionFactory 生产,所以可以将 SqlSessionFactoryBuilder 当成一个工具类使用,最佳使用范围是方法范围即方法体内局部变量。
SqlSessionFactory 是一个接口,接口中定义了 openSession 的不同重载方法,SqlSessionFactory 的最佳使用范围是整个应用运行期间,一旦创建后可以重复使用,通常以单例模式管理 SqlSessionFactory。
SqlSession 中封装了对数据库的操作
对SqlSessionFactory进行封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class SqlSessionFactoryUtils { private static SqlSessionFactory sqlSessionFactory = null ; public static SqlSessionFactory getInstance () { if (sqlSessionFactory == null ) { try { sqlSessionFactory = new SqlSessionFactoryBuilder ().build(Resources.getResourceAsStream("mybatis-config.xml" )); } catch (IOException e) { e.printStackTrace(); } } return sqlSessionFactory; } }
使用log4j日志
使用log4j需要添加依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-api</artifactId > <version > 2.13.3</version > </dependency > <dependency > <groupId > log4j</groupId > <artifactId > log4j</artifactId > <version > 1.2.17</version > </dependency > <dependency > <groupId > org.slf4j</groupId > <artifactId > slf4j-log4j12</artifactId > <version > 1.7.21</version > </dependency >
在resources下新建日志文件log4j.properties:
1 2 3 4 5 log4j.rootLogger =debug, stdout log4j.appender.stdout =org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target =System.out log4j.appender.stdout.layout =org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern =[%-5p] %d{yyyy-MM-dd} method:%l%n%m%n
MyBatis增删改查 增 添加记录,id 有两种不同的处理方式,一种就是自增长,另一种则是 Java 代码传一个 ID 进来。这个 ID可以是一个UUID,也可以是其他的自定义的ID。在 MyBatis 中,对这两种方式都提供了相应的支持。
主键自增长
首先我们在 Mapper 中,添加 SQL 插入语句:
1 2 3 <insert id ="addUser" parameterType ="com.cwz.mybatis.model.User" > insert into user (username,address) values (#{username},#{address}); </insert >
这里有一个 parameterType 表示传入的参数类型。参数都是通过 # 来引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Main { public static void main (String[] args) throws IOException { SqlSessionFactory factory = new SqlSessionFactoryBuilder ().build(Resources.getResourceAsStream("mybatis-config.xml" )); SqlSession sqlSession = factory.openSession(); User user = new User (); user.setUsername("赵六" ); user.setAddress("北京" ); int insert = sqlSession.insert("com.cwz.mybatis.mapper.addUser" , user); System.out.println(insert); sqlSession.commit(); sqlSession.close(); } }
注意,SQL 插入完成后,一定要提交,即 sqlSession.commit()
使用UUID做主键
使用 UUID 做主键,又有两种不同的思路,第一种思路,就是在 Java 代码中生成 UUID,直接作为参数传入到 SQL 中,这种方式就和传递普通参数一样,另一种方式,就是使用 MySQL 自带的 UUID 函数来生成 UUID。
使用 MySQL 自带的 UUID 函数,整体思路是这样:首先调用 MySQL 中的 UUID 函数,获取到一个 UUID,然后,将这个 UUID 赋值给 User 对象的 ID 属性,然后再去执行 SQL 插入操作,再插入时使用这个 UUID。
mapper文件:
1 2 3 4 5 6 <insert id ="addUser2" parameterType ="com.cwz.mybatis.model.User" > <selectKey resultType ="java.lang.String" keyProperty ="id" order ="BEFORE" > select uuid(); </selectKey > insert into user (id,username,address) values (#{id},#{username},#{address}); </insert >
上述xml文件中参数:
selectKey 表示查询 key
keyProperty 属性表示将查询的结果赋值给传递进来的 User 对象的 id 属性
resultType 表示查询结果的返回类型
order 表示这个查询操作的执行时机,BEFORE 表示这个查询操作在 insert 之前执行
在 selectKey 节点的外面定义 insert 操作
最后调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Main { public static void main (String[] args) throws IOException { SqlSessionFactory factory = new SqlSessionFactoryBuilder ().build(Resources.getResourceAsStream("mybatis-config.xml" )); SqlSession sqlSession = factory.openSession(); User user = new User (); user.setUsername("赵六" ); user.setAddress("北京" ); int insert = sqlSession.insert("com.cwz.mybatis.mapper.addUser" , user); System.out.println(insert); sqlSession.commit(); sqlSession.close(); } }
删 在 UserMapper 中定义删除 SQL:
1 2 3 <delete id ="deleteUserById" parameterType ="java.lang.Integer" > delete from user where id=#{id} </delete >
在 Java 代码中调用该方法:
1 2 3 4 5 6 7 8 9 10 public class Main { public static void main (String[] args) throws IOException { SqlSessionFactory factory = new SqlSessionFactoryBuilder ().build(Resources.getResourceAsStream("mybatis-config.xml" )); SqlSession sqlSession = factory.openSession(); int delete = sqlSession.delete("com.cwz.mybatis.mapper.deleteUserById" , 2 ); System.out.println(delete); sqlSession.commit(); sqlSession.close(); } }
改 1 2 3 <update id ="updateUser" parameterType ="com.cwz.mybatis.model.User" > update user set username = #{username} where id=#{id}; </update >
查 查一条数据
1 2 3 <select id ="getUserById" resultType ="com.cwz.mybatis.model.User" > select * from user where id=#{id} </select >
java调用
1 2 3 4 5 6 7 8 9 public class Main { public static void main (String[] args) throws IOException { SqlSessionFactory factory = new SqlSessionFactoryBuilder ().build(Resources.getResourceAsStream("mybatis-config.xml" )); SqlSession sqlSession = factory.openSession(); User user = sqlSession.selectOne("com.cwz.mybatis.mapper.UserMapper.getUserById" , 1 ); sqlSession.close(); } }
MyBatis 架构介绍
mybatis 配置:mybatis-config.xml,此文件作为 mybatis 的全局配置文件,配置了 mybatis 的运行环境等信息。另一个 mapper.xml 文件即 sql 映射文件,文件中配置了操作数据库的 sql 语句。此文件需要在 mybatis-config.xml 中加载
通过 mybatis 环境等配置信息构造 SqlSessionFactory 即会话工厂
由会话工厂创建 sqlSession 即会话,操作数据库需要通过 sqlSession 进行
mybatis 底层自定义了 Executor 执行器接口操作数据库,Executor 接口有两个实现,一个是基本执行器、一个是缓存执行器
Mapped Statement 也是 mybatis 一个底层封装对象,它包装了 mybatis 配置信息及 sql 映射信息等。mapper.xml 文件中一个 sql 对应一个 Mapped Statement 对象,sql 的 id 即是Mapped statement 的 id
Mapped Statement 对 sql 执行输入参数进行定义,包括 HashMap、基本类型、pojo,Executor 通过 Mapped Statement 在执行 sql 前将输入的 java 对象映射至 sql 中,输入参数映射就是 jdbc 编程中对 preparedStatement 设置参数
Mapped Statement 对 sql 执行输出结果进行定义,包括 HashMap、基本类型、pojo,Executor 通过 Mapped Statement 在执行 sql 后将输出结果映射至 java 对象中,输出结果映射过程相当于 jdbc 编程中对结果的解析处理过程
MyBatis中主键回填的两种实现方式 在数据添加的过程中,我们经常需要添加完数据之后,需要获取刚刚添加的数据 id
使用JDBC JDBC 中实现主键回填,主要是在构造 PreparedStatement 时指定需要主键回填,然后在插入成功后,查询刚刚插入数据的 id
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public int insert (Person person) { Connection con = null ; PreparedStatement ps = null ; ResultSet rs = null ; con = DBUtils.getConnection(); ps = con.prepareStatement("INSERT INTO person(username,password,money) VALUES(?,?,?)" , PreparedStatement.RETURN_GENERATED_KEYS); ps.setObject(1 , person.getUsername()); ps.setObject(2 , person.getPassword()); ps.setObject(3 , person.getMoney()); int i = ps.executeUpdate(); rs = ps.getGeneratedKeys(); int id = -1 ; if (rs.next()) { id = rs.getInt(1 ); } return id; }
构造 PreparedStatement 时,多了一个参数,指定了需要主键回填
在更新操作执行完成之后,调用 getGeneratedKeys ,然后又会获取到一个 ResultSet 对象,从而可以获取到刚刚插入数据的id
MyBatis的写法 方法1:
1 2 3 <insert id ="addUser" useGeneratedKeys ="true" keyProperty ="id" > insert into user (username,address) values (#{username},#{address}); </insert >
这种方式比较简单,就是在插入节点上添加 useGeneratedKeys 属性,同时设置接收回传主键的属性。配置完成后,我们执行一个插入操作,插入时传入一个对象,插入完成后,这个对象的 id 就会被自动赋值,值就是刚刚插入成功的id。
方法2:
利用MySQL自带的 last_insert_id()
函数查询刚刚插入的id
1 2 3 4 5 6 <insert id ="addUser" > <selectKey keyProperty ="id" resultType ="java.lang.Integer" > SELECT LAST_INSERT_ID() </selectKey > insert into user (username,address) values (#{username},#{address}); </insert >
这种方式是在 insert 节点中添加 selectKey 来实现主键回填
selectKey 节点中的 SQL可以在插入之前或者插入之后执行,这可以通过设置节点的 Order 属性为 AFTER 或者 BEFORE 来实现
MyBatis Mapper mapper接口 之前简单写的增删改查 冗余代码太多。比如我想写一个UserDao
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class UserDao { private SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getInstance(); public User getUserById (Integer id) { SqlSession sqlSession = sqlSessionFactory.openSession(); User user = (User) sqlSession.selectOne("com.cwz.mybatis.mapper.UserDao.getUserById" , id); sqlSession.close(); return user; } public Integer addUser (User user) { SqlSession sqlSession = sqlSessionFactory.openSession(); int insert = sqlSession.insert("com.cwz.mybatis.mapper.UserDao.addUser" , user); sqlSession.commit(); sqlSession.close(); return insert; } public Integer deleteUserById (Integer id) { SqlSession sqlSession = sqlSessionFactory.openSession(); int delete = sqlSession.delete("com.cwz.mybatis.mapper.UserDao.deleteUserById" , id); sqlSession.commit(); sqlSession.close(); return delete; } public Integer updateUser (User user) { SqlSession sqlSession = sqlSessionFactory.openSession(); int delete = sqlSession.delete("com.cwz.mybatis.mapper.UserDao.updateUser" , user); sqlSession.commit(); sqlSession.close(); return delete; } public List<User> getAllUser () { SqlSession sqlSession = sqlSessionFactory.openSession(); List<User> users = sqlSession.selectList("com.cwz.mybatis.mapper.UserDao.getAllUser" ); sqlSession.close(); return users; } }
与之对应的UserMapper.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?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.cwz.mybatis.mapper.UserDao" > <select id ="getUserById" resultType ="com.cwz.mybatis.model.User" > select * from user where id=#{id}; </select > <insert id ="addUser" parameterType ="com.cwz.mybatis.model.User" > insert into user (username,address) values (#{username},#{address}); </insert > <delete id ="deleteUserById" parameterType ="java.lang.Integer" > delete from user where id=#{id} </delete > <update id ="updateUser" parameterType ="com.cwz.mybatis.model.User" > update user set username = #{username} where id=#{id}; </update > <select id ="getAllUser" resultType ="com.cwz.mybatis.model.User" > select * from user; </select > </mapper >
上面的代码有很多优化的地方,比如:每个方法中都要获取 SqlSession,然后要commit之类的操作。我们可以将当前方法简化成一个接口:
1 2 3 4 5 6 7 8 9 10 11 public interface UserMapper { User getUserById (Integer id) ; Integer addUser (User user) ; Integer deleteUserById (Integer id) ; Integer updateUser (User user) ; List<User> getAllUser () ; }
这个接口对应的mapper文件内容还是不变。
这个接口提供了 UserDao 所需要的最核心的东西,根据这个接口,就可以自动生成 UserDao:
UserDao 中定义了 SqlSessionFactory,这是一套固定的代码
UserDao 中定义了 SqlSessionFactory,这是一套固定的代码
在MyBatis配置文件中配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" > <configuration > <environments default ="development" > <environment id ="development" > <transactionManager type ="JDBC" /> <dataSource type ="POOLED" > <property name ="driver" value ="com.mysql.jdbc.Driver" /> <property name ="url" value ="jdbc:mysql:///test1?useSSL=false& serverTimezone=Asia/Shanghai" /> <property name ="username" value ="root" /> <property name ="password" value ="123456" /> </dataSource > </environment > </environments > <mappers > <package name ="com.cwz.mybatis.mapper" /> </mappers > </configuration >
值得注意的是:
默认情况下,Maven 要求我们将 XML 配置、properties 配置等,都放在 resources 目录下,如果我们强行放在 java 目录下,默认情况下,打包的时候这个配置文件会被自动忽略掉,我们可以这样解决:
在pom.xml中添加配置,让 Maven 不要忽略我在 java 目录下的 XML 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <build > <resources > <resource > <directory > src/main/java</directory > <includes > <include > **/*.xml</include > </includes > </resource > <resource > <directory > src/main/resources</directory > </resource > </resources > </build >
手动在 resources 目录下,创建一个和 UserMapper 接口相同的目录
Mapper映射文件 mapper映射文件有很多的参数使用细节
parameterType 这个表示输入的参数类型
$
和#
的区别在 MyBatis 中,我们在 mapper 引用变量时,默认使用的是 #
,像下面这样:
1 2 3 <select id ="getUserById" resultType ="com.cwz.mybatis.model.User" > select * from user where id=#{id}; </select >
除了使用 #
之外,我们也可以使用 $
来引用一个变量:
1 2 3 <select id ="getUserById" resultType ="com.cwz.mybatis.model.User" > select * from user where id=${id}; </select >
在旧的MyBatis版本中,这两者是有很大区别的:如果使用 $
,变量需要通过 @Param 取别名;而新版本中,不用取别名:
1 2 3 public interface UserMapper { User getUserById (Integer id) ; }
#
和 $
符号的根本区别其实就是Statement 和 PreparedStatement 之间的区别
$
使用时 SQL是直接参数拼接好的,一般来说这样有SQL注入的问题
#
使用了预编译的方式,通过占位符的方式传递参数
有些时候使用$
进行SQL拼接可以使用数据库函数解决,如进行模糊查询
1 2 3 <select id ="getUserByName" resultType ="com.cwz.mybatis.model.User" > select * from user where username like concat('%',${name},'%'); </select >
但是有的 SQL 无法使用 #
来拼接,例如传入一个动态字段进来,假设我想查询所有数据,要排序查询,但是排序的字段不确定,需要通过参数传入,这种场景就只能使用 $
,例如如下方法:
1 List<User> getAllUser (String orderBy) ;
XML 文件:
1 2 3 <select id ="getUserOrderBy" resultType ="com.cwz.mybatis02.model.User" parameterType ="java.lang.String" > select * from user order by ${order} desc; </select >
简单类型的传递 像前面根据id查询只需要传一个参数就行了。但是多个参数传递会怎么样?
比如根据id修改名字:
1 Integer updateUsernameById (String username, Integer id) ;
但是对应的mapper怎么传递参数?
其实可以这样传递:
1 2 3 <update id ="updateUsernameById" > update user set username = #{arg0} where id=#{arg1}; </update >
或者:
1 2 3 <update id ="updateUsernameById" > update user set username = #{param1} where id=#{param2}; </update >
默认就是:[arg0,arg1,…] 或者 [param1,param2,…]
但是参数很多,这样是不容易记住的,我们可以给参数添加@Param注解来指定参数名:
1 Integer updateUsernameById (@Param("username") String username, @Param("id") Integer id) ;
对应的mapper:
1 2 3 <update id ="updateUsernameById" > update user set username=#{username} where id=#{id}; </update >
对象参数的传递 添加用户:
1 Integer addUser (User user) ;
对应的mapper:
1 2 3 <insert id ="addUser" parameterType ="com.cwz.mybatis02.model.User" > insert into user (username,address) values(#{username}, #{address}); </insert >
在引用的时候,直接使用属性名就能够定位到对象了。如果对象存在多个,我们也需要给对象添加 @Param 注解
1 Integer addUser (@Param("user") User user) ;
对应的mapper:
1 2 3 <insert id ="addUser" parameterType ="com.cwz.mybatis02.model.User" > insert into user (username,address) values(#{user.username}, #{user.address}); </insert >
Map参数传递 一般在项目中不适用Map参数,一般使用传对象。
1 Integer updateUsernameById (HashMap<String,Object> map) ;
mapper:
1 2 3 <update id ="updateUsernameById" > update user set username = #{username} where id=#{id}; </update >
引用的变量名,就是 map 中的 key。基本上和实体类是一样的
resultType resultType 是返回类型,在实际开发中,如果返回的数据类型比较复杂,一般我们使用 resultMap,但是,对于一些简单的返回,使用 resultType 就够用了。
resultType返回的可以是简单类型,也可以是对象、集合等
resultMap 在实际开发中,resultMap 是使用较多的返回数据类型配置。因为实际项目中,一般的返回数据类型比较丰富,字段和属性名对应不上。我们需要使用 resultMap,自定义映射的结果集
看一下book实体类:
1 2 3 4 5 public class Book { private Integer id; private String name; private String author; }
数据库中书名的字段名是b_name
,和实体类的属性不对应
接口方法:
1 2 3 public interface BookMapper { List<Book> getAllBooks () ; }
对应的mapper:
1 2 3 4 5 6 7 8 9 <resultMap id ="BookMap" type ="com.cwz.mybatis02.model.Book" > <id property ="id" column ="id" /> <result property ="name" column ="b_name" /> <result property ="author" column ="author" /> </resultMap > <select id ="getAllBooks" resultMap ="BookMap" > select * from t_book; </select >
在这个 resultMap 中,id 用来描述主键,column 是数据库查询出来的列名,property 则是对象中的属性名。
以前老版本的MyBatis要求实体类要有无参构造方法,新版的没这个要求了。一般有参和无参构造方法都有,会先调用无参的。
我们也可以在 resultMap 中,自己指定要调用的构造方法,指定方式如下:
1 2 3 4 5 6 7 <resultMap id ="BookMap" type ="com.cwz.mybatis02.model.Book" > <constructor > <idArg column ="id" name ="id" /> <arg column ="b_name" name ="name" /> <arg column ="author" name ="author" /> </constructor > </resultMap >
这个就表示使用两个参数的构造方法取构造一个 Book实例。需要注意的是,name 属性表示构造方法中的变量名,默认情况下,变量名是 arg0、arg1或者param1、param2 如果需要自定义,我们可以在构造方法中,手动加上 @Param 注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Book { private Integer id; private String name; private String author; public Book () { System.out.println("Book-----init-----无参" ); } public Book (@Param("id") Integer id, @Param("name") String name, @Param("author") String author) { System.out.println("Book-----init---有参" ); this .id = id; this .name = name; this .author = author; } }
动态SQL if if 是一个判断节点,如果满足某个条件,节点中的 SQL 就会生效。比如分页查询:
1 List<Book> getBooksByPage (@Param("start") Integer start, @Param("size") Integer size) ;
在 XML 中定义 SQL:
1 2 3 4 5 6 <select id ="getBooksByPage" resultMap ="BookMap" > select * from t_book <if test ="start != null and size!=null" > limit #{start},#{size} </if > </select >
if 节点中,test 表示判断条件,如果判断结果为 true,则 if 节点的中的 SQL 会生效,否则不会生效。也就是说,在方法调用时,如果分页的两个参数都为 null,则表示查询所有数据
where where 用来处理查询参数
1 List<Book> getBookByNameOrAuthor (Book book) ;
xml:
1 2 3 4 5 6 7 8 9 10 11 12 <select id ="getBookByNameOrAuthor" resultMap ="BookMap" parameterType ="com.cwz.mybatis02.model.Book" > select * from t_book <where > <if test ="name!=null and name!=''" > and b_name=#{name} </if > <if test ="author!=null and author!=''" > and author=#{author} </if > </where > </select >
用 where 节点将所有的查询条件包起来,如果有满足的条件,where 节点会自动加上,如果没有,where 节点也将不存在,在有满足条件的情况下,where 还会自动处理 and 关键字。
set set 关键字一般用在更新中。因为大部分情况下,更新的字段可能不确定,如果对象中存在该字段的值,就更新该字段,不存在,就不更新。
1 Integer updateBook (Book book) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 <update id ="updateBook" parameterType ="com.cwz.mybatis02.model.Book" > update t_book <set > <if test ="name!=null and name!=''" > b_name = #{name}, </if > <if test ="author!=null and author!=''" > author=#{author}, </if > </set > where id=#{id} </update >
trim 可以代替where set
1 2 3 4 5 6 7 8 9 10 11 12 <select id ="getBookByNameOrAuthor2" resultMap ="BookMap" parameterType ="com.cwz.mybatis02.model.Book" > select * from t_book <trim prefix ="where" prefixOverrides ="and " > <if test ="name!=null and name!=''" > and b_name=#{name} </if > <if test ="author!=null and author!=''" > and author=#{author} </if > </trim > </select >
prefixOverrides指定条件关系
1 2 3 4 5 6 7 8 9 10 11 12 13 <update id ="updateBook2" parameterType ="com.cwz.mybatis02.model.Book" > update t_book <trim prefix ="set" suffixOverrides ="," > <if test ="name!=null and name!=''" > b_name = #{name}, </if > <if test ="author!=null and author!=''" > author=#{author}, </if > </trim > where id=#{id} </update >
foreach foreach 用来处理数组/集合参数
如批量查询:
1 List<Book> getBooksByIds (@Param("ids") List<Integer> ids) ;
1 2 3 4 5 6 <select id ="getBooksByIds" resultMap ="BookMap" > select * from t_book where id in <foreach collection ="ids" open ="(" close =")" item ="id" separator ="," > #{id} </foreach > </select >
在 mapper 中,通过 foreach 节点来遍历数组,collection 表示数组变量,open 表示循环结束后,左边的符号,close 表示循环结束后,右边的符号,item 表示循环时候的单个变量,separator 表示循环的元素之间的分隔符。
批量插入:
1 Integer batchAddBooks (@Param("books") List<Book> books) ;
1 2 3 4 5 6 <insert id ="batchAddBooks" > insert into t_book (b_name,author) values <foreach collection ="books" separator ="," item ="book" > (#{book.name},#{book.author}) </foreach > </insert >
bind bind 相当于在mapper文件中定义变量然后引用这个变量
比如模糊查询,根据书的作者的姓氏查询:
1 List<Book> getBooksByAuthorFirstName (String author) ;
1 2 3 4 <select id ="getBooksByAuthorFirstName" resultMap ="BookMap" > <bind name ="authorLike" value ="author+'%'" /> select * from t_book where author like #{authorLike}; </select >
SQL片段 在 SQL 查询中,一般不建议写 *
,因为 select *
会降低查询效率。但是,每次查询都要把字段名列出来,太麻烦。这种使用,我们可以利用 SQL 片段来解决这个问题。
先在 mapper 中定义一个 SQL 片段:
1 2 3 4 <sql id ="Base_Column" > id,usename,address </sql >
然后,在其他 SQL 中,就可以引用这个变量:
1 2 3 4 5 6 <select id ="getUserByIds" resultType ="com.cwz.mybatis.model.User" > select <include refid ="Base_Column" /> from user where id in <foreach collection ="ids" open ="(" close =")" item ="id" separator ="," > #{id} </foreach > </select >
MyBatis进阶查询 一对一查询 比如每篇博客有一个作者,作者有自己的属性。
数据库两张表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 CREATE TABLE `article` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `title` varchar (255 ) DEFAULT NULL , `content` text, `aid` int (11 ) DEFAULT NULL , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 3 DEFAULT CHARSET= utf8; CREATE TABLE `author` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `name` varchar (255 ) DEFAULT NULL , `age` int (11 ) DEFAULT NULL , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 2 DEFAULT CHARSET= utf8;
先定义两个实体类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Article { private Integer id; private String title; private String content; private Author author; } public class Author { private Integer id; private String name; private Integer age; }
新建一个ArticleMapper.java:
1 2 3 public interface ArticleMapper { Article getArticleById (Integer id) ; }
ArticleMapper中定义了一个查询Article的方法,希望查出Article的同时也能查出Author。
ArticleMapper.xml:
1 2 3 4 5 6 7 8 9 10 <?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.cwz.mybatis02.mapper.ArticleMapper" > <select id ="getArticleById" resultType ="com.cwz.mybatis02.model.Article" > select a.*,au.id as 'author.id',au.name as 'author.name',au.age as 'author.age' from article a,author au where a.aid=au.id and a.id=#{id}; </select > </mapper >
这样写太low了,而且很容易写错。
优化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?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.cwz.mybatis02.mapper.ArticleMapper" > <resultMap id ="ArticleMap2" type ="com.cwz.mybatis02.model.Article" > <id property ="id" column ="id" /> <result property ="title" column ="title" /> <result property ="content" column ="content" /> <association property ="author" javaType ="com.cwz.mybatis02.model.Author" columnPrefix ="author_" > <id column ="id" property ="id" /> <result column ="name" property ="name" /> <result column ="age" property ="age" /> </association > </resultMap > <select id ="getArticleById2" resultMap ="ArticleMap2" > select a.*,au.id as author_id,au.name as author_name,au.age as author_age from article a,author au where a.aid=au.id and a.id=#{id}; </select > </mapper >
在这个查询 SQL 中,首先应该做好一对一查询,返回值一定要定义成 resultMap,注意,这里千万不能写错。然后,在 resultMap 中,来定义查询结果的映射关系。其中,association 节点用来描述一对一的关系。这个节点中的内容,和 resultMap 一样,也是 id,result 等,在这个节点中,我们还可以继续描述一对一。columnPrefix
表示的是查询SQL字段的前缀。
由于在实际项目中,每次返回的数据类型可能都会有差异,这就需要定义多个 resultMap,而这多个 resultMap 中,又有一部份属性是相同的,所以,我们可以将相同的部分抽出来,做成一个公共的模板,然后被其他 resultMap 继承:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <resultMap id ="BaseArticleMap" type ="com.cwz.mybatis02.model.Article" > <id property ="id" column ="id" /> <result property ="title" column ="title" /> <result property ="content" column ="content" /> </resultMap > <resultMap id ="ArticleMap2" type ="com.cwz.mybatis02.model.Article" extends ="BaseArticleMap" > <association property ="author" javaType ="com.cwz.mybatis02.model.Author" columnPrefix ="author_" resultMap ="com.cwz.mybatis02.mapper.AuthorMapper.AuthorMap" > </association > </resultMap > <select id ="getArticleById3" resultMap ="ArticleMap3" > select a.*,au.id as author_id,au.name as author_name,au.age as author_age from article a,author au where a.aid=au.id and a.id=#{id}; </select >
AuthorMapper.xml:
1 2 3 4 5 6 7 8 9 10 11 <?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.cwz.mybatis02.mapper.AuthorMapper" > <resultMap id ="AuthorMap" type ="com.cwz.mybatis02.model.Author" > <id property ="id" column ="id" /> <result property ="name" column ="name" /> <result property ="age" column ="age" /> </resultMap > </mapper >
在association节点中,还有有一个resultMap属性,可以用来指定其他查询的mapper
懒加载 上面这种加载方式,是一次性的读取到所有数据。然后在 resultMap 中做映射。如果一对一的属性使用不是很频繁,可能偶尔用一下,这种情况下,我们也可以启用懒加载。
懒加载,就是先查询 article,查询 article的过程中,不去查询 author,当用户第一次调用了 article 中的 author 属性后,再去查询 author。
定义一个 Article的查询方法:
1 Article getArticleById4 (Integer id) ;
ArticleMapper.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 <resultMap id ="BaseArticleMap" type ="com.cwz.mybatis02.model.Article" > <id property ="id" column ="id" /> <result property ="title" column ="title" /> <result property ="content" column ="content" /> </resultMap > <resultMap id ="ArticleMap4" type ="com.cwz.mybatis02.model.Article" extends ="BaseArticleMap" > <association property ="author" javaType ="com.cwz.mybatis02.model.Author" select ="com.cwz.mybatis02.mapper.AuthorMapper.getAuthorById" column ="{id=aid}" fetchType ="lazy" > </association > </resultMap > <select id ="getArticleById4" resultMap ="ArticleMap4" > select * from article where id=#{id}; </select >
这里定义 association 的时候,不直接指定映射的字段,而是指定要执行的方法,通过 select 字段来指定查询author的方法,column 表示执行方法时传递的参数字段,最后的 fetchType 表示开启懒加载。
AuthorMapper.java:
1 2 3 public interface AuthorMapper { Author getAuthorById (Integer id) ; }
AuthorMapper.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?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.cwz.mybatis02.mapper.AuthorMapper" > <resultMap id ="AuthorMap" type ="com.cwz.mybatis02.model.Author" > <id property ="id" column ="id" /> <result property ="name" column ="name" /> <result property ="age" column ="age" /> </resultMap > <select id ="getAuthorById" resultMap ="AuthorMap" > select * from author where id=#{id}; </select > </mapper >
一对多查询 比如,用户和角色,一个用户可以有多个角色
先准备三张表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 CREATE TABLE `user ` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `username` varchar (255 ) DEFAULT NULL , `address` varchar (255 ) DEFAULT NULL , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 10 DEFAULT CHARSET= utf8; CREATE TABLE `role` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `name` varchar (255 ) DEFAULT NULL , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 3 DEFAULT CHARSET= utf8; CREATE TABLE `user_role` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `uid` int (11 ) DEFAULT NULL , `rid` int (11 ) DEFAULT NULL , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 4 DEFAULT CHARSET= utf8;
创建实体类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Role { private Integer id; private String name; } public class User { private Integer id; private String username; private String address; private List<Role> roles; }
定义一个根据 id 查询用户的方法:
UserMapper.java
1 List<User> getAllUsersWithRole () ;
对应的xml UserMapper.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <resultMap id ="BaseUserMap" type ="com.cwz.mybatis02.model.User" > <id property ="id" column ="id" /> <result property ="username" column ="username" /> <result property ="address" column ="address" /> </resultMap > <resultMap id ="UserMapWithRole" type ="com.cwz.mybatis02.model.User" extends ="BaseUserMap" > <collection property ="roles" ofType ="com.cwz.mybatis02.model.Role" columnPrefix ="role_" resultMap ="com.cwz.mybatis02.mapper.RoleMapper.BaseRoleMap" > </collection > </resultMap > <select id ="getAllUsersWithRole" resultMap ="UserMapWithRole" > select u.*,r.id as role_id,r.name as role_name from user u left join user_role ur on u.id=ur.uid left join role r on ur.rid=r.id </select >
RoleMapper.xml:
1 2 3 4 5 6 7 8 9 10 <?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.cwz.mybatis02.mapper.RoleMapper" > <resultMap id ="BaseRoleMap" type ="com.cwz.mybatis02.model.Role" > <id column ="id" property ="id" /> <result column ="name" property ="name" /> </resultMap > </mapper >
在 resultMap 中,通过 collection 节点来描述集合的映射关系。在映射时,会自动将一的一方数据集合并,然后将多的一方放到集合中,能实现这一点,靠的就是 id 属性。
当然,这个一对多,也可以做成懒加载的形式:
查询的方法:
1 List<User> getAllUsersWithRole2 () ;
对应的mapper:
1 2 3 4 5 6 7 8 <resultMap id ="UserMapWithRole2" type ="com.cwz.mybatis02.model.User" extends ="BaseUserMap" > <collection property ="roles" ofType ="com.cwz.mybatis02.model.Role" select ="com.cwz.mybatis02.mapper.RoleMapper.getRoleByUid" column ="{uid=id}" fetchType ="lazy" > </collection > </resultMap > <select id ="getAllUsersWithRole2" resultMap ="UserMapWithRole2" > select * from user; </select >
RoleMapper.java:
1 2 3 public interface RoleMapper { List<Role> getRoleByUid (Integer uid) ; }
RoleMapper.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 <?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.cwz.mybatis02.mapper.RoleMapper" > <resultMap id ="BaseRoleMap" type ="com.cwz.mybatis02.model.Role" > <id column ="id" property ="id" /> <result column ="name" property ="name" /> </resultMap > <select id ="getRoleByUid" resultMap ="BaseRoleMap" > select r.* from role r,user_role ur where r.id=ur.rid and ur.uid=#{uid} </select > </mapper >
鉴别映射器 将上面的user表改一下:
1 2 3 4 5 6 7 CREATE TABLE `user ` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `username` varchar (255 ) DEFAULT NULL , `address` varchar (255 ) DEFAULT NULL , `enabled` tinyint(4 ) DEFAULT NULL , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 10 DEFAULT CHARSET= utf8;
加了一个enabled字段,表示这个角色是可用的,就把它映射进来。如果不可用,就不把角色的映射进来,直接返回user本身。
先定义一个查询方法:
1 List<User> getAllUsersWithRole3 () ;
对应的mapper:
UserMapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <resultMap id ="BaseUserMap" type ="com.cwz.mybatis02.model.User" > <id property ="id" column ="id" /> <result property ="username" column ="username" /> <result property ="address" column ="address" /> <result property ="enabled" column ="enabled" /> </resultMap > <resultMap id ="UserMapWithRole" type ="com.cwz.mybatis02.model.User" extends ="BaseUserMap" > <collection property ="roles" ofType ="com.cwz.mybatis02.model.Role" columnPrefix ="role_" resultMap ="com.cwz.mybatis02.mapper.RoleMapper.BaseRoleMap" > </collection > </resultMap > <resultMap id ="UserMapWithRole3" type ="com.cwz.mybatis02.model.User" > <discriminator javaType ="int" column ="enabled" > <case value ="1" resultMap ="UserMapWithRole" > </case > <case value ="0" resultMap ="BaseUserMap" > </case > </discriminator > </resultMap > <select id ="getAllUsersWithRole3" resultMap ="UserMapWithRole3" > select u.*,r.id as role_id,r.name as role_name from user u left join user_role ur on u.id=ur.uid left join role r on ur.rid=r.id </select >
当enabled为0代表角色不可用,直接使用BaseUserMap映射,不返回role,只返回user本身
自定义类型处理器 比如在上面的user表中新增一个字段:
新增了一个字段favorites,类型是varchar
实体类修改:
1 2 3 4 5 6 7 8 9 public class User { private Integer id; private String username; private String address; private List<Role> roles; private boolean enabled; private List<String> favorites; }
这里favorites存的是list,爱好可能不只一个。
MyBatis有默认的类型处理器,但是没有把list转为varchar的,所以需要自定义类型处理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package com.cwz.mybatis02.typehandler;import org.apache.ibatis.type.JdbcType;import org.apache.ibatis.type.MappedJdbcTypes;import org.apache.ibatis.type.MappedTypes;import org.apache.ibatis.type.TypeHandler;import java.sql.CallableStatement;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException;import java.util.Arrays;import java.util.List;@MappedJdbcTypes(JdbcType.VARCHAR) @MappedTypes(List.class) public class List2Varchar implements TypeHandler <List<String>> { @Override public void setParameter (PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException { StringBuffer sb = new StringBuffer (); for (String s : parameter) { sb.append(s).append("," ); } ps.setString(i, sb.toString()); } @Override public List<String> getResult (ResultSet rs, String columnName) throws SQLException { String s = rs.getString(columnName); if (s != null ){ return Arrays.asList(s.split("," )); } return null ; } @Override public List<String> getResult (ResultSet rs, int columnIndex) throws SQLException { String s = rs.getString(columnIndex); if (s != null ){ return Arrays.asList(s.split("," )); } return null ; } @Override public List<String> getResult (CallableStatement cs, int columnIndex) throws SQLException { String s = cs.getString(columnIndex); if (s != null ){ return Arrays.asList(s.split("," )); } return null ; } }
定义一个添加用户的方法:
1 Integer addUser3 (User user) ;
对应的sql:
1 2 3 <insert id ="addUser3" parameterType ="com.cwz.mybatis02.model.User" > insert into user (username,address,favorites) values(#{username},#{address},#{favorites,typeHandler=com.cwz.mybatis02.typehandler.List2Varchar}) </insert >
测试方法:
1 2 3 4 5 6 7 8 9 @Test public void addUser3 () { User user = new User (); user.setUsername("赵子龙" ); user.setFavorites(Arrays.asList("足球" , "篮球" )); Integer result = userMapper.addUser3(user); System.out.println("result = " + result); sqlSession.commit(); }
这样就实现了类型转换插入数据
但是查询的时候也要指定类型处理器:
1 2 3 4 5 6 7 <resultMap id ="BaseUserMap" type ="com.cwz.mybatis02.model.User" > <id property ="id" column ="id" /> <result property ="username" column ="username" /> <result property ="address" column ="address" /> <result property ="enabled" column ="enabled" /> <result property ="favorites" column ="favorites" typeHandler ="com.cwz.mybatis02.typehandler.List2Varchar" /> </resultMap >
MyBatis查询缓存 一级缓存 Mybatis 一级缓存的作用域是同一个 SqlSession,在同一个 sqlSession 中两次执行相同的 sql 语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。当一个 sqlSession 结束后该 sqlSession 中的一级缓存也就不存在了。
Mybatis 默认开启一级缓存。
如果开启了一个新的 SqlSession,则新的 SqlSession 无法就是之前的缓存,必须是同一个 SqlSession 中,缓存才有效
二级缓存
Mybatis 二级缓存是多个 SqlSession 共享的,其作用域是 mapper 的同一个 namespace,不同的 sqlSession 两次执行相同 namespace 下的 sql 语句且向 sql 中传递参数也相同即最终执行相同的 sql 语句
第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率
Mybatis 默认没有开启二级缓存,需要在 setting 全局参数中配置开启二级缓存
在mybatis-config.xml中:
1 2 3 <settings > <setting name ="cacheEnabled" value ="true" /> </settings >
然后在对应的mapper加上<cache/>
即可:
1 2 3 4 5 6 7 8 9 10 11 12 <?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.cwz.mybatis03.mapper.UserMapper" > <cache /> <select id ="getUserById" resultType ="com.cwz.mybatis03.model.User" > select * from user where id=#{id}; </select > </mapper >
cache节点有一些参数:
<cache eviction="LRU"/>
缓存默认使用的策略是LRU
<cache flushInterval="60000"/>
flushInterval 配置刷新间隔,一般不用配置
<cache size="2048"/>
size 缓存对象的数目,最多可以缓存多少个对象,默认1024
<cache readOnly="true"/>
readOnly 为true,缓存会给调用者返回缓存对象的相同实例,这相同实例不可被修改
验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void test2 () { SqlSession session1 = sqlSessionFactory.openSession(); UserMapper um1 = session1.getMapper(UserMapper.class); User user1 = um1.getUserById(1 ); System.out.println("user1 = " + user1); user1.setUsername("666666666" ); session1.close(); SqlSession session2 = sqlSessionFactory.openSession(); UserMapper um2 = session2.getMapper(UserMapper.class); User user2 = um2.getUserById(1 ); System.out.println("user2 = " + user2); }
自定义MyBatis插件 内存分页 在UserMapper.java中新增方法:
1 List<User> getAllUsersByPage (RowBounds rowBounds) ;
对应的mapper:
1 2 3 <select id ="getAllUsersByPage" resultType ="com.cwz.mybatis03.model.User" > select * from user; </select >
测试:
1 2 3 4 5 6 7 8 @Test public void test3 () { UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class); List<User> list = userMapper.getAllUsersByPage(new RowBounds (1 , 2 )); for (User user : list) { System.out.println("user = " + user); } }
在RowBounds中传递分页参数,SQL中不用写就可以内存分页。但是数据量大这种分页没有意义,需要的是物理分页。
MyBatis 插件接口 MyBatis 插件是通过拦截器来起作用的,MyBatis 框架在设计的时候,就已经为插件的开发预留了相关接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 public interface Interceptor { Object intercept (Invocation invocation) throws Throwable; default Object plugin (Object target) { return Plugin.wrap(target, this ); } default void setProperties (Properties properties) { } }
这个接口中就三个方法,第一个方法必须实现,后面两个方法都是可选的。三个方法作用分别如下:
intercept:具体的拦截方法,自定义 MyBatis 插件时,一般都需要重写该方法
plugin:这个方法的参数 target 就是拦截器要拦截的对象,一般来说我们不需要重写该方法。Plugin.wrap 方法会自动判断拦截器的签名和被拦截对象的接口是否匹配,如果匹配,才会通过动态代理拦截目标对象
setProperties:这个方法用来传递插件的参数,可以通过参数来改变插件的行为。我们定义好插件之后,需要对插件进行配置,在配置的时候,可以给插件设置相关属性,设置的属性可以通过该方法获取到
MyBatis拦截器签名 拦截器签名是一个名为 @Intercepts 的注解,该注解中可以通过 @Signature 配置多个签名。@Signature 注解中则包含三个属性:
type: 拦截器需要拦截的接口,有 4 个可选项,分别是:Executor、ParameterHandler、ResultSetHandler 以及 StatementHandler。
method: 拦截器所拦截接口中的方法名,也就是前面四个接口中的方法名,接口和方法要对应上。
args: 拦截器所拦截方法的参数类型,通过方法名和参数类型可以锁定唯一一个方法。
被拦截的对象 根据前面的介绍,被拦截的对象主要有如下四个:
Executor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public interface Executor { ResultHandler NO_RESULT_HANDLER = null ; int update (MappedStatement ms, Object parameter) throws SQLException; <E> List<E> query (MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException; <E> List<E> query (MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException; <E> Cursor<E> queryCursor (MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException; List<BatchResult> flushStatements () throws SQLException; void commit (boolean required) throws SQLException; void rollback (boolean required) throws SQLException; CacheKey createCacheKey (MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) ; boolean isCached (MappedStatement ms, CacheKey key) ; void clearLocalCache () ; void deferLoad (MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) ; Transaction getTransaction () ; void close (boolean forceRollback) ; boolean isClosed () ; void setExecutorWrapper (Executor executor) ; }
各方法含义分别如下:
update:该方法会在所有的 INSERT、 UPDATE、 DELETE 执行时被调用,如果想要拦截这些操作,可以通过该方法实现。
query:该方法会在 SELECT 查询方法执行时被调用,方法参数携带了很多有用的信息,如果需要获取,可以通过该方法实现。
queryCursor:当 SELECT 的返回类型是 Cursor 时,该方法会被调用。
flushStatements:当 SqlSession 方法调用 flushStatements 方法或执行的接口方法中带有 @Flush 注解时该方法会被触发。
commit:当 SqlSession 方法调用 commit 方法时该方法会被触发。
rollback:当 SqlSession 方法调用 rollback 方法时该方法会被触发。
getTransaction:当 SqlSession 方法获取数据库连接时该方法会被触发。
close:该方法在懒加载获取新的 Executor 后会被触发。
isClosed:该方法在懒加载执行查询前会被触发。
ParameterHandler
1 2 3 4 5 6 7 public interface ParameterHandler { Object getParameterObject () ; void setParameters (PreparedStatement ps) throws SQLException; }
各方法含义分别如下:
getParameterObject:在执行存储过程处理出参的时候该方法会被触发。
setParameters:设置 SQL 参数时该方法会被触发。
ResultSetHandler
1 2 3 4 5 6 7 8 9 public interface ResultSetHandler { <E> List<E> handleResultSets (Statement stmt) throws SQLException; <E> Cursor<E> handleCursorResultSets (Statement stmt) throws SQLException; void handleOutputParameters (CallableStatement cs) throws SQLException; }
handleResultSets:该方法会在所有的查询方法中被触发(除去返回值类型为 Cursor 的查询方法),一般来说,如果我们想对查询结果进行二次处理,可以通过拦截该方法实现。
handleCursorResultSets:当查询方法的返回值类型为 Cursor 时,该方法会被触发。
handleOutputParameters:使用存储过程处理出参的时候该方法会被调用。
StatementHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public interface StatementHandler { Statement prepare (Connection connection, Integer transactionTimeout) throws SQLException; void parameterize (Statement statement) throws SQLException; void batch (Statement statement) throws SQLException; int update (Statement statement) throws SQLException; <E> List<E> query (Statement statement, ResultHandler resultHandler) throws SQLException; <E> Cursor<E> queryCursor (Statement statement) throws SQLException; BoundSql getBoundSql () ; ParameterHandler getParameterHandler () ; }
prepare:该方法在数据库执行前被触发。
parameterize:该方法在 prepare 方法之后执行,用来处理参数信息。
batch:如果 MyBatis 的全剧配置中配置了 defaultExecutorType=”BATCH”
,执行数据操作时该方法会被调用。
update:更新操作时该方法会被触发。
query:该方法在 SELECT 方法执行时会被触发。
queryCursor:该方法在 SELECT 方法执行时,并且返回值为 Cursor 时会被触发。
在开发一个具体的插件时,我们应当根据自己的需求来决定到底拦截哪个方法。
自定义分页插件 首先我们需要自定义一个 RowBounds,因为 MyBatis 原生的 RowBounds 是内存分页,并且没有办法获取到总记录数(一般分页查询的时候我们还需要获取到总记录数),所以我们自定义 PageRowBounds,对原生的 RowBounds 功能进行增强,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class PageRowBounds extends RowBounds { private Integer total; public PageRowBounds (int offset, int limit) { super (offset, limit); } public PageRowBounds () { } public Long getTotal () { return total; } public void setTotal (Long total) { this .total = total; } }
自定义的 PageRowBounds 中增加了 total 字段,用来保存查询的总记录数。
接下来我们自定义拦截器 PageInterceptor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 package com.cwz.mybatis03.plugin;import org.apache.ibatis.cache.CacheKey;import org.apache.ibatis.executor.Executor;import org.apache.ibatis.mapping.BoundSql;import org.apache.ibatis.mapping.MappedStatement;import org.apache.ibatis.mapping.ResultMap;import org.apache.ibatis.plugin.Interceptor;import org.apache.ibatis.plugin.Intercepts;import org.apache.ibatis.plugin.Invocation;import org.apache.ibatis.plugin.Signature;import org.apache.ibatis.session.ResultHandler;import org.apache.ibatis.session.RowBounds;import java.lang.reflect.Field;import java.util.*;@Intercepts(@Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} )) public class PageInterceptor implements Interceptor { @Override public Object intercept (Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0 ]; Object parameterObject = args[1 ]; RowBounds rowBounds = (RowBounds) args[2 ]; if (rowBounds != RowBounds.DEFAULT) { Executor executor = (Executor) invocation.getTarget(); BoundSql boundSql = ms.getBoundSql(parameterObject); Field additionalParametersFields = BoundSql.class.getDeclaredField("additionalParameters" ); additionalParametersFields.setAccessible(true ); Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersFields.get(boundSql); if (rowBounds instanceof PageRowBounds) { MappedStatement countMs = newMappedStatement(ms, Integer.class); CacheKey countKey = executor.createCacheKey(countMs, parameterObject, RowBounds.DEFAULT, boundSql); String countSql = "select count(*) from (" + boundSql.getSql() + ") temp" ; BoundSql countBoundSql = new BoundSql (ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameterObject); Set<String> keySet = additionalParameters.keySet(); for (String key : keySet) { countBoundSql.setAdditionalParameter(key, additionalParameters.get(key)); } List<Object> countQueryResult = executor.query(countMs, parameterObject, RowBounds.DEFAULT, ((ResultHandler) args[3 ]), countKey, countBoundSql); Integer total = (Integer) countQueryResult.get(0 ); ((PageRowBounds) rowBounds).setTotal(total); } CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql); pageKey.update("RowBounds" ); String pageSql = boundSql.getSql() + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit(); BoundSql pageBoundSql = new BoundSql (ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject); Set<String> keySet = additionalParameters.keySet(); for (String key : keySet) { pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key)); } List<Object> list = executor.query(ms, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3 ], pageKey, pageBoundSql); return list; } return invocation.proceed(); } private MappedStatement newMappedStatement (MappedStatement ms, Class<Integer> typeClass) { MappedStatement.Builder builder = new MappedStatement .Builder(ms.getConfiguration(), ms.getId() + "_count" , ms.getSqlSource(), ms.getSqlCommandType()); ResultMap resultMap = new ResultMap .Builder(ms.getConfiguration(), ms.getId(), typeClass, new ArrayList <>(0 )).build(); builder.resource(ms.getResource()) .fetchSize(ms.getFetchSize()) .statementType(ms.getStatementType()) .timeout(ms.getTimeout()) .parameterMap(ms.getParameterMap()) .resultSetType(ms.getResultSetType()) .cache(ms.getCache()) .flushCacheRequired(ms.isFlushCacheRequired()) .useCache(ms.isUseCache()) .resultMaps(Arrays.asList(resultMap)); return builder.build(); } }
首先通过 @Intercepts 注解配置拦截器签名,从 @Signature 的定义中我们可以看到,拦截的是 Executor的query 方法,该方法有一个重载方法,通过 args 指定了方法参数,进而锁定了重载方法
将查询操作拦截下来之后,接下来我们的操作主要在 PageInterceptor的intercept 方法中完成,该方法的参数重包含了拦截对象的诸多信息
通过 invocation.getArgs()
获取拦截方法的参数,获取到的是一个数组,正常来说这个数组的长度为 4。
数组第一项是一个 MappedStatement,我们在 Mapper.xml 中定义的各种操作节点和 SQL,都被封装成一个个的 MappedStatement 对象了
数组第二项就是所拦截方法的具体参数,也就是你在 Mapper 接口中定义的方法参数
数组的第三项是一个 RowBounds 对象,我们在 Mapper 接口中定义方法时不一定使用了 RowBounds 对象,如果我们没有定义 RowBounds 对象,系统会给我们提供一个默认的 RowBounds.DEFAULT
数组第四项则是一个处理返回值的 ResultHandler
接下来判断上一步提取到的 rowBounds 对象是否不为 RowBounds.DEFAULT
,如果为RowBounds.DEFAULT
,说明用户不想分页;如果不为 RowBounds.DEFAULT
,则说明用户想要分页,如果用户不想分页,则直接执行最后的 return invocation.proceed();
,让方法继续往下走就行了。
如果需要进行分页,则先从 invocation 对象中取出执行器 Executor、BoundSql 以及通过反射拿出来 BoundSql 中保存的额外参数(如果我们使用了动态 SQL,可能会存在该参数)。BoundSql 中封装了我们执行的 Sql 以及相关的参数
接下来判断 rowBounds 是否是 PageRowBounds 的实例,如果是,说明除了分页查询,还想要查询总记录数,如果不是,则说明 rowBounds 可能是 RowBounds 实例,此时只要分页即可,不用查询总记录数
如果需要查询总记录数,则首先调用 newMappedStatement 方法构造出一个新的 MappedStatement 对象出来,这个新的 MappedStatement 对象的返回值是 Long 类型的。然后分别创建查询的 CacheKey、拼接查询的 countSql,再根据 countSql 构建出 countBoundSql,再将额外参数添加进 countBoundSql 中。最后通过 executor.query 方法完成查询操作,并将查询结果赋值给 PageRowBounds 中的 total 属性。
接下来进行分页查询,需要强调的是,当我们启动了这个分页插件之后,MyBatis 原生的 RowBounds 内存分页会变成物理分页,原因就在这里我们修改了查询 SQL
将查询结果返回
测试插件 在全局配置中配置分页插件:
1 2 3 <plugins > <plugin interceptor ="com.cwz.mybatis03.plugin.PageInterceptor" > </plugin > </plugins >
接下来我们在 Mapper 中定义查询接口:
1 2 3 public interface UserMapper { List<User> getAllUsersByPage (RowBounds rowBounds) ; }
定义 UserMapper.xml:
1 2 3 <select id ="getAllUsersByPage" resultType ="com.cwz.mybatis03.model.User" > select * from user </select >
测试:
1 2 3 4 5 6 7 8 9 10 @Test public void test3 () { UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class); PageRowBounds pageRowBounds = new PageRowBounds (1 , 2 ); List<User> list = userMapper.getAllUsersByPage(pageRowBounds); for (User user : list) { System.out.println("user = " + user); } System.out.println("pageRowBounds.getTotal() = " + pageRowBounds.getTotal()); }
ssm整合 先创建一个maven工程,添加依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-webmvc</artifactId > <version > 5.3.6</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > 3.5.6</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis-spring</artifactId > <version > 2.0.6</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.23</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-databind</artifactId > <version > 2.12.3</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-jdbc</artifactId > <version > 5.3.6</version > </dependency > </dependencies >
创建java web工程:
配置web.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 <?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" > <context-param > <param-name > contextConfigLocation</param-name > <param-value > classpath:applicationContext.xml</param-value > </context-param > <listener > <listener-class > org.springframework.web.context.ContextLoaderListener</listener-class > </listener > <servlet > <servlet-name > springmvc</servlet-name > <servlet-class > org.springframework.web.servlet.DispatcherServlet</servlet-class > <init-param > <param-name > contextConfigLocation</param-name > <param-value > classpath:spring-servlet.xml</param-value > </init-param > </servlet > <servlet-mapping > <servlet-name > springmvc</servlet-name > <url-pattern > /</url-pattern > </servlet-mapping > <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 > <init-param > <param-name > forceRequestEncoding</param-name > <param-value > true</param-value > </init-param > <init-param > <param-name > forceResponseEncoding</param-name > <param-value > true</param-value > </init-param > </filter > <filter-mapping > <filter-name > encodingFilter</filter-name > <url-pattern > /*</url-pattern > </filter-mapping > </web-app >
数据库资源文件db.properties:
1 2 3 4 db.username =root db.password =123456 db.url =jdbc:mysql:///test1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai db.driverClass =com.mysql.cj.jdbc.Driver
配置spring配置文件applicationContext.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <?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.cwz.ssm" use-default-filters ="true" > <context:exclude-filter type ="annotation" expression ="org.springframework.stereotype.Controller" /> </context:component-scan > <context:property-placeholder location ="classpath:db.properties" /> <bean class ="org.springframework.jdbc.datasource.DriverManagerDataSource" id ="dataSource" > <property name ="username" value ="${db.username}" /> <property name ="password" value ="${db.password}" /> <property name ="url" value ="${db.url}" /> <property name ="driverClassName" value ="${db.driverClass}" /> </bean > <bean class ="org.mybatis.spring.SqlSessionFactoryBean" id ="sqlSessionFactoryBean" > <property name ="dataSource" ref ="dataSource" /> <property name ="typeAliasesPackage" value ="com.cwz.ssm.model" /> <property name ="mapperLocations" > <list > <value > classpath*:com/cwz/ssm/mapper/*.xml</value > </list > </property > </bean > <bean class ="org.mybatis.spring.mapper.MapperScannerConfigurer" id ="mapperScannerConfigurer" > <property name ="sqlSessionFactoryBeanName" value ="sqlSessionFactoryBean" /> <property name ="basePackage" value ="com.cwz.ssm.mapper" /> </bean > </beans >
配置springmvc spring-servlet.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?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" xmlns:mvc ="http://www.springframework.org/schema/mvc" 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 http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd" > <context:component-scan base-package ="com.cwz.ssm" use-default-filters ="false" > <context:include-filter type ="annotation" expression ="org.springframework.stereotype.Controller" /> </context:component-scan > <mvc:annotation-driven /> </beans >
实体类User.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.cwz.ssm.model;public class User { private Integer id; private String username; private String address; @Override public String toString () { return "User{" + "id=" + id + ", username='" + username + '\'' + ", address='" + address + '\'' + '}' ; } public Integer getId () { return id; } public void setId (Integer id) { this .id = id; } public String getUsername () { return username; } public void setUsername (String username) { this .username = username; } public String getAddress () { return address; } public void setAddress (String address) { this .address = address; } }
定义查询方法UserMapper.java
1 2 3 4 5 6 7 8 9 package com.cwz.ssm.mapper;import com.cwz.ssm.model.User;import java.util.List;public interface UserMapper { List<User> getAllUsers () ; }
对应的mapper文件UserMapper.xml:
1 2 3 4 5 6 7 8 9 10 11 <?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.cwz.ssm.mapper.UserMapper" > <select id ="getAllUsers" resultType ="User" > select * from user </select > </mapper >
由于UseMapper.xml和UserMapper.java放在一起,需要在maven中build配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 <build > <resources > <resource > <directory > src/main/java</directory > <includes > <include > **/*.xml</include > </includes > </resource > <resource > <directory > src/main/resources</directory > </resource > </resources > </build >
UserService.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.cwz.ssm.service;import com.cwz.ssm.mapper.UserMapper;import com.cwz.ssm.model.User;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.util.List;@Service public class UserService { @Autowired UserMapper userMapper; public List<User> getAllUsers () { return userMapper.getAllUsers(); } }
UserController.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.cwz.ssm.comtroller;import com.cwz.ssm.model.User;import com.cwz.ssm.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import java.util.List;@RestController public class UserController { @Autowired UserService userService; @GetMapping("/users") public List<User> getAllUsers () { return userService.getAllUsers(); } }