SSM
视频地址
1.spring
用途: 简化javaweb开发
- IOC
- AOP(事务)
官网: spring.io
Spring Framework
Spring Boot
1.1spring 架构
1.2 IOC
IOC: inversion of control 控制反转, 使用对象时由自己new对象, 转换成, 由外部提供对象, 对象创建的控制权限转移到外部.
IOC容器: spring提供了一个容器, 就是上面的外部. IOC容器负责对象的创建, 初始化的工作, 被创建的对象在IOC容器中称为Bean.
DI: dependency injection 依赖注入,
- 在容器中建立bean与bean之间的依赖关系, 自动注入bean
解耦
- 使用IOC容器管理bean (IOC)
- 在IOC容器中将有依赖关系的bean进行绑定(DI)
- 效果: 使用对象时可以从IOC容器中获取, 并且bean已经绑定对其他bean的依赖关系
- 不用new对象, 依赖自动处理好
1.2.1 spring quickstart
- 管理什么? (service与dao)
- 如何将管理对象告知IOC容器? (配置)
- 如何获取IOC容器? (接口)
- 如何从IOC中获取bean?
- 如何使用spring? (pom.xml)
1.配置maven坐标
1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.30</version>
</dependency>
2.在resources目录下配置applicationContext.xml
1
2
3
4
5
6
7
8
9
10
<?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.xxx.spring.dao.impl.BookDaoImpl" id="bookDao"/>
<bean class="com.xxx.spring.servcie.impl.BookServiceImpl" id="bookService"></bean>
</beans>
3.从IOC容器获取bean测试
1
2
3
4
5
6
7
8
9
10
11
public class APP {
public static void main(String[] args) {
// 获取容器
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:*.xml");
BookDao bookDao = context.getBean("bookDao", BookDao.class);
bookDao.addBook();
BookService bookService = (BookService) context.getBean("bookService");
bookService.addBook();
}
}
4.DI测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BookServiceImpl implements BookService {
// 不用new对象了,只用声明有这个属性
private BookDao bookDao;
@Override
public void addBook() {
bookDao.addBook();
System.out.println("add success [BookServiceImpl]");
}
// 必须添加setter方法
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
}
绑定service和dao的关系
1
2
3
4
5
6
<bean class="com.xxx.spring.dao.impl.BookDaoImpl" id="bookDao"/>
<!-- 配置service与dao的关系, 绑他两的关系 -->
<bean class="com.xxx.spring.servcie.impl.BookServiceImpl" id="bookService">
<property name="bookDao" ref="bookDao"></property>
</bean>
1.2.2 bean的配置
bean标签的属性
id和name效果相同, ref=name/id和ctx.get(name/id)
property:设置属性,利用setter方法
scop: 决定ctx.get()出来对象是不是同一个
- singleton 单例
- prototype 非单例
- 适合单例的对象: 表现层对象, 业务层,dao层,工具类, 不适合的: 请求对象/属性经常变化
1.2.3bean的实例化
1.无参构造方法
暴力破解反射,私有构造器也可以造对象 ,但是必须提供无参构造器, 否则会异常:
No default constructor found; nested exception is java.lang.NoSuchMethodException: com.xxx.spring.dao.impl.BookDaoImpl.<init>()
1
2
3
4
try {
BookDao obj = (BookDao) clazz.newInstance();
obj.addBook();
} catch (Exception e) {}
2.静态工厂
1
2
<!-- 使用静态工厂实例化bean,class写工厂类 -->
<bean class="com.xxx.spring.factory.BookDaoFactory" id="bookDao" factory-method="getBookDao"/>
1
2
3
4
5
6
// 静态工厂
public class BookDaoFactory {
public static BookDao getBookDao() {
return new BookDaoImpl();
}
}
3.实例工厂
1
2
3
<!-- 实例工厂 -->
<bean id="instanceFactory" class="com.xxx.spring.factory.BookDaoInstanceFactory"/>
<bean id="bookDao" factory-method="getBookDao" factory-bean="instanceFactory"/>
1
2
3
4
5
6
// 实例工厂
public class BookDaoInstanceFactory {
public BookDao getBookDao() {
return new BookDaoImpl();
}
}
- FactoryBean 实例工厂的简化(配置两个bean简化一个bean) 实现FactoryBean接口
1
2
<!-- FactoryBean方式 -->
<bean id="bookDao" class="com.xxx.spring.factory.BookDaoFactoryBean"/>
1
2
3
4
5
6
7
8
9
10
public class BookDaoFactoryBean implements FactoryBean<BookDao> {
// 代替原始实例工厂中创建对象的方法
public BookDao getObject() throws Exception {
return new BookDaoImpl();
}
public Class<?> getObjectType() {
return BookDao.class;
}
}
1.2.4bean的生命周期
- 初始化容器
- 创建对象
- 执行构造方法
- 执行属性注入(setter)
- 执行bean初始化方法 (init-method InitializingBean接口的afterPropertiesSet方法)
- 使用bean
- 执行业务操作
- 关闭/销毁容器
- 执行bean销毁方法 (DisposableBean.destroy()方法)
bean的销毁方式两种 (ApplicationContext的子接口ConfigurableApplicationContext下面两个方法)
ctx.close()
// 注册关闭钩子
ctx.registerShutdownHook()
1.3 DI 依赖注入
1.setter和构造器注入
-
依赖注入的方式
- 普通方法
- 构造方法
-
注入的类型
- 引用类型
- 简单类型(含String)
-
依赖注入方式 (2*2 四种)
-
1.setter注入(用property标签 setter方法必须)
-
简单类型 (value)
1
<property name="databaseUrl" value="jdbc:mysql://localhost:3306/top_news"></property>
-
引用类型(ref)
1
<property name="bookDao" ref="bookDao"></property>
-
-
构造器注入(必须有全部参数构造器, 可以没有空参构造器)
-
简单类型(用constructor-arg标签)
-
引用类型
-
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 形参名要和构造器一致,形参耦合问题 -->
<bean id="userService" class="com.xxx.spring.servcie.impl.UserServiceImpl">
<constructor-arg name="username" value="root"></constructor-arg>
<constructor-arg name="userDao" ref="userDao"></constructor-arg>
</bean>
<!-- 按类型注入 不靠谱的解决方法 -->
<bean id="userService" class="com.xxx.spring.servcie.impl.UserServiceImpl">
<constructor-arg type="com.xxx.spring.dao.UserDao" ref="userDao"></constructor-arg>
<constructor-arg type="java.lang.String" value="root"></constructor-arg>
</bean>
<!-- 按参数位置 费力的解决方法 -->
<bean id="userService" class="com.xxx.spring.servcie.impl.UserServiceImpl">
<constructor-arg index="0" ref="userDao"></constructor-arg>
<constructor-arg index="1" value="root"></constructor-arg>
</bean>
<bean id="userDao" class="com.xxx.spring.dao.impl.UserDaoImpl"/>
2.依赖注入方式选择
- 强制依赖使用构造器进行, 使用setter有概率不进行注入导致null对象出现
- 可选依赖使用setter注入进行,灵活性强
- spring倡导使用构造器,第三方框架内部大多数采用构造器注入进行数据初始化,相对严谨
- 如果有必要可以二者一起用
- 根据实际情况, 没有setter方法就只能用构造器
- 自己开发的模块使用setter注入(简单好用)
3.依赖自动装配 autowire
-
按类型 (autowire=”byType”)
ioc找到的目标类型应只有一个
需要setter方法
1
<bean class="com.xxx.spring.servcie.impl.BookServiceImpl" id="bookService" autowire="byType">
-
按名称装配: (autowire=”byName”) 名称指的是setter方法的名字
1 2
<!-- 配置service与dao的关系, 绑他两的关系 --> <bean class="com.xxx.spring.servcie.impl.BookServiceImpl" id="bookService" autowire="byName">
自动装配的优先级低于setter注入和构造器注入, 同时出现自动装配失效
优先选择按类型自动装配, 按名称自动装配不推荐使用,使用名称耦合度提高了
4.集合注入
数组, list, set, map, properties
1
2
3
4
5
6
7
8
9
10
11
public class BookDaoImpl implements BookDao {
private int[] array;
private List<String> list;
private Set<String> set;
private Map<String, String> map;
private Properties properties;
// setter 必须
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<bean id="bookDao" class="com.xxx.dao.impl.BookDaoImpl">
<property name="array">
<array>
<value>100</value>
<value>300</value>
<value>500</value>
</array>
</property>
<property name="list">
<list>
<value>hello</value>
<value>world</value>
<value>bye</value>
</list>
</property>
<property name="set">
<set>
<value>喜洋洋</value>
<value>美洋洋</value>
<value>懒羊羊</value>
<value>懒羊羊</value>
</set>
</property>
<property name="map">
<map>
<entry key="username" value="zangxin"/>
<entry key="age" value="11"/>
<entry key="birthday" value="2000-01-01"/>
</map>
</property>
<property name="properties">
<props>
<prop key="country">CHN</prop>
<prop key="province">SHANGHAI</prop>
<prop key="city">SHANGHAI</prop>
</props>
</property>
</bean>
<!--添加引用类型时,把value改成ref即可-->
1.4 数据源对象管理
mysql-connector-j 8.0版本可以使用的mysql server version包括5.7 8.0
com.mysql.cj.jdbc.Driver 是 mysql-connector-java 6以及以上中的
mysql配置(jdbc8.0)
1
2
3
4
jdbc.driver-class-name=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false
jdbc.username=root
jdbc.password=root
1.配置文件管理数据源对象
druid和c3p0数据源对象交给spring管理
1
2
3
4
5
6
7
8
9
10
11
12
13
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/top_news"/>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
</bean>
<bean id="c3p0DataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/top_news"/>
<property name="user" value="root"/>
<property name="password" value="root"/>
</bean>
maven依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
2.jdbc.properites文件方式
1.在spring配置文件中开始context命名空间
2.利用context命名空间加载jdbc.properties文件
3.使用属性占位符${}读取properties文件中的属性
1
2
<!--system-properties-mode="NEVER" 不使用系统环境变量,windows系统环境有个username他的优先度高于${username},所以要关掉-->
<context:property-placeholder location="classpath*:jdbc.properties" system-properties-mode="NEVER"/>
1
2
3
4
5
6
7
<context:property-placeholder location="classpath*:jdbc.properties"/>
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="$${driver-class-name}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"></property>
<property name="password" value="${password}"></property>
</bean>
1
2
3
// 获取环境变量
Map<String, String> getenv = System.getenv();
System.out.println("username = " + getenv.get("USERNAME"));
1
2
3
4
5
6
7
8
9
// 路径获取配置文件
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:*.xml");
// 绝对路径
// ApplicationContext ctx1 = new FileSystemXmlApplicationContext("C:\\Users\\....\\src\\main\\resources\\applicationContext.xml");
// 获取bean的三种方式
ctx.getBean("bookDao");
BookDao bookDao = ctx.getBean("bookDao", BookDao.class);
BookDao bookDao1 = ctx.getBean(BookDao.class);
bookDao1.add();
BeanFactory创建完毕后,所有bean均为延迟加载, ApplicationContext默认为非懒加载
通过bean标签的lazy-init属性可以配置是否懒加载
1
<bean id="bookDao" class="dao.impl.BookDaoImpl" lazy-init="true"/>
BeanFactory是所有容器的顶层接口
1
2
3
4
5
// 使用BeanFactory
Resource resource = new ClassPathResource("applicationContext.xml");
BeanFactory bf = new XmlBeanFactory(resource);
BookDao bookDao = bf.getBean(BookDao.class);
bookDao.add();
1.5 容器总结
1.6 注解开发
1.定义bean
- 使用@Component注解, 等价的注解还有三个(Controller,Service,Repository)
- 配置包扫描
1
2
@Component("bookDao")
public class BookDaoImpl implements BookDao {}
1
<context:component-scan base-package="com.xxx.dao"/>
2.纯注解开发
利用java类替代了ApplicationContext.xml配置文件 @ComponentScan替代了context:component-scan标签
AnnotationConfigApplicationContext获取容器对象
1
2
3
@Configuration
@ComponentScan({"com.xxx", "com.xxx.dao"})
public class SpringConfig {}
1
2
3
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean("bookDao", BookDao.class);
bookDao.add();
作用范围和声明周期
1
2
3
4
5
6
7
8
9
10
11
12
@Component("bookDao")
@Scope("singleton") // 作用范围
public class BookDaoImpl implements BookDao {
@PostConstruct // 生命周期
public void init() {
System.out.println("BookDaoImpl.init");
}
@PreDestroy // 生命周期
public void destroy() {
System.out.println("BookDaoImpl.destroy");
}
}
3.自动装配
1.引用类型
1
2
3
4
5
6
@Service
public class BookServiceImpl implements BookService {
@Autowired
@Qualifier("bookDao2")
private BookDao bookDao;
}
- 自动装配基于发射创建对象并暴力反射对应属性为私有属性初始化数据, 因此无需提供setter方法
- 必须要有一个无参构造器
- @Qualifier()注解不能单独使用, 要配合@Autowired一起使用
2.基本类型
1
2
3
4
5
6
7
@Repository("bookDao")
@Scope("singleton")
public class BookDaoImpl implements BookDao {
// 基本类型注入
@Value("${name}")
private String name;
}
1
2
3
4
5
@Configuration
@ComponentScan({"com.xxx", "com.xxx.dao"})
@PropertySource("classpath:jdbc.properties") // 加载外部properties配置文件,这里不允许使用通配符
public class SpringConfig {
}
4.第三方bean管理&第三方依赖管理
1.使用@Bean定义第三方bean
1
2
3
4
5
6
7
8
9
10
11
12
public class JdbcConfig {
// 1.顶一个方法获取要管理的对象
@Bean
public DataSource dataSource() {
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/top_news);
ds.setUsername("root");
ds.setPassword("roots");
return ds;
}
}
2.将独立的 JdbcConfig配置类加入核心配置(使用@Import注解)
1
2
3
4
@Configuration
@ComponentScan("com.xxx")
@Import(JdbcConfig.class)
public class Config {}
3.第三方基本类型(成员变量)和引用类型(方法形参)的注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class JdbcConfig {
// 普通类型
@Value("${driver-class-name}")
private String driver;
@Value("${url}")
private String url;
@Value("${username}")
private String username;
@Value("${password}")
private String password;
// 1.顶一个方法获取要管理的对象
// 2.BookDao采用的按类型自动装配
@Bean
public DataSource dataSource(BookDao bookDao) {
System.out.println(bookDao);
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
return ds;
}
}
5.注解开发总结
1.7 spring整合mysbatis
整合依赖
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
<!--整合mybatis专用包: mybatis-spring spring-jdbc-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.30</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.30</version>
</dependency>
mybatis配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MybatisConfig {
// 构造sessionFactory, 数据源采用自动装配
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setTypeAliasesPackage("com.xxx.bean");
ssfb.setDataSource(dataSource);
return ssfb;
}
// mapper扫描
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.xxx.dao");
return msc;
}
}
jdbc数据源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class JdbcConfig {
// 普通类型
@Value("${jdbc.driver-class-name}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
return ds;
}
}
spring核心配置类
1
2
3
4
5
@Configuration
@ComponentScan("com.xxx")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class, MybatisConfig.class})
public class SpringConfig {}
测试
1
2
3
4
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
AccountService accountService = ctx.getBean(AccountService.class);
Account account = accountService.findById(1);
System.out.println("account = " + account);
1.8 Junit整合
依赖
1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.30</version>
</dependency>
指定类运行器(@RunWith)和spring配置位置(@ContextConfiguration)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceImplTest {
@Autowired
private AccountService accountService;
@Test
public void findById() {
System.out.println(accountService.findById(1));
}
@Test
public void findAll() {
System.out.println(accountService.findAll());
}
}
1.9 AOP
1.aop基本概念
aspect oriented programming 面向切面编程
作用: 在不改动原始设计的基础上为其进行功能增强
实现: 代理模式
连接点(joinPonit):所有方法,执行过程中任意位置
切入点(pointcut):匹配连接点的式子
范围:切入点是连接点的子集
通知(advice): 在切入点执行的操作,也就是共性操作,或者说增强的功能
切面(Aspect): 描述通知与切入点的对应关系
2.quickstart
目标: 在接口执行时打印系统当前时间
依赖
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.30</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
切面类(@Aspect注解)
1
2
3
4
5
6
7
8
9
10
11
@Aspect // 切面
@Component
public class MyAdvice {
@Pointcut("execution(void com.xxx.dao.BookDao.update())") // 切入点
private void pt() {} // 切入点方法
@Before("pt()") // 通知
public void method() {
System.out.println(System.currentTimeMillis());
}
}
在配置类中开启aspectj自动代理
1
2
3
4
@Configuration
@ComponentScan("com.xxx")
@EnableAspectJAutoProxy
public class SpringConfig {}
注意: 切入点方法必须是私有方法,空参,无返回值
3.aop工作流程
- spring容器启动
- 读取所有切面配置中的切入点
- 初始化bean,判断bean对应的类中的方式是否匹配到任意切入点
- 匹配失败,创建对象
- 匹配成功,创建原始对象的代理对象
- 获取bean的执行方法
- 获取bean, 调用方法执行
- 获取bean是代理对象时, 根据代理对象的运行模式执行增强的内容(包括原来的内容)
AOP核心概念: 代理模式
- 目标对象
- 代理对象
4.AOP切入点表达式
切入点: 要增强的方法
切入点: 要进行增强的方法的描述方式
切入点表达式: execution (访问控制修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
1
execution(void com.xxx.dao.impl.BookDaoImpl.update())
通配符:
* : 单个独立的任意符号,可以独立出现,也可以作为前缀和后缀的通配符出现
.. : 多个连续的任意符号, 可以独立出现, 常用于简化包名和参数的书写
+ : 专用与匹配子类
例子:
com.xxx.dao.impl.BookDaoImpl#update()的匹配
1
2
3
4
5
6
7
@Pointcut("execution(void com.xxx.dao.impl.BookDaoImpl.update())") // yes
@Pointcut("execution(* com.xxx.dao.impl.BookDaoImpl.update(*))") // no
@Pointcut("execution(void com.*.*.*.*.update())") // yes
@Pointcut("execution(void com.*.*.*.update())") //yes
@Pointcut("execution(* *..*(..))") //yes 匹配全部方法
@Pointcut("execution(* *..u*(..))") //yes
@Pointcut("execution(* com.xxx.*.*Service.*(..))") // 匹配所有com.xxx.service层方法
书写技巧
- 按照代码规范书写
- 描述切入点通常描述接口, 而不采用实现类
- 访问控制符针对接口开发均采用public描述(可以省略)
- 返回值类型对于增删改类使用精准类型加速匹配, 对查询使用*通配符快速描述
- 包名尽量不用..匹配,效率过低,通常采用*做单个包描述,或精准匹配
- 接口名/类名采用*匹配, 如UserService书写成*Service
- 方法名书写以动词进行精准匹配,名词采用*匹配, 如getById书写成getBy*,selectAll写成selectAll
- 参数规则复杂, 根据业务方法灵活调整
- 通常不使用异常作为匹配规则
5.AOP通知类型
1
2
3
// 准备切入点
@Pointcut("execution(void com.xxx.dao.BookDao.update())")
private void pt() {}
前置通知
1
2
3
4
@Before("pt()")
public void before() {
System.out.println("MyAdvice.before");
}
后置通知
1
2
3
4
@After("pt()")
public void after() {
System.out.println("MyAdvice.after");
}
环绕通知(重点 ProceedingJoinPoint参数, 调用proceed方法,获取返回值, 必须返回值Object)
1
2
3
4
5
6
7
8
@Around("pt2()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("MyAdvice.around before");
// 表示对原始操作的调用
Object result = pjp.proceed();
System.out.println("MyAdvice.around after");
return result;
}
返回后通知, 与after的区别是, 原方法抛出异常后, afterReturning中断, after执行完毕
1
2
3
4
@AfterReturning("pt2()")
public void afterReturning() {
System.out.println("MyAdvice.afterReturning");
}
异常通知(只有在发生异常时,才调用)
1
2
3
4
@AfterThrowing("pt2()")
public void afterThrowing() {
System.out.println("MyAdvice.afterThrowing");
}
Around通知总结
- 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
- 通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
- 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型
- 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
- 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常
6.测量业务层接口执行效率(耗时)
需求: 任意业务层接口执行事件均可显示其执行效率
分析:
- 业务功能: 业务层接口执行前后分别记录时间,求差值得到执行效率
- 通知类型选择前后均可以增强的类型—–>环绕通知
切面类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Aspect
@Component
public class ProjectAdvice {
@Pointcut("execution(* com.xxx.service.*Service.*(..))") // 匹配业务层所有方法
private void servicePt() {
}
@Around("ProjectAdvice.servicePt()")
public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
// 获取方法名和类名
Signature signature = pjp.getSignature();
String className = signature.getDeclaringTypeName();
String methodName = signature.getName();
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
pjp.proceed();
}
long end = System.currentTimeMillis();
System.out.println("业务层接口" + className + "." + methodName + "万次执行时间: " + (end - start) + "ms");
}
}
配置类
1
2
3
4
5
6
@Configuration
@ComponentScan("com.xxx")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class, MybatisConfig.class})
@EnableAspectJAutoProxy
public class SpringConfig {}
测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceImplTest {
@Autowired
private AccountService accountService;
@Test
public void findById() {
accountService.findById(1);
}
@Test
public void findAll() {
accountService.findAll();
}
}
// output
// 业务层接口com.xxx.service.AccountService.findAll万次执行时间: 1498ms
// 业务层接口com.xxx.service.AccountService.findById万次执行时间: 1015ms
7.AOP获取通知的数据
获取切入点方法的参数
- JoinPoint: 适用于前置,后置,返回后,异常抛出后
- ProceedingJoinPoint: 适用于环绕通知
获取切入点方法返回值
- 返回后通知
- 环绕通知
获取切入点方法运行异常信息
- 抛出异常后通知
- 环绕通知
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
@Pointcut("execution(int com.xxx.dao.BookDao.update(..))")
private void pt2() {
}
@Before("pt2()")
public void before(JoinPoint jp) {
Object[] args = jp.getArgs();
System.out.println("before args = " + Arrays.toString(args));
System.out.println("MyAdvice.before");
}
@After("pt2()")
public void after(JoinPoint jp) {
Object[] args = jp.getArgs();
System.out.println("after args = " + Arrays.toString(args));
System.out.println("MyAdvice.after");
}
@Around("pt2()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
System.out.println("around args = " + Arrays.toString(args));
args[0] = 666;
System.out.println("MyAdvice.around before");
Object result = pjp.proceed(args);
System.out.println("MyAdvice.around after ret=" + result);
return result;
}
@AfterReturning(value = "pt2()", returning = "ret")
public void afterReturning(JoinPoint jp, Object ret) {
Object[] args = jp.getArgs();
System.out.println("afterReturning args = " + Arrays.toString(args));
System.out.println("MyAdvice.afterReturning ret=" + ret);
}
@AfterThrowing(value = "pt2()", throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("MyAdvice.afterThrowing: t=" + t);
}
8.网盘密码数据兼容案例
需求: 密码输入时去除尾部多余的空格
分析:
- 在所有业务方法执行之前对所有输入的参数进行格式处理 trim()
- 使用处理后的参数调用原始方法—-环绕通知中存在对原始方法的调用
配置类
1
2
3
4
@Configuration
@ComponentScan("com.xxx")
@EnableAspectJAutoProxy
public class SpringConfig {}
切面类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
@Aspect
public class DataAdvice {
@Pointcut("execution(boolean com.xxx.service.ResourceService.openURL(*,*))")
private void servicePt() {}
@Around("DataAdvice.servicePt()")
public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
// 改参数
for (int i = 0; i < args.length; i++) {
// 判断参数是不是字符串
if (args[i].getClass().equals(String.class)) {
args[i] = args[i].toString().trim();
}
}
// 将改的参数进去,必须
Object ret = pjp.proceed(args);
return ret;
}
}
测试
1
2
3
4
5
6
7
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
ResourceService resourceService = ctx.getBean(ResourceService.class);
boolean isSuccess = resourceService.openURL("http://pan.baidu.com", " root ");
System.out.println("isSuccess = " + isSuccess);
// output
// password.length = 4
// isSuccess = true
1.10 Spring事务
1.Spring事务概念
事务作用: 在数据库层面保障一系列的数据库操作同时成功/失败
Spring事务作用: 在数据层或业务层保障一系列的数据库操作同时成功/失败
Spring事务接口:
1
2
3
4
public interface PlatformTransactionManager extends TransactionManager {
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
2.案例: 银行账户转账(事务quickstart)
需求:实现任务两个账户间转账操作
需求微缩: A账户减钱, B账户加钱
分析:
- 数据层: 指定账户减钱outMoney, 指定账户加钱(inMoney)
- 业务层提供转账操作(transfer), 调用加钱和减钱操作
- 提供两个账号和操作金额执行转账操作
- 基于Spring整合Mybatis环境搭建上述操作
结果分析:
- 程序正常执行时, 账号金额A加B减
- 程序出现异常时, 转账失败, 但是之前操作成功, 异常之后操作失败, 业务整体失败
使用Spring事务quickstart
1.在业务层上添加Spring事务管理
1
2
@Transactional
void transfer(String out, String in, Double money);
@Transactional通常加在接口方法上, 而不是实现类上, 降低耦合
注解式事务可以添加业务方法上表示当前方法开启事务, 也可以添加到接口上, 表示当前接口中所有方法开启事务
2.设置事务管理器
1
2
3
4
5
6
7
// 定义事务管理器, mybatis使用的是jdbc事务管理器
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource);
return tm;
}
3.开启注解式事务驱动
1
2
3
4
5
6
@Configuration
@ComponentScan("com.xxx")
@Import({MybatisConfig.class, JdbcConfig.class})
@PropertySource("classpath:jdbc.properties")
@EnableTransactionManagement // 开启注解事务
public class SpringConfig {}
案例代码
dao
1
2
3
4
5
6
7
public interface AccountDao {
@Update("update tbl_account set money = money + #{money} where name = #{name}")
void inMoney(@Param("name") String name, @Param("money") Double money);
@Update("update tbl_account set money = money - #{money} where name = #{name}")
void outMoney(@Param("name") String name, @Param("money") Double money);
}
service
1
2
3
4
public interface AccountService {
@Transactional
void transfer(String out, String in, Double money);
}
1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Override
public void transfer(String out, String in, Double money) {
accountDao.outMoney(out, money);
int i = 1/0;
accountDao.inMoney(in, money);
}
}
测试类
1
2
3
4
5
6
7
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
AccountService accountService = ctx.getBean(AccountService.class);
accountService.transfer("tom","jerry",100.0);
}
}
bean
1
2
3
4
5
public class Account {
private Integer id;
private String name;
private Double money;
}
配置类
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
public class JdbcConfig {
// 普通类型
@Value("${jdbc.driver-class-name}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
// 定义数据源
@Bean
public DataSource dataSource() {
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
return ds;
}
// 定义事务管理器
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource);
return tm;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MybatisConfig {
// 构造sessionFactory, 数据源采用自动装配
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setTypeAliasesPackage("com.xxx.bean");
ssfb.setDataSource(dataSource);
return ssfb;
}
// mapper扫描
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.xxx.dao");
return msc;
}
}
1
2
3
4
5
6
7
@Configuration
@ComponentScan("com.xxx")
@Import({MybatisConfig.class, JdbcConfig.class})
@PropertySource("classpath:jdbc.properties")
@EnableTransactionManagement // 开启注解事务
public class SpringConfig {}
1
2
3
4
jdbc.driver-class-name=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false
jdbc.username=root
jdbc.password=root
1
2
3
4
5
6
7
8
9
create schema if not exists spring_db collate utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `tbl_account`;
CREATE TABLE `tbl_account` (
`id` int(11) NOT NULL,
`name` varchar(35) DEFAULT NULL,
`money` double DEFAULT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `tbl_account` VALUES (1,'tom',900),(2,'jerry',1100);
3.spring事务角色
事务管理员: 发起事务方, 在Spring中通常指代业务层开始事务的方法
事务协调员: 加入事务方, 在Spring中通常指代数据层方法, 也可是业务层方法
事务的统一管理必须使用同一个数据源
4.事务配置
上面这些属性都可以在@Transactional
注解的参数上进行设置。
-
readOnly:true只读事务,false读写事务,增删改要设为false,查询设为true。
-
timeout:设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1表示不设置超时时间。
-
rollbackFor:当出现指定异常进行事务回滚
-
noRollbackFor:当出现指定异常不进行事务回滚
-
思考:出现异常事务会自动回滚,这个是我们之前就已经知道的
-
noRollbackFor是设定对于指定的异常不回滚,这个好理解
-
rollbackFor是指定回滚异常,对于异常事务不应该都回滚么,为什么还要指定?
- 这块需要更正一个知识点,并不是所有的异常都会回滚事务,比如下面的代码就不会回滚
-
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
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
public void transfer(String out,String in ,Double money) throws IOException;
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Transactional
public void transfer(String out,String in ,Double money) throws IOException{
accountDao.outMoney(out,money);
//int i = 1/0; //这个异常事务会回滚
if(true){
throw new IOException(); //这个异常事务就不会回滚
}
accountDao.inMoney(in,money);
}
}
出现这个问题的原因是,Spring的事务只会对Error异常
和RuntimeException异常
及其子类进行事务回滚,其他的异常类型是不会回滚的,对应IOException不符合上述条件所以不回滚
- 此时就可以使用rollbackFor属性来设置出现IOException异常不回滚
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Transactional(rollbackFor = {IOException.class})
public void transfer(String out,String in ,Double money) throws IOException{
accountDao.outMoney(out,money);
//int i = 1/0; //这个异常事务会回滚
if(true){
throw new IOException(); //这个异常事务就不会回滚
}
accountDao.inMoney(in,money);
}
}
-
rollbackForClassName等同于rollbackFor,只不过属性为异常的类全名字符串
-
noRollbackForClassName等同于noRollbackFor,只不过属性为异常的类全名字符串
-
isolation设置事务的隔离级别
- DEFAULT :默认隔离级别, 会采用数据库的隔离级别
- READ_UNCOMMITTED : 读未提交
- READ_COMMITTED : 读已提交
- REPEATABLE_READ : 重复读取
- SERIALIZABLE: 串行化
介绍完上述属性后,还有最后一个事务的传播行为,为了讲解该属性的设置,我们需要完成下面的案例。
5.案例: 转账业务追加日志
需求: 实现任意两个账户之间转账操作, 并对每次转账操作在数据库进行留痕
需求微缩: A账户减钱, B账户价钱, 数据库要记录日志
分析:
- 基于转账操作案例添加日志模块,实现数据库中记录日志
- 业务层转账操作(transfer), 调用减钱, 价钱与记录日志功能
预期效果:
无论转账是否成功, 均进行转账日志留痕
新增表 tbl_log
1
2
3
4
5
6
7
8
create table if not exists spring_db.tbl_log
(
info varchar(100) null comment '日志信息',
create_date datetime null comment '创建时间',
id int auto_increment
primary key
)
comment '转账日记记录表' collate = utf8mb4_unicode_ci;
在logService和AccountService都加上@Transactional注解后, 发现失败后tbl_log日志和tbl_account的金额都会滚
这样就不满足要求: 无论转账是否成功, 均进行转账日志留痕, 要实现该功能需要事务传播性功能:
propagation = Propagation.REQUIRES_NEW
1
2
3
4
public interface LogService {
@Transactional
void log(String out, String in, Double money);
}
1
2
3
4
public interface AccountService {
@Transactional
void transfer(String out, String in, Double money);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private LogService logService;
@Override
public void transfer(String out, String in, Double money) {
try {
accountDao.outMoney(out, money);
int i = 1/0;
accountDao.inMoney(in, money);
} catch (Exception e) {
// 这里异常必须抛出去,否则事务不生效
throw new RuntimeException(e);
} finally {
logService.log(out, in, money);
}
}
}
2. SpringMVC
2.1概述
springmvc <—> 和servlet等价, 都是表现层技术
表现层: springmvc/servlet
业务层:..
dao层: jdbc/mybatis/hibernate
springmvc是一种表现层框架技术
springmvc用于进行表现层功能开发
2.2 springmvc quickstart(注意依赖版本号)
1.依赖 jdk1.8 + tomcat8(本地tomcat) tomcat10跑不起来, spring依赖使用5.3.0以下的
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
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.1</version>
<configuration>
<port>80</port>
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>
2.创建springMVC控制器(等同于Servlet)
1
2
3
4
5
6
7
8
9
@Controller
public class UserController {
@RequestMapping("/save")
@ResponseBody
public String save() {
System.out.println("user save");
return "{'module':'springmvc'}";
}
}
3.spring配置类
1
2
3
@Configuration
@ComponentScan("com.xxx.controller")
public class SpringMvcConfig {}
4.初始化Servlet容器, 加在springMVC环境, 并设置MVC技术处理的请求
定义一个servlet容器启动的配置类, 在里面加在spring的配置(有了这个就可以删除web.xml文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ServletContainerInitConfig extends AbstractDispatcherServletInitializer {
// 加载springmvc容器
@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
ctx.register(SpringMvcConfig.class);
return ctx;
}
// 设置哪些请求归属springmvc处理
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
// 加载spring容器配置
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
}
入门案例工作流程分析
启动服务的过程
- 服务启动, 执行ServletContainerInitConfig类型, 初始web化容器
- 执行createServletApplicationContext()方法, 创建WebApplicationContext对象
- 加载SpringMVCConfig配置类型(包扫描)
- 执行ComponentScan加载对应的bean
- 加载UserController, 每个@RequestMapping的名称对应一个具体方法
- 执行getServletMappings方法,设定SpringMVC拦截请求的路径规则
单次请求过程
- 发送请求
http://localhost/save
- web容器发现该请求满足SpringMVC拦截规则,将请求交给SpringMVC处理
- 解析请求路径/save
- 由/save匹配执行对应的方法save()
- 上面的第五步已经将请求路径和方法建立了对应关系,通过/save就能找到对应的save方法
- 执行save()
- 检测到有@ResponseBody直接将save()方法的返回值作为响应体返回给请求方
2.3 bean加载与控制(分容器加载)
SpringMVC相关bean (表现层bean)
Spring控制的bean
- 业务bean (Service)
- 功能bean (DataSource等)
因功能不同, 如何避免Spring错误加载到SpringMVC的bean–加载Spring控制的bean的时候排除掉SpringMVC控制的bean
spring包扫描dao和service
方式一: 精确扫描Service和dao
方式二: 扫描所有包,排除controller
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
//@ComponentScan({"com.xxx.service", "com.xxx.dao"})
@ComponentScan(value = "com.xxx",
excludeFilters = @ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = Controller.class
)
)
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class, MybatisConfig.class})
public class SpringConfig {}
SpringMVC扫描controller
1
2
3
@Configuration
@ComponentScan("com.xxx.controller")
public class SpringMvcConfig {}
注册Spring和SpringMVC到web容器中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ServletContainersInitConfig1 extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{SpringConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{SpringMvcConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
知识点1:@ComponentScan
名称 | @ComponentScan |
---|---|
类型 | 类注解 |
位置 | 类定义上方 |
作用 | 设置spring配置类扫描路径,用于加载使用注解格式定义的bean |
相关属性 | excludeFilters:排除扫描路径中加载的bean,需要指定类别(type)和具体项(classes) includeFilters:加载指定的bean,需要指定类别(type)和具体项(classes) |
2.4请求
2.4.1请求路径
1
2
3
4
5
6
7
8
9
10
@Controller
@RequestMapping("/book")
public class BookController {
@RequestMapping("/save")
@ResponseBody
public String save() {
System.out.println("book save ...");
return "{'info':'success}";
}
}
2.4.2 请求方式
1
2
3
4
5
6
7
8
9
10
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping("/save")
@ResponseBody
public String save(String name, Integer age) {
System.out.println("user save ... name=" + name);
return "{'info':'success,'name':" + name + ",age=" + age + "}";
}
}
GET传参
普通参数:url地址传参, 地址参数名与形参变量名相同,定义形参即可接收参数
1
http://localhost/user/save?name=zhangsan&age=23
POST请求传参
1
http://localhost/user/save
2.4.3请求乱码问题
get请求: 请求行编码格式与tomcat的请求行解码方式不一致就会导致, 解决方法: 将tomcat uriencoding修改为utf-8
post请求: 请求体乱码, 修改后端默认解码字符集: request.setCharacterEncoding(“UTF-8”), 这一行代码的意思是设置解析请求体使用的字符集
1
2
3
4
5
6
7
8
9
10
// Springmvc解决请求体乱码问题, 设置一个过滤器
public class ServletContainerInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
// 处理post请求乱码
@Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter filter = new CharacterEncodingFilter("UTF-8", true);
return new Filter[]{filter};
}
// 略去其他三个方法
}
响应乱码问题: 浏览器在接收响应信息时,使用了不同的字符集或者是不支持中文的字符集就会出现乱码
response.setContentType(“text/html;charset=utf-8”), 设置响应头ContentType: text/html;charset=utf-8, 告知浏览器用该方式解析响应
Springmvc默认的ContentType字符集为iso-8859-1
1
2
3
4
// org.springframework.http.converter.StringHttpMessageConverter
static {
DEFAULT_CHARSET = StandardCharsets.ISO_8859_1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 将默认的iso-iso-8859-1修改为UTF-8
@Configuration
@ComponentScan("com.xxx.controller")
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {
// 通过@EnableWebMVC配置的时候起作用,
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> httpMessageConverter : converters) {
if (StringHttpMessageConverter.class.isAssignableFrom(httpMessageConverter.getClass())) {
((StringHttpMessageConverter) httpMessageConverter).setDefaultCharset(Charset.forName("UTF-8"));
}
}
}
}
2.4.4 5种参数传递方式
1.形参和请求参数一样
1
2
// http://localhost/user/save?name=张三&age=23
public String save(String name, Integer age) {
2.形参和请求参数不一致(@RequestParam注解)
1
http://localhost/user/save?name=张三&age=23
1
public String save(@RequestParam("name") String username, Integer age) {
3.实体类形参接收请求参数
1
2
3
POST http://localhost/user/save1
name:张三
age:33
1
2
3
4
5
6
@RequestMapping("/save1")
@ResponseBody
public String save1(User user) {
System.out.println("user save ... username=" + user);
return "{'info':'success,'user':" + user + "}";
}
1
2
3
4
public class User {
private String name;
private String age;
} // setter方法必须
4.嵌套实体对象接收请求参数
1
http://localhost/user/save2?name=张三&age=23&address.province=广东&address.city=深圳
1
2
3
@RequestMapping("/save2")
@ResponseBody
public String save2(User user) {}
1
2
3
4
5
6
7
public class User {
private String name;
private String age;
// 地址实体类
private Address address;
// setter方法和address的getter方法必须
}
1
2
3
4
public class Address {
private String province;
private String city;
}
5.数组参数
1
http://localhost/user/save3?hobby=唱&hobby=跳&hobby=篮球
1
public String save3(String hobby[]) {}
6.集合参数
传参与数组没有区别, 就是需要在形参前加@RequestParam
1
http://localhost/user/save4?hobby=唱&hobby=跳&hobby=篮球
1
public String save4(@RequestParam List<String> hobby) {
2.4.5 json参数传递 (@RequestBody)
1
1
2
3
4
@Configuration
@ComponentScan("com.xxx.controller")
@EnableWebMvc // 开启webmvc注解
public class SpringMvcConfig implements WebMvcConfigurer {}
1.集合参数传递
http://localhost/user/save5 body: [“sing”,”dance”,”rap”,”basketball”,”6666”]
1
2
3
@RequestMapping("/save5")
@ResponseBody
public String save5(@RequestBody List<String> hobby) {}
2.对象参数传递
1
2
3
4
5
6
7
8
9
http://localhost/user/save6
{
"name":"张三",
"age":22,
"address":{
"privince":"广东",
"city":"深圳"
}
}
1
2
3
@RequestMapping("/save6")
@ResponseBody
public String save6(@RequestBody User user) {}
3.对象集合参数传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
http://localhost/user/save7
[
{
"name":"张三",
"age":22,
"address":{
"privince":"广东",
"city":"深圳"
}
},
{
"name":"李四",
"age":34,
"address":{
"privince":"北京",
"city":"北京"
}
}
]
1
2
3
@RequestMapping("/save7")
@ResponseBody
public String save7(@RequestBody List<User> user) {}
总结:
@RequestParam与@RequestBody的区别
@RequestParam用于接收url地址传参,和表单传参 application/x-www-form-urlencoded
@RequestBody用于接收json数据[application/json]
2.4.6日期类型参数传递(@DateTimeFormat注解使用)
1
http://localhost/book/dateParam?date=2022-1-1&date2=2022/1/1&date3=2022/1/1 08:30:30
1
2
3
4
5
6
7
8
9
10
@RequestMapping("/dateParam")
@ResponseBody
public String dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd") Date date,
@DateTimeFormat(pattern = "yyyy/MM/dd") Date date2,
@DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss") Date date3) {
System.out.println("dateParam yyyy-MM-dd ..." + date);
System.out.println("dateParam yyyy/MM/dd ..." + date2);
System.out.println("dateParam yyyy/MM/dd HH:mm:ss ..." + date3);
return "{'info':'success,date:" + date + "}";
}
2.4.7类型转换器
1
2
3
4
5
6
@FunctionalInterface
public interface Converter<S, T> {
@Nullable
T convert(S source);
}
请求参数年龄数据: String–>Integer
日期格式转换: String –> Date
@EnableWebMvc注解功能之一: 根据类型匹配对应的类型转换器
2.5响应
2.5.1四种响应方式
响应页面
响应数据
- 文本数据
- json数据
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
@Controller
public class UserController {
// 响应页面/跳转页面
@RequestMapping("/toPage")
public String toPage() {
System.out.println("跳转页面");
return "page.jsp";
}
// 响应纯文本数据
@RequestMapping("/toText")
@ResponseBody
public String toText() {
System.out.println("返回纯文本数据");
return "reponse text";
}
// 响应pojo对象
@RequestMapping("/toJsonPojo")
@ResponseBody
public User toJsonPOJO() {
System.out.println("返回json对象数据");
User user = new User();
user.setName("张三");
user.setAge(22);
return user;
}
// 响应pojo对象集合
@RequestMapping("/toJsonPojoList")
@ResponseBody
public List<User> toJsonPOJOList() {
List<User> userList = new ArrayList<>();
System.out.println("返回json对象数据集合");
for (int i = 0; i < 3; i++) {
User user = new User();
user.setName("张三" + i);
user.setAge(10 * i);
userList.add(user);
}
return userList;
}
}
2.5.2 @ResponseBody注解 响应json数据
-
方法注解: 位置SpringMVC控制器方法上面
-
作用: 设置当前控制器返回值作为响应体
public interface HttpMessageConverter<T> {} 接口定义了将对象转为json数据
1
2
3
4
5
6
7
8
9
10
11
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> var1, @Nullable MediaType var2);
boolean canWrite(Class<?> var1, @Nullable MediaType var2);
List<MediaType> getSupportedMediaTypes();
T read(Class<? extends T> var1, HttpInputMessage var2) throws IOException, HttpMessageNotReadableException;
void write(T var1, @Nullable MediaType var2, HttpOutputMessage var3) throws IOException, HttpMessageNotWritableException;
}
2.6 REST风格
2.6.1 简介
REST: Representational State Transfer, 表现形式状态转换
传统风格资源描述形式
1
2
http://localhost/user/getById?id=1
http://localhost/user/saveUser
REST风格资源描述形式
1
2
http://localhost/user/1
http://localhost/user/
优点
- 隐藏资源的访问行为, 无法通过地址得知对资源是什么操作
- 书写简化
按照REST风格访问资源时使用==行为动作==区分对资源进行了何种操作
http://localhost/users
查询全部用户信息 GET(查询)http://localhost/users/1
查询指定用户信息 GET(查询)http://localhost/users
添加用户信息 POST(新增/保存)http://localhost/users
修改用户信息 PUT(修改/更新)http://localhost/users/1
删除用户信息 DELETE(删除)
通过路径+请求方式=确定访问行为(增/删/改/查)
描述模块的名称通常使用复数,也就是加s的格式描述,表示此类资源,而非单个资源,例如:users、books、accounts……
根据REST风格对资源进行访问称为==RESTful==。
2.6.2 RESTful quickstart
依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.5</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Controller
public class UserController {
@RequestMapping(value = "/users", method = RequestMethod.POST)
@ResponseBody
public String save() {
System.out.println("user save ... ");
return "{'module':'user save'}";
}
@RequestMapping(value = "/users/{id}", method = RequestMethod.DELETE)
@ResponseBody
public String delete(@PathVariable Integer id) {
System.out.println("user delete ... " + id);
return "{'module':'user delete'}";
}
@RequestMapping(value = "/users", method = RequestMethod.PUT)
@ResponseBody
public String update(@RequestBody User user) {
System.out.println("user update ... " + user);
return "{'module':'user update'}";
}
@RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
@ResponseBody
public String getById(@PathVariable Integer id) {
System.out.println("user getById ... " + id);
return "{'module':'user getById'}";
}
@RequestMapping(value = "/users/", method = RequestMethod.GET)
@ResponseBody
public String getAll() {
System.out.println("user getAll ... ");
return "{'module':'user getAll'}";
}
}
@PathVariable注解
- 类型: 形参注解
- 位置: SpringMVC控制器方法形参定义面前
- 作用: 绑定路径参数与处理器方法形参之间的关系, 要求路径参数名与形参名一一对应
三个注解@RequestBody
、@RequestParam
、@PathVariable
,这三个注解之间的区别和应用分别是什么?
- 区别
- @RequestParam用于接收url地址传参或表单传参
- @RequestBody用于接收json数据
- @PathVariable用于接收路径参数,使用{参数名称}描述路径参数
- 应用
- 后期开发中,发送请求参数超过1个时,以json格式为主,@RequestBody应用较广
- 如果发送非json格式数据,选用@RequestParam接收请求参数
- 采用RESTful进行开发,当参数数量较少时,例如1个,可以采用@PathVariable接收请求路径变量,通常用于传递id值
2.6.3简化版本RESTful风格
利用@RestController, @GetMapping, @PostMapping等注解
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
@RestController
@RequestMapping("/books")
public class BookController {
@PostMapping
public String save(@RequestBody Book book) {
System.out.println("book save ... book=" + book);
return "{'module':'book save'}";
}
@DeleteMapping("/{id}")
public String delete(@PathVariable Integer id) {
System.out.println("book delete ... " + id);
return "{'module':'book delete'}";
}
@PutMapping
public String update(@RequestBody Book book) {
System.out.println("book update ... " + book);
return "{'module':'book update'}";
}
@GetMapping("/{id}")
public String getById(@PathVariable Integer id) {
System.out.println("book getById ... " + id);
return "{'module':'book getById'}";
}
@GetMapping
public String getAll() {
System.out.println("book getAll ... ");
return "{'module':'book getAll'}";
}
}
2.6.4 基于RESTFul的案例(完整配置版)
图书的增删改查(只有Controller和视图层)
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
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!--这里必须打成war,否则部署不上-->
<packaging>war</packaging>
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.5</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
SpringMVC配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@ComponentScan("com.xxx.controller")
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {
// 处理response乱码问题
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> converter : converters) {
if (StringHttpMessageConverter.class.isAssignableFrom(converter.getClass())) {
((StringHttpMessageConverter) converter).setDefaultCharset(Charset.forName("UTF-8"));
}
}
}
// 处理静态资源
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 当访问uri匹配到/pages/**的时候,走/pages目录下的内容
registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
registry.addResourceHandler("/css/**").addResourceLocations("/css/");
registry.addResourceHandler("/js/**").addResourceLocations("/js/");
registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/");
}
}
Web容器初始化配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ServletContainerInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{SpringMvcConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter filter = new CharacterEncodingFilter("UTF-8", true);
return new Filter[]{filter};
}
}
domain实体类
1
2
3
4
5
6
7
public class Book {
private Integer id;
private String type;
private String name;
private String description;
// getter setter...
}
controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@RestController
@RequestMapping("/books")
public class BookController {
private static final List<Book> BOOK_LIST = new ArrayList<>();
static {
Book book1 = new Book(1, "轻小说", "刀剑神域", "sword artist");
Book book2 = new Book(2, "数学", "偏微分方程", "PDE, 姜萍同款");
Collections.addAll(BOOK_LIST, book1, book2);
}
@GetMapping("/{id}")
public Book getById(@PathVariable Integer id) {
return BOOK_LIST.get(id);
}
@PostMapping
public String save(@RequestBody Book book) {
book.setId(BOOK_LIST.size());
BOOK_LIST.add(book);
System.out.println("BookController.save");
return "{'msg':'book save success'}";
}
@DeleteMapping("/{id}")
public String delete(@PathVariable Integer id) {
System.out.println("BookController.delete");
boolean isSuccess = (BOOK_LIST.remove(id.intValue()) != null);
return "{'msg':'book delete success id='" + id + ", isSuccess:" + isSuccess + "}";
}
@GetMapping()
public List<Book> getAll() {
return BOOK_LIST;
}
}
前端html(css/js/plugins略,到时候代码上传到github去)
这里用的技术是vue2+elementUI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
<!DOCTYPE html>
<html>
<head>
<!-- 页面meta -->
<meta charset="utf-8">
<title>SpringMVC案例</title>
<!-- 引入样式 -->
<link rel="stylesheet" href="../plugins/elementui/index.css">
<link rel="stylesheet" href="../plugins/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="../css/style.css">
</head>
<body class="hold-transition">
<div id="app">
<div class="content-header">
<h1>图书管理</h1>
</div>
<div class="app-container">
<div class="box">
<div class="filter-container">
<el-input placeholder="图书名称" v-model="searchBookId" style="width: 200px;"
class="filter-item"></el-input>
<el-button type="primary" class="butT" @click="getById()">查询</el-button>
<el-button type="primary" class="butT" @click="openSave()">新建</el-button>
</div>
<el-table size="small" current-row-key="id" :data="dataList" stripe highlight-current-row>
<el-table-column type="index" align="center" label="序号"></el-table-column>
<el-table-column prop="type" label="图书类别" align="center"></el-table-column>
<el-table-column prop="name" label="图书名称" align="center"></el-table-column>
<el-table-column prop="description" label="描述" align="center"></el-table-column>
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button type="primary" size="mini">编辑</el-button>
<el-button size="mini" type="danger" @click="removeById(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
class="pagiantion"
@current-change="handleCurrentChange"
:current-page="pagination.currentPage"
:page-size="pagination.pageSize"
layout="total, prev, pager, next, jumper"
:total="pagination.total">
</el-pagination>
</div>
<!-- 新增标签弹层 -->
<div class="add-form">
<el-dialog title="新增图书" :visible.sync="dialogFormVisible">
<el-form ref="dataAddForm" :model="formData" :rules="rules" label-position="right"
label-width="100px">
<el-row>
<el-col :span="12">
<el-form-item label="图书类别" prop="type">
<el-input v-model="formData.type"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="图书名称" prop="name">
<el-input v-model="formData.name"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="描述">
<el-input v-model="formData.description" type="textarea"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="saveBook()">确定</el-button>
</div>
</el-dialog>
</div>
</div>
</div>
</div>
</body>
<!-- 引入组件库 -->
<script src="../js/vue.js"></script>
<script src="../plugins/elementui/index.js"></script>
<script type="text/javascript" src="../js/jquery.min.js"></script>
<script src="../js/axios-0.18.0.js"></script>
<script>
var vue = new Vue({
el: '#app',
data: {
searchBookId: null,
dataList: [],//当前页要展示的分页列表数据
formData: {},//表单数据
dialogFormVisible: false,//增加表单是否可见
dialogFormVisible4Edit: false,//编辑表单是否可见
pagination: {},//分页模型数据,暂时弃用
},
mounted() {
this.getById()
},
methods: {
// 重置表单
resetForm() {
//清空输入框
this.formData = {};
},
// 弹出添加窗口
openSave() {
this.dialogFormVisible = true;
this.resetForm();
},
//添加
saveBook() {
axios.post("/books", this.formData).then((res) => {
this.dialogFormVisible = false
window.location.reload(true)
});
},
removeById(index) {
let isDel = confirm("是否删除")
if (isDel) {
axios.delete("/books/" + index).then((res) => {
if (res.status == 200){
alert("删除成功")
window.location.reload(true)
}
});
}
},
// getById 或getAll, 根据bookId是否有值
getById() {
let url = this.searchBookId == null ? '' : this.searchBookId;
axios.get("/books/" + url).then((res) => {
if (this.searchBookId) {
console.log(res)
this.dataList = [res.data];
} else {
this.dataList = res.data
}
});
}
}
})
</script>
</html>
2.7 SSM整合
1.创建工程
2.SSM整合
- spring
- springConfig
- mybatis
- MybatisConfig
- JdbcConfig
- jdbc.properties
- SpringMVC
- ServletConfig
- SpringMvcConfig
3.功能模块
- 表与实体类
- dao (接口+自动代理)
- servic(接口+实现类)
- 业务层接口测试(Junit)
- Controller
- 表现层接口测试(postman)
2.7.1maven依赖
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
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.5</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
2.7.2配置类
Spring配置
1
2
3
4
5
6
7
@Configuration
@ComponentScan({"com.xxx.service"})
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class, MybatisConfig.class})
@EnableTransactionManagement // 开启注解事务
public class SpringConfig {
}
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
public class JdbcConfig {
@Value("${jdbc.driver-class-name}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
// jdbc事务管理器
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource);
return tm;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MybatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setDataSource(dataSource);
ssfb.setTypeAliasesPackage("com.xxx.domain");
return ssfb;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.xxx.dao");
return msc;
}
}
springmvc配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ServletConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{SpringConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{SpringMvcConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
protected Filter[] getServletFilters() { // 过滤器,用utf-8解析请求参数
CharacterEncodingFilter filter = new CharacterEncodingFilter("UTF-8", true);
return new Filter[]{filter};
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@ComponentScan("com.xxx.controller")
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {
// 设置响应头ContentType: text/html;charset=utf-8, 告知浏览器用该方式解析响应
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> converter : converters) {
if (StringHttpMessageConverter.class.isAssignableFrom(converter.getClass())) {
((StringHttpMessageConverter) converter).setDefaultCharset(Charset.forName("UTF-8"));
}
}
}
// 处理静态资源
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 请求匹配/pages/**时转发到/pages目录下
registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
registry.addResourceHandler("/css/**").addResourceLocations("/css/");
registry.addResourceHandler("/js/**").addResourceLocations("/js/");
registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/");
}
2.7.3表与实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DROP TABLE IF EXISTS `tbl_book`;
CREATE TABLE `tbl_book`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`type` varchar(20) DEFAULT NULL,
`name` varchar(50) DEFAULT NULL,
`description` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `tbl_book`
VALUES (1, '文学', '老人与海', '海明威代表作, 一个人可以被消灭,但是不能被打败-修改test'),
(2, '文学', '月亮与六便士', '艺术之于生命'),
(3, '哲学', '斐多篇', '苏格拉底论灵魂不朽'),
(4, '哲学', '存在与时间', '此在自始至终都处于沉沦状态,芸芸众生'),
(5, '数学', '代数几何', '仿佛来自虚空'),
(8, '文学', '浮士德', '与魔鬼交易, 重活一世');
实体类
1
2
3
4
5
6
public class Book {
private Integer id;
private String type;
private String name;
private String description;
}
2.7.4 dao层(自动代理)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface BookDao {
@Insert("insert into tbl_book(id, type, name, description) values (null,#{type},#{name},#{description})")
void save(Book book);
@Update("update tbl_book set type=#{type},name=#{name},description=#{description} where id=#{id}")
void update(Book book);
@Delete("delete from tbl_book where id=#{id}")
void delete(Integer id);
@Select("select id, type, name, description from tbl_book where id = #{id}")
Book getById(Integer id);
@Select("select id, type, name, description from tbl_book")
List<Book> getAll();
}
2.7.5service层及单元测试
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
public interface BookService {
/**
* 保存
*
* @param book
* @return
*/
boolean save(Book book);
/**
* 修改
*
* @param book
* @return
*/
boolean update(Book book);
/**
* 根据id删除
*
* @param id
* @return
*/
boolean delete(Integer id);
/**
* 按id查询
*
* @param id
* @return
*/
Book getById(Integer id);
/**
* 查询全部
*
* @return
*/
List<Book> getAll();
}
实现类
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
@Service
@Transactional(rollbackFor = Exception.class)
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
public boolean save(Book book) {
bookDao.save(book);
return true;
}
@Override
public boolean update(Book book) {
bookDao.update(book);
return true;
}
@Override
public boolean delete(Integer id) {
bookDao.delete(id);
return true;
}
@Override
public Book getById(Integer id) {
return bookDao.getById(id);
}
@Override
public List<Book> getAll() {
return bookDao.getAll();
}
}
service层接口测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class BookServiceImplTest {
@Autowired
private BookService bookService;
@Test
public void getById() {
Book book = bookService.getById(1);
Assert.assertNotNull(book);
System.out.println(book);
}
@Test
public void getAll() {
List<Book> bookList = bookService.getAll();
Assert.assertNotNull(bookList);
Assert.assertTrue(!bookList.isEmpty());
System.out.println("bookList = " + bookList);
}
}
2.7.6controller层及表现层接口测试(idea HttpClient)
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
@RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private BookService bookService;
@PostMapping
public boolean save(@RequestBody Book book) {
return bookService.save(book);
}
@PutMapping
public boolean update(@RequestBody Book book) {
return bookService.update(book);
}
@DeleteMapping("/{id}")
public boolean delete(@PathVariable Integer id) {
return bookService.delete(id);
}
@GetMapping("/{id}")
public Book getById(@PathVariable Integer id) {
return bookService.getById(id);
}
@GetMapping
public List<Book> getAll() {
return bookService.getAll();
}
}
表现层接口测试
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
# springmvc_08_ssm
###
GET http://localhost/books
###
GET http://localhost/books/1
###
PUT http://localhost/books
Content-Type: application/json
{
"id": 1,
"type": "文学",
"name": "老人与海",
"description": "海明威代表作, 一个人可以被消灭,但是不能被打败-修改test"
}
###
POST http://localhost/books
Content-Type: application/json
{
"id": null,
"type": "文学",
"name": "浮士德",
"description": "与魔鬼交易, 重活一世"
}
###
DELETE http://localhost/books/7
2.7.7表现层与前端数据传输协议定义
统一数返回结果数据格式: 错误码code,数据data对象,消息对象msg
1
2
3
4
5
6
public class Result{
private Object data;
private Integer code;
private String msg;
// 构造方法可以有空参的, 带msg(失败)和不带msg(成功)
}
定义统一响应码
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.xxx.controller;
public class Code {
public static final Integer SAVE_OK = 20011;
public static final Integer DELETE_OK = 20021;
public static final Integer UPDATE_OK = 20031;
public static final Integer GET_OK = 20041;
public static final Integer SAVE_ERR = 20010;
public static final Integer DELETE_ERR = 20020;
public static final Integer UPDATE_ERR = 20030;
public static final Integer GET_ERR = 20040;
}
改造controller层返回数据封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@PostMapping
public Result save(@RequestBody Book book) {
boolean isSuccess = bookService.save(book);
return new Result(isSuccess ? Code.SAVE_OK : Code.SAVE_ERR, isSuccess);
}
@PutMapping
public Result update(@RequestBody Book book) {
boolean isSuccess = bookService.update(book);
return new Result(isSuccess ? Code.UPDATE_OK : Code.UPDATE_ERR, isSuccess);
}
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) {
boolean isSuccess = bookService.delete(id);
return new Result(isSuccess ? Code.DELETE_OK : Code.DELETE_ERR, isSuccess);
}
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
Book book = bookService.getById(id);
Integer code = book != null ? Code.GET_OK : Code.GET_ERR;
String msg = book != null ? "" : "数据查询失败, 请重试";
return new Result(code, book, msg);
}
@GetMapping
public Result getAll() {
List<Book> bookList = bookService.getAll();
Integer code = bookList != null ? Code.GET_OK : Code.GET_ERR;
String msg = bookList != null ? "" : "数据查询失败, 请重试";
return new Result(code, bookList, msg);
}
测试
1
2
3
4
5
6
7
8
9
10
11
12
GET http://localhost/books/1
// 响应结果
{
"data": {
"id": 1,
"type": "文学",
"name": "老人与海",
"description": "海明威代表作, 一个人可以被消灭,但是不能被打败-修改test11"
},
"code": 20041,
"msg": ""
}
2.7.8异常处理器
2.7.8.1异常的种类及出现异常的原因:
- 框架内部抛出的异常:因使用不合规导致
- 数据层抛出的异常:因外部服务器故障导致(例如:服务器访问超时)
- 业务层抛出的异常:因业务逻辑书写错误导致(例如:遍历业务书写操作,导致索引异常等)
- 表现层抛出的异常:因数据收集、校验等规则导致(例如:不匹配的数据类型间导致异常)
- 工具类抛出的异常:因工具类书写不严谨不够健壮导致(例如:必要释放的连接长期未释放等)
看完上面这些出现异常的位置,你会发现,在我们开发的任何一个位置都有可能出现异常,而且这些异常是不能避免的。所以我们就得将异常进行处理。
-
各个层级均出现异常,异常处理代码书写在哪一层?
==所有的异常均抛出到表现层进行处理==
-
异常的种类很多,表现层如何将所有的异常都处理到呢?
==异常分类==
-
表现层处理异常,每个方法中单独书写,代码书写量巨大且意义不强,如何解决?
==AOP==
2.7.8.2 SpringMVC的统一异常处理
对于上面这些问题及解决方案,SpringMVC已经为我们提供了一套解决方案:
-
异常处理器:
- 集中的、统一的处理项目中出现的异常
1 2 3 4 5 6 7 8 9
@RestControllerAdvice // 标识当前类为REST风格对应的异常处理器 public class ProjectExceptionAdvice { // 除了自定义的异常处理器,保留对Exception类型的异常处理,用于处理非预期的异常 @ExceptionHandler(Exception.class) public Result doException(Exception ex) { System.out.println("catch a ex! " + ex); return new Result(666, null,"catch a exception."); } }
配置包扫描的时候, 确保SpringMvcConfig能够扫到该类
在程序getById方法中加个异常,调用该方法后结果如下
1
2
3
4
5
6
7
###
GET http://localhost/books/1
{
"data": null,
"code": 666,
"msg": "catch a exception."
}
这样, 就算后端服务出了异常, 也能按照和前端约好的数据格式返回给前端了
知识点1:@RestControllerAdvice
名称 | @RestControllerAdvice |
---|---|
类型 | ==类注解== |
位置 | Rest风格开发的控制器增强类定义上方 |
作用 | 为Rest风格开发的控制器类做增强 |
说明:此注解自带@ResponseBody注解与@Component注解,具备对应的功能
知识点2:@ExceptionHandler
名称 | @ExceptionHandler |
---|---|
类型 | ==方法注解== |
位置 | 专用于异常处理的控制器方法上方 |
作用 | 设置指定异常的处理方案,功能等同于控制器方法, 出现异常后终止原始控制器执行,并转入当前方法执行 |
说明:此类方法可以根据处理的异常不同,制作多个方法分别处理对应的异常
2.7.8.2 项目异常处理方案
异常的分类的必要性 如果对每一个异常都进行配置一个异常处理器, 异常太多, 无法面面俱到,也比较繁琐.
如果只使用一个异常的话, 也无法区分出到底是哪个层面出现异常, 定位问题
项目异常分类
- 业务异常
- 规范的用户行为产生的异常
- 不规范的用户行为产生的异常
- 系统异常
- 项目运行过程中可以预计但是无法避免的异常
- 其他异常
- 编程人员未预期到的异常
项目异常处理方案
- 业务异常(Business Exception)
- 发送对应消息传递给用户, 提醒规范操作
- 系统异常(System Exception)
- 发送固定消息传递给用户, 安抚用户
- 发送特定消息给运维人员, 提醒用户
- 记录日志
- 其他异常(Exception)
- 发送固定消息传递给用户, 安抚用户
- 发送特定消息给运编程人员, 提醒维护(纳入预期范围内)
- 记录日志
2.7.8.3异常处理实现
代码目录
思路: 定义系统异常和业务异常类, 对应业务场景, 把异常抛出去, 然后通过异常处理器在表现层捕捉, 在异常通知对应的异常类型方法进行处理, 获取异常错误码,和错误信息, 最后响应给客户端
1.定义业务异常和系统异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BusinessException extends RuntimeException{
private Integer code;
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public BusinessException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SystemException extends RuntimeException{
private Integer code;
public SystemException(Integer code,String message) {
super(message);
this.code = code;
}
public SystemException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}
2.定义异常错误码
1
2
3
4
5
6
7
8
public class Code {
// 异常错误码
public static final Integer SYSTEM_ERR = 50001;
public static final Integer SYSTEM_TIMEOUT_ERR = 50002;
public static final Integer BUSINESS_ERR = 50003;
public static final Integer SYSTEM_UNKNOW_ERR = 50009;
}
3.定义表现层异常通知类异常处理方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestControllerAdvice
public class ProjectExceptionAdvice {
// 兜底异常,处理其他异常
@ExceptionHandler(Exception.class)
public Result doException(Exception ex) {
// 记录日志, 发送消息给运维, 发送邮件给开发人员, ex对象发送给开发人员
return new Result(Code.SYSTEM_UNKNOW_ERR, null, "系统繁忙, 请稍后再试!");
}
@ExceptionHandler(SystemException.class)
public Result doSystemException(SystemException ex) {
return new Result(ex.getCode(), null, ex.getMessage());
}
@ExceptionHandler(BusinessException.class)
public Result doBusinessException(BusinessException ex) {
return new Result(ex.getCode(), null, ex.getMessage());
}
}
4.使用异常场景
1
2
3
4
5
6
7
8
// 根据id获取book
public Book getById(Integer id) {
// 传入的id值不规范, 抛出业务异常, 让异常处理器捕捉
if (id < 0) {
throw new BusinessException(Code.BUSINESS_ERR, "请勿进行非法操作");
}
return bookDao.getById(id);
}
5.Test
1
2
3
4
5
6
GET http://localhost/books/-1
{
"data": null,
"code": 50003,
"msg": "请勿进行非法操作"
}
2.7.8.4前端页面
前端页面的 增 删 改 查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
<!DOCTYPE html>
<html>
<head>
<!-- 页面meta -->
<meta charset="utf-8">
<title>SpringMVC案例</title>
<!-- 引入样式 -->
<link rel="stylesheet" href="../plugins/elementui/index.css">
<link rel="stylesheet" href="../plugins/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="../css/style.css">
</head>
<body class="hold-transition">
<div id="app">
<div class="content-header">
<h1>图书管理</h1>
</div>
<div class="app-container">
<div class="box">
<div class="filter-container">
<el-input placeholder="图书名称" v-model="searchBookName" style="width: 200px;"
class="filter-item"></el-input>
<el-button type="primary" class="butT" @click="getByName()">查询</el-button>
<el-button type="primary" class="butT" @click="resetSearch()">重置</el-button>
<el-button type="primary" class="butT" @click="openSave()">新建</el-button>
</div>
<el-table size="small" current-row-key="id" :data="dataList" stripe highlight-current-row>
<el-table-column type="index" align="center" label="序号"></el-table-column>
<el-table-column prop="type" label="图书类别" align="center"></el-table-column>
<el-table-column prop="name" label="图书名称" align="center"></el-table-column>
<el-table-column prop="description" label="描述" align="center"></el-table-column>
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="openEdit(scope.row.id)">编辑</el-button>
<el-button size="mini" type="danger" @click="removeById(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
class="pagiantion"
@current-change="handleCurrentChange"
:current-page="pagination.currentPage"
:page-size="pagination.pageSize"
layout="total, prev, pager, next, jumper"
:total="pagination.total">
</el-pagination>
</div>
<!-- 新增标签弹层 -->
<div class="add-form">
<el-dialog title="新增图书" :visible.sync="dialogFormVisible">
<el-form ref="dataAddForm" :model="formData" :rules="rules" label-position="right"
label-width="100px">
<el-row>
<el-col :span="12">
<el-form-item label="图书类别" prop="type">
<el-input v-model="formData.type"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="图书名称" prop="name">
<el-input v-model="formData.name"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="描述">
<el-input v-model="formData.description" type="textarea"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="saveBook()">确定</el-button>
</div>
</el-dialog>
</div>
<!-- 编辑标签弹层 -->
<div class="add-form">
<el-dialog title="编辑图书" :visible.sync="dialogFormVisible4Edit">
<el-form ref="dataAddForm" :model="formData" :rules="rules" label-position="right"
label-width="100px">
<el-row>
<el-col :span="12">
<el-form-item label="图书类别" prop="type">
<el-input v-model="formData.type"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="图书名称" prop="name">
<el-input v-model="formData.name"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="描述">
<el-input v-model="formData.description" type="textarea"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible4Edit = false">取消</el-button>
<el-button type="primary" @click="editSaveBook()">确定</el-button>
</div>
</el-dialog>
</div>
</div>
</div>
</div>
</body>
<!-- 引入组件库 -->
<script src="../js/vue.js"></script>
<script src="../plugins/elementui/index.js"></script>
<script type="text/javascript" src="../js/jquery.min.js"></script>
<script src="../js/axios-0.18.0.js"></script>
<script>
var vue = new Vue({
el: '#app',
data: {
searchBookName: '',
dataList: [],//当前页要展示的分页列表数据
formData: {},//表单数据
dialogFormVisible: false,//增加表单是否可见
dialogFormVisible4Edit: false,//编辑表单是否可见
pagination: {},//分页模型数据,暂时弃用
},
created() {
this.getById()
},
methods: {
// 重置表单
resetForm() {
//清空输入框
this.formData = {};
},
// 弹出添加窗口
openSave() {
this.dialogFormVisible = true;
this.resetForm();
},
//添加
saveBook() {
axios.post("/books", this.formData).then((res) => {
console.log(res)
if (res.data.code == 20011) {
this.$message.success("添加成功")
this.dialogFormVisible = false;
} else if (res.data.code != 20011) {
this.$message.error(res.data.msg)
}
}).finally(() => {
this.getById()
});
},
// 弹出编辑窗口, 然后掉后端接口getById回显数据
openEdit(bookId) {
this.dialogFormVisible4Edit = true;
axios.get("/books/" + bookId).then((res) => {
this.formData = res.data.data
})
},
// 编辑保存书籍
editSaveBook() {
axios.put("/books", this.formData).then((res) => {
if (res.data.code == 20031) {
this.dialogFormVisible4Edit = false;
this.$message.success("修改成功")
} else {
this.$message.error(res.data.msg)
}
}).finally(() => {
this.getById()
})
},
// 删除
removeById(bookId) {
this.$confirm("是否确认删除?", "提示", {
type: 'info'
}).then(() => {
axios.delete("/books/" + bookId).then((res) => {
if (res.data.code == 20021) {
this.$message.success("删除成功")
this.getById()
} else {
this.$message.error("删除失败")
}
})
}).catch(() => {
this.$message.error("取消删除")
})
},
// getById 或getAll, 根据bookId是否有值
getById(bookId) {
console.log('bookId=' + bookId)
// 如果bookId不为空,就把id传给后端,调用getById,为空时就不传递id,调用getAll
axios.get("/books/" + (!bookId ? '' : bookId)).then((res) => {
if (!bookId) {
this.dataList = res.data.data
} else {
this.dataList = [res.data.data];
}
});
},
// 按书名查找
getByName() {
if (!this.searchBookName) {
return
} else {
axios.get('/books/search/' + this.searchBookName).then((res) => {
this.dataList = res.data.data
});
}
},
// 重置查询
resetSearch() {
this.searchBookName = '';
this.getById();
}
}
})
</script>
</html>
2.8拦截器
2.8.1拦截器概念
(1)浏览器发送一个请求会先到Tomcat的web服务器
(2)Tomcat服务器接收到请求以后,会去判断请求的是静态资源还是动态资源
(3)如果是静态资源,会直接到Tomcat的项目部署目录下去直接访问
(4)如果是动态资源,就需要交给项目的后台代码进行处理
(5)在找到具体的方法之前,我们可以去配置过滤器(可以配置多个),按照顺序进行执行
(6)然后进入到到中央处理器(SpringMVC中的内容),SpringMVC会根据配置的规则进行拦截
(7)如果满足规则,则进行处理,找到其对应的controller类中的方法进行执行,完成后返回结果
(8)如果不满足规则,则不进行处理
(9)这个时候,如果我们需要在每个Controller方法执行的前后添加业务,具体该如何来实现?
这个就是拦截器要做的事。
- 拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行
- 作用:
- 在指定的方法调用前后执行预先设定的代码
- 阻止原始方法的执行
- 总结:拦截器就是用来做增强
看完以后,大家会发现
- 拦截器和过滤器在作用和执行顺序上也很相似
所以这个时候,就有一个问题需要思考:拦截器和过滤器之间的区别是什么?
- 归属不同:Filter属于Servlet技术,Interceptor属于SpringMVC技术
- 拦截内容不同:Filter对所有访问进行增强,Interceptor仅针对SpringMVC的访问进行增强
2.8.1拦截器quickstart
1.配置拦截器功能类 (实现HandlerInterceptor接口)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class ProjectInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("ProjectInterceptor.preHandle");
return true; // true向下执行, false, 请求在prePost拦截住了
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("ProjectInterceptor.postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("ProjectInterceptor.afterCompletion");
}
}
2.配置拦截器执行的位置
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
@Configuration
@ComponentScan("com.xxx.controller")
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {
@Autowired
private ProjectInterceptor projectInterceptor;
// 设置默认响应为UTF-8解码
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> converter : converters) {
if (StringHttpMessageConverter.class.isAssignableFrom(converter.getClass())) {
((StringHttpMessageConverter) converter).setDefaultCharset(Charset.forName("UTF-8"));
}
}
}
// 静态资源映射路径
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
}
// 配置拦截器拦截路径
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(projectInterceptor).addPathPatterns("/books", "/books/*");
}
}
2.8.2拦截器执行流程
2.8.3拦截器的三个方法
前置处理
1
2
3
4
5
6
7
8
9
10
11
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 最有用处
// 可以做校验, 来决定是否进入控制器
String contentType = request.getHeader("Content-Type");
System.out.println("handler = " + handler.getClass());
HandlerMethod hm = (HandlerMethod) handler;
Method method = hm.getMethod();
// 反射方法
System.out.println("ProjectInterceptor.preHandle ... contentType" + contentType);
return true;
}
参数
- requestd: 请求对象
-
response: 响应对象
- handler:被调用的处理器对象, 本质上是一个对象方法, 对反射技术中的Method对象进行了包装
返回值
- 返回值为false, 被拦截的处理器将不执行
后置处理
1
2
3
4
5
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
// modelAndView 页面跳转
System.out.println("ProjectInterceptor.postHandle");
}
参数:
- modelAndView: 如果处理器执行完成具有返回结果, 可以读取到对应的数据与页面信息, 并进行调整
完成后处理(前后端分离后, 用json传参, 基本不用页面跳转了)
1
2
3
4
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 可以拿到异常
System.out.println("ProjectInterceptor.afterCompletion");
}
参数
- ex如果处理器执行过程中出现异常对象, 可以针对异常情况进行单独处理
2.8.4多个拦截器执行顺序
当配置多个拦截器的执行顺序, 形成拦截器链
拦截器的运行顺序以拦截器添加的顺序为准
1
2
3
4
5
6
// 配置拦截器拦截路径
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(projectInterceptor).addPathPatterns("/books", "/books/*");
registry.addInterceptor(projectInterceptor2).addPathPatterns("/books", "/books/*");
}
- 当拦截器中出现对原始处理器的拦截,后面的拦截器均终止运行
- 当拦截器运行中断,仅运行配置在前面的拦截器的afterCompletion操作
preHandle:与配置顺序相同,必定运行
postHandle:与配置顺序相反,可能不运行
afterCompletion:与配置顺序相反,可能不运行。
3.SpringBoot
3.1quickstart
-
创建新模块, 选择Spring Initializer, 并配置模块基础信息, jdk,maven
-
选择当前模块需要使用的技术集
勾选Web下面的Spring Web
-
开发控制器类
1 2 3 4 5 6 7 8 9
@RestController @RequestMapping("/book") public class BookController { @GetMapping("/{id}") public String getById(@PathVariable Integer id) { System.out.println("id ==>" + id); return "hello, spring boot!"; } }
-
运行Application类的main方法
1 2 3 4 5 6
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
还可以再Spring官网上创建Springboot项目
Spring boot程序所包含基础文件
Application.java启动类
pom.xml
Springboot与SpringMVC/Spring的对比
3.2 SpringBoot项目快速启动
-
对SpringBoot项目打包(执行maven package命令
-
执行命令
java -jar springboot_01_quickstart-0.0.1-SNAPSHOT.jar
java -jar命令启动需要依赖maven插件支持, 请确认打包时是否具有SpringBoot对应的maven插件
1 2 3 4 5 6 7 8
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
这个打印的时fat jar, 生成对应入口类的信息(main方法的类)
非fat jar
.\\spring_01_quickstart-1.0-SNAPSHOT.jar中没有主清单属性
3.3SpringBoot起步依赖
Spring缺点(在boot的对比下)
- 配置繁琐
- 依赖配置繁琐
SpringBoot的优点
- 自动配置
- 起步依赖(简化依赖配置)
- 辅助配置(内置服务器, …)
我们使用 Spring Initializr
方式创建的 Maven
工程的的 pom.xml
配置文件中自动生成了很多包含 starter
的依赖,如下图
这些依赖就是==启动依赖==,接下来我们探究一下他是如何实现的。
3.3.1 探索父工程
从上面的文件中可以看到指定了一个父工程,我们进入到父工程,发现父工程中又指定了一个父工程,如下图所示
再进入到该父工程中,在该工程中我们可以看到配置内容结构如下图所示
上图中的 properties
标签中定义了各个技术软件依赖的版本,避免了我们在使用不同软件技术时考虑版本的兼容问题。在 properties
中我们找 servlet
和 mysql
的版本如下图
dependencyManagement
标签是进行依赖版本锁定,但是并没有导入对应的依赖;如果我们工程需要那个依赖只需要引入依赖的 groupid
和 artifactId
不需要定义 version
。
而 build
标签中也对插件的版本进行了锁定,如下图
看完了父工程中 pom.xml
的配置后不难理解我们工程的的依赖为什么都没有配置 version
。
3.1.2 探索依赖
在我们创建的工程中的 pom.xml
中配置了如下依赖
进入到该依赖,查看 pom.xml
的依赖会发现它引入了如下的依赖
里面的引入了 spring-web
和 spring-webmvc
的依赖,这就是为什么我们的工程中没有依赖这两个包还能正常使用 springMVC
中的注解的原因。
而依赖 spring-boot-starter-tomcat
,从名字基本能确认内部依赖了 tomcat
,所以我们的工程才能正常启动。
==结论:以后需要使用技术,只需要引入该技术对应的起步依赖即可==
3.1.3 小结
starter
SpringBoot
中常见项目名称,定义了当前项目使用的所有项目坐标,以达到减少依赖配置的目的
parent
-
所有
SpringBoot
项目要继承的项目,定义了若干个坐标版本号(依赖管理,而非依赖),以达到减少依赖冲突的目的 -
spring-boot-starter-parent
(2.5.0)与spring-boot-starter-parent
(2.4.6)共计57处坐标版本不同
实际开发
-
使用任意坐标时,仅书写GAV中的G和A,V由SpringBoot提供
G:groupid
A:artifactId
V:version
-
如发生坐标错误,再指定version(要小心版本冲突)
3.4 程序启动
创建的每一个 SpringBoot
程序时都包含一个类似于下面的类,我们将这个类称作引导类
1
2
3
4
5
6
@SpringBootApplication
public class Springboot01QuickstartApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot01QuickstartApplication.class, args);
}
}
==注意:==
-
SpringBoot
在创建项目时,采用jar的打包方式 -
SpringBoot
的引导类是项目的入口,运行main
方法就可以启动项目因为我们在
pom.xml
中配置了spring-boot-starter-web
依赖,而该依赖通过前面的学习知道它依赖tomcat
,所以运行main
方法就可以使用tomcat
启动咱们的工程。
3.5 切换web服务器
现在我们启动工程使用的是 tomcat
服务器,那能不能不使用 tomcat
而使用 jetty
服务器,jetty
在我们 maven
高级时讲 maven
私服使用的服务器。而要切换 web
服务器就需要将默认的 tomcat
服务器给排除掉,怎么排除呢?使用 exclusion
标签
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
现在我们运行引导类可以吗?运行一下试试,打印的日志信息如下
程序直接停止了,为什么呢?那是因为排除了 tomcat
服务器,程序中就没有服务器了。所以此时不光要排除 tomcat
服务器,还要引入 jetty
服务器。在 pom.xml
中引入jetty
的起步依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
接下来再次运行引导类,在日志信息中就可以看到使用的是 jetty
服务器
jetty比tomcat更轻量级, 扩展性更强(相较于tomcat, 谷歌应用引擎GAE已经全面切换成jetty)
小结:
通过切换服务器,我们不难发现在使用 SpringBoot
换技术时只需要导入该技术的起步依赖即可。
3.6配置文件
3.6.1 配置文件格式
我们现在启动服务器默认的端口号是 8080
,访问路径可以书写为
1
http://localhost:8080/books/1
在线上环境我们还是希望将端口号改为 80
,这样在访问的时候就可以不写端口号了,如下
1
http://localhost/books/1
而 SpringBoot
程序如何修改呢?SpringBoot
提供了多种属性配置方式
-
application.properties
1
server.port=80
-
application.yml
1 2
server: port: 81
-
application.yaml
1 2
server: port: 82
==注意:
SpringBoot
程序的配置文件名必须是application
,只是后缀名不同而已。==
3.6.2 环境准备
创建一个新工程 springboot_02_base_config
用来演示不同的配置文件,工程环境和入门案例一模一样,结构如下:
在该工程中的 com.itheima.controller
包下创建一个名为 BookController
的控制器。内容如下:
1
2
3
4
5
6
7
8
9
10
@RestController
@RequestMapping("/books")
public class BookController {
@GetMapping("/{id}")
public String getById(@PathVariable Integer id){
System.out.println("id ==> "+id);
return "hello , spring boot!";
}
}
3.6.3 不同配置文件演示
- application.properties配置文件
现在需要进行配置,配合文件必须放在 resources
目录下,而该目录下有一个名为 application.properties
的配置文件,我们就可以在该配置文件中修改端口号,在该配置文件中书写 port
,Idea
就会提示,如下
application.properties
配置文件内容如下:
1
server.port=80
启动服务,会在控制台打印出日志信息,从日志信息中可以看到绑定的端口号已经修改了
- application.yml配置文件
删除 application.properties
配置文件中的内容。在 resources
下创建一个名为 application.yml
的配置文件,在该文件中书写端口号的配置项,格式如下:
1
2
server:
port: 81
==注意: 在
:
后,数据前一定要加空格。==
而在 yml
配置文件中也是有提示功能的,我们也可以在该文件中书写 port
,然后 idea
就会提示并书写成上面的格式
启动服务,可以在控制台看到绑定的端口号是 81
- application.yaml配置文件
删除 application.yml
配置文件和 application.properties
配置文件内容,然后在 resources
下创建名为 application.yaml
的配置文件,配置内容和后缀名为 yml
的配置文件中的内容相同,只是使用了不同的后缀名而已
application.yaml
配置文件内容如下:
1
2
server:
port: 83
启动服务,在控制台可以看到绑定的端口号
==注意:在配合文件中如果没有提示,可以使用一下方式解决==
- 点击
File
选中Project Structure
- 弹出如下窗口,按图中标记红框进行选择
- 通过上述操作,会弹出如下窗口
- 点击上图的
+
号,弹出选择该模块的配置文件
- 通过上述几步后,就可以看到如下界面。
properties
类型的配合文件有一个,ymal
类型的配置文件有两个
3.6.3 三种配合文件的优先级
在三种配合文件中分别配置不同的端口号,启动服务查看绑定的端口号。用这种方式就可以看到哪个配置文件的优先级更高一些
application.properties
文件内容如下:
1
server.port=80
application.yml
文件内容如下:
1
2
server:
port: 81
application.yaml
文件内容如下:
1
2
server:
port: 82
启动服务,在控制台可以看到使用的端口号是 80
。说明 application.properties
的优先级最高
注释掉 application.properties
配置文件内容。再次启动服务,在控制台可以看到使用的端口号是 81
,说明 application.yml
配置文件为第二优先级。
从上述的验证结果可以确定三种配置文件的优先级是:
==application.properties
> application.yml
> application.yaml
==
==注意:==
SpringBoot
核心配置文件名为application
SpringBoot
内置属性过多,且所有属性集中在一起修改,在使用时,通过提示键+关键字修改属性例如要设置日志的级别时,可以在配置文件中书写
logging
,就会提示出来。配置内容如下
1 2 3 logging: level: root: info
3.6.4 yaml
3.7 yaml格式
上面讲了三种不同类型的配置文件,而 properties
类型的配合文件之前我们学习过,接下来我们重点学习 yaml
类型的配置文件。
YAML(YAML Ain’t Markup Language),一种数据序列化格式。这种格式的配置文件在近些年已经占有主导地位,那么这种配置文件和前期使用的配置文件是有一些优势的,我们先看之前使用的配置文件。
最开始我们使用的是 xml
,格式如下:
1
2
3
4
5
<enterprise>
<name>itcast</name>
<age>16</age>
<tel>4006184000</tel>
</enterprise>
而 properties
类型的配置文件如下
1
2
3
enterprise.name=itcast
enterprise.age=16
enterprise.tel=4006184000
yaml
类型的配置文件内容如下
1
2
3
4
enterprise:
name: itcast
age: 16
tel: 4006184000
优点:
-
容易阅读
yaml
类型的配置文件比xml
类型的配置文件更容易阅读,结构更加清晰 -
容易与脚本语言交互
-
以数据为核心,重数据轻格式
yaml
更注重数据,而xml
更注重格式
YAML 文件扩展名:
.yml
(主流).yaml
上面两种后缀名都可以,以后使用更多的还是 yml
的。
3.7.1 语法规则
-
大小写敏感
-
属性层级关系使用多行描述,每行结尾使用冒号结束
-
使用缩进表示层级关系,同层级左侧对齐,只允许使用空格(不允许使用Tab键)
空格的个数并不重要,只要保证同层级的左侧对齐即可。
-
属性值前面添加空格(属性名与属性值之间使用冒号+空格作为分隔)
-
# 表示注释
==核心规则:数据前面要加空格与冒号隔开==
数组数据在数据书写位置的下方使用减号作为数据开始符号,每行书写一个数据,减号与数据间空格分隔,例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 80
logging:
level:
root: info
person:
name: zhangsan
age: 33
gender: female
hobbies:
- sing
- dance
- rap
- basketball
address: sh
3.7.2 yaml数据读取
3.7.2.1 使用 @Value注解
使用 @Value("表达式")
注解可以从配合文件中读取数据,注解中用于读取属性名引用方式是:${一级属性名.二级属性名……}
3.7.2.2 Environment对象
上面方式读取到的数据特别零散,SpringBoot
还可以使用 @Autowired
注解注入 Environment
对象的方式读取数据。这种方式 SpringBoot
会将配置文件中所有的数据封装到 Environment
对象中,如果需要使用哪个数据只需要通过调用 Environment
对象的 getProperty(String name)
方法获取。
3.7.2.3 自定义对象
SpringBoot
还提供了将配置文件中的数据封装到我们自定义的实体类对象中的方式。具体操作如下:
-
将实体类
bean
的创建交给Spring
管理。在类上添加
@Component
注解 -
使用
@ConfigurationProperties
注解表示加载配置文件在该注解中也可以使用
prefix
属性指定只加载指定前缀的数据 -
在
BookController
中进行注入
实体类对象如下
1
2
3
4
5
6
7
8
9
10
@ConfigurationProperties(prefix = "person")
@Component
@Data
public class Person {
private String name;
private Integer age;
private String gender;
private String[] hobbies;
private String address;
}
yaml数据读取代码演示
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
@RestController
@RequestMapping("/books")
public class BookController {
@Value("${person.name}")
private String personName;
@Value("${person.age}")
private Integer age;
@Value("${person.hobbies[0]}")
private String firstHobby;
@Autowired
private Environment environment;
@Autowired
private Person person;
@GetMapping("/{id}")
public String getById(@PathVariable Integer id) {
System.out.println("id ==>" + id);
System.out.println("----------------------------------------");
System.out.println(environment.getProperty("server.port"));
System.out.println(environment.getProperty("logging.level.root"));
System.out.println(environment.getProperty("person.name"));
System.out.println(environment.getProperty("person.age"));
System.out.println(environment.getProperty("person.gender"));
System.out.println(environment.getProperty("person.hobbies"));
System.out.println(environment.getProperty("person.address"));
System.out.println(environment.getProperty("person.girlfriend"));
System.out.println("----------------------------------------");
System.out.println("person = " + person);
return "hello, spring boot!1" + personName + " " + age + " " + firstHobby;
}
}
3.8 yaml多环境配置
application.yml
激活dev环境
1
2
3
spring:
profiles:
active: dev
application-dev.yml
1
2
server:
port: 80
application-prod.yml
1
2
server:
port: 81
现在在idea启动Application.main()的话, 使用的是application.yml激活的application-dev.yml dev环境配置文件
还有一种启动方法不用修改spring.profiles.active=dev/prod, 直接在命令行启动时 加上这行配置
#默认dev环境
java -jar .\springboot_01_quickstart-0.0.1-SNAPSHOT.jar
# prod环境
java -jar .\springboot_01_quickstart-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
# 用dev环境且不用dev的80端口
java -jar .\springboot_01_quickstart-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev --server.port=8080
命令行参数的优先度参数比配置文件高
进行测试后就会发现命令行设置的端口号优先级高(也就是使用的是命令行设置的端口号),配置的优先级其实 SpringBoot
官网已经进行了说明,参见 :
1
https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config
进入上面网站后会看到如下页面
如果使用了多种方式配合同一个配置项,优先级高的生效。
3.9 maven与springBoot多环境兼容
在maven中配置属性, 然后application.yml读取maven中的配置
- application.yml
1
2
3
4
spring:
profiles:
active: ${profile.active}
- 在maven中设置属性: profile.active
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<profiles>
<profile>
<id>prod</id>
<properties>
<profile.active>prod</profile.active>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>test</id>
<properties>
<profile.active>test</profile.active>
</properties>
</profile>
<profile>
<id>dev</id>
<properties>
<profile.active>dev</profile.active>
</properties>
</profile>
</profiles>
- 添加maven插件(用来解析${profile.active})
1
2
3
4
5
6
7
8
9
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<encoding>UTF-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
</configuration>
</plugin>
3.10 配置文件分类
有这样的场景,我们开发完毕后需要测试人员进行测试,由于测试环境和开发环境的很多配置都不相同,所以测试人员在运行我们的工程时需要临时修改很多配置,如下
1
java –jar springboot.jar –-spring.profiles.active=test --server.port=85 --server.servlet.context-path=/heima --server.tomcat.connection-timeout=-1 …… …… …… …… ……
针对这种情况,SpringBoot
定义了配置文件不同的放置的位置;而放在不同位置的优先级时不同的。
SpringBoot
中4级配置文件放置位置:
- 1级:classpath:application.yml
- 2级:classpath:config/application.yml
- 3级:file :application.yml
- 4级:file :config/application.yml
==说明:==级别越高优先级越高
3.10.1 代码演示
在这里我们只演示不同级别配置文件放置位置的优先级。
1 环境准备
创建一个名为 springboot_06_config_file
的 SpringBoot
工程,目录结构如下
在 resources
下创建一个名为 config
的目录,在该目录中创建 application.yml
配置文件,而在该配置文件中将端口号设置为 81
,内容如下
1
2
server:
port: 81
而在 resources
下创建的 application.yml
配置文件中并将端口号设置为 80
,内容如下
1
2
server:
port: 80
2 验证1级和2级的优先级
运行启动引导类,可以在控制台看到如下日志信息
通过这个结果可以得出==类路径下的 config
下的配置文件优先于类路径下的配置文件。==
3 验证2级和4级的优先级
要验证4级,按照以下步骤完成
-
将工程打成
jar
包点击工程的
package
来打jar
包 -
在硬盘上找到
jar
包所在位置 -
在
jar
包所在位置创建config
文件夹,在该文件夹下创建application.yml
配置文件,而在该配合文件中将端口号设置为82
-
在命令行使用以下命令运行程序
1
java -jar springboot_06_config_file-0.0.1-SNAPSHOT.jar
运行后日志信息如下
通过这个结果可以得出==file:
config
下的配置文件优先于类路径下的配置文件。==
3.11 SpringBoot整合junit
在 test/java
下创建 com.itheima
包,在该包下创建测试类,将 BookService
注入到该测试类中
1
2
3
4
5
6
7
8
9
10
@SpringBootTest
class BookServiceTest {
@Autowired
private BookService bookService;
@Test
void save() {
bookService.save();
}
}
==注意:==这里的引导类所在包必须是测试类所在包及其子包。
例如:
- 引导类所在包是
example
- 测试类所在包是
example
如果不满足这个要求的话,就需要在使用
@SpringBootTest
注解时,使用classes
属性指定引导类的字节码对象。如@SpringBootTest(classes = Application.class)
3.12 springboot整合mybatis
1.复习spring整合mybatis
- springConfig
- 导入jdbcConfig
- 导入MybatisConfig
- jdbcConfig
- 定义数据源, 加载jdbc.properties
- MybatisConfig
- 定义SqlSessionFactoryBean
- 定义映射配置路径
2.springboot整合mybatis
1.创建SpringBoot工程, 选择mybatis,mysql, lombok依赖
2.实体类
1
2
3
4
5
6
7
@Data
public class Book {
private Integer id;
private String type;
private String name;
private String description;
}
3.dao层 设置@Mapper注解
1
2
3
4
5
@Mapper
public interface BookDao {
@Select("select * from tbl_book where id=#{id}")
Book getById(Integer id);
}
4.配置数据源
1
2
3
4
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
5.测试
1
2
3
4
5
6
7
8
9
10
@SpringBootTest
class SpringbootApplicationTest {
@Autowired
private BookDao bookDao;
@Test
void getById() {
Book book = bookDao.getById(1);
System.out.println("book = " + book);
}
}
如果不想使用默认的hikari数据源, 可以选择druid数据源, 加入配置和maven依赖
1 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
1 2 3 4 5 <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.6</version> </dependency>
3.13 SpringBoot改造之前的ssm整合项目
改造之前 SSM
时做的三大框架整合的案例用 SpringBoot
来实现一下。我们完成这个案例基本是将之前做的拷贝过来,修改成 SpringBoot
的即可,主要从以下几部分完成
-
pom.xml
配置起步依赖,必要的资源坐标(druid)
-
application.yml
设置数据源、端口等
-
配置类
全部删除
-
dao
设置@Mapper
-
测试类
-
页面
放置在resources目录下的static目录中
3.13.1 创建工程
创建 SpringBoot
工程,在创建工程时需要勾选 web
、mysql
、mybatis
,工程目录结构如下
由于我们工程中使用到了 Druid
,所以需要导入 Druid
的坐标
1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
3.13.2 代码拷贝
将 springmvc_11_page
工程中的 java
代码及测试代码连同包拷贝到 springboot_09_ssm
工程,按照下图进行拷贝
需要修改的内容如下:
-
Springmvc_11_page
中config
包下的是配置类,而SpringBoot
工程不需要这些配置类,所以这些可以直接删除 -
dao
包下的接口上在拷贝到springboot_09-ssm
工程中需要在接口中添加@Mapper
注解 -
BookServiceTest
测试需要改成SpringBoot
整合junit
的1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
@SpringBootTest public class BookServiceTest { @Autowired private BookService bookService; @Test public void testGetById(){ Book book = bookService.getById(2); System.out.println(book); } @Test public void testGetAll(){ List<Book> all = bookService.getAll(); System.out.println(all); } }
3.13.3 配置文件
在 application.yml
配置文件中需要配置如下内容
- 服务的端口号
- 连接数据库的信息
- 数据源
1
2
3
4
5
6
7
8
9
10
server:
port: 80
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db #?servierTimezone=UTC
username: root
password: root
3.13.4 静态资源
在 SpringBoot
程序中是没有 webapp
目录的,那么在 SpringBoot
程序中静态资源需要放在什么位置呢?
静态资源需要放在 resources
下的 static
下,如下图所示
加index.html, 跳转pages/books.html
1
2
3
<script>
document.location.href="pages/books.html"
</script>
4.MybatisPlus
MybatisPlus (简称MP) 是基于MybatisPlus基础框架上开发的增强型工具, 简化开发, 提高效率
4.1.quickstart
- SpringBoot整合Mybatis开发过程(概要)
- 创建Spring工程
- 勾选使用的依赖
- 设置DataSource相关属性(JDBC参数, 数据源druid)
- 定义数据层接口映射配置(加@Mapper注解)
4.1.1添加MybatisPlus依赖(重要)
注意SpringBoot3需要的mybatis-spring的版本也要大于3, 否则启动不了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter-test</artifactId>
<version>3.5.7</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
4.1.2准备数据
sql
1
2
3
4
5
6
7
8
9
10
11
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL,
`name` varchar(32) NOT NULL,
`password` varchar(32) NOT NULL,
`age` int(3) NOT NULL,
`tel` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `user` VALUES (1,'zhangsan','123456',20,'17712345678'),(2,'lisi','123456',22,'17612345678'),(3,'zangxin','123456',32,'17512345678'),(4,'TomCat','123456',42,'17412345678');
实体类
1
2
3
4
5
6
7
8
@Data
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
}
4.1.3 dao层(核心: 继承BaseMapper接口)
1
2
3
@Mapper
public interface UserDao extends BaseMapper<User> {
}
说明:Dao接口要想被容器扫描到,有两种解决方案:
- 方案一:在Dao接口上添加
@Mapper
注解,并且确保Dao处在引导类所在包或其子包中
- 该方案的缺点是需要在每一Dao接口中添加注解
- 方案二:在引导类上添加
@MapperScan
注解,其属性为所要扫描的Dao所在包
- 该方案的好处是只需要写一次,则指定包下的所有Dao接口都能被扫描到,
@Mapper
就可以不写。
4.1.4配置数据源
1
2
3
4
5
6
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: root
4.1.5测试
1
2
3
4
5
6
7
8
9
10
@SpringBootTest
class UserDaoTest {
@Autowired
private UserDao userDao;
@Test
void testGetAll() {
List<User> users = userDao.selectList(null);
System.out.println(users);
}
}
4.2 Mybatis简介
MP的特性:
- 无侵入:只做增强不做改变,不会对现有工程产生影响
- 强大的 CRUD 操作:内置通用 Mapper,少量配置即可实现单表CRUD 操作
- 支持 Lambda:编写查询条件无需担心字段写错
- 支持主键自动生成
- 内置分页插件
- ……
4.3标准数据层开发
4.3.1基本crud
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
@SpringBootTest
class UserDaoTest {
@Autowired
private UserDao userDao;
@Test
void testSave() {
User user = new User(null,"hyy","123hyy",25,"312321323");
int insert = userDao.insert(user);
Assertions.assertEquals(1,insert);
}
@Test
void testDel() {
int rows = userDao.deleteById(1820685750235181058L);
Assertions.assertEquals(1,rows);
}
@Test
void testUpdate() {
User user = new User(1L, "zhangsan333", "1234", null, null);
int rows = userDao.updateById(user);
Assertions.assertEquals(1,rows);
}
@Test
void testGetById() {
User user = userDao.selectById(1L);
Assertions.assertNotNull(user);
Assertions.assertNotNull(user.getName());
}
@Test
void testGetAll() {
List<User> users = userDao.selectList(null);
System.out.println(users);
}
}
4.3.2分页
1.添加MP分页拦截器
1
2
3
4
5
6
7
8
9
10
11
@SpringBootConfiguration
public class MpConfig {
@Bean
public MybatisPlusInterceptor mpInterceptor() {
// 1.定义MP拦截器
MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
// 2.添加分页拦截器
mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mpInterceptor;
}
}
2.测试
1
2
3
4
5
6
7
8
9
10
11
@Test
void testGetByPage() {
// 开启分页拦截器
IPage page = new Page(2, 3);
userDao.selectPage(page, null);
System.out.println("pageNum=" + page.getCurrent());
System.out.println("pageSize=" + page.getSize());
System.out.println("totalRows=" + page.getTotal());
System.out.println("totalPages=" + page.getPages());
System.out.println("数据=" + page.getRecords());
}
3.开启MP日志, 查看SQL
1
2
3
4
# 开启mp日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
4.3.3.查询
4.3.3.1 LambdaQueryWrapper表达查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
void testGetAllByLambda() {
// 方式二 lambda格式查询
// QueryWrapper<User> lqw = new QueryWrapper<>();
// lqw.lambda().lt(User::getAge, 25);
// 方式三
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.lt(User::getAge, 40)
.gt(User::getAge,22)
.eq(User::getPassword,"123hyy");
List<User> users = userDao.selectList(lqw);
Assertions.assertTrue(users != null && !users.isEmpty());
}
@Test
void testGetAll() {
// 方式一按条件查询
QueryWrapper<User> qw = new QueryWrapper<>();
qw.le("age", 30);
List<User> users = userDao.selectList(qw);
Assertions.assertTrue(users != null && !users.isEmpty());
}
-
测试的时候,控制台打印的日志比较多,速度有点慢而且不利于查看运行结果,所以接下来我们把这个日志处理下:
-
取消初始化spring日志打印,resources目录下添加logback.xml,名称固定,内容如下:
1 2 3
<?xml version="1.0" encoding="UTF-8"?> <configuration> </configuration>
说明:logback.xml的配置内容,不是我们学习的重点,如果有兴趣可以自行百度查询。
-
取消MybatisPlus启动banner图标
application.yml添加如下内容:
1 2 3 4 5 6
# mybatis-plus日志控制台输出 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: banner: off # 关闭mybatisplus启动图标
-
取消SpringBoot的log打印
application.yml添加如下内容:
1 2 3
spring: main: banner-mode: off # 关闭SpringBoot启动图标(banner)
-
解决控制台打印日志过多的相关操作可以不用去做,一般会被用来方便我们查看程序运行的结果。
4.3.3.2 条件查询null值判断
先来看一张图,
- 我们在做条件查询的时候,一般会有很多条件可以供用户进行选择查询。
- 这些条件用户可以选择使用也可以选择不使用,比如我要查询价格在8000以上的手机
- 在输入条件的时候,价格有一个区间范围,按照需求只需要在第一个价格输入框中输入8000
- 后台在做价格查询的时候,一般会让 price>值1 and price <值2
- 因为前端没有输入值2,所以如果不处理的话,就会出现 price>8000 and price < null问题
- 这个时候查询的结果就会出问题,具体该如何解决?
需求:查询数据库表中,根据输入年龄范围来查询符合条件的记录
用户在输入值的时候,
如果只输入第一个框,说明要查询大于该年龄的用户
如果只输入第二个框,说明要查询小于该年龄的用户
如果两个框都输入了,说明要查询年龄在两个范围之间的用户
思考第一个问题:后台如果想接收前端的两个数据,该如何接收?
我们可以使用两个简单数据类型,也可以使用一个模型类,但是User类中目前只有一个age属性,如:
1
2
3
4
5
6
7
8
@Data
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
}
使用一个age属性,如何去接收页面上的两个值呢?这个时候我们有两个解决方案
方案一:添加属性age2,这种做法可以但是会影响到原模型类的属性内容
1
2
3
4
5
6
7
8
9
@Data
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
private Integer age2;
}
方案二:新建一个模型类,让其继承User类,并在其中添加age2属性,UserQuery在拥有User属性后同时添加了age2属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
}
@Data
public class UserQuery extends User {
private Integer age2;
}
环境准备好后,我们来实现下刚才的需求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
//模拟页面传递过来的查询数据
UserQuery uq = new UserQuery();
uq.setAge(10);
uq.setAge2(30);
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
if(null != uq.getAge2()){
lqw.lt(User::getAge, uq.getAge2());
}
if( null != uq.getAge()) {
lqw.gt(User::getAge, uq.getAge());
}
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
}
}
上面的写法可以完成条件为非空的判断,但是问题很明显,如果条件多的话,每个条件都需要判断,代码量就比较大,来看MP给我们提供的简化方式:
1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testNullValueQuery() {
UserQuery uq = new UserQuery();
uq.setAge(30);
uq.setAgeUpper(null);
// null判断
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.ge(null != uq.getAge(), User::getAge, uq.getAge())
.le(null != uq.getAgeUpper(), User::getAge, uq.getAgeUpper());
List<User> users = userDao.selectList(lqw);
System.out.println("users = " + users);
}
lt()方法
condition为boolean类型,返回true,则添加条件,返回false则不添加条件
4.3.3.3查询投影
目前我们在查询数据的时候,什么都没有做默认就是查询表中所有字段的内容,我们所说的查询投影即不查询所有字段,只查询出指定内容的数据。
具体如何来实现?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.select(User::getId,User::getName,User::getAge);
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
}
}
-
select(…)方法用来设置查询的字段列,可以设置多个,最终的sql语句为:
1
SELECT id,name,age FROM user
-
如果使用的不是lambda,就需要手动指定字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14
@SpringBootTest class Mybatisplus02DqlApplicationTests { @Autowired private UserDao userDao; @Test void testGetAll(){ QueryWrapper<User> lqw = new QueryWrapper<User>(); lqw.select("id","name","age","tel"); List<User> userList = userDao.selectList(lqw); System.out.println(userList); } }
- 最终的sql语句为:SELECT id,name,age,tel FROM user
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
@Test
void testGroupBy() {
// 分组
QueryWrapper<User> qw = new QueryWrapper<>();
qw.select("count(*) as count, password")
.groupBy("password");
List<Map<String, Object>> maps = userDao.selectMaps(qw);
System.out.println(maps);
//outputs: [{password=1234, count=1}, {password=123456, count=3}, {password=123hyy, count=2}]
}
@Test
void testCount() {
// 计数
QueryWrapper<User> qw = new QueryWrapper<>();
qw.select("count(*) as count");
List<Map<String, Object>> maps = userDao.selectMaps(qw);
System.out.println(maps); // [{count=6}]
}
@Test
void testProjection() {
// 查询投影
LambdaQueryWrapper<User> qw = new LambdaQueryWrapper<>();
qw.select(User::getId, User::getName, User::getAge);
List<User> users = userDao.selectList(qw);
System.out.println("users = " + users);
}
4.3.4 查询条件
前面我们只使用了lt()和gt(),除了这两个方法外,MP还封装了很多条件对应的方法,这一节我们重点把MP提供的查询条件方法进行学习下。
MP的查询条件有很多:
- 范围匹配(> 、 = 、between)
- 模糊匹配(like)
- 空判定(null)
- 包含性匹配(in)
- 分组(group)
- 排序(order)
- ……
4.3.4.1 等值查询
需求:根据用户名和密码查询用户信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.eq(User::getName, "Jerry").eq(User::getPassword, "jerry");
User loginUser = userDao.selectOne(lqw);
System.out.println(loginUser);
}
}
-
eq(): 相当于
=
,对应的sql语句为1
SELECT id,name,password,age,tel FROM user WHERE (name = ? AND password = ?)
-
selectList:查询结果为多个或者单个
-
selectOne:查询结果为单个
4.3.4.2 范围查询
需求:对年龄进行范围查询,使用lt()、le()、gt()、ge()、between()进行范围查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.between(User::getAge, 10, 30);
//SELECT id,name,password,age,tel FROM user WHERE (age BETWEEN ? AND ?)
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
}
}
- gt():大于(>)
- ge():大于等于(>=)
- lt():小于(<)
- lte():小于等于(<=)
- between():between ? and ?
4.3.4.3 模糊查询
需求:查询表中name属性的值以
J
开头的用户信息,使用like进行模糊查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.likeLeft(User::getName, "J");
//SELECT id,name,password,age,tel FROM user WHERE (name LIKE ?)
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
}
}
- like():前后加百分号,如 %J%
- likeLeft():前面加百分号,如 %J
- likeRight():后面加百分号,如 J%
4.3.4.4 排序查询
需求:查询所有数据,然后按照id降序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SpringBootTest
class Mybatisplus02DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testGetAll(){
LambdaQueryWrapper<User> lwq = new LambdaQueryWrapper<>();
/**
* condition :条件,返回boolean,
当condition为true,进行排序,如果为false,则不排序
* isAsc:是否为升序,true为升序,false为降序
* columns:需要操作的列
*/
lwq.orderBy(true,false, User::getId);
userDao.selectList(lw
}
}
除了上面演示的这种实现方式,还有很多其他的排序方法可以被调用,如图:
- orderBy排序
- condition:条件,true则添加排序,false则不添加排序
- isAsc:是否为升序,true升序,false降序
- columns:排序字段,可以有多个
- orderByAsc/Desc(单个column):按照指定字段进行升序/降序
- orderByAsc/Desc(多个column):按照多个字段进行升序/降序
- orderByAsc/Desc
- condition:条件,true添加排序,false不添加排序
- 多个columns:按照多个字段进行排序
除了上面介绍的这几种查询条件构建方法以外还会有很多其他的方法,比如isNull,isNotNull,in,notIn等等方法可供选择,具体参考官方文档的条件构造器来学习使用,具体的网址为:
https://mp.baomidou.com/guide/wrapper.html#abstractwrapper
4.3.5映射匹配兼容性
前面我们已经能从表中查询出数据,并将数据封装到模型类中,这整个过程涉及到一张表和一个模型类:
之所以数据能够成功的从表中获取并封装到模型对象中,原因是表的字段列名和模型类的属性名一样。
那么问题就来了:
问题1:表字段与编码属性设计不同步
当表的列名和模型类的属性名发生不一致,就会导致数据封装不到模型对象,这个时候就需要其中一方做出修改,那如果前提是两边都不能改又该如何解决?
MP给我们提供了一个注解@TableField
,使用该注解可以实现模型类属性名和表的列名之间的映射关系
问题2:编码中添加了数据库中未定义的属性
当模型类中多了一个数据库表不存在的字段,就会导致生成的sql语句中在select的时候查询了数据库不存在的字段,程序运行就会报错,错误信息为:
==Unknown column ‘多出来的字段名称’ in ‘field list’==
具体的解决方案用到的还是@TableField
注解,它有一个属性叫exist
,设置该字段是否在数据库表中存在,如果设置为false则不存在,生成sql语句查询的时候,就不会再查询该字段了。
问题3:采用默认查询开放了更多的字段查看权限
查询表中所有的列的数据,就可能把一些敏感数据查询到返回给前端,这个时候我们就需要限制哪些字段默认不要进行查询。解决方案是@TableField
注解的一个属性叫select
,该属性设置默认是否需要查询该字段的值,true(默认值)表示默认查询该字段,false表示默认不查询该字段。
知识点1:@TableField
名称 | @TableField |
---|---|
类型 | ==属性注解== |
位置 | 模型类属性定义上方 |
作用 | 设置当前属性对应的数据库表中的字段关系 |
相关属性 | value(默认):设置数据库表字段名称 exist:设置属性在数据库表字段中是否存在,默认为true,此属性不能与value合并使用 select:设置属性是否参与查询,此属性与select()映射配置不冲突 |
问题4:表名与编码开发设计不同步
该问题主要是表的名称和模型类的名称不一致,导致查询失败,这个时候通常会报如下错误信息:
==Table ‘databaseName.tableNaem’ doesn’t exist==,翻译过来就是数据库中的表不存在。
解决方案是使用MP提供的另外一个注解@TableName
来设置表与模型类之间的对应关系。
知识点2:@TableName
名称 | @TableName |
---|---|
类型 | ==类注解== |
位置 | 模型类定义上方 |
作用 | 设置当前类对应于数据库表关系 |
相关属性 | value(默认):设置数据库表名称 |
代码演示
接下来我们使用案例的方式把刚才的知识演示下:
步骤1:修改数据库表user为tbl_user
直接查询会报错,原因是MP默认情况下会使用模型类的类名首字母小写当表名使用。
步骤2:模型类添加@TableName注解
1
2
3
4
5
6
7
8
9
@Data
@TableName("tbl_user")
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
}
步骤3:将字段password修改成pwd
直接查询会报错,原因是MP默认情况下会使用模型类的属性名当做表的列名使用
步骤4:使用@TableField映射关系
1
2
3
4
5
6
7
8
9
10
@Data
@TableName("tbl_user")
public class User {
private Long id;
private String name;
@TableField(value="pwd")
private String password;
private Integer age;
private String tel;
}
步骤5:添加一个数据库表不存在的字段
1
2
3
4
5
6
7
8
9
10
11
@Data
@TableName("tbl_user")
public class User {
private Long id;
private String name;
@TableField(value="pwd")
private String password;
private Integer age;
private String tel;
private Integer online;
}
直接查询会报错,原因是MP默认情况下会查询模型类的所有属性对应的数据库表的列,而online不存在
步骤6:使用@TableField排除字段
1
2
3
4
5
6
7
8
9
10
11
12
@Data
@TableName("tbl_user")
public class User {
private Long id;
private String name;
@TableField(value="pwd")
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
}
步骤7:查询时将pwd隐藏
1
2
3
4
5
6
7
8
9
10
11
12
@Data
@TableName("tbl_user")
public class User {
private Long id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
}
4.4DML控制编程
4.4.1 id生成策略
知识点1:@TableId
名称 | @TableId |
---|---|
类型 | ==属性注解== |
位置 | 模型类中用于表示主键的属性定义上方 |
作用 | 设置当前类中主键属性的生成策略 |
相关属性 | value(默认):设置数据库表主键名称 type:设置主键属性的生成策略,值查照IdType的枚举值 |
4.4.1.2 代码演示
AUTO策略
步骤1:设置生成策略为AUTO
1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
@TableName("tbl_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
}
步骤2:删除测试数据并修改自增值
-
删除测试数据
-
因为之前生成主键ID的值比较长,会把MySQL的自动增长的值变的很大,所以需要将其调整为目前最新的id值。
步骤3:运行新增方法
会发现,新增成功,并且主键id也是从5开始
经过这三步的演示,会发现AUTO
的作用是==使用数据库ID自增==,在使用该策略的时候一定要确保对应的数据库表设置了ID主键自增,否则无效。
接下来,我们可以进入源码查看下ID的生成策略有哪些?
打开源码后,你会发现并没有看到中文注释,这就需要我们点击右上角的Download Sources
,会自动帮你把这个类的java文件下载下来,我们就能看到具体的注释内容。因为这个技术是国人制作的,所以他代码中的注释还是比较容易看懂的。
从源码中可以看到,除了AUTO这个策略以外,还有如下几种生成策略:
- NONE: 不设置id生成策略
- INPUT:用户手工输入id
- ASSIGN_ID:雪花算法生成id(可兼容数值型与字符串型)
- ASSIGN_UUID:以UUID生成算法作为id生成策略
- 其他的几个策略均已过时,都将被ASSIGN_ID和ASSIGN_UUID代替掉。
拓展:
分布式ID是什么?
- 当数据量足够大的时候,一台数据库服务器存储不下,这个时候就需要多台数据库服务器进行存储
- 比如订单表就有可能被存储在不同的服务器上
- 如果用数据库表的自增主键,因为在两台服务器上所以会出现冲突
- 这个时候就需要一个全局唯一ID,这个ID就是分布式ID。
INPUT策略
步骤1:设置生成策略为INPUT
1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
@TableName("tbl_user")
public class User {
@TableId(type = IdType.INPUT)
private Long id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
}
注意:这种ID生成策略,需要将表的自增策略删除掉
步骤2:添加数据手动设置ID
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootTest
class Mybatisplus03DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testSave(){
User user = new User();
//设置主键ID的值
user.setId(666L);
user.setName("黑马程序员");
user.setPassword("itheima");
user.setAge(12);
user.setTel("4006184000");
userDao.insert(user);
}
}
步骤3:运行新增方法
如果没有设置主键ID的值,则会报错,错误提示就是主键ID没有给值:
如果设置了主键ID,则数据添加成功,如下:
ASSIGN_ID策略
步骤1:设置生成策略为ASSIGN_ID
1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
@TableName("tbl_user")
public class User {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
}
步骤2:添加数据不设置ID
1
2
3
4
5
6
@Test
void testInsert() {
User user = new User(null, "hyy", "123", 23, "2324324234", 1);
int rows = userDao.insert(user);
Assertions.assertTrue(rows > 0);
}
注意:这种生成策略,不需要手动设置ID,如果手动设置ID,则会使用自己设置的值。
步骤3:运行新增方法
生成的ID就是一个Long类型的数据。
ASSIGN_UUID策略
步骤1:设置生成策略为ASSIGN_UUID
使用uuid需要注意的是,主键的类型不能是Long,而应该改成String类型
1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
@TableName("tbl_user")
public class User {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
}
步骤2:修改表的主键类型
主键类型设置为varchar,长度要大于32,因为UUID生成的主键为32位,如果长度小的话就会导致插入失败。
步骤3:添加数据不设置ID
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SpringBootTest
class Mybatisplus03DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testSave(){
User user = new User();
user.setName("黑马程序员");
user.setPassword("itheima");
user.setAge(12);
user.setTel("4006184000");
userDao.insert(user);
}
}
步骤4:运行新增方法
接下来我们来聊一聊雪花算法:
雪花算法(SnowFlake),是Twitter官方给出的算法实现 是用Scala写的。其生成的结果是一个64bit大小整数,它的结构如下图:
- 1bit,不用,因为二进制中最高位是符号位,1表示负数,0表示正数。生成的id一般都是用整数,所以最高位固定为0。
- 41bit-时间戳,用来记录时间戳,毫秒级
- 10bit-工作机器id,用来记录工作机器id,其中高位5bit是数据中心ID其取值范围0-31,低位5bit是工作节点ID其取值范围0-31,两个组合起来最多可以容纳1024个节点
- 序列号占用12bit,每个节点每毫秒0开始不断累加,最多可以累加到4095,一共可以产生4096个ID
4.4.1.3 ID生成策略对比
介绍了这些主键ID的生成策略,我们以后该用哪个呢?
- NONE: 不设置id生成策略,MP不自动生成,约等于INPUT,所以这两种方式都需要用户手动设置,但是手动设置第一个问题是容易出现相同的ID造成主键冲突,为了保证主键不冲突就需要做很多判定,实现起来比较复杂
- AUTO:数据库ID自增,这种策略适合在数据库服务器只有1台的情况下使用,不可作为分布式ID使用
- ASSIGN_UUID:可以在分布式的情况下使用,而且能够保证唯一,但是生成的主键是32位的字符串,长度过长占用空间而且还不能排序,查询性能也慢
- ASSIGN_ID:可以在分布式的情况下使用,生成的是Long类型的数字,可以排序性能也高,但是生成的策略和服务器时间有关,如果修改了系统时间就有可能导致出现重复主键
- 综上所述,每一种主键策略都有自己的优缺点,根据自己项目业务的实际情况来选择使用才是最明智的选择。
4.4.1.4 简化配置
前面我们已经完成了表关系映射、数据库主键策略的设置,接下来对于这两个内容的使用,我们再讲下他们的简化配置:
模型类主键策略设置
对于主键ID的策略已经介绍完,但是如果要在项目中的每一个模型类上都需要使用相同的生成策略,如:
确实是稍微有点繁琐,我们能不能在某一处进行配置,就能让所有的模型类都可以使用该主键ID策略呢?
答案是肯定有,我们只需要在配置文件中添加如下内容:
1
2
3
4
mybatis-plus:
global-config:
db-config:
id-type: assign_id
配置完成后,每个模型类的主键ID策略都将成为assign_id.
数据库表与模型类的映射关系
MP会默认将模型类的类名名首字母小写作为表名使用,假如数据库表的名称都以tbl_
开头,那么我们就需要将所有的模型类上添加@TableName
,如:
配置起来还是比较繁琐,简化方式为在配置文件中配置如下内容:
1
2
3
4
mybatis-plus:
global-config:
db-config:
table-prefix: tbl_
设置表的前缀内容,这样MP就会拿 tbl_
加上模型类的首字母小写,就刚好组装成数据库的表名。
4.4.1.5 批量Batch操作
先来看下问题:
之前添加了很多商品到购物车,过了几天发现这些东西又不想要了,该怎么办呢?
很简单删除掉,但是一个个删除的话还是比较慢和费事的,所以一般会给用户一个批量操作,也就是前面有一个复选框,用户一次可以勾选多个也可以进行全选,然后删一次就可以将购物车清空,这个就需要用到批量删除
的操作了。
具体该如何实现多条删除,我们找找对应的API方法
1
2
3
4
5
6
7
8
9
/**
* 删除(根据ID或实体 批量删除)
*
* @param idList 主键ID列表或实体列表(不能为 null 以及 empty)
* @since 3.5.7
*/
default int deleteByIds(@Param(Constants.COLL) Collection<?> idList) {
return deleteByIds(idList, true);
}
翻译方法的字面意思为:删除(根据ID 批量删除),参数是一个集合,可以存放多个id值。
需求:根据传入的id集合将数据库表中的数据删除掉。
1
2
3
4
5
6
@Test
void testBatchDel() {
List<Long> ids = List.of(1820750681303367681L,1820753108895211521L,1820752458442514433L);
int rows = userDao.deleteByIds(ids);
Assertions.assertEquals(3,rows);
}
执行成功后,数据库表中的数据就会按照指定的id进行删除。
除了按照id集合进行批量删除,也可以按照id集合进行批量查询,还是先来看下API
1
2
3
4
5
6
/**
* 查询(根据ID 批量查询)
*
* @param idList 主键ID列表(不能为 null 以及 empty)
*/
List<T> selectBatchIds(@Param(Constants.COLL) Collection<? extends Serializable> idList);
方法名称翻译为:查询(根据ID 批量查询),参数是一个集合,可以存放多个id值。
需求:根据传入的ID集合查询用户信息
1
2
3
4
5
6
@Test
void testBatchSelect() {
List<User> users = userDao.selectBatchIds(List.of(1, 2, 3));
Assertions.assertEquals(3,users.size());
}
查询结果就会按照指定传入的id值进行查询
4.4.1.6逻辑删除
接下来要讲解是删除中比较重要的一个操作,逻辑删除,先来分析下问题:
-
这是一个员工和其所签的合同表,关系是一个员工可以签多个合同,是一个一(员工)对多(合同)的表
-
员工ID为1的张业绩,总共签了三个合同,如果此时他离职了,我们需要将员工表中的数据进行删除,会执行delete操作
-
如果表在设计的时候有主外键关系,那么同时也得将合同表中的前三条数据也删除掉
-
后期要统计所签合同的总金额,就会发现对不上,原因是已经将员工1签的合同信息删除掉了
-
如果只删除员工不删除合同表数据,那么合同的员工编号对应的员工信息不存在,那么就会出现垃圾数据,就会出现无主合同,根本不知道有张业绩这个人的存在
-
所以经过分析,我们不应该将表中的数据删除掉,而是需要进行保留,但是又得把离职的人和在职的人进行区分,这样就解决了上述问题,如:
-
区分的方式,就是在员工表中添加一列数据
deleted
,如果为0说明在职员工,如果离职则将其改完1,(0和1所代表的含义是可以自定义的)
所以对于删除操作业务问题来说有:
- 物理删除:业务数据从数据库中丢弃,执行的是delete操作
- 逻辑删除:为数据设置是否可用状态字段,删除时设置状态字段为不可用状态,数据保留在数据库中,执行的是update操作
MP中逻辑删除具体该如何实现?
步骤1:修改数据库表添加deleted
列
字段名可以任意,内容也可以自定义,比如0
代表正常,1
代表删除,可以在添加列的同时设置其默认值为0
正常。
步骤2:实体类添加属性
(1)添加与数据库表的列对应的一个属性名,名称可以任意,如果和数据表列名对不上,可以使用@TableField进行关系映射,如果一致,则会自动对应。
(2)标识新增的字段为逻辑删除字段,使用@TableLogic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
//@TableName("tbl_user") 可以不写是因为配置了全局配置
public class User {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
@TableLogic(value="0",delval="1")
//value为正常数据的值,delval为删除数据的值
private Integer deleted;
}
步骤3:运行删除方法
1
2
3
4
5
6
@Test
void testBatchDel() {
List<Long> ids = List.of(1L,2L,3L);
int rows = userDao.deleteByIds(ids);
Assertions.assertEquals(3,rows);
}
从测试结果来看,逻辑删除最后走的是update操作,会将指定的字段修改成删除状态对应的值。
思考
逻辑删除,对查询有没有影响呢?
-
执行查询操作
1 2 3 4 5
@Test void testBatchSelect() { List<User> users = userDao.selectBatchIds(List.of(1, 2, 3)); Assertions.assertEquals(3,users.size()); }
运行测试,会发现打印出来的sql语句中会多一个查询条件,如:
可想而知,MP的逻辑删除会将所有的查询都添加一个未被删除的条件,也就是已经被删除的数据是不应该被查询出来的。
-
如果还是想把已经删除的数据都查询出来该如何实现呢?
1 2 3 4 5 6
@Mapper public interface UserDao extends BaseMapper<User> { //查询所有数据包含已经被删除的数据 @Select("select * from tbl_user") public List<User> selectAll(); }
-
如果每个表都要有逻辑删除,那么就需要在每个模型类的属性上添加
@TableLogic
注解,如何优化?在配置文件中添加全局配置,如下:
1 2 3 4 5 6 7 8 9
mybatis-plus: global-config: db-config: # 逻辑删除字段名 logic-delete-field: deleted # 逻辑删除字面值:未删除为0 logic-not-delete-value: 0 # 逻辑删除字面值:删除为1 logic-delete-value: 1
介绍完逻辑删除,逻辑删除的本质为:
逻辑删除的本质其实是修改操作。如果加了逻辑删除字段,查询数据时也会自动带上逻辑删除字段。
执行的SQL语句为:
UPDATE tbl_user SET ==deleted===1 where id = ? AND ==deleted===0
执行数据结果为:
知识点1:@TableLogic
名称 | @TableLogic |
---|---|
类型 | ==属性注解== |
位置 | 模型类中用于表示删除字段的属性定义上方 |
作用 | 标识该字段为进行逻辑删除的字段 |
相关属性 | value:逻辑未删除值 delval:逻辑删除值 |
4.4.2乐观锁
4.4.2.1 概念
在讲解乐观锁之前,我们还是先来分析下问题:
业务并发现象带来的问题:==秒杀==
- 假如有100个商品或者票在出售,为了能保证每个商品或者票只能被一个人购买,如何保证不会出现超买或者重复卖
- 对于这一类问题,其实有很多的解决方案可以使用
- 第一个最先想到的就是锁,锁在一台服务器中是可以解决的,但是如果在多台服务器下锁就没有办法控制,比如12306有两台服务器在进行卖票,在两台服务器上都添加锁的话,那也有可能会导致在同一时刻有两个线程在进行卖票,还是会出现并发问题
- 我们接下来介绍的这种方式是针对于小型企业的解决方案,因为数据库本身的性能就是个瓶颈,如果对其并发量超过2000以上的就需要考虑其他的解决方案了。
简单来说,乐观锁主要解决的问题是当要更新一条记录的时候,希望这条记录没有被别人更新。
4.4.2.2 实现思路
乐观锁的实现方式:
- 数据库表中添加version列,比如默认值给1
- 第一个线程要修改数据之前,取出记录时,获取当前数据库中的version=1
- 第二个线程要修改数据之前,取出记录时,获取当前数据库中的version=1
- 第一个线程执行更新时,set version = newVersion where version = oldVersion
- newVersion = version+1 [2]
- oldVersion = version [1]
- 第二个线程执行更新时,set version = newVersion where version = oldVersion
- newVersion = version+1 [2]
- oldVersion = version [1]
- 假如这两个线程都来更新数据,第一个和第二个线程都可能先执行
- 假如第一个线程先执行更新,会把version改为2,
- 第二个线程再更新的时候,set version = 2 where version = 1,此时数据库表的数据version已经为2,所以第二个线程会修改失败
- 假如第二个线程先执行更新,会把version改为2,
- 第一个线程再更新的时候,set version = 2 where version = 1,此时数据库表的数据version已经为2,所以第一个线程会修改失败
- 不管谁先执行都会确保只能有一个线程更新数据,这就是MP提供的乐观锁的实现原理分析。
上面所说的步骤具体该如何实现呢?
4.4.2.3 实现步骤
分析完步骤后,具体的实现步骤如下:
步骤1:数据库表添加列
列名可以任意,比如使用version
,给列设置默认值为1
步骤2:在模型类中添加对应的属性
根据添加的字段列名,在模型类中添加对应的属性值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
//@TableName("tbl_user") 可以不写是因为配置了全局配置
public class User {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
private Integer deleted;
@Version
private Integer version;
}
步骤3:添加乐观锁的拦截器
1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class MpConfig {
@Bean
public MybatisPlusInterceptor mpInterceptor() {
//1.定义Mp拦截器
MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
//2.添加乐观锁拦截器
mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mpInterceptor;
}
}
步骤4:执行更新操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest
class Mybatisplus03DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testUpdate(){
User user = new User();
user.setId(3L);
user.setName("Jock666");
userDao.updateById(user);
}
}
你会发现,这次修改并没有更新version字段,原因是没有携带version数据。
添加version数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
class Mybatisplus03DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testUpdate(){
User user = new User();
user.setId(3L);
user.setName("Jock666");
user.setVersion(1);
userDao.updateById(user);
}
}
你会发现,我们传递的是1,MP会将1进行加1,然后,更新回到数据库表中。
所以要想实现乐观锁,首先第一步应该是拿到表中的version,然后拿version当条件在将version加1更新回到数据库表中,所以我们在查询的时候,需要对其进行查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
class Mybatisplus03DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testUpdate(){
//1.先通过要修改的数据id将当前数据查询出来
User user = userDao.selectById(3L);
//2.将要修改的属性逐一设置进去
user.setName("Jock888");
userDao.updateById(user);
}
}
大概分析完乐观锁的实现步骤以后,我们来模拟一种加锁的情况,看看能不能实现多个人修改同一个数据的时候,只能有一个人修改成功。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
class Mybatisplus03DqlApplicationTests {
@Autowired
private UserDao userDao;
@Test
void testUpdate(){
//1.先通过要修改的数据id将当前数据查询出来
User user = userDao.selectById(3L); //version=3
User user2 = userDao.selectById(3L); //version=3
user2.setName("Jock aaa");
userDao.updateById(user2); //version=>4
user.setName("Jock bbb");
userDao.updateById(user); //verion=3?条件还成立吗?
}
}
运行程序,分析结果:
乐观锁就已经实现完成了,如果对于上面的这些步骤记不住咋办呢?
参考官方文档来实现:
https://mp.baomidou.com/guide/interceptor-optimistic-locker.html#optimisticlockerinnerinterceptor
5,快速开发
5.1 代码生成器原理分析
造句:
我们可以往空白内容进行填词造句,比如:
在比如:
观察我们之前写的代码,会发现其中也会有很多重复内容,比如:
那我们就想,如果我想做一个Book模块的开发,是不是只需要将红色部分的内容全部更换成Book
即可,如:
所以我们会发现,做任何模块的开发,对于这段代码,基本上都是对红色部分的调整,所以我们把去掉红色内容的东西称之为==模板==,红色部分称之为==参数==,以后只需要传入不同的参数,就可以根据模板创建出不同模块的dao代码。
除了Dao可以抽取模块,其实我们常见的类都可以进行抽取,只要他们有公共部分即可。再来看下模型类的模板:
- ① 可以根据数据库表的表名来填充
- ② 可以根据用户的配置来生成ID生成策略
- ③到⑨可以根据数据库表字段名称来填充
所以只要我们知道是对哪张表进行代码生成,这些内容我们都可以进行填充。
分析完后,我们会发现,要想完成代码自动生成,我们需要有以下内容:
- 模板: MyBatisPlus提供,可以自己提供,但是麻烦,不建议
- 数据库相关配置:读取数据库获取表和字段信息
- 开发者自定义配置:手工配置,比如ID生成策略
5.2 代码生成器实现
步骤1:创建一个Maven项目
代码2:导入对应的jar包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
</parent>
<groupId>com.example</groupId>
<artifactId>mybatisplus_03_generator</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!--spring web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter-test</artifactId>
<version>3.5.7</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--mybatis plus-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--代码生成器的两个依赖,mybatis-plus-generator和velocity模板
这两个依赖生成完毕后注释掉, 免得测试时启动不了
-->
<!-- <dependency>-->
<!-- <groupId>com.baomidou</groupId>-->
<!-- <artifactId>mybatis-plus-generator</artifactId>-->
<!-- <version>3.4.1</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.apache.velocity</groupId>-->
<!-- <artifactId>velocity-engine-core</artifactId>-->
<!-- <version>2.3</version>-->
<!-- </dependency>-->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
步骤3:编写引导类
1
2
3
4
5
6
@SpringBootApplication
public class Mybatisplus04GeneratorApplication {
public static void main(String[] args) {
SpringApplication.run(Mybatisplus04GeneratorApplication.class, args);
}
}
步骤4:创建代码生成类
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
public class Generator {
public static void main(String[] args) {
AutoGenerator autoGenerator = new AutoGenerator();
// 设置数据源
DataSourceConfig datasource = new DataSourceConfig();
datasource.setDriverName("com.mysql.cj.jdbc.Driver");
datasource.setUrl("jdbc:mysql://localhost:3306/spring_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false");
datasource.setUsername("root");
datasource.setPassword("root");
autoGenerator.setDataSource(datasource);
// 设置包名相关配置
PackageConfig packageConfig = new PackageConfig();
// 设置生成的包名
packageConfig.setParent("com.example");
// 设置生成的实体类包名
packageConfig.setEntity("domain");
// 设置生成的数据层包名
packageConfig.setMapper("dao");
// --加入配置
autoGenerator.setPackageInfo(packageConfig);
// 设置全局配置
GlobalConfig globalConfig = new GlobalConfig();
// 设置代码生成位置
globalConfig.setOutputDir("C:\\Users\\32929\\Desktop\\cai\\backend\\ssm\\code\\mybatisplus_03_generator\\src\\main\\java");
// globalConfig.setOutputDir(System.getProperty("user.dir")+"/mybatisplus_04_generator/src/main/java"); //设置代码生成位置
// 生成完毕是否打开资源管理器
globalConfig.setOpen(false);
// 生成作者
globalConfig.setAuthor("zangxin");
// 是否覆盖原始生成的文件
globalConfig.setFileOverride(true);
// 设置数据层接口名, %s为占位符,指代模块名称
globalConfig.setMapperName("%sDao");
// 设置Id生成策略
globalConfig.setIdType(IdType.ASSIGN_ID);
// -加入配置
autoGenerator.setGlobalConfig(globalConfig);
// 策略配置
StrategyConfig strategyConfig = new StrategyConfig();
// 生成那张表, 不设置默认生成全部
strategyConfig.setInclude("tbl_user");
// 设置数据库的表名称前缀, 生成时不包含这个tbl_ -->Tbl_User
strategyConfig.setTablePrefix("tbl_");
// 是否启用RESTful风格
strategyConfig.setRestControllerStyle(true);
// 设置乐观锁字段
strategyConfig.setVersionFieldName("version");
// 设置逻辑删除字段
strategyConfig.setLogicDeleteFieldName("deleted");
// 设置是否启用lombok
strategyConfig.setEntityLombokModel(true);
// -加入配置
autoGenerator.setStrategy(strategyConfig);
// -*-执行
autoGenerator.execute();
}
}
对于代码生成器中的代码内容,我们可以直接从官方文档中获取代码进行修改,
https://mp.baomidou.com/guide/generator.html
步骤5:运行程序
运行成功后,会在当前项目中生成很多代码,代码包含controller
,service
,mapper
和entity
至此代码生成器就已经完成工作,我们能快速根据数据库表来创建对应的类,简化我们的代码开发。
5.3 MP中Service的CRUD
回顾我们之前业务层代码的编写,编写接口和对应的实现类:
1
2
3
4
public interface UserService{}
@Service
public class UserServiceImpl implements UserService{}
接口和实现类有了以后,需要在接口和实现类中声明方法
1
2
3
4
5
6
7
8
9
10
11
12
13
public interface UserService{
public List<User> findAll();
}
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserDao userDao;
public List<User> findAll(){
return userDao.selectList(null);
}
}
MP看到上面的代码以后就说这些方法也是比较固定和通用的,那我来帮你抽取下,所以MP提供了一个Service接口和实现类,分别是:IService
和ServiceImpl
,后者是对前者的一个具体实现。
以后我们自己写的Service就可以进行如下修改:
1
2
3
4
public interface UserService extends IService<User>{}
@Service
public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserService{}
修改以后的好处是,MP已经帮我们把业务层的一些基础的增删改查都已经实现了,可以直接进行使用。
编写测试类进行测试:
1
2
3
4
5
6
7
8
9
@SpringBootTest
class Mybatisplus04GeneratorApplicationTests {
private IUserService userService;
@Test
void testFindAll() {
List<User> list = userService.list();
System.out.println(list);
}
}
注意:mybatisplus_04_generator项目中对于MyBatis的环境是没有进行配置,如果想要运行,需要提取将配置文件中的内容进行完善后在运行。(在配置文件中application.yml中加入jdbc配置, 在dao类上加上@Mapper注解)
查看官方文档:https://mp.baomidou.com/guide/crud-interface.html
,这些提供的方法大家可以参考官方文档进行学习使用,方法的名称可能有些变化,但是方法对应的参数和返回值基本类似。