在这一部分,我们主要了解以下3个问题:
什么是SpringBoot为什么要学习SpringBootSpringBoot的特点springboot是spring快速开发脚手架,通过约定大于配置的方式,快速构建和启动spring项目
spring boot 可以解决上面2个问题
Spring Boot 特点:
快速开发spring应用的框架内嵌tomcat和jetty容器,不需要单独安装容器,jar包直接发布一个web应用简化maven配置,parent这种方式,一站式引入需要的各种依赖基于注解的零配置思想和各种流行框架,spring web mvc,mybatis,spring cloud无缝整合更多细节,大家可以到官网查看。
spring boot 是spring快速开发脚手架,通过约定大于配置,优化了混乱的依赖管理,和复杂的配置,让我们用java -jar方式,运行启动java web项目
需求:创建HelloController,在页面中打印hello spring boot...
我们先新建一个空的工程:
工程名为project:
设置jdk版本为1.8:
新建一个module:
使用maven来构建:
然后填写项目坐标:
目录结构:
项目结构:
SpringBoot提供了一个名为spring-boot-starter-parent的构件,里面已经对各种常用依赖(并非全部)的版本进行了管理,我们的项目需要以这个项目为父工程,这样我们就不用操心依赖的版本问题了,需要什么依赖,直接引入坐标即可!
为了让SpringBoot帮我们完成各种自动配置,我们必须引入SpringBoot提供的自动配置依赖,我们称为 启动器 。因为我们是web项目,这里我们引入web启动器:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>需要注意的是,我们并没有在这里指定版本信息。因为SpringBoot的父工程已经对版本进行了管理了。 这个时候,我们会发现项目中多出了大量的依赖:
这些都是SpringBoot根据spring-boot-starter-web这个依赖自动引入的,而且所有的版本都已经管理好,不会出现冲突。
默认情况下,maven工程的jdk版本是1.5,而我们开发使用的是1.8,因此这里我们需要修改jdk版本,只需要简单的添加以下属性即可:
<properties> <java.version>1.8</java.version> </properties>Spring Boot项目通过main函数即可启动,我们需要创建一个启动类:
然后编写main函数:
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class,args); } }接下来,我们就可以像以前那样开发SpringMVC的项目了!
我们编写一个controller:
代码:
@RestController public class HelloController { @GetMapping("/hello") public String hello(){ return "hello, spring boot!"; } }接下来,我们运行main函数,查看控制台:
并且可以看到监听的端口信息:
1)监听的端口是80802)SpringMVC的映射路径是:/3) /hello 路径已经映射到了 HelloController 中的 hello() 方法打开页面访问:http://localhost:8080/hello
测试成功了!
在入门案例中,我们没有任何的配置,就可以实现一个SpringMVC的项目了,快速、高效!
但是有同学会有疑问,如果没有任何的xml,那么我们如果要配置一个Bean该怎么办?比如我们要配置一个数据库连接池,以前会这么玩:
现在该怎么做呢?
事实上,在Spring3.0开始,Spring官方就已经开始推荐使用java配置来代替传统的xml配置了,我们不妨来回顾一下Spring的历史:
Spring1.0时代在此时因为jdk1.5刚刚出来,注解开发并未盛行,因此一切Spring配置都是xml格式,想象一下所有的bean都用xml配置,细思极恐啊,心疼那个时候的程序员2秒
Spring2.0时代Spring引入了注解开发,但是因为并不完善,因此并未完全替代xml,此时的程序员往往是把xml与注解进行结合,貌似我们之前都是这种方式。
Spring3.0及以后3.0以后Spring的注解已经非常完善了,因此Spring推荐大家使用完全的java配置来代替以前的xml,不过似乎在国内并未推广盛行。然后当SpringBoot来临,人们才慢慢认识到java配置的优雅。
spring全注解配置主要靠java类和一些注解,比较常用的注解有:
@Configuration :声明一个类作为配置类,代替xml文件@Bean :声明在方法上,将方法的返回值加入Bean容器,代替 <bean> 标签@value :属性注入@PropertySource :指定外部属性文件,我们接下来用java配置来尝试实现连接池配置:
首先引入Druid连接池依赖:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency>创建一个jdbc.properties文件,编写jdbc属性(可以拷贝):
jdbc.driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/lxs jdbc.username=root jdbc.password=123456然后编写代码:
@Configuration @PropertySource("classpath:application.properties") public class JdbcConfig { //spring全注解方式 @Value("${jdbc.url}") String url; @Value("${jdbc.driverClassName}") String driverClassName; @Value("${jdbc.username}") String username; @Value("${jdbc.password}") String password; @Bean public DataSource dataSource(){ DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(url); dataSource.setDriverClassName(driverClassName); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; } }解读:
@Configuration :声明我们 JdbcConfig 是一个配置类@PropertySource :指定属性文件的路径是: classpath:jdbc.properties通过 @Value 为属性注入值通过@Bean将 dataSource() 方法声明为一个注册Bean的方法,Spring会自动调用该方法,将方法的返回值加入Spring容器中。默认的对象名id=方法名,可以通过@Bean("自定义名字"),来指定新的对象名然后我们就可以在任意位置通过 @Autowired 注入DataSource了!
我们在 HelloController 中测试:
@RestController public class HelloController { @Autowired private DataSource dataSource; @GetMapping("hello") public String hello() { return "hello, spring boot!" + dataSource; } }然后Debug运行并查看:
属性注入成功了!
在上面的案例中,我们实验了java配置方式。不过属性注入使用的是@Value注解。这种方式虽然可行,但是不够强大,因为它只能注入基本类型值。
在SpringBoot中,提供了一种新的属性注入方式,支持各种java基本数据类型及复杂类型的注入。
1)我们新建一个类,用来进行属性注入:
@ConfigurationProperties(prefix = "jdbc") public class JdbcProperties { private String url; private String driverClassName; private String username; private String password; // ... 略 // getters 和 setters } 在类上通过@ConfigurationProperties注解声明当前类为属性读取类prefix="jdbc" 读取属性文件中,前缀为jdbc的值。在类上定义各个属性,名称必须与属性文件中 jdbc. 后面部分一致需要注意的是,这里我们并没有指定属性文件的地址,所以我们需要把jdbc.properties名称改为application.properties,这是SpringBoot默认读取的属性文件名:2)在JdbcConfig中使用这个属性:
@Configuration @EnableConfigurationProperties(JdbcProperties.class) public class JdbcConfig { //springboot自动注入方式 @Bean public DataSource dataSource(JdbcProperties jdbc){ DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(jdbc.getUrl()); dataSource.setDriverClassName(jdbc.getDriverClassName()); dataSource.setUsername(jdbc.getUsername()); dataSource.setPassword(jdbc.getPassword()); return dataSource; } } 通过 @EnableConfigurationProperties(JdbcProperties.class) 来声明要使用 JdbcProperties 这个类的对象然后你可以通过以下方式注入JdbcProperties: @Autowired注入 @Autowired private JdbcProperties prop;构造函数注入 private JdbcProperties prop; public JdbcConfig(Jdbcproperties prop){ this.prop = prop; }
声明有@Bean的方法参数注入 @Bean public Datasource dataSource(JdbcProperties prop){ // ... }
本例中,我们采用第三种方式。
3)测试结果:
大家会觉得这种方式似乎更麻烦了,事实上这种方式有更强大的功能,也是SpringBoot推荐的注入方式。两者对比关系:
优势:
Relaxed binding:松散绑定 不严格要求属性文件中的属性名与成员变量名一致。支持驼峰,中划线,下划线等等转换,甚至支持对象引导。比如:user.friend.name:代表的是user对象中的friend属性中的name属性,显然friend也是对象。@value注解就难以完成这样的注入方式。meta-data support:元数据支持,帮助IDE生成属性提示(写开源框架会用到)。事实上,如果一段属性只有一个Bean需要使用,我们无需将其注入到一个类(JdbcProperties)中。而是直接在需要的地方声明即可:
@Configuration public class JdbcConfig { @Bean // 声明要注入的属性前缀,SpringBoot会自动把相关属性通过set方法注入到DataSource中 @ConfigurationProperties(prefix = "jdbc") public DataSource dataSource() { DruidDataSource dataSource = new DruidDataSource(); return dataSource; } }我们直接把 @ConfigurationProperties(prefix = "jdbc") 声明在需要使用的 @Bean 的方法上,然后SpringBoot就会自动调用这个Bean(此处是DataSource)的set方法,然后完成注入。使用的前提是:该类必须有对应属性的set方法!
我们将jdbc的url改成:/lxs,再次测试:
通过刚才的案例看到,一个整合了SpringMVC的WEB工程开发,变的无比简单,那些繁杂的配置都消失不见了,这是如何做到的?
这些都是从springboot启动器开始的
我们重点关注@SpringBootApplication注解
点击进入,查看源码:
这里重点的注解有3个:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
我们继续点击查看源码:
通过这段我们可以看出,在这个注解上面,又有一个 @Configuration 注解。这个注解的作用就是声明当前类是一 个配置类,然后Spring会自动扫描到添加了 @Configuration 的类,并且读取其中的配置信息。
我们跟进源码:
并没有看到什么特殊的地方。我们查看注释:
大概的意思:
配置组件扫描的指令。提供了类似与 标签的作用 通过basePackageClasses或者basePackages属性来指定要扫描的包。如果没有指定这些属性,那么将从声 明这个注解的类所在的包开始,扫描包及子包
而我们的@SpringBootApplication注解声明的类就是main函数所在的启动类,因此扫描的包是该类所在包及其子 包。因此,一般启动类会放在一个比较前的包目录中。
关于这个注解,官网上有一段说明:
The second class-level annotation is @EnableAutoConfiguration . This annotation tells Spring Boot to “guess” how you want to configure Spring, based on the jar dependencies that you have added. Since spring-boot-starter-web added Tomcat and Spring MVC, the auto-configuration assumes that you are developing a web application and sets up Spring accordingly.
简单翻译以下:
第二级的注解 @EnableAutoConfiguration ,告诉SpringBoot基于你所添加的依赖,去“猜测”你想要如何配置Spring。比如我们引入了 spring-boot-starter-web ,而这个启动器中帮我们添加了 tomcat 、 SpringMVC 的依赖。此时自动配置就知道你是要开发一个web应用,所以就帮你完成了web及SpringMVC的默认配置了!
总结,SpringBoot内部对大量的第三方库进行了默认配置,我们引入对应库所需的依赖,那么默认配置就会生效。
@EnableAutoConfiguration会开启SpringBoot的自动配置,并且根据你引入的依赖来生效对应的默认配置,
springboot如何做到的?
其实在我们的项目中,已经引入了一个依赖:spring-boot-autoconfigure,其中定义了大量自动配置类:
还有:
非常多,几乎涵盖了现在主流的开源框架,例如:
redisjmsamqpjdbcjacksonmongodbjpasolrelasticsearch... 等等
我们来看一个我们熟悉的,例如SpringMVC,查看mvc 的自动配置类:
打开WebMvcAutoConfiguration:
我们看到这个类上的4个注解:
@Configuration :声明这个类是一个配置类@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) 这里的条件是OnClass,也就是满足以下类存在:Servlet、DispatcherServlet、WebMvcConfigurer,其中Servlet只要引入了tomcat依赖自然会有,后两个需要引入SpringMVC才会有。这里就是判断你是否引入了相关依赖,引入依赖后该条件成立,当前类的配置才会生效!@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) 这个条件与上面不同,OnMissingBean,是说环境中没有指定的Bean这个才生效。其实这就是自定义配置的入口,也就是说,如果我们自己配置了一个WebMVCConfigurationSupport的类,那么这个默认配置就会失效!接着,我们查看该类中定义了什么:
视图解析器:
处理器适配器(HandlerAdapter):
还有很多,这里就不一一截图了。
SpringBoot为我们提供了默认配置,而默认配置生效的条件一般有两个:
引入了相关依赖没有自定义配置类刚才案例已经能实现mvc自动配置,这里我们主要解决以下3个问题
修改端口静态资源拦截器配置查看SpringBoot的全局属性可知,端口通过以下方式配置:
# 映射端口
server.port=80
重启服务后测试:
ResourceProperties的类,里面就定义了静态资源的默认查找路径:
默认的静态资源路径为:
classpath:/META-INF/resources/classpath:/resources/classpath:/static/classpath:/public只要静态资源放在这些目录中任何一个,SpringMVC都会帮我们处理。
我们习惯会把静态资源放在 classpath:/static/ 目录下。我们创建目录,并且添加一些静态资源:
重启项目后测试:
拦截器也是我们经常需要使用的,在SpringBoot中该如何配置呢?
首先我们定义一个拦截器:
public class LoginInterceptor implements HandlerInterceptor { private Logger logger = LoggerFactory.getLogger(LoginInterceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { logger.debug("处理器执行前执行!"); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { logger.debug("处理器执行后执行!"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { logger.debug("跳转后执行!"); } }通过实现 WebMvcConfigurer 并添加 @Configuration 注解来实现自定义部分SpringMvc配置:
@Configuration public class MvcConfig implements WebMvcConfigurer { /** * 通过@Bean注解,将我们定义的拦截器注册到Spring容器 * @return */ @Bean public LoginInterceptor loginInterceptor(){ return new LoginInterceptor(); } /** * 重写接口中的addInterceptors方法,添加自定义拦截器 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { // 通过registry来注册拦截器,通过addPathPatterns来添加拦截路径 registry.addInterceptor(this.loginInterceptor()).addPathPatterns("/**"); } }ant path路径匹配通配符
‘?’ 匹配任何单字符‘*’ 匹配0或者任意数量的字符‘/**’ 匹配0或者更多的目录结构如下:
接下来运行并查看日志:
你会发现日志中什么都没有,因为我们记录的log级别是debug,默认是显示info以上,我们需要进行配置。
SpringBoot通过 logging.level.*=debug 来配置日志级别,*填写包名
# 设置com.lxs包的日志级别为debug
logging.level.com.lxs=debug
再次运行查看:
2018-05-05 17:50:01.811 DEBUG 4548 --- [p-nio-80-exec-1] com.lxs.interceptor.LoginInterceptor : preHandle method is now running! 2018-05-05 17:50:01.854 DEBUG 4548 --- [p-nio-80-exec-1] com.lxs.interceptor.LoginInterceptor : postHandle method is now running! 2018-05-05 17:50:01.854 DEBUG 4548 --- [p-nio-80-exec-1] com.lxs.interceptor.LoginInterceptor : afterCompletion method is now running!
导入资料中的t_user.sql文件
当然,不要忘了数据库驱动,SpringBoot并不知道我们用的什么数据库,这里我们选择MySQL:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.46</version> </dependency>其实,在刚才引入jdbc启动器的时候,SpringBoot已经自动帮我们引入了一个连接池:
HikariCP应该是目前速度最快的连接池了,我们看看它与c3p0的对比:
因此,我们只需要指定连接池参数即可:
# 连接四大参数 spring.datasource.url=jdbc:mysql://localhost:3306/springboot spring.datasource.username=root spring.datasource.password=123 # 可省略,SpringBoot自动推断 spring.datasource.driverClassName=com.mysql.jdbc.Driver spring.datasource.hikari.idle-timeout=60000 spring.datasource.hikari.maximum-pool-size=30 spring.datasource.hikari.minimum-idle=10SpringBoot官方并没有提供Mybatis的启动器,不过Mybatis官网自己实现了:
<!--mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency>配置,基本没有需要配置的:
# mybatis 别名扫描 mybatis.type-aliases-package=com.lxs.domain # mapper.xml文件位置,如果没有映射文件,请注释掉 mybatis.mapper-locations=classpath:mappers/*.xml直接使用jdbc用到的实体类
public class User { private Long id; // 用户名 //自动转换下换线到驼峰命名user_name -> userName private String userName; // 密码 private String password; // 姓名 private String name; // 年龄 private Integer age; // 性别,1男性,2女性 private Integer sex; // 出生日期 private Date birthday; // 创建时间 private Date created; // 更新时间 private Date updated; // 备注 private String note; //getter和setter方法 }第一种:使用@Mapper注解(不推荐)
需要注意,这里没有配置mapper接口扫描包,因此我们需要给每一个Mapper接口添加 @Mapper 注解,才能被识别。
@Mapper public interface UserMapper { }第二种设置MapperScan,注解扫描的包(推荐)
@MapperScan("dao所在的包"),自动搜索包中的接口,产生dao的代理对象
@SpringBootApplication @MapperScan("com.lxs.demo.dao") public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }引入测试构建
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>测试代码
@RunWith(SpringRunner.class) @SpringBootTest public class UserDaoTest { @Autowired private UserDao userDao; @Test public void testFindAll() { List<User> list = userDao.findAll(); } }使用Mybatis时,最大的问题是,要写大量的重复SQL语句在xml文件中,除了特殊的业务逻辑SQL语句之外,还有 大量结构类似的增删改查SQL。而且,当数据库表结构改动时,对应的所有SQL以及实体类都需要更改。这大量增 加了程序员的负担。避免重复书写CRUD映射的框架有两个
通用mybatis(tk mybatis) mybatis plus,通能更加强大,后面实战项目中讲解通用Mapper的作者也为自己的插件编写了启动器,我们直接引入即可:
<!-- 通用mapper --> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.0.2</version> </dependency>tk mybatis 实体类使用的注解是jpa注解(jpa注解:https://blog.csdn.net/yiyelanxin/article/details/100107335)
@Table(name = "tb_user") public class User implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // 用户名 private String userName; ....注意事项:
1. 默认表名=类名,字段名=属性名2. 表名可以使用 @Table(name = "tableName") 进行指定3. 字段名可用@Column(name = "fieldName") 指定4. 使用 @Transient 注解表示跟字段不进行映射5.主键映射不能省略,@GeneratedValue(strategy = GenerationType.IDENTITY)6.自动转换下换线到驼峰命名 例:user_name -> userName不需要做任何配置就可以使用了。
@Mapper public interface UserMapper extends tk.mybatis.mapper.common.Mapper<User>{ public List<User> findByUser(User user); }映射复杂方法 resources/mappers/UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!--命名空间:namespace="接口的完整路径"--> <mapper namespace="com.lxs.demo.dao.UserMapper"> <!--resultType/resultMap--> <select id="findByUser" resultType="user"> SELECT * FROM tb_user <where> <if test="name != null"> name like '%${name}%' </if> <if test="note != null"> and note like '%${note}%' </if> </where> </select> </mapper>一旦继承了Mapper,继承的Mapper就拥有了Mapper所有的通用方法:
注意:要把MapperScan类改成tk-mybatis构件的类
import tk.mybatis.spring.annotation.MapperScan; @SpringBootApplication @EnableConfigurationProperties @MapperScan("com.lxs.demo.dao") public class Application {注意:必须使用tk mybatis的MapperScan
安装插件JBLSpringBootAppGen可自动生成启动类
Thymeleaf 是一个跟 FreeMarker 类似的模板引擎,它可以完全替代 JSP 。相较与其他的模板引擎,它有如下特点:
动静结合:Thymeleaf 在有网络和无网络的环境下皆可运行,无网络显示静态内容,有网络用后台得到数据替换静态内容与SpringBoot完美整合,springboot默认整合thymeleaf编写UserService,调用UserMapper的查询所有方法
@Service public class UserService { @Autowired private UserDao userDao; public List<User> queryAll() { return this.userDao.selectAll(); } }编写一个controller,返回一些用户数据,放入模型中,等会在页面渲染 编写一个controller,返回一些用户数
据,放入模型中,等会在页面渲染
@Controller public class UserController { @Autowired private UserService userService; @RequestMapping("/all") public String all(Model model) { List<User> list = userService.findAll(); model.addAttribute("users", list); // 返回模板名称(就是classpath:/templates/目录下的html文件名) return "users"; } }直接引入启动器:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>SpringBoot会自动为Thymeleaf注册一个视图解析器:
与解析JSP的InternalViewResolver类似,Thymeleaf也会根据前缀和后缀来确定模板文件的位置:
默认前缀: classpath:/templates/默认后缀: .html所以如果我们返回视图: users ,会指向到 classpath:/templates/users.html
一般我们无需进行修改,默认即可。
根据上面的文档介绍,模板默认放在classpath下的templates文件夹,我们新建一个html文件放入其中:
编写html模板,渲染模型中的数据:
注意,把html 的名称空间,改成: xmlns:th="http://www.thymeleaf.org" 会有语法提示
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>首页</title> <style type="text/css"> table { border-collapse: collapse; font-size: 14px; width: 80%; margin: auto } table, th, td { border: 1px solid darkslategray; padding: 10px } </style> </head> <body> <div style="text-align: center"> <span style="color: darkslategray; font-size: 30px">欢迎光临!</span> <hr/> <table class="list"> <tr> <th>id</th> <th>姓名</th> <th>用户名</th> <th>年龄</th> <th>性别</th> <th>生日</th> <th>备注</th> <th>操作</th> </tr> <tr th:each="user, status : ${users}" th:object="${user}"> <td th:text="${user.id}">1</td> <td th:text="*{name}">张三</td> <td th:text="*{userName}">zhangsan</td> <td th:text="${user.age}">20</td> <td th:text="${user.sex} == 1 ? '男': '女'">男</td> <td th:text="${#dates.format(user.birthday, 'yyyy-MM-dd')}">1980-02-30</td> <td th:text="${user.note}">1</td> <td> <a th:href="@{/delete(id=${user.id}, userName=*{userName})}">删除</a> <a th:href="|/update/${user.id}|">修改</a> <a th:href="'/approve/' + ${user.id}">审核</a> </td> </tr> </table> </div> </body> </html>我们看到这里使用了以下语法:
${} :这个类似与el表达式,但其实是ognl的语法,比el表达式更加强大th- 指令: th- 是利用了Html5中的自定义属性来实现的。如果不支持H5,可以用 data-th- 来代替 th:each :类似于 c:foreach 遍历集合,但是语法更加简洁th:text :声明标签中的文本 例如 <td th-text='${user.id}'>1</td> ,如果user.id有值,会覆盖默认的1如果没有值,则会显示td中默认的1。这正是thymeleaf能够动静结合的原因,模板解析失败不影响页面的显示效果,因为会显示默认值!接下来,我们打开页面测试一下:
Thymeleaf会在第一次对模板解析之后进行缓存,极大的提高了并发处理能力。但是这给我们开发带来了不便,修改页面后并不会立刻看到效果,我们开发阶段可以关掉缓存使用:
# 开发阶段关闭thymeleaf的模板缓存 spring.thymeleaf.cache=false注意:
在Idea中,我们需要在修改页面后按快捷键: Ctrl + Shift + F9 对项目进行rebuild才可以。
我们可以修改页面,测试一下。
它们分为三类
1. 变量表达式2. 选择或星号表达式3. URL表达式变量表达式即OGNL表达式或Spring EL表达式(在Spring中用来获取model attribute的数据)。如下所示:
${session.user.name}它们将以HTML标签的一个属性来表示:
<h5>表达式</h5> <span>${text}</span> <span th:text="${text}">你好 thymleaf</span>选择表达式很像变量表达式,不过它们用一个预先选择的对象来代替上下文变量容器(map)来执行,如下: *{customer.name}
被指定的object由th:object属性定义:
users.html
<tr th:each="user : ${users}" th:object="${user}"> <td th:text="${user.id}">1</td> <td th:text="*{name}">张三</td> <td th:text="*{userName}">zhangsan</td> ....
URL表达式指的是把一个有用的上下文或回话信息添加到URL,这个过程经常被叫做URL重写。 @{/order/list}
URL还可以
设置参数: @{/order/details(id=${orderId}, name=*{name})}相对路径:@{../documents/report}让我们看这些表达式:
<form th:action="@{/createOrder}"> <a href="main.html" th:href="@{/main}">url表达式
<a th:href="@{/delete(id=${user.id}, userName=*{userName})}">删除</a>文本替换
<a th:href="|/update/${user.id}|">修改</a>字符串拼接
<a th:href="'/approve/' + ${user.id}">审核</a>字面(Literals)
文本文字(Text literals): 'one text', 'Another one!',…数字文本(Number literals): 0, 34, 3.0, 12.3,…布尔文本(Boolean literals): true, false空(Null literal): null文字标记(Literal tokens): one, sometext, main,…文本操作(Text operations)
字符串连接(String concatenation): +文本替换(Literal substitutions): |The name is ${name}|算术运算(Arithmetic operations)
二元运算符(Binary operators): +, -, *, /, %减号(单目运算符)Minus sign (unary operator): -布尔操作(Boolean operations)
二元运算符(Binary operators): and, or布尔否定(一元运算符)Boolean negation (unary operator): !, not比较和等价(Comparisons and equality)
比较(Comparators): >, <, >=, <= (gt, lt, ge, le)等值运算符(Equality operators): ==, != (eq, ne)条件运算符(Conditional operators)
If-then: (if) ? (then)If-then-else: (if) ? (then) : (else)Default: (value) ?: (defaultvalue)还有非常多的标签,这里只列出最常用的几个
1. 赋值、字符串拼接
字符串拼接还有另外一种简洁的写法
<a th:href="|/update/${user.id}|">修改</a> <a th:href="'/approve/' + ${user.id}">审核</a>2. 条件判断 If/Unless
Thymeleaf中使用th:if和th:unless属性进行条件判断,下面的例子中, <a> 标签只有在 th:if 中条件成立时才显示:
<h5>if指令</h5> <a th:if="${users.size() > 0}">查询结果存在</a><br> <a th:if="${users.size() <= 0}">查询结果不存在</a><br> <a th:unless="${session.user != null}" href="#">登录</a><br>th:unless于th:if恰好相反,只有表达式中的条件不成立,才会显示其内容。
也可以使用 (if) ? (then) : (else) 这种语法来判断显示的内容
3. for 循环
<tr th:each="user, status : ${users}" th:object="${user}" th:class="${status.even} ?'grey'"> <td th:text="${status.even}"></td> <td th:text="${user.id}">1</td> <td th:text="*{name}">张三</td> <td th:text="*{userName}">zhangsan</td> <td th:text="${user.age}">20</td> <td th:text="${user.sex} == 1 ? '男' : '女'">男</td> <td th:text="${#dates.format(user.birthday, 'yyyy-MM-dd')}">1980-02-30</td> <td th:text="${user.note}">1</td> <td> <a th:href="@{/delete(id=${user.id}, userName=*{userName})}">删除</a> <a th:href="|/update/${user.id}|">修改</a> <a th:href="'/approve/' + ${user.id}">审核</a> </td> </tr>status称作状态变量,属性有:
index:当前迭代对象的index(从0开始计算)count: 当前迭代对象的index(从1开始计算)size:被迭代对象的大小current:当前迭代变量even/odd:布尔值,当前循环是否是偶数/奇数(从0开始计算)first:布尔值,当前循环是否是第一个last:布尔值,当前循环是否是最后一个4. 内联文本
内联文本:[[…]]内联文本的表示方式,使用时,必须先用th:inline=”text/javascript/none”激活,th:inline可以在父级标签内使用,甚至作为body的标签。内联文本尽管比th:text的代码少,不利于原型显示。
在thymeleaf指令中显示 <h6 th:text="${text}">静态内容</h6> 使用内联文本显示model attribute <h5>内联文本</h5> <div> <h6 th:inline="text">[[${text}]]</h6> <h6 th:inline="none">[[${text}]]</h6> <h6>[[${text}]]</h6> <h6>${text}</h6> </div>原则能用指令就用th指令
5. 内联js
内联文本:[[…]]内联文本的表示方式,使用时,必须先用th:inline=”text/javascript/none”激活,th:inline可以在父级标签内使用,甚至作为body的标签。内联文本尽管比th:text的代码少,不利于原型显示。
<h5>内联js</h5> <script th:inline="javascript">/*inline="none"关闭,默认开启*/ /*<![CDATA[*/ var text = '[[${text}]]'; console.log(text); /*]]>*/ </script>6. 内嵌变量
为了模板更加易用,Thymeleaf还提供了一系列Utility对象(内置于Context中),可以通过#直接访问:
dates : java.util.Date**的功能方法类。calendars : 类似#dates,面向java.util.Calendarnumbers : 格式化数字的功能方法类strings : 字符串对象的功能类,contains,startWiths,prepending/appending等等。objects: 对objects的功能类操作。bools: 对布尔值求值的功能方法。arrays:对数组的功能类方法。lists: 对lists功能类方法setsmaps…
下面用一段代码来举例一些常用的方法:
dates
<h5>内置变量</h5> <h6 th:text="${#dates.createNow()}">获取当前日期</h6> <h6 th:text="*{#dates.format(birthday,'yyyy-MM-dd')}>日期格式转换</h6>strings
<h5>内置变量</h5> <h6 th:text="${#dates.createNow()}">获取当前日期</h6> <h6 th:text="${#strings.substring(text, 6, 9)}">截取字符串</h6> <h6 th:text="${#strings.length(text)}">获得长度</h6> <h6 th:text="${#strings.randomAlphanumeric(6)}">随机字符串</h6> <h6 th:text="${#strings.equals(text, 'hello text....')}"></h6>使用thymeleaf布局非常的方便
在/resources/templates/目录下创建footer.html,内容如下
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <body> <footer th:fragment="copy(title)"> © 2020 开课吧版权所有<br> <span th:text="${title}">title footer</span> </footer> </body> </html>在页面任何地方引入:
<h5>thymeleaf布局</h5> <div th:insert="footer :: copy('开课吧1')"></div> <div th:replace="footer :: copy('开课吧2')"></div> <div th:include="footer :: copy('开课吧3')"></div> th:insert :保留自己的主标签,保留th:fragment的主标签。th:replace :不要自己的主标签,保留th:fragment的主标签。th:include :保留自己的主标签,不要th:fragment的主标签。(官方3.0后不推荐)返回的HTML如下:
Mybatis-Plus(简称MP)是一个 Mybatis 的增强工具,在 Mybatis 的基础上只做增强不做改变,避免了我 们重复CRUD语句。
yml配置简介
在Springboot中,推荐使用properties或者YAML文件来完成配置,但是对于较复杂的数据结构来说,YAML又远远优于properties。我们快速介绍YAML的常见语法格式。
先来看一个Springboot中的properties文件和对应YAML文件的对比:
#properties(示例来源于Springboot User guide): environments.dev.url=http://dev.bar.com environments.dev.name=Developer Setup environments.prod.url=http://foo.bar.com environments.prod.name=My Cool App my.servers[0]=dev.bar.com my.servers[1]=foo.bar.com可以明显的看到,在处理层级关系的时候,properties需要使用大量的路径来描述层级(或者属性),比如environments.dev.url和environments.dev.name。其次,对于较为复杂的结构,比如数组(my.servers),写起来更为复杂。而对应的YAML格式文件就简单的多:
#YAML格式
environments: dev: url: http://dev.bar.com name: Developer Setup prod: url: http://foo.bar.com name: My Cool App my: servers: - dev.bar.com - foo.bar.comapplication.yml
# DataSource Config spring: datasource: driver-class-name: org.h2.Driver schema: classpath:db/schema-h2.sql data: classpath:db/data-h2.sql url: jdbc:h2:mem:test username: root password: test # Logger Config logging: level: com.sh.quickstart: debug # 配置mybatis plus mybatis-plus: type-aliases-package: com.sh.quickstart.entity #别名搜索 mapper-locations: classpath:/mappers/*.xml #加载映射文件数据库脚本文件/db/data-h2.sql和/db/schema-h2.sql(拷贝)
data-h2.sql:
DELETE FROM user2; INSERT INTO user2 (id, name, age, email) VALUES (1, 'Jone', 18, 'test1@baomidou.com'), (2, 'Jack', 20, 'test2@baomidou.com'), (3, 'Tom', 28, 'test3@baomidou.com');schema-h2.sql:
DROP TABLE IF EXISTS user2; CREATE TABLE user2 ( id BIGINT(20) NOT NULL COMMENT '主键ID', name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名', age INT(11) NULL DEFAULT NULL COMMENT '年龄', email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (id) );h2数据库是一个基于内存的数据库,在jvm启动时,自动执行脚本加载相应的数据
springboot 中使用h2数据库直接按照上面配置,配置schema表结构脚本和data数据脚本即可
注意这里用户名密码可以省略不写,或者随意设定
启动类
@SpringBootApplication @MapperScan("com.lxs.quickstart.mapper") public class QuickstartApplication { public static void main(String[] args) { SpringApplication.run(QuickstartApplication.class, args); } }实体类
@Data public class User { private Long id; private String name; private Integer age; private String email; }dao
public interface UserMapper extends BaseMapper<User> { }测试
@RunWith(SpringRunner.class) @SpringBootTest public class SampleTest { @Resource private UserMapper userMapper; @Test public void testSelect() { System.out.println(("----- selectAll method test ------")); List<User> userList = userMapper.selectList(null); Assert.assertEquals(6, userList.size()); userList.forEach(System.out::println); } }MyBatisPlus提供了一些注解供我们在实体类和表信息出现不对应的时候使用。通过使用注解完成逻辑上匹配。
注解名称说明@TableName实体类的类名和数据库表名不一致@TableId实体类的主键名称和表中主键名称不一致@TableField实体类中的成员名称和表中字段名称不一致>mybatis plus注解策略配置
mysql自增主键注解策略设置如下 @TableId(type = IdType.AUTO) private Long id; 默认主键策略 /** * 采用雪花算法生成全局唯一主键 **/ ASSIGN_ID(3), 排除实体类中非表字段 使用 @TableField(exist = false) 注解主键策略参考源码IdType优化left join count场景
在一对一join操作时,也存在优化可能,看下面sql
select u.id,ua.account from user u left join user_account ua on u.id=ua.uid #本来生成的count语句像这样 select count(1) from (select u.id,ua.account from user u left join user_account ua on u.id=ua.uid)这时候分页查count时,其实可以去掉left join直查user,因为user与user_account是1对1关系,如下:
查count: select count(1) from user u 查记录: select u.id,ua.account from user u left join user_account ua on u.id=ua.uid limit 0,50测试
@Test public void testPage() { System.out.println("------ baseMapper 自带分页 ------"); //参数1:第几页;参数2:每页行数 Page<User> page = new Page<>(2, 5); IPage<User> pageResult = userMapper.selectPage(page, Wrappers.<User>query()); //IPage<User> pageResult = userMapper.selectPage(page, new QueryWrapper<User>().eq("age", 20)); System.out.println("总条数 ------> " + pageResult.getTotal()); System.out.println("总页数 ------> " + pageResult.getPages()); System.out.println("当前页数 ------> " + pageResult.getCurrent()); System.out.println("当前每页显示数 ------> " + pageResult.getSize()); pageResult.getRecords().forEach(System.out :: println); }
application.yml配置文件
# 配置mybatis plus mybatis-plus: type-aliases-package: com.lxs.crud.entity #别名搜索 mapper-locations: classpath:/mappers/*.xml #加载映射文件UserMapper接口
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.sh.quickstart.entity.User; import org.apache.ibatis.annotations.Param; public interface UserMapper extends BaseMapper<User> { /** * 如果映射的接口方法有2个参数需要@Param定义参数名,定义参数名后,映射文件中使用p.属性 c.属性,具体访问 * @param page * @param conditioin * @return */ public IPage<User> selectUserByPage(@Param("p") IPage<User> page, @Param("c") User conditioin); public List<User> selectUserByPage2(User conditioin); }UserMapper.xml映射文件
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.sh.quickstart.mapper.UserMapper"> <sql id="selectSql"> SELECT * FROM user2 </sql> <select id="selectUserByPage" resultType="user"> <include refid="selectSql"></include> <where> <if test="c.age !=null"> age = #{c.age} </if> <if test="c.email !=null"> and email like '%${c.email}%' </if> </where> </select> </mapper>测试
@Test public void testXmlPage() { System.out.println("------ baseMapper 自定义xml分页 ------"); //参数1:第几页;参数2:每页行数 Page<User> page = new Page<>(2, 5); //条件对象 User user = new User(); user.setAge(331); user.setEmail("test4"); IPage<User> pageResult = userMapper.selectUserByPage(page,user); System.out.println("总条数 ------> " + pageResult.getTotal()); System.out.println("总页数 ------> " + pageResult.getPages()); System.out.println("当前页数 ------> " + pageResult.getCurrent()); System.out.println("当前每页显示数 ------> " + pageResult.getSize()); pageResult.getRecords().forEach(System.out :: println); }引入pageHelper依赖
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.1.11</version> </dependency>mybatis plus 整合pageHelper的配置类
@Configuration public class MybatisPlusConfig { /** * mp分页插件 */ @Bean public PaginationInterceptor paginationInterceptor() { // 开启 count 的 join 优化,只针对 left join !!! return new PaginationInterceptor().setCountSqlParser(new JsqlParserCountOptimize(true)); } /** * 两个分页插件都配置,不会冲突 * pagehelper的分页插件 */ @Bean public PageInterceptor pageInterceptor() { return new PageInterceptor(); } }映射文件
<select id="selectUserByPage2" resultType="user"> <include refid="selectSql"></include> <where> <if test="age !=null"> age = #{age} </if> <if test="email !=null"> and email like '%${email}%' </if> </where> </select>测试
@Test public void testPageHelper() { System.out.println("------ PageHelper分页 ------"); //条件对象 User u = new User(); u.setAge(331); u.setEmail("test4"); PageInfo<User> page = PageHelper.startPage(1, 2).doSelectPageInfo(()->{ userMapper.selectUserByPage2(u); }); //参数:当前页,每页行数 //PageHelper.startPage(1,2); //PageInfo<User> page = new PageInfo<>(userMapper.selectList(Wrappers.<User>query())); //PageInfo<User> page = new PageInfo<>(userMapper.selectUserByPage2(u)); page.getList().forEach(System.out::println); System.out.println("总行数=" + page.getTotal()); System.out.println("当前页=" + page.getPageNum()); System.out.println("每页行数=" + page.getPageSize()); System.out.println("总页数=" + page.getPages()); System.out.println("起始行数=" + page.getStartRow()); System.out.println("是第一页=" + page.isIsFirstPage()); System.out.println("是最后页=" + page.isIsLastPage()); System.out.println("还有下一页=" + page.isHasNextPage()); System.out.println("还有上一页=" + page.isHasPreviousPage()); System.out.println("页码列表" + Arrays.toString(page.getNavigatepageNums())); }
