菜单
本页目录

SpringBoot学习笔记

SpringBootWeb请求响应

请求响应:

请求(HttpServletRequest):获取请求数据

响应(HttpServletResponse):设置响应数据

BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。(维护方便,体验一般)

CS架构:Client/Server,客户端/服务器架构模式。(开发,维护麻烦,体验不错)

简单参数

  • 原始方式
  • SpringBoot方式

原始方式

@RestController
public class RequestController {
    //原始方式
    @RequestMapping("/simpleParam")
    public String simpleParam(HttpServletRequest request){
        // http://localhost:8080/simpleParam?name=Tom&age=10
        // 请求参数: name=Tom&age=10   (有2个请求参数)
        // 第1个请求参数: name=Tom   参数名:name,参数值:Tom
        // 第2个请求参数: age=10     参数名:age , 参数值:10

        String name = request.getParameter("name");//name就是请求参数名
        String ageStr = request.getParameter("age");//age就是请求参数名

        int age = Integer.parseInt(ageStr);//需要手动进行类型转换
        System.out.println(name+"  :  "+age);
        return "OK";
    }
}

SpringBoot方式

在Springboot的环境中,对原始的API进行了封装,接收参数的形式更加简单。 如果是简单参数,参数名与形参变量名相同,定义同名的形参即可接收参数。

@RestController
public class RequestController {
    // http://localhost:8080/simpleParam?name=Tom&age=10
    // 第1个请求参数: name=Tom   参数名:name,参数值:Tom
    // 第2个请求参数: age=10     参数名:age , 参数值:10
    
    //springboot方式
    @RequestMapping("/simpleParam")
    public String simpleParam(String name , Integer age ){//形参名和请求参数名保持一致
        System.out.println(name+"  :  "+age);
        return "OK";
    }
}

简单实体参数

@RestController
public class RequestController {
    //实体参数:简单实体对象
    @RequestMapping("/simplePojo")
    public String simplePojo(User user){
        System.out.println(user);
        return "OK";
    }
}

复杂实体参数

@RestController
public class RequestController {
    //实体参数:复杂实体对象
    @RequestMapping("/complexPojo")
    public String complexPojo(User user){
        System.out.println(user);
        return "OK";
    }
}

数组参数

@RestController
public class RequestController {
    //数组集合参数
    @RequestMapping("/arrayParam")
    public String arrayParam(String[] hobby){
        System.out.println(Arrays.toString(hobby));
        return "OK";
    }
}

集合参数

@RestController
public class RequestController {
    //数组集合参数
    @RequestMapping("/listParam")
    public String listParam(@RequestParam List<String> hobby){
        System.out.println(hobby);
        return "OK";
    }
}

日期时间参数

@RestController
public class RequestController {
    //日期时间参数
   @RequestMapping("/dateParam")
    public String dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime){
        System.out.println(updateTime);
        return "OK";
    }
}

JSON参数

@RestController
public class RequestController {
    //JSON参数
    @RequestMapping("/jsonParam")
    public String jsonParam(@RequestBody User user){
        System.out.println(user);
        return "OK";
    }
}

路径参数

@RestController
public class RequestController {
    //路径参数
    @RequestMapping("/path/{id}")
    public String pathParam(@PathVariable Integer id){
        System.out.println(id);
        return "OK";
    }
}

多路径参数

@RestController
public class RequestController {
    //路径参数
    @RequestMapping("/path/{id}/{name}")
    public String pathParam2(@PathVariable Integer id, @PathVariable String name){
        System.out.println(id+ " : " +name);
        return "OK";
    }
}

SpringBootWeb响应

controller方法中的return的结果,怎么就可以响应给浏览器呢? 使用@ResponseBody注解

@ResponseBody注解:

  • 类型:方法注解、类注解
  • 位置:书写在Controller方法上或类上
  • 作用:将方法返回值直接响应给浏览器
    • 如果返回值类型是实体对象/集合,将会转换为JSON格式后在响应给浏览器

但是在我们所书写的Controller中,只在类上添加了@RestController注解、方法添加了@RequestMapping注解,并没有使用@ResponseBody注解,怎么给浏览器响应呢?

@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String hello(){
        System.out.println("Hello World ~");
        return "Hello World ~";
    }
}

原因:在类上添加的@RestController注解,是一个组合注解。

  • @RestController = @Controller + @ResponseBody

@RestController源码:

@Target({ElementType.TYPE})   //元注解(修饰注解的注解)
@Retention(RetentionPolicy.RUNTIME)  //元注解
@Documented    //元注解
@Controller   
@ResponseBody 
public @interface RestController {
    @AliasFor(
        annotation = Controller.class
    )
    String value() default "";
}

结论:在类上添加@RestController就相当于添加了@ResponseBody注解。

  • 类上有@RestController注解或@ResponseBody注解时:表示当前类下所有的方法返回值做为响应数据
  • 方法的返回值,如果是一个POJO对象或集合时,会先转换为JSON格式,再响应给浏览器

测试响应数据:

@RestController
public class ResponseController {
    //响应字符串
    @RequestMapping("/hello")
    public String hello(){
        System.out.println("Hello World ~");
        return "Hello World ~";
    }
    //响应实体对象
    @RequestMapping("/getAddr")
    public Address getAddr(){
        Address addr = new Address();//创建实体类对象
        addr.setProvince("广东");
        addr.setCity("深圳");
        return addr;
    }
    //响应集合数据
    @RequestMapping("/listAddr")
    public List<Address> listAddr(){
        List<Address> list = new ArrayList<>();//集合对象
        
        Address addr = new Address();
        addr.setProvince("广东");
        addr.setCity("深圳");

        Address addr2 = new Address();
        addr2.setProvince("陕西");
        addr2.setCity("西安");

        list.add(addr);
        list.add(addr2);
        return list;
    }
}

统一响应结果

前端:只需要按照统一格式的返回结果进行解析(仅一种解析方案),就可以拿到数据。

统一的返回结果使用类来描述,在这个结果中包含:

  • 响应状态码:当前请求是成功,还是失败

  • 状态码信息:给页面的提示信息

  • 返回的数据:给前端响应的数据(字符串、对象、集合)

定义在一个实体类Result来包含以上信息。代码如下:

public class Result {
    private Integer code;//响应码,1 代表成功; 0 代表失败
    private String msg;  //响应码 描述字符串
    private Object data; //返回的数据

    public Result() { }
    public Result(Integer code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    //增删改 成功响应(不需要给前端返回数据)
    public static Result success(){
        return new Result(1,"success",null);
    }
    //查询 成功响应(把查询结果做为返回数据响应给前端)
    public static Result success(Object data){
        return new Result(1,"success",data);
    }
    //失败响应
    public static Result error(String msg){
        return new Result(0,msg,null);
    }
}

改造Controller:

@RestController
public class ResponseController { 
    //响应统一格式的结果
    @RequestMapping("/hello")
    public Result hello(){
        System.out.println("Hello World ~");
        //return new Result(1,"success","Hello World ~");
        return Result.success("Hello World ~");
    }

    //响应统一格式的结果
    @RequestMapping("/getAddr")
    public Result getAddr(){
        Address addr = new Address();
        addr.setProvince("广东");
        addr.setCity("深圳");
        return Result.success(addr);
    }

    //响应统一格式的结果
    @RequestMapping("/listAddr")
    public Result listAddr(){
        List<Address> list = new ArrayList<>();

        Address addr = new Address();
        addr.setProvince("广东");
        addr.setCity("深圳");

        Address addr2 = new Address();
        addr2.setProvince("陕西");
        addr2.setCity("西安");

        list.add(addr);
        list.add(addr2);
        return Result.success(list);
    }
}

分层解耦

三层架构

  • 数据访问:负责业务数据的维护操作,包括增、删、改、查等操作。
  • 逻辑处理:负责业务逻辑处理的代码。
  • 请求处理、响应数据:负责,接收页面的请求,给页面响应数据。

按照上述的三个组成部分,在我们项目开发中呢,可以将代码分为三层:

  • Controller:控制层。接收前端发送的请求,对请求进行处理,并响应数据。
  • Service:业务逻辑层。处理具体的业务逻辑。
  • Dao(又叫mapper层):数据访问层(Data Access Object),也称为持久层。负责数据访问操作,包括数据的增、删、改、查。

基于三层架构的程序执行流程:

  • 前端发起的请求,由Controller层接收(Controller响应数据给前端)

  • Controller层调用Service层来进行逻辑处理(Service层处理完后,把处理结果返回给Controller层)

  • Serivce层调用Dao层(逻辑处理过程中需要用到的一些数据要从Dao层获取)

  • Dao层操作文件中的数据(Dao拿到的数据会返回给Service层)

三层架构思想

  • 控制层包名:xxxx.controller
  • 业务逻辑层包名:xxxx.service
  • 数据访问层包名:xxxx.dao

**控制层:**接收前端发送的请求,对请求进行处理,并响应数据 **业务逻辑层:**处理具体的业务逻辑 **数据访问层:**负责数据的访问操作,包含数据的增、删、改、查

三层架构的好处:

  1. 复用性强
  2. 便于维护
  3. 利用扩展

分层解耦

软件开发涉及到的两个概念:内聚和耦合。

  • 内聚:软件中各个功能模块内部的功能联系。

  • 耦合:衡量软件中各个层/模块之间的依赖、关联的程度。

软件设计原则:高内聚低耦合。

  • 高内聚指的是:一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联系程度越高,则内聚性越高,即 "高内聚"。

  • 低耦合指的是:软件中各个层、模块之间的依赖关联程序越低越好。

解耦思路

  • 控制反转: Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。

对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。这个容器称为:IOC容器或Spring容器

  • 依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。

程序运行时需要某个资源,此时容器就为其提供这个资源。

例:EmpController程序运行时需要EmpService对象,Spring容器就为其提供并注入EmpService对象 第1步:删除Controller层、Service层中new对象的代码 第2步:Service层及Dao层的实现类,交给IOC容器管理 第3步:为Controller及Service注入运行时依赖的对象

IOC容器中创建、管理的对象,称之为:bean对象

IOC&DI

IOC控制反转详解

@Component 将当前对象交给IOC容器管理,成为IOC容器的bean @Autowired 运行时,需要从IOC容器中获取该类型对象,赋值给该变量

要把某个对象交给IOC容器管理,需要在类上添加一个注解:@Component @Component 声明bean的基础注解 而Spring框架为了更好的标识web应用程序开发当中,bean对象到底归属于哪一层,又提供了@Component的衍生注解:

  • @Controller (标注在控制层类上)
  • @Service (标注在业务层类上)
  • @Repository (标注在数据访问层类上)

要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:

注解说明位置
@Controller@Component的衍生注解标注在控制器类上
@Service@Component的衍生注解标注在业务类上
@Repository@Component的衍生注解标注在数据访问类上(由于与mybatis整合,用的少)
@Component声明bean的基础注解不属于以上三类时,用此注解

在IOC容器中,每一个Bean都有一个属于自己的名字,可以通过注解的value属性指定bean的名字。如果没有指定,默认为类名首字母小写。

**注意事项: **

  • 声明bean的时候,可以通过value属性指定bean的名字,如果没有指定,默认为类名首字母小写。
  • 使用以上四个注解都可以声明bean,但是在springboot集成web开发中,声明控制器bean只能用@Controller。

使用四大注解声明的bean,要想生效,还需要被组件扫描注解@ComponentScan扫描

@ComponentScan注解虽然没有显式配置,但是实际上已经包含在了引导类声明注解 @SpringBootApplication 中,默认扫描的范围是SpringBoot启动类所在包及其子包。

DI依赖注入详解

依赖注入,是指IOC容器要为应用程序去提供运行时所依赖的资源,而资源指的就是对象。 @Autowired这个注解,完成了依赖注入的操作,而这个Autowired翻译过来叫:自动装配。 @Autowired注解,默认是按照类型进行自动装配的(去IOC容器中找某个类型的对象,然后完成注入操作) 那如果在IOC容器中,存在多个相同类型的bean对象,会出现什么情况呢? 如何解决上述问题呢?Spring提供了以下几种解决方案:

  • @Primary
  • @Qualifier
  • @Resource
  1. 使用@Primary注解:当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现。

  2. 使用@Qualifier注解:指定当前要注入的bean对象。 在@Qualifier的value属性中,指定注入的bean的名称。

  • @Qualifier注解不能单独使用,必须配合@Autowired使用
  1. 使用@Resource注解:是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。

面试题 : @Autowird 与 @Resource的区别

  • @Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解
  • @Autowired 默认是按照类型注入,而@Resource是按照名称注入
  1. 依赖注入的注解 @Autowired:默认按照类型自动装配。 如果同类型的bean存在多个: @Primary @Autowired + @Qualifier("bean的名称") @Resource(name="bean的名称")
  2. @Resource 与 @Autowired区别 @Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解。 @Autowired 默认是按照类型注入,而@Resource默认是按照名称注入。

Mybatis入门

什么是Mybatis?

  • MyBatis是一款优秀的 持久层 框架,用于简化JDBC的开发。

  • MyBatis本是 Apache的一个开源项目iBatis,2010年这个项目由apache迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github。

  • 官网:https://mybatis.org/mybatis-3/zh/index.html

Java程序操作数据库,现在主流的方式是:Mybatis。

引入Mybatis依赖

<!-- 仅供参考:只粘贴了pom.xml中部分内容 -->
<dependencies>
        <!-- mybatis起步依赖 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.3.0</version>
        </dependency>

        <!-- mysql驱动包依赖 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- spring单元测试 (集成了junit) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
</dependencies>

配置application.properties

在application.properties文件中,配置数据库连接信息。我们要连接数据库,就需要配置数据库连接的基本信息,包括:driver-class-name、url 、username,password。

application.properties:

#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=1234

@Mapper注解:表示是mybatis中的Mapper接口 程序运行时:框架会自动生成接口的实现类对象(代理对象),并给交Spring的IOC容器管理 @Select注解:代表的就是select查询,用于书写select查询语句

在创建出来的SpringBoot工程中,在src下的test目录下,已经自动帮我们创建好了测试类 ,并且在测试类上已经添加了注解 @SpringBootTest,代表该测试类已经与SpringBoot整合。

该测试类在运行时,会自动通过引导类加载Spring的环境(IOC容器)。我们要测试那个bean对象,就可以直接通过@Autowired注解直接将其注入进行,然后就可以测试了。

JDBC

( Java DataBase Connectivity ),就是使用Java语言操作关系型数据库的一套API。

本质: sun公司官方定义的一套操作所有关系型数据库的规范,即接口。 各个数据库厂商去实现这套接口,提供数据库驱动jar包。 我们可以使用这套接口(JDBC)编程,真正执行的代码是驱动jar包中的实现类。 原始的JDBC程序是如何操作数据库的。操作步骤如下:

  1. 注册驱动
  2. 获取连接对象
  3. 执行SQL语句,返回执行结果
  4. 处理执行结果
  5. 释放资源

在pom.xml文件中已引入MySQL驱动依赖,我们直接编写JDBC代码即可

原始的JDBC程序,存在以下几点问题:

  1. 数据库链接的四要素(驱动、链接、用户名、密码)全部硬编码在java代码中
  2. 查询结果的解析及封装非常繁琐
  3. 每一次查询数据库都需要获取连接,操作完毕后释放连接, 资源浪费, 性能降低

JDBC和Mybatis技术对比

分析了JDBC的缺点之后,我们再来看一下在mybatis中,是如何解决这些问题的:

  1. 数据库连接四要素(驱动、链接、用户名、密码),都配置在springboot默认的配置文件 application.properties中

  2. 查询结果的解析及封装,由mybatis自动完成映射封装,我们无需关注

  3. 在mybatis中使用了数据库连接池技术,从而避免了频繁的创建连接、销毁连接而带来的资源浪费。

Druid连接池

数据库连接池是个容器,负责分配、管理数据库连接(Connection)

  • 程序在启动时,会在数据库连接池(容器)中,创建一定数量的Connection对象

允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个

  • 客户端在执行SQL时,先从连接池中获取一个Connection对象,然后在执行SQL语句,SQL语句执行完之后,释放Connection时就会把Connection对象归还给连接池(Connection对象可以复用)

释放空闲时间超过最大空闲时间的连接,来避免因为没有释放连接而引起的数据库连接遗漏

  • 客户端获取到Connection对象了,但是Connection对象并没有去访问数据库(处于空闲),数据库连接池发现Connection对象的空闲时间 > 连接池中预设的最大空闲时间,此时数据库连接池就会自动释放掉这个连接对象

数据库连接池的好处:

  1. 资源重用
  2. 提升系统响应速度
  3. 避免数据库连接遗漏

Druid(德鲁伊)

Druid连接池是阿里巴巴开源的数据库连接池项目

功能强大,性能优秀,是Java语言最好的数据库连接池之一

  1. 在pom.xml文件中引入依赖
<dependency>
    <!-- Druid连接池依赖 -->
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.8</version>
</dependency>
  1. 在application.properties中引入数据库连接配置

方式1:

spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.druid.username=root
spring.datasource.druid.password=1234

方式2:

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.username=root
spring.datasource.password=1234

lombok封装类

lombok介绍

Lombok是一个实用的Java类库,可以通过简单的注解来简化和消除一些必须有但显得很臃肿的Java代码。

通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString等方法,并可以自动化生成日志变量,简化java开发、提高效率。

注解作用
@Getter/@Setter为所有的属性提供get/set方法
@ToString会给类自动生成易阅读的 toString 方法
@EqualsAndHashCode根据类所拥有的非静态字段自动重写 equals 方法和 hashCode 方法
@Data提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @EqualsAndHashCode)
@NoArgsConstructor为实体类生成无参的构造器方法
@AllArgsConstructor为实体类生成除了static修饰的字段之外带有各参数的构造器方法。

lombok使用

第1步:在pom.xml文件中引入依赖

<!-- 在springboot的父工程中,已经集成了lombok并指定了版本号,故当前引入依赖时不需要指定version -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

第2步:在实体类上添加注解

import lombok.Data;

@Data
public class User {
    private Integer id;
    private String name;
    private Short age;
    private Short gender;
    private String phone;
}

在实体类上添加了@Data注解,那么这个类在编译时期,就会生成getter/setter、equals、hashcode、toString等方法。

说明:@Data注解中不包含全参构造方法,通常在实体类上,还会添加上:全参构造、无参构造

import lombok.Data;

@Data //getter方法、setter方法、toString方法、hashCode方法、equals方法
@NoArgsConstructor //无参构造
@AllArgsConstructor//全参构造
public class User {
    private Integer id;
    private String name;
    private Short age;
    private Short gender;
    private String phone;
}

Lombok的注意事项:

  • Lombok会在编译时,会自动生成对应的java代码
  • 在使用lombok时,还需要安装一个lombok的插件(新版本的IDEA中自带)

Mybatis基础操作(增删改查)

准备工作

application.properties中引入数据库连接信息

#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=1234
#配置mybatis的日志, 指定输出到控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#开启mybatis的驼峰命名自动映射开关 a_column ------> aCloumn
mybatis.configuration.map-underscore-to-camel-case=true

创建对应的实体类Emp(实体类属性采用驼峰命名)

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {
    private Integer id;
    private String username;
    private String password;
    private String name;
    private Short gender;
    private String image;
    private Short job;
    private LocalDate entrydate;     //LocalDate类型对应数据表中的date类型
    private Integer deptId;
    private LocalDateTime createTime;//LocalDateTime类型对应数据表中的datetime类型
    private LocalDateTime updateTime;
}

准备Mapper接口:EmpMapper

/*@Mapper注解:表示当前接口为mybatis中的Mapper接口
  程序运行时会自动创建接口的实现类对象(代理对象),并交给Spring的IOC容器管理
*/
@Mapper
public interface EmpMapper {

}

删除

功能实现

当我们点击后面的"删除"按钮时,前端页面会给服务端传递一个参数,也就是该行数据的ID。 我们接收到ID后,根据ID删除数据即可。 功能:根据主键删除数据

  • SQL语句
-- 删除id=17的数据
delete from emp where id = 17;

Mybatis框架让程序员更关注于SQL语句

  • 接口方法
@Mapper
public interface EmpMapper {
    
    //@Delete("delete from emp where id = 17")
    //public void delete();
    //以上delete操作的SQL语句中的id值写成固定的17,就表示只能删除id=17的用户数据
    //SQL语句中的id值不能写成固定数值,需要变为动态的数值
    //解决方案:在delete方法中添加一个参数(用户id),将方法中的参数,传给SQL语句
    
    /**
     * 根据id删除数据
     * @param id    用户id
     */
    @Delete("delete from emp where id = #{id}")//使用#{key}方式获取方法中的参数值
    public void delete(Integer id);
    
}

@Delete注解:用于编写delete操作的SQL语句

如果mapper接口方法形参只有一个普通类型的参数,#{…} 里面的属性名可以随便写,如:#{id}、#{value}。但是建议保持名字一致。

  • 测试 在单元测试类中通过@Autowired注解注入EmpMapper类型对象
@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
    @Autowired //从Spring的IOC容器中,获取类型是EmpMapper的对象并注入
    private EmpMapper empMapper;

    @Test
    public void testDel(){
        //调用删除方法
        empMapper.delete(16);
    }

}

日志输入

在Mybatis当中我们可以借助日志,查看到sql语句的执行、执行传递的参数以及执行结果。具体操作如下:

  1. 打开application.properties文件

  2. 开启mybatis的日志,并指定输出到控制台

#指定mybatis输出日志的位置, 输出控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

开启日志之后,我们再次运行单元测试,可以看到在控制台中,输出了以下的SQL语句信息:

但是我们发现输出的SQL语句:delete from emp where id = ?,我们输入的参数16并没有在后面拼接,id的值是使用?进行占位。那这种SQL语句我们称为预编译SQL。

预编译SQL

预编译SQL介绍

预编译SQL有两个优势:

  1. 性能更高
  2. 更安全(防止SQL注入)

性能更高:预编译SQL,编译一次之后会将编译后的SQL语句缓存起来,后面再次执行这条语句时,不会再次编译。(只是输入的参数不同)

更安全(防止SQL注入):将敏感字进行转义,保障SQL的安全性。

SQL注入

SQL注入:是通过操作输入的数据来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法。

由于没有对用户输入进行充分检查,而SQL又是拼接而成,在用户输入参数时,在参数中添加一些SQL关键字,达到改变SQL运行结果的目的,也可以完成恶意攻击。

把整个' or '1'='1作为一个完整的参数,赋值给第2个问号(' or '1'='1进行了转义,只当做字符串使用)

参数占位符

在Mybatis中提供的参数占位符有两种:${...} 、#{...}

  • #{...}

    • 执行SQL时,会将#{…}替换为?,生成预编译SQL,会自动设置参数值
    • 使用时机:参数传递,都使用#{…}
  • ${...}

    • 拼接SQL。直接将参数拼接在SQL语句中,存在SQL注入问题
    • 使用时机:如果对表名、列表进行动态设置时使用

注意事项:在项目开发中,建议使用#{...},生成预编译SQL,防止SQL注入安全。

主键返回

如何实现在插入数据之后返回所插入行的主键值呢?

  • 默认情况下,执行插入操作时,是不会主键值返回的。如果我们想要拿到主键值,需要在Mapper接口中的方法上添加一个Options注解,并在注解中指定属性useGeneratedKeys=true和keyProperty="实体类属性名"

主键返回代码实现:

@Mapper
public interface EmpMapper {
    
    //会自动将生成的主键值,赋值给emp对象的id属性
    @Options(useGeneratedKeys = true,keyProperty = "id")
    @Insert("insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) values (#{username}, #{name}, #{gender}, #{image}, #{job}, #{entrydate}, #{deptId}, #{createTime}, #{updateTime})")
    public void insert(Emp emp);

}

测试:

@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
    @Autowired
    private EmpMapper empMapper;

    @Test
    public void testInsert(){
        //创建员工对象
        Emp emp = new Emp();
        emp.setUsername("jack");
        emp.setName("杰克");
        emp.setImage("1.jpg");
        emp.setGender((short)1);
        emp.setJob((short)1);
        emp.setEntrydate(LocalDate.of(2000,1,1));
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());
        emp.setDeptId(1);
        //调用添加方法
        empMapper.insert(emp);

        System.out.println(emp.getDeptId());
    }
}

更新

功能:修改员工信息 点击"编辑"按钮后,会查询所在行记录的员工信息,并把员工信息回显在修改员工的窗体上 在修改员工的窗体上,可以修改的员工数据:用户名、员工姓名、性别、图像、职位、入职日期、归属部门

思考:在修改员工数据时,要以什么做为条件呢? 答案:员工id

SQL语句:

update emp set username = 'linghushaoxia', name = '令狐少侠', gender = 1 , image = '1.jpg' , job = 2, entrydate = '2012-01-01', dept_id = 2, update_time = '2022-10-01 12:12:12' where id = 18;

接口方法:

@Mapper
public interface EmpMapper {
    /**
     * 根据id修改员工信息
     * @param emp
     */
    @Update("update emp set username=#{username}, name=#{name}, gender=#{gender}, image=#{image}, job=#{job}, entrydate=#{entrydate}, dept_id=#{deptId}, update_time=#{updateTime} where id=#{id}")
    public void update(Emp emp);
    
}

测试类:

@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
    @Autowired
    private EmpMapper empMapper;

    @Test
    public void testUpdate(){
        //要修改的员工信息
        Emp emp = new Emp();
        emp.setId(23);
        emp.setUsername("songdaxia");
        emp.setPassword(null);
        emp.setName("老宋");
        emp.setImage("2.jpg");
        emp.setGender((short)1);
        emp.setJob((short)2);
        emp.setEntrydate(LocalDate.of(2012,1,1));
        emp.setCreateTime(null);
        emp.setUpdateTime(LocalDateTime.now());
        emp.setDeptId(2);
        //调用方法,修改员工数据
        empMapper.update(emp);
    }
}

查询

根据ID查询

在员工管理的页面中,当我们进行更新数据时,会点击 “编辑” 按钮,然后此时会发送一个请求到服务端,会根据Id查询该员工信息,并将员工数据回显在页面上。

SQL语句:

select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp;

接口方法:

@Mapper
public interface EmpMapper {
    @Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp where id=#{id}")
    public Emp getById(Integer id);
}

测试类:

@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
    @Autowired
    private EmpMapper empMapper;

    @Test
    public void testGetById(){
        Emp emp = empMapper.getById(1);
        System.out.println(emp);
    }
}

而在测试的过程中,我们会发现有几个字段(deptId、createTime、updateTime)是没有数据值的

数据封装

我们看到查询返回的结果中大部分字段是有值的,但是deptId,createTime,updateTime这几个字段是没有值的,而数据库中是有对应的字段值的,这是为什么呢?

原因如下:

  • 实体类属性名和数据库表查询返回的字段名一致,mybatis会自动封装。
  • 如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。

解决方案:

  1. 起别名
  2. 结果映射
  3. 开启驼峰命名

起别名:在SQL语句中,对不一样的列名起别名,别名和实体类属性名一样

@Select("select id, username, password, name, gender, image, job, entrydate, " +
        "dept_id AS deptId, create_time AS createTime, update_time AS updateTime " +
        "from emp " +
        "where id=#{id}")
public Emp getById(Integer id);

手动结果映射:通过 @Results及@Result 进行手动结果映射

@Results({@Result(column = "dept_id", property = "deptId"),
          @Result(column = "create_time", property = "createTime"),
          @Result(column = "update_time", property = "updateTime")})
@Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp where id=#{id}")
public Emp getById(Integer id);

@Results源代码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Results {
String id() default "";
 Result[] value() default {};  //Result类型的数组
 }

@Result源代码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Repeatable(Results.class)
public @interface Result {
boolean id() default false;//表示当前列是否为主键(true:是主键)
String column() default "";//指定表中字段名
String property() default "";//指定类中属性名
Class<?> javaType() default void.class;
JdbcType jdbcType() default JdbcType.UNDEFINED;
Class<? extends TypeHandler> typeHandler() default   UnknownTypeHandler.class;
 One one() default @One;
 Many many() default @Many;
 }

开启驼峰命名(推荐):如果字段名与属性名符合驼峰命名规则,mybatis会自动通过驼峰命名规则映射

驼峰命名规则: abc_xyz => abcXyz

  • 表中字段名:abc_xyz
  • 类中属性名:abcXyz
# 在application.properties中添加:
mybatis.configuration.map-underscore-to-camel-case=true

要使用驼峰命名前提是 实体类的属性 与 数据库表中的字段名严格遵守驼峰命名。

条件查询

在员工管理的列表页面中,我们需要根据条件查询员工信息,查询条件包括:姓名、性别、入职时间。

通过页面原型以及需求描述我们要实现的查询:

  • 姓名:要求支持模糊匹配
  • 性别:要求精确匹配
  • 入职时间:要求进行范围查询
  • 根据最后修改时间进行降序排序

SQL语句:

select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time 
from emp 
where name like '%张%' 
      and gender = 1 
      and entrydate between '2010-01-01' and '2020-01-01 ' 
order by update_time desc;

接口方法:

  • 方式一
@Mapper
public interface EmpMapper {
    @Select("select * from emp " +
            "where name like '%${name}%' " +
            "and gender = #{gender} " +
            "and entrydate between #{begin} and #{end} " +
            "order by update_time desc")
    public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);
}

以上方式注意事项:

  1. 方法中的形参名和SQL语句中的参数占位符名保持一致

  2. 模糊查询使用${...}进行字符串拼接,这种方式呢,由于是字符串拼接,并不是预编译的形式,所以效率不高、且存在sql注入风险。

  • 方式二(解决SQL注入风险)
    • 使用MySQL提供的字符串拼接函数:concat('%' , '关键字' , '%')
@Mapper
public interface EmpMapper {

    @Select("select * from emp " +
            "where name like concat('%',#{name},'%') " +
            "and gender = #{gender} " +
            "and entrydate between #{begin} and #{end} " +
            "order by update_time desc")
    public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);

}

执行结果:生成的SQL都是预编译的SQL语句(性能高、安全)

在上面我们所编写的条件查询功能中,我们需要保证接口中方法的形参名和SQL语句中的参数占位符名相同。

当方法中的形参名和SQL语句中的占位符参数名不相同时,就会出现以下问题: image-20221212151628715

参数名在不同的SpringBoot版本中,处理方案还不同:

  • 在springBoot的2.x版本(保证参数名一致)

image-20221212151736274

springBoot的父工程对compiler编译插件进行了默认的参数parameters配置,使得在编译时,会在生成的字节码文件中保留原方法形参的名称,所以#{…}里面可以直接通过形参名获取对应的值

  • 在springBoot的1.x版本/单独使用mybatis(使用@Param注解来指定SQL语句中的参数名)

在编译时,生成的字节码文件当中,不会保留Mapper接口中方法的形参名称,而是使用var1、var2、...这样的形参名字,此时要获取参数值时,就要通过@Param注解来指定SQL语句中的参数名

Mybatis的XML配置文件

Mybatis的开发有两种方式:

  1. 注解
  2. XML

XML配置文件规范

使用Mybatis的注解方式,主要是来完成一些简单的增删改查功能。如果需要实现复杂的SQL功能,建议使用XML来配置映射语句,也就是将SQL语句写在XML配置文件中。

在Mybatis中使用XML映射文件方式开发,需要符合一定的规范:

  1. XML映射文件的名称与Mapper接口名称一致,并且将XML映射文件和Mapper接口放置在相同包下(同包同名)

  2. XML映射文件的namespace属性为Mapper接口全限定名一致

  3. XML映射文件中sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致。

<select>标签:就是用于编写select查询语句的。

resultType属性,指的是查询返回的单条记录所封装的类型。

XML配置文件实现

第1步:创建XML映射文件

第2步:编写XML映射文件 xml映射文件中的dtd约束,直接从mybatis官网复制即可

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="">
 
</mapper>

配置:XML映射文件的namespace属性为Mapper接口全限定名

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper">

</mapper>

配置:XML映射文件中sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper">

    <!--查询操作-->
    <select id="list" resultType="com.itheima.pojo.Emp">
        select * from emp
        where name like concat('%',#{name},'%')
              and gender = #{gender}
              and entrydate between #{begin} and #{end}
        order by update_time desc
    </select>
</mapper>

MybatisX的使用

MybatisX是一款基于IDEA的快速开发Mybatis的插件,为效率而生。

可以通过MybatisX快速定位:

学习了Mybatis中XML配置文件的开发方式了,大家可能会存在一个疑问:到底是使用注解方式开发还是使用XML方式开发?

官方说明:https://mybatis.net.cn/getting-started.html

使用Mybatis的注解,主要是来完成一些简单的增删改查功能。如果需要实现复杂的SQL功能,建议使用XML来配置映射语句。

Mybatis动态SQL

什么是动态SQL

在页面原型中,列表上方的条件是动态的,是可以不传递的,也可以只传递其中的1个或者2个或者全部。

而在我们刚才编写的SQL语句中,我们会看到,我们将三个条件直接写死了。 如果页面只传递了参数姓名name 字段,其他两个字段 性别 和 入职时间没有传递,那么这两个参数的值就是null。

传递了参数,再组装这个查询条件;如果没有传递参数,就不应该组装这个查询条件。

比如:如果姓名输入了"张", 对应的SQL为:

select *  from emp where name like '%张%' order by update_time desc;

如果姓名输入了"张",,性别选择了"男",则对应的SQL为:

select *  from emp where name like '%张%' and gender = 1 order by update_time desc;

SQL语句会随着用户的输入或外部条件的变化而变化,我们称为:动态SQL

在Mybatis中提供了很多实现动态SQL的标签,我们学习Mybatis中的动态SQL就是掌握这些动态SQL标签。

动态SQL-if

<if>:用于判断条件是否成立。使用test属性进行条件判断,如果条件为true,则拼接SQL。

<if test="条件表达式">
   要拼接的sql语句
</if>

接下来,我们就通过<if>标签来改造之前条件查询的案例。

条件查询

示例:把SQL语句改造为动态SQL方式

  • 原有的SQL语句
<select id="list" resultType="com.itheima.pojo.Emp">
        select * from emp
        where name like concat('%',#{name},'%')
              and gender = #{gender}
              and entrydate between #{begin} and #{end}
        order by update_time desc
</select>
  • 动态SQL语句
<select id="list" resultType="com.itheima.pojo.Emp">
        select * from emp
        where
    
             <if test="name != null">
                 name like concat('%',#{name},'%')
             </if>
             <if test="gender != null">
                 and gender = #{gender}
             </if>
             <if test="begin != null and end != null">
                 and entrydate between #{begin} and #{end}
             </if>
    
        order by update_time desc
</select>

测试方法:

@Test
public void testList(){
    //性别数据为null、开始时间和结束时间也为null
    List<Emp> list = empMapper.list("张", null, null, null);
    for(Emp emp : list){
        System.out.println(emp);
    }
}

修改测试方法中的代码,再次进行测试,观察执行情况:

@Test
public void testList(){
    //姓名为null
    List<Emp> list = empMapper.list(null, (short)1, null, null);
    for(Emp emp : list){
        System.out.println(emp);
    }
}

再次修改测试方法中的代码,再次进行测试:

@Test
public void testList(){
    //传递的数据全部为null
    List<Emp> list = empMapper.list(null, null, null, null);
    for(Emp emp : list){
        System.out.println(emp);
    }
}

以上问题的解决方案:使用<where>标签代替SQL语句中的where关键字

  • <where>只会在子元素有内容的情况下才插入where子句,而且会自动去除子句的开头的AND或OR
<select id="list" resultType="com.itheima.pojo.Emp">
        select * from emp
        <where>
             <!-- if做为where标签的子元素 -->
             <if test="name != null">
                 and name like concat('%',#{name},'%')
             </if>
             <if test="gender != null">
                 and gender = #{gender}
             </if>
             <if test="begin != null and end != null">
                 and entrydate between #{begin} and #{end}
             </if>
        </where>
        order by update_time desc
</select>

测试方法:

@Test
public void testList(){
    //只有性别
    List<Emp> list = empMapper.list(null, (short)1, null, null);
    for(Emp emp : list){
        System.out.println(emp);
    }
}

更新员工

案例:完善更新员工功能,修改为动态更新员工数据信息

  • 动态更新员工信息,如果更新时传递有值,则更新;如果更新时没有传递值,则不更新
  • 解决方案:动态SQL

修改Mapper接口:

@Mapper
public interface EmpMapper {
    //删除@Update注解编写的SQL语句
    //update操作的SQL语句编写在Mapper映射文件中
    public void update(Emp emp);
}

修改Mapper映射文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper">

    <!--更新操作-->
    <update id="update">
        update emp
        set
            <if test="username != null">
                username=#{username},
            </if>
            <if test="name != null">
                name=#{name},
            </if>
            <if test="gender != null">
                gender=#{gender},
            </if>
            <if test="image != null">
                image=#{image},
            </if>
            <if test="job != null">
                job=#{job},
            </if>
            <if test="entrydate != null">
                entrydate=#{entrydate},
            </if>
            <if test="deptId != null">
                dept_id=#{deptId},
            </if>
            <if test="updateTime != null">
                update_time=#{updateTime}
            </if>
        where id=#{id}
    </update>

</mapper>

测试方法:

@Test
public void testUpdate2(){
        //要修改的员工信息
        Emp emp = new Emp();
        emp.setId(20);
        emp.setUsername("Tom111");
        emp.setName("汤姆111");

        emp.setUpdateTime(LocalDateTime.now());

        //调用方法,修改员工数据
        empMapper.update(emp);
}

再次修改测试方法,观察SQL语句执行情况:

@Test
public void testUpdate2(){
        //要修改的员工信息
        Emp emp = new Emp();
        emp.setId(20);
        emp.setUsername("Tom222");
      
        //调用方法,修改员工数据
        empMapper.update(emp);
}

以上问题的解决方案:使用<set>标签代替SQL语句中的set关键字

  • <set>:动态的在SQL语句中插入set关键字,并会删掉额外的逗号。(用于update语句中)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper">

    <!--更新操作-->
    <update id="update">
        update emp
        <!-- 使用set标签,代替update语句中的set关键字 -->
        <set>
            <if test="username != null">
                username=#{username},
            </if>
            <if test="name != null">
                name=#{name},
            </if>
            <if test="gender != null">
                gender=#{gender},
            </if>
            <if test="image != null">
                image=#{image},
            </if>
            <if test="job != null">
                job=#{job},
            </if>
            <if test="entrydate != null">
                entrydate=#{entrydate},
            </if>
            <if test="deptId != null">
                dept_id=#{deptId},
            </if>
            <if test="updateTime != null">
                update_time=#{updateTime}
            </if>
        </set>
        where id=#{id}
    </update>
</mapper>

小结

  • <if>

    • 用于判断条件是否成立,如果条件为true,则拼接SQL

    • 形式:

      <if test="name != null"> … </if>
      
  • <where>

    • where元素只会在子元素有内容的情况下才插入where子句,而且会自动去除子句的开头的AND或OR
  • <set>

    • 动态地在行首插入 SET 关键字,并会删掉额外的逗号。(用在update语句中)

动态SQL-foreach

案例:员工删除功能(既支持删除单条记录,又支持批量删除)

SQL语句:

delete from emp where id in (1,2,3);

Mapper接口:

@Mapper
public interface EmpMapper {
    //批量删除
    public void deleteByIds(List<Integer> ids);
}

XML映射文件:

  • 使用<foreach>遍历deleteByIds方法中传递的参数ids集合
<foreach collection="集合名称" item="集合遍历出来的元素/项" separator="每一次遍历使用的分隔符" 
         open="遍历开始前拼接的片段" close="遍历结束后拼接的片段">
</foreach>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper">
    <!--删除操作-->
    <delete id="deleteByIds">
        delete from emp where id in
        <foreach collection="ids" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
    </delete>
</mapper> 

动态SQL-sql&include

问题分析:

  • 在xml映射文件中配置的SQL,有时可能会存在很多重复的片段,此时就会存在很多冗余的代码

我们可以对重复的代码片段进行抽取,将其通过<sql>标签封装到一个SQL片段,然后再通过<include>标签进行引用。

  • <sql>:定义可重用的SQL片段

  • <include>:通过属性refid,指定包含的SQL片段

SQL片段: 抽取重复的代码

<sql id="commonSelect">
 	select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp
</sql>

然后通过<include> 标签在原来抽取的地方进行引用。操作如下:

<select id="list" resultType="com.itheima.pojo.Emp">
    <include refid="commonSelect"/>
    <where>
        <if test="name != null">
            name like concat('%',#{name},'%')
        </if>
        <if test="gender != null">
            and gender = #{gender}
        </if>
        <if test="begin != null and end != null">
            and entrydate between #{begin} and #{end}
        </if>
    </where>
    order by update_time desc
</select>

新增员工

接口文档规定: 请求路径:/emps 请求方式:POST 请求参数:Json格式数据 响应数据:Json格式数据 问题1:如何限定请求方式是POST?

 @PostMapping

问题2:怎么在controller中接收json格式的请求参数?

@RequestBody  //把前端传递的json数据填充到实体类中

新增员工的具体的流程 功能开发

EmpController

@Slf4j
@RestController
@RequestMapping("/emps")
public class EmpController {

    @Autowired
    private EmpService empService;

    //新增
    @PostMapping
    public Result save(@RequestBody Emp emp){
        //记录日志
        log.info("新增员工, emp:{}",emp);
        //调用业务层新增功能
        empService.save(emp);
        //响应
        return Result.success();
    }

    //省略...
}

EmpService

public interface EmpService {

    /**
     * 保存员工信息
     * @param emp
     */
    void save(Emp emp);
    
    //省略...
}

EmpServiceImpl

@Slf4j
@Service
public class EmpServiceImpl implements EmpService {
    @Autowired
    private EmpMapper empMapper;

    @Override
    public void save(Emp emp) {
        //补全数据
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());
        //调用添加方法
        empMapper.insert(emp);
    }

    //省略...
}

EmpMapper

@Mapper
public interface EmpMapper {
    //新增员工
    @Insert("insert into emp (username, name, gender, image, job, entrydate, dept_id, create_time, update_time) " +
            "values (#{username}, #{name}, #{gender}, #{image}, #{job}, #{entrydate}, #{deptId}, #{createTime}, #{updateTime});")
    void insert(Emp emp);

    //省略...
}

文件上传

文件上传,是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程。

文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。

  • 表单必须有file域,用于选择要上传的文件
<input type="file" name="image"/>
  • 表单提交方式必须为POST

    通常上传的文件会比较大,所以需要使用 POST 提交方式

  • 表单的编码类型enctype必须要设置为:multipart/form-data

    普通默认的编码格式是不适合传输大型的二进制数据的,所以在文件上传时,表单的编码格式必须设置为multipart/form-data

  • 首先在服务端定义这么一个controller,用来进行文件上传,然后在controller当中定义一个方法来处理/upload 请求

  • 在定义的方法中接收提交过来的数据 (方法中的形参名和请求参数的名字保持一致)

    • 用户名:String name
    • 年龄: Integer age
    • 文件: MultipartFile image

    Spring中提供了一个API:MultipartFile,使用这个API就可以来接收到上传的文件

问题:如果表单项的名字和方法中形参名不一致,该怎么办?

public Result upload(String username,Integer age, MultipartFile file) //file形参名和请求参数名image不一致

解决:使用@RequestParam注解进行参数绑定

public Result upload(String username, Integer age,@RequestParam("image") MultipartFile file)


**UploadController代码:**

```java
@Slf4j
@RestController
public class UploadController {

    @PostMapping("/upload")
    public Result upload(String username, Integer age, MultipartFile image)  {
        log.info("文件上传:{},{},{}",username,age,image);
        return Result.success();
    }

}

打开浏览器输入:http://localhost:8080/upload.html , 录入数据并提交

通过后端程序控制台可以看到,上传的文件是存放在一个临时目录

表单提交的三项数据(姓名、年龄、文件),分别存储在不同的临时文件中:

当我们程序运行完毕之后,这个临时文件会自动删除。

所以,我们如果想要实现文件上传,需要将这个临时文件,要转存到我们的磁盘目录中。

本地存储

代码实现:

  1. 在服务器本地磁盘上创建images目录,用来存储上传的文件(例:E盘创建images目录)
  2. 使用MultipartFile类提供的API方法,把临时文件转存到本地磁盘目录下

MultipartFile 常见方法: String getOriginalFilename(); //获取原始文件名 void transferTo(File dest); //将接收的文件转存到磁盘文件中 long getSize(); //获取文件的大小,单位:字节 byte[] getBytes(); //获取文件内容的字节数组 InputStream getInputStream(); //获取接收到的文件内容的输入流

@Slf4j
@RestController
public class UploadController {

    @PostMapping("/upload")
    public Result upload(String username, Integer age, MultipartFile image) throws IOException {
        log.info("文件上传:{},{},{}",username,age,image);

        //获取原始文件名
        String originalFilename = image.getOriginalFilename();

        //将文件存储在服务器的磁盘目录
        image.transferTo(new File("E:/images/"+originalFilename));

        return Result.success();
    }

}

利用postman测试: 通过postman测试,我们发现文件上传是没有问题的。但是由于我们是使用原始文件名作为所上传文件的存储名字,当我们再次上传一个名为1.jpg文件时,发现会把之前已经上传成功的文件覆盖掉。

解决方案:保证每次上传文件时文件名都唯一的(使用UUID获取随机文件名)

@Slf4j
@RestController
public class UploadController {

    @PostMapping("/upload")
    public Result upload(String username, Integer age, MultipartFile image) throws IOException {
        log.info("文件上传:{},{},{}",username,age,image);

        //获取原始文件名
        String originalFilename = image.getOriginalFilename();

        //构建新的文件名
        String extname = originalFilename.substring(originalFilename.lastIndexOf("."));//文件扩展名
        String newFileName = UUID.randomUUID().toString()+extname;//随机名+文件扩展名

        //将文件存储在服务器的磁盘目录
        image.transferTo(new File("E:/images/"+newFileName));

        return Result.success();
    }

}

在解决了文件名唯一性的问题后,我们再次上传一个较大的文件(超出1M)时发现,后端程序报错: 报错原因呢是因为:在SpringBoot中,文件上传时默认单个文件最大大小为1M 那么如果需要上传大文件,可以在application.properties进行如下配置:

#配置单个文件最大上传大小
spring.servlet.multipart.max-file-size=10MB

#配置单个请求最大上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB

到时此,我们文件上传的本地存储方式已完成了。但是这种本地存储方式还存在一问题:

如果直接存储在服务器的磁盘目录中,存在以下缺点:

  • 不安全:磁盘如果损坏,所有的文件就会丢失
  • 容量有限:如果存储大量的图片,磁盘空间有限(磁盘不可能无限制扩容)
  • 无法直接访问

为了解决上述问题呢,通常有两种解决方案:

  • 自己搭建存储服务器,如:fastDFS 、MinIO
  • 使用现成的云服务,如:阿里云,腾讯云,华为云

阿里云OSS

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;

import java.io.FileInputStream;
import java.io.InputStream;

public class AliOssTest {
    public static void main(String[] args) throws Exception {
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "oss-cn-shanghai.aliyuncs.com";
        
        // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        String accessKeyId = "LTAI5t9MZK8iq5T2Av5GLDxX";
        String accessKeySecret = "C0IrHzKZGKqU8S7YQcevcotD3Zd5Tc";
        
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "web-framework01";
        // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
        String objectName = "1.jpg";
        // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
        // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
        String filePath= "C:\\Users\\Administrator\\Pictures\\1.jpg";

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            InputStream inputStream = new FileInputStream(filePath);
            // 创建PutObjectRequest对象。
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
            // 设置该属性可以返回response。如果不设置,则返回的response为空。
            putObjectRequest.setProcess("true");
            // 创建PutObject请求。
            PutObjectResult result = ossClient.putObject(putObjectRequest);
            // 如果上传成功,则返回200。
            System.out.println(result.getResponse().getStatusCode());
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }
}

在以上代码中,需要替换的内容为:

  • accessKeyId:阿里云账号AccessKey
  • accessKeySecret:阿里云账号AccessKey对应的秘钥
  • bucketName:Bucket名称
  • objectName:对象名称,在Bucket中存储的对象的名称
  • filePath:文件路径

AccessKey : 运行以上程序后,会把本地的文件上传到阿里云OSS服务器上

  {
      "code": 1,
      "msg": "success",
      "data": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-0400.jpg"
  }

引入阿里云OSS上传文件工具类(由官方的示例代码改造而来)

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;

@Component
public class AliOSSUtils {
    private String endpoint = "https://oss-cn-shanghai.aliyuncs.com";
    private String accessKeyId = "LTAI5t9MZK8iq5T2Av5GLDxX";
    private String accessKeySecret = "C0IrHzKZGKqU8S7YQcevcotD3Zd5Tc";
    private String bucketName = "web-framework01";

    /**
     * 实现上传图片到OSS
     */
    public String upload(MultipartFile multipartFile) throws IOException {
        // 获取上传的文件的输入流
        InputStream inputStream = multipartFile.getInputStream();

        // 避免文件覆盖
        String originalFilename = multipartFile.getOriginalFilename();
        String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));

        //上传文件到 OSS
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        ossClient.putObject(bucketName, fileName, inputStream);

        //文件访问路径
        String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;

        // 关闭ossClient
        ossClient.shutdown();
        return url;// 把上传到oss的路径返回
    }
}

修改UploadController代码:

import com.itheima.pojo.Result;
import com.itheima.utils.AliOSSUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;

@Slf4j
@RestController
public class UploadController {

    @Autowired
    private AliOSSUtils aliOSSUtils;

    @PostMapping("/upload")
    public Result upload(MultipartFile image) throws IOException {
        //调用阿里云OSS工具类,将上传上来的文件存入阿里云
        String url = aliOSSUtils.upload(image);
        //将图片上传完成后的url返回,用于浏览器回显展示
        return Result.success(url);
    }
    
}

修改员工

需求:修改员工信息

在进行修改员工信息的时候,我们首先先要根据员工的ID查询员工的信息用于页面回显展示,然后用户修改员工数据之后,点击保存按钮,就可以将修改的数据提交到服务端,保存到数据库。 具体操作为:

  1. 根据ID查询员工信息
  2. 保存修改的员工信息
  • EmpMapper.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.itheima.mapper.EmpMapper">

    <!--更新员工信息-->
    <update id="update">
        update emp
        <set>
            <if test="username != null and username != ''">
                username = #{username},
            </if>
            <if test="password != null and password != ''">
                password = #{password},
            </if>
            <if test="name != null and name != ''">
                name = #{name},
            </if>
            <if test="gender != null">
                gender = #{gender},
            </if>
            <if test="image != null and image != ''">
                image = #{image},
            </if>
            <if test="job != null">
                job = #{job},
            </if>
            <if test="entrydate != null">
                entrydate = #{entrydate},
            </if>
            <if test="deptId != null">
                dept_id = #{deptId},
            </if>
            <if test="updateTime != null">
                update_time = #{updateTime}
            </if>
        </set>
        where id = #{id}
    </update>

    <!-- 省略... -->
   
</mapper>
  • EmpService
public interface EmpService {
    /**
     * 更新员工
     * @param emp
     */
    public void update(Emp emp);
   
    //省略...
}
  • EmpServiceImpl
@Slf4j
@Service
public class EmpServiceImpl implements EmpService {
    @Autowired
    private EmpMapper empMapper;

    @Override
    public void update(Emp emp) {
        emp.setUpdateTime(LocalDateTime.now()); //更新修改时间为当前时间
        
        empMapper.update(emp);
    }
    
    //省略...
}
  • EmpController
@Slf4j
@RestController
@RequestMapping("/emps")
public class EmpController {

    @Autowired
    private EmpService empService;

    //修改员工
    @PutMapping
    public Result update(@RequestBody Emp emp){
        empService.update(emp);
        return Result.success();
    }
    
    //省略...
}

配置文件

员工管理的增删改查功能我们已开发完成,但在我们所开发的程序中还一些小问题,下面我们就来分析一下当前案例中存在的问题以及如何优化解决。

参数配置化

在我们之前编写的程序中进行文件上传时,需要调用AliOSSUtils工具类,将文件上传到阿里云OSS对象存储服务当中。而在调用工具类进行文件上传时,需要一些参数:

  • endpoint //阿里云OSS域名
  • accessKeyID //用户身份ID
  • accessKeySecret //用户密钥
  • bucketName //存储空间的名字

关于以上的这些阿里云相关配置信息,我们是直接写死在java代码中了(硬编码),如果我们在做项目时每涉及到一个第三方技术服务,就将其参数硬编码,那么在Java程序中会存在两个问题:

  1. 如果这些参数发生变化了,就必须在源程序代码中改动这些参数,然后需要重新进行代码的编译,将Java代码编译成class字节码文件再重新运行程序。(比较繁琐)
  2. 如果我们开发的是一个真实的企业级项目, Java类可能会有很多,如果将这些参数分散的定义在各个Java类当中,我们要修改一个参数值,我们就需要在众多的Java代码当中来定位到对应的位置,再来修改参数,修改完毕之后再重新编译再运行。(参数配置过于分散,是不方便集中的管理和维护)

为了解决以上分析的问题,我们可以将参数配置在配置文件中。如下:

#自定义的阿里云OSS配置信息
aliyun.oss.endpoint=https://oss-cn-hangzhou.aliyuncs.com
aliyun.oss.accessKeyId=LTAI4GCH1vX6DKqJWxd6nEuW
aliyun.oss.accessKeySecret=yBshYweHOpqDuhCArrVHwIiBKpyqSL
aliyun.oss.bucketName=web-tlias

在将阿里云OSS配置参数交给properties配置文件来管理之后,我们的AliOSSUtils工具类就变为以下形式:

@Component
public class AliOSSUtils {
    /*以下4个参数没有指定值(默认值:null)*/
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

    //省略其他代码...
}

而此时如果直接调用AliOSSUtils类当中的upload方法进行文件上传时,这4项参数全部为null,原因是因为并没有给它赋值。

此时我们是不是需要将配置文件当中所配置的属性值读取出来,并分别赋值给AliOSSUtils工具类当中的各个属性呢?那应该怎么做呢?

因为application.properties是springboot项目默认的配置文件,所以springboot程序在启动时会默认读取application.properties配置文件,而我们可以使用一个现成的注解:@Value,获取配置文件中的数据。

@Value 注解通常用于外部配置的属性注入,具体用法为: @Value("${配置文件中的key}")

@Component
public class AliOSSUtils {

    @Value("${aliyun.oss.endpoint}")
    private String endpoint;
    
    @Value("${aliyun.oss.accessKeyId}")
    private String accessKeyId;
    
    @Value("${aliyun.oss.accessKeySecret}")
    private String accessKeySecret;
    
    @Value("${aliyun.oss.bucketName}")
    private String bucketName;
 	
 	//省略其他代码...
 }   

yml配置文件

前面我们一直使用springboot项目创建完毕后自带的application.properties进行属性的配置,那其实呢,在springboot项目当中是支持多种配置方式的,除了支持properties配置文件以外,还支持另外一种类型的配置文件,就是我们接下来要讲解的yml格式的配置文件。

  • application.properties

    server.port=8080
    server.address=127.0.0.1
    
  • application.yml

    server:
      port: 8080
      address: 127.0.0.1
    
  • application.yaml

    server:
      port: 8080
      address: 127.0.0.1
    

yml 格式的配置文件,后缀名有两种:

  • yml (推荐)
  • yaml

yml格式的数据有以下特点:

  • 容易阅读
  • 容易与脚本语言交互
  • 以数据为核心,重数据轻格式

简单的了解过springboot所支持的配置文件,以及不同类型配置文件之间的优缺点之后,接下来我们就来了解下yml配置文件的基本语法:

  • 大小写敏感
  • 数值前边必须有空格,作为分隔符
  • 使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(idea中会自动将Tab转换为空格)
  • 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
  • #表示注释,从这个字符一直到行尾,都会被解析器忽略

yml文件中常见的数据格式,最为常见的两类:

  1. 定义对象或Map集合
  2. 定义数组、list或set集合

对象/Map集合

user:
  name: zhangsan
  age: 18
  password: 123456

数组/List/Set集合

hobby: 
  - java
  - game
  - sport

熟悉完了yml文件的基本语法后,我们修改下之前案例中使用的配置文件,变更为application.yml配置方式:

  1. 修改application.properties名字为:_application.properties(名字随便更换,只要加载不到即可)
  2. 创建新的配置文件: application.yml

新建的application.yml文件:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/tlias
    username: root
    password: 1234
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 100MB
      
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true
	
aliyun:
  oss:
    endpoint: https://oss-cn-hangzhou.aliyuncs.com
    accessKeyId: LTAI4GCH1vX6DKqJWxd6nEuW
    accessKeySecret: yBshYweHOpqDuhCArrVHwIiBKpyqSL
    bucketName: web-397

@ConfigurationProperties

讲解完了yml配置文件之后,最后再来介绍一个注解@ConfigurationProperties。:

Spring提供的简化方式套路:

  1. 需要创建一个实现类,且实体类中的属性名和配置文件当中key的名字必须要一致

    比如:配置文件当中叫endpoints,实体类当中的属性也得叫endpoints,另外实体类当中的属性还需要提供 getter / setter方法

  2. 需要将实体类交给Spring的IOC容器管理,成为IOC容器当中的bean对象

  3. 在实体类上添加@ConfigurationProperties注解,并通过perfect属性来指定配置参数项的前缀

实体类:AliOSSProperties

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/*阿里云OSS相关配置*/
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSProperties {
    //区域
    private String endpoint;
    //身份ID
    private String accessKeyId ;
    //身份密钥
    private String accessKeySecret ;
    //存储空间
    private String bucketName;
}

AliOSSUtils工具类:

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;

@Component //当前类对象由Spring创建和管理
public class AliOSSUtils {

    //注入配置参数实体类对象
    @Autowired
    private AliOSSProperties aliOSSProperties;
   
    
    /**
     * 实现上传图片到OSS
     */
    public String upload(MultipartFile multipartFile) throws IOException {
        //获取阿里云OSS参数
        String endpoint = aliOSSProperties.getEndpoint();
        String accessKeyId = aliOSSProperties.getAccessKeyId();
        String accessKeySecret = aliOSSProperties.getAccessKeySecret();
        String bucketName = aliOSSProperties.getBucketName();
        // 获取上传的文件的输入流
        InputStream inputStream = multipartFile.getInputStream();

        // 避免文件覆盖
        String originalFilename = multipartFile.getOriginalFilename();
        String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));

        //上传文件到 OSS
        OSS ossClient = new OSSClientBuilder().build(aliOSSProperties.getEndpoint(),
                aliOSSProperties.getAccessKeyId(), aliOSSProperties.getAccessKeySecret());
        ossClient.putObject(aliOSSProperties.getBucketName(), fileName, inputStream);

        //文件访问路径
        String url =aliOSSProperties.getEndpoint().split("//")[0] + "//" + aliOSSProperties.getBucketName() + "." + aliOSSProperties.getEndpoint().split("//")[1] + "/" + fileName;

        // 关闭ossClient
        ossClient.shutdown();
        return url;// 把上传到oss的路径返回
    }
}

在我们添加上注解后,会发现idea窗口上面出现一个红色警告:

这个警告提示是告知我们还需要引入一个依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

当我们在pom.xml文件当中配置了这项依赖之后,我们重新启动服务,大家就会看到在properties或者是yml配置文件当中,就会提示阿里云 OSS 相关的配置项。所以这项依赖它的作用就是会自动的识别被@Configuration Properties注解标识的bean对象。

刚才的红色警告,已经变成了一个灰色的提示,提示我们需要重新运行springboot服务

@ConfigurationProperties注解我们已经介绍完了,接下来我们就来区分一下@ConfigurationProperties注解以及我们前面所介绍的另外一个@Value注解:

相同点:都是用来注入外部配置的属性的。

不同点:

  • @Value注解只能一个一个的进行外部属性的注入。

  • @ConfigurationProperties可以批量的将外部的属性配置注入到bean对象的属性中。

如果要注入的属性非常的多,并且还想做到复用,就可以定义这么一个bean对象。通过 configuration properties 批量的将外部的属性配置直接注入到 bin 对象的属性当中。在其他的类当中,我要想获取到注入进来的属性,我直接注入 bin 对象,然后调用 get 方法,就可以获取到对应的属性值了

SpringBootWeb登录认证

java后端开发登录认证

登录功能

功能开发

LoginController

@RestController
public class LoginController {

    @Autowired
    private EmpService empService;

    @PostMapping("/login")
    public Result login(@RequestBody Emp emp){
        Emp e = empService.login(emp);
	    return  e != null ? Result.success():Result.error("用户名或密码错误");
    }
}

EmpService

public interface EmpService {

    /**
     * 用户登录
     * @param emp
     * @return
     */
    public Emp login(Emp emp);

    //省略其他代码...
}
~~~

**EmpServiceImpl**

~~~java
@Slf4j
@Service
public class EmpServiceImpl implements EmpService {
    @Autowired
    private EmpMapper empMapper;

    @Override
    public Emp login(Emp emp) {
        //调用dao层功能:登录
        Emp loginEmp = empMapper.getByUsernameAndPassword(emp);

        //返回查询结果给Controller
        return loginEmp;
    }   
    
    //省略其他代码...
}
~~~

**EmpMapper**

~~~java
@Mapper
public interface EmpMapper {

    @Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time " +
            "from emp " +
            "where username=#{username} and password =#{password}")
    public Emp getByUsernameAndPassword(Emp emp);
    
    //省略其他代码...
}

登录校验

什么是登录校验? 所谓登录校验,指的是我们在服务器端接收到浏览器发送过来的请求之后,首先我们要对请求进行校验。先要校验一下用户登录了没有,如果用户已经登录了,就直接执行对应的业务操作就可以了;如果用户没有登录,此时就不允许他执行相关的业务操作,直接给前端响应一个错误的结果,最终跳转到登录页面,要求他登录成功之后,再来访问对应的数据。

首先我们在宏观上先有一个认知:

我们提到HTTP协议是无状态协议。什么又是无状态的协议? 所谓无状态,指的是每一次请求都是独立的,下一次请求并不会携带上一次请求的数据。而浏览器与服务器之间进行交互,基于HTTP协议也就意味着现在我们通过浏览器来访问了登陆这个接口,实现了登陆的操作,接下来我们在执行其他业务操作时,服务器也并不知道这个员工到底登陆了没有。因为HTTP协议是无状态的,两次请求之间是独立的,所以是无法判断这个员工到底登陆了没有。

现登录校验的操作,具体的实现思路可以分为两部分:

  1. 在员工登录成功后,需要将用户登录成功的信息存起来,记录用户已经登录成功的标记。
  2. 在浏览器发起请求时,需要在服务端进行统一拦截,拦截后进行登录校验。

想要判断员工是否已经登录,我们需要在员工登录成功之后,存储一个登录成功的标记,接下来在每一个接口方法执行之前,先做一个条件判断,判断一下这个员工到底登录了没有。如果是登录了,就可以执行正常的业务操作,如果没有登录,会直接给前端返回一个错误的信息,前端拿到这个错误信息之后会自动的跳转到登录页面。 我们程序中所开发的查询功能、删除功能、添加功能、修改功能,都需要使用以上套路进行登录校验。此时就会出现:相同代码逻辑,每个功能都需要编写,就会造成代码非常繁琐。 为了简化这块操作,我们可以使用一种技术:统一拦截技术。 通过统一拦截的技术,我们可以来拦截浏览器发送过来的所有的请求,拦截到这个请求之后,就可以通过请求来获取之前所存入的登录标记,在获取到登录标记且标记为登录成功,就说明员工已经登录了。如果已经登录,我们就直接放行(意思就是可以访问正常的业务接口了)。

我们要完成以上操作,会涉及到web开发中的两个技术:

  1. 会话技术
  2. 统一拦截技术

而统一拦截技术现实方案也有两种:

  1. Servlet规范中的Filter过滤器
  2. Spring提供的interceptor拦截器

会话技术

会话技术介绍

什么是会话?

  • 在我们日常生活当中,会话指的就是谈话、交谈。

  • 在web开发当中,会话指的就是浏览器与服务器之间的一次连接,我们就称为一次会话。

会话跟踪方案

会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。

为什么要共享数据呢? 由于HTTP是无状态协议,在后面请求中怎么拿到前一次请求生成的数据呢?此时就需要在一次会话的多次请求之间进行数据共享

会话跟踪技术有两种:

  1. Cookie(客户端会话跟踪技术)
    • 数据存储在客户端浏览器当中
  2. Session(服务端会话跟踪技术)
    • 数据存储在储在服务端
  3. 令牌技术

方案一 - Cookie

cookie 是客户端会话跟踪技术,它是存储在客户端浏览器的,我们使用 cookie 来跟踪会话,我们就可以在浏览器第一次发起请求来请求服务器的时候,我们在服务器端来设置一个cookie。

比如第一次请求了登录接口,登录接口执行完成之后,我们就可以设置一个cookie,在 cookie 当中我们就可以来存储用户相关的一些数据信息。比如我可以在 cookie 当中来存储当前登录用户的用户名,用户的ID。

服务器端在给客户端在响应数据的时候,会自动的将 cookie 响应给浏览器,浏览器接收到响应回来的 cookie 之后,会自动的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中,都会将浏览器本地所存储的 cookie 自动地携带到服务端。

接下来在服务端我们就可以获取到 cookie 的值。我们可以去判断一下这个 cookie 的值是否存在,如果不存在这个cookie,就说明客户端之前是没有访问登录接口的;如果存在 cookie 的值,就说明客户端之前已经登录完成了。这样我们就可以基于 cookie 在同一次会话的不同请求之间来共享数据。

  • 服务器会 自动 的将 cookie 响应给浏览器。

  • 浏览器接收到响应回来的数据之后,会 自动 的将 cookie 存储在浏览器本地。

  • 在后续的请求当中,浏览器会 自动 的将 cookie 携带到服务器端。

为什么这一切都是自动化进行的?

是因为 cookie 它是 HTP 协议当中所支持的技术,而各大浏览器厂商都支持了这一标准。在 HTTP 协议官方给我们提供了一个响应头和请求头:

  • 响应头 Set-Cookie :设置Cookie数据的

  • 请求头 Cookie:携带Cookie数据的

代码测试

@Slf4j
@RestController
public class SessionController {

    //设置Cookie
    @GetMapping("/c1")
    public Result cookie1(HttpServletResponse response){
        response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
        return Result.success();
    }
	
    //获取Cookie
    @GetMapping("/c2")
    public Result cookie2(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();
        for (Cookie cookie : cookies) {
            if(cookie.getName().equals("login_username")){
                System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
            }
        }
        return Result.success();
    }
}    

A. 访问c1接口,设置Cookie,http://localhost:8080/c1

我们可以看到,设置的cookie,通过响应头Set-Cookie响应给浏览器,并且浏览器会将Cookie,存储在浏览器端。

B. 访问c2接口 http://localhost:8080/c2,此时浏览器会自动的将Cookie携带到服务端,是通过请求头Cookie,携带的。

优缺点

  • 优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的)
  • 缺点:
    • 移动端APP(Android、IOS)中无法使用Cookie
    • 不安全,用户可以自己禁用Cookie
    • Cookie不能跨域

跨域介绍:

  • 现在的项目,大部分都是前后端分离的,前后端最终也会分开部署,前端部署在服务器 192.168.150.200 上,端口 80,后端部署在 192.168.150.100上,端口 8080
  • 我们打开浏览器直接访问前端工程,访问url:http://192.168.150.200/login.html
  • 然后在该页面发起请求到服务端,而服务端所在地址不再是localhost,而是服务器的IP地址192.168.150.100,假设访问接口地址为:http://192.168.150.100:8080/login
  • 那此时就存在跨域操作了,因为我们是在 http://192.168.150.200/login.html 这个页面上访问了http://192.168.150.100:8080/login 接口
  • 此时如果服务器设置了一个Cookie,这个Cookie是不能使用的,因为Cookie无法跨域

方案二 - Session

  • 获取Session
  • 响应Cookie (JSESSIONID)
  • 查找Session

优缺点

  • 优点:Session是存储在服务端的,安全
  • 缺点:
    • 服务器集群环境下无法直接使用Session
    • 移动端APP(Android、IOS)中无法使用Cookie
    • 用户可以自己禁用Cookie
    • Cookie不能跨域

PS:Session 底层是基于Cookie实现的会话跟踪,如果Cookie不可用,则该方案,也就失效了。

方案三 - 令牌技术 优缺点

  • 优点:
    • 支持PC端、移动端
    • 解决集群环境下的认证问题
    • 减轻服务器的存储压力(无需在服务器端存储)
  • 缺点:需要自己实现(包括令牌的生成、令牌的传递、令牌的校验)

JWT令牌

JWT全称:JSON Web Token (官网:https://jwt.io/)

  • 定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。

JWT的组成: (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)

  • 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}

  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}

  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

    签名的目的就是为了防jwt令牌被篡改,而正是因为jwt令牌最后一个部分数字签名的存在,所以整个jwt 令牌是非常安全可靠的。一旦jwt令牌当中任何一个部分、任何一个字符被篡改了,整个令牌在校验的时候都会失败,所以它是非常安全可靠的。

JWT是如何将原始的JSON格式数据,转变为字符串的呢? 其实在生成JWT令牌时,会对JSON格式的数据进行一次编码:进行base64编码 Base64:是一种基于64个可打印的字符来表示二进制数据的编码方式。既然能编码,那也就意味着也能解码。所使用的64个字符分别是A到Z、a到z、 0- 9,一个加号,一个斜杠,加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号 需要注意的是Base64是编码方式,而不是加密方式。

JWT令牌最典型的应用场景就是登录认证:

  1. 在浏览器发起请求来执行登录操作,此时会访问登录的接口,如果登录成功之后,我们需要生成一个jwt令牌,将生成的 jwt令牌返回给前端。
  2. 前端拿到jwt令牌之后,会将jwt令牌存储起来。在后续的每一次请求中都会将jwt令牌携带到服务端。
  3. 服务端统一拦截请求之后,先来判断一下这次请求有没有把令牌带过来,如果没有带过来,直接拒绝访问,如果带过来了,还要校验一下令牌是否是有效。如果有效,就直接放行进行请求的处理。

在JWT登录认证的场景中我们发现,整个流程当中涉及到两步操作:

  1. 在登录成功之后,要生成令牌。
  2. 每一次请求当中,要接收令牌并对令牌进行校验。

生成和校验

  1. 首先我们先来实现JWT令牌的生成。要想使用JWT令牌,需要先引入JWT的依赖:
<!-- JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

在引入完JWT来赖后,就可以调用工具包中提供的API来完成JWT令牌的生成和校验 工具类:Jwts

  1. 生成JWT代码实现:
@Test
public void genJwt(){
    Map<String,Object> claims = new HashMap<>();
    claims.put("id",1);
    claims.put("username","Tom");
    
    String jwt = Jwts.builder()
        .setClaims(claims) //自定义内容(载荷)          
        .signWith(SignatureAlgorithm.HS256, "itheima") //签名算法        
        .setExpiration(new Date(System.currentTimeMillis() + 24*3600*1000)) //有效期   
        .compact();
    
    System.out.println(jwt);
}

运行测试方法:

eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk

输出的结果就是生成的JWT令牌,,通过英文的点分割对三个部分进行分割,我们可以将生成的令牌复制一下,然后打开JWT的官网,将生成的令牌直接放在Encoded位置,此时就会自动的将令牌解析出来。

第一部分解析出来,看到JSON格式的原始数据,所使用的签名算法为HS256。 第二个部分是我们自定义的数据,之前我们自定义的数据就是id,还有一个exp代表的是我们所设置的过期时间。 由于前两个部分是base64编码,所以是可以直接解码出来。但最后一个部分并不是base64编码,是经过签名算法计算出来的,所以最后一个部分是不会解析的。

实现了JWT令牌的生成,下面我们接着使用Java代码来校验JWT令牌(解析生成的令牌):

@Test
public void parseJwt(){
    Claims claims = Jwts.parser()
        .setSigningKey("itheima")//指定签名密钥(必须保证和生成令牌时使用相同的签名密钥)  
	    .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk")
        .getBody();

    System.out.println(claims);
}

运行测试方法:

{id=1, exp=1672729730}

令牌解析后,我们可以看到id和过期时间,如果在解析的过程当中没有报错,就说明解析成功了。

使用JWT令牌时需要注意:

  • JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。

  • 如果JWT令牌解析校验时报错,则说明 JWT令牌被篡改 或 失效了,令牌非法。

登录下发令牌

  1. 生成令牌
    • 在登录成功之后来生成一个JWT令牌,并且把这个令牌直接返回给前端
  2. 校验令牌
    • 拦截前端请求,从请求中获取到令牌,对令牌进行解析校验

过滤器Filter

Filter快速入门

什么是Filter?

  • Filter表示过滤器,是 JavaWeb三大组件(Servlet、Filter、Listener)之一。
  • 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
    • 使用了过滤器之后,要想访问web服务器上的资源,必须先经过滤器,过滤器处理完毕之后,才可以访问对应的资源。
  • 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。

过滤器的基本使用操作:

  • 第1步,定义过滤器 :1.定义一个类,实现 Filter 接口,并重写其所有方法。
  • 第2步,配置过滤器:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。

Filter详解

拦截路径 过滤器的拦截路径,Filter可以根据需求,配置不同的拦截资源路径:

拦截路径urlPatterns值含义
拦截具体路径/login只有访问 /login 路径时,才会被拦截
目录拦截/emps/*访问/emps下的所有资源,都会被拦截
拦截所有/*访问所有资源,都会被拦截

以注解方式配置的Filter过滤器,它的执行优先级是按时过滤器类名的自动排序确定的,类名排名越靠前,优先级越高。

登录校验-Filter

  1. 所有的请求,拦截到了之后,都需要校验令牌吗?

    • 答案:登录请求例外
  2. 拦截到请求后,什么情况下才可以放行,执行业务操作?

    • 答案:有令牌,且令牌校验通过(合法);否则都返回未登录错误结果

登录校验过滤器:LoginCheckFilter

@Slf4j
@WebFilter(urlPatterns = "/*") //拦截所有请求
public class LoginCheckFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        //前置:强制转换为http协议的请求对象、响应对象 (转换原因:要使用子类中特有方法)
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //1.获取请求url
        String url = request.getRequestURL().toString();
        log.info("请求路径:{}", url); //请求路径:http://localhost:8080/login


        //2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
        if(url.contains("/login")){
            chain.doFilter(request, response);//放行请求
            return;//结束当前方法的执行
        }


        //3.获取请求头中的令牌(token)
        String token = request.getHeader("token");
        log.info("从请求头中获取的令牌:{}",token);


        //4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
        if(!StringUtils.hasLength(token)){
            log.info("Token不存在");

            Result responseResult = Result.error("NOT_LOGIN");
            //把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
            String json = JSONObject.toJSONString(responseResult);
            response.setContentType("application/json;charset=utf-8");
            //响应
            response.getWriter().write(json);

            return;
        }

        //5.解析token,如果解析失败,返回错误结果(未登录)
        try {
            JwtUtils.parseJWT(token);
        }catch (Exception e){
            log.info("令牌解析失败!");

            Result responseResult = Result.error("NOT_LOGIN");
            //把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
            String json = JSONObject.toJSONString(responseResult);
            response.setContentType("application/json;charset=utf-8");
            //响应
            response.getWriter().write(json);

            return;
        }


        //6.放行
        chain.doFilter(request, response);

    }
}

在上述过滤器的功能实现中,我们使用到了一个第三方json处理的工具包fastjson。我们要想使用,需要引入如下依赖:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.76</version>
</dependency>

拦截器Interceptor

快速入门

什么是拦截器?

  • 是一种动态拦截方法调用的机制,类似于过滤器。
  • 拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行。

拦截器的作用:

  • 拦截请求,在指定方法调用前后,根据业务需要执行预先设定的代码。

Interceptor详解

拦截器的使用步骤和过滤器类似,也分为两步:

  1. 定义拦截器

  2. 注册配置拦截器

**自定义拦截器:**实现HandlerInterceptor接口,并重写其所有方法

//自定义拦截器
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    //目标资源方法执行前执行。 返回true:放行    返回false:不放行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle .... ");
        
        return true; //true表示放行
    }

    //目标资源方法执行后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle ... ");
    }

    //视图渲染完毕后执行,最后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion .... ");
    }
}

注意:

preHandle方法:目标资源方法执行前执行。 返回true:放行 返回false:不放行

postHandle方法:目标资源方法执行后执行

afterCompletion方法:视图渲染完毕后执行,最后执行

注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptors方法

@Configuration  
public class WebConfig implements WebMvcConfigurer {

    //自定义的拦截器对象
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;

    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       //注册自定义拦截器对象
        registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
    }
}

拦截路径

在拦截器中除了可以设置/**拦截所有资源外,还有一些常见拦截路径设置:

拦截路径含义举例
/*一级路径能匹配/depts,/emps,/login,不能匹配 /depts/1
/**任意级路径能匹配/depts,/depts/1,/depts/1/2
/depts/*/depts下的一级路径能匹配/depts/1,不能匹配/depts/1/2,/depts
/depts/**/depts下的任意级路径能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1

过滤器和拦截器之间的区别

其实它们之间的区别主要是两点:

  • 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
  • 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。

异常处理

全局异常处理器

定义全局异常处理器?

  • 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。
  • 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。
@RestControllerAdvice
public class GlobalExceptionHandler {

    //处理异常
    @ExceptionHandler(Exception.class) //指定能够处理的异常类型
    public Result ex(Exception e){
        e.printStackTrace();//打印堆栈中的异常信息

        //捕获到异常之后,响应一个标准的Result
        return Result.error("对不起,操作失败,请联系管理员");
    }
}

@RestControllerAdvice = @ControllerAdvice + @ResponseBody 处理异常的方法返回值会转换为json后再响应给前端

全局异常处理器的使用,主要涉及到两个注解:

  • @RestControllerAdvice //表示当前类为全局异常处理器
  • @ExceptionHandler //指定可以捕获哪种类型的异常进行处理

事务&AOP

事务

  • 在Spring的事务管理中,默认只有运行时异常 RuntimeException才会回滚。

  • 如果还需要回滚指定类型的异常,可以通过rollbackFor属性来指定。

@Transactional(rollbackFor = Exception.class) //spring事务管理

if(true){throw new Exception("出错啦...");}//不是运行时异常 需要加上面那段代码

什么是事务的传播行为呢?

  • 就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。

要想控制事务的传播行为,在@Transactional注解的后面指定一个属性propagation,通过 propagation 属性来指定传播行为。接下来我们就来介绍一下常见的事务传播行为。

属性值含义
REQUIRED【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW需要新事务,无论有无,总是创建新事务
SUPPORTS支持事务,有则加入,无则在无事务状态中运行
NOT_SUPPORTED不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY必须有事务,否则抛异常
NEVER必须没事务,否则抛异常

对于这些事务传播行为,我们只需要关注以下两个就可以了:

  1. REQUIRED(默认值)
  2. REQUIRES_NEW

AOP

Spring中AOP的通知类型:

  • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  • @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  • @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  • @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行

SpingBoot原理

Maven高级

javaweb-thymeleaf

应用程序结构分类

  • C/S 结构(客户机/服务器结构)
  • B/S 结构(浏览器/服务器结构)

Web 应用服务器

  • 网站
  • Web 服务器
  • 静态网页
  • 动态网页

主流的三大动态网页开发技术有哪些?

  • ASP(Active Server Page)

ASP是Active Server Page的缩写,意为“活动服务器网页”。ASP是微软公司开发的代替CGI脚本程序的一种应用,它可以与数据库和其它程序进行交互,是一种简单、方便的编程工具。ASP的网页文件的格式是.asp,常用于各种动态网站中。 ASP是一种服务器端脚本编写环境,可以用来创建和运行动态网页或web应用程序。ASP网页可以包含HTML标记、普通文本、脚本命令以及COM组件等。利用ASP可以向网页中添加交互式内容(如在线表单),也可以创建使用HTML网页作为用户界面的web应用程序。 与HTML相比,ASP网页具有以下特点:

  • 利用ASP可以实现突破静态网页的一些功能限制,实现动态网页技术;
  • ASP文件是包含在HTML代码所组成的文件中的,易于修改和测试;
  • 服务器上的ASP解释程序会在服务器端制定ASP程序,并将结果以HTML格式传送到客户端浏览器上,因此使用各种浏览器都可以正常浏览ASP所产生的网页;
  • ASP提供了一些内置对象,使用这些对象可以使服务器端脚本功能更强。例如可以从web浏览器中获取用户通过HTML表单提交的信息,并在脚本中对这些信息进行处理,然后向web浏览器发送信息;
  • ASP可以使用服务器端ActiveX组建来执行各种各样的任务,例如存取数据库、发送Email或访问文件系统等。
  • 由于服务器是将ASP程序执行的结果以HTML格式传回客户端浏览器,因此使用者不会看到ASP所编写的原始程序代码,可防止ASP程序代码被窃取。
  • PHP(Personal Home Page)

PHP是一种通用的开源脚本语言,也被称作“超文本预处理器”,PHP被广 泛应用于Web网站的开发,它能够嵌入到HTML中使用。PHP的语法吸收了C语言、Java和Per|等不同开发语言的特点,更加便于开发人员的学习。

  • JSP(JavaServer Page)

JSP 是 Sun 公司推出的以 Java 语言作为脚本语言的新一代网站开发语言。 JSP就是Java,只是它是一个特别的Java语言,加入了一个特殊的引擎,这个引擎将HTTPServlet这个类的一些对象自动进行初始化好让用户使用,而用户不用再去操心前面的工作。 JSP是技术。JSP技术以Java语言作为脚本语言,JSP是由Sun Microsystems公司主导创建的一种动态网页技术标准。JSP部署于网络服务器上,可以响应客户端发送的请求,并根据请求内容动态地生成HTML、XML或其他格式文档的Web网页,然后返回给请求者。 JSP技术能以模板化的方式简单、高效添加动态网页内容;可利用JavaBean和标签库技术复用常用功能代码;有良好工具支持;继承了Java语言相对易用性;继承了Java跨平台优势;页面的动静区域以分散又有序的形式组合一起,能更直观看出页面代码整体结构。

一个JSP页面可以被分为以下几部分:

1、静态数据 静态数据在输入文件中的内容和输出给HTTP响应的内容完全一致。此时,该JSP输入文件会是一个没有内嵌JAVA或动作的HTML页面。而且,客户端每次请求都会得到相同的响应内容。 2、JSP指令 JSP指令控制JSP编译器如何去生成servlet,包含指令include –包含指令,通知JSP编译器把另外一个文件完全包含入当前文件中。被包含文件的扩展名一般都是"jspf"。 3、JSP脚本 标准脚本变量,永远可用的脚本变量有out – JSPWriter用来写入响应流的数据;page – servlet自身;request –HTTP request对象;session –用于保持客户端与服务器连接的对象。 4、脚本元素 有三个基本的脚本元素,作用是使JAVA代码可以直接插入servlet。声明标签,在JAVA SERVLET的类体中放入一个变量的定义;脚本标签,在JAVA SERVLET中放入所包含的语句;表达式标签,在JAVA SERVLET的类中放入待赋值的表达式。 5、JSP动作 一系列可以调用内建于网络服务器中的功能的XML标签。

IP地址分类

IP地址根据网络ID的不同分为5种类型,A类地址、B类地址、C类地址、D类地址和E类地址。

  • A类IP地址 一个A类IP地址由1字节的网络地址和3字节主机地址组成,它主要为大型网络而设计的,网络地址的最高位必须是“0”, 地址范围从1.0.0.0 到127.0.0.0)。可用的A类网络有127个,每个网络能容纳16777214个主机。其中127.0.0.1是一个特殊的IP地址,表示主机本身,用于本地机器的测试。 注:A:0-127,其中0代表任何地址,127为回环测试地址,因此,A类ip地址的实际范围是1-126.默认子网掩码为255.0.0.0
  • B类IP地址 一个B类IP地址由2个字节的网络地址和2个字节的主机地址组成,网络地址的最高位必须是“10”,地址范围从128.0.0.0到191.255.255.255。可用的B类网络有16382个,每个网络能容纳6万多个主机 。 注:B:128-191,其中128.0.0.0和191.255.0.0为保留ip,实际范围是128.1.0.0–191.254.0.0。
  • C类IP地址 一个C类IP地址由3字节的网络地址和1字节的主机地址组成,网络地址的最高位必须是“110”。范围从192.0.0.0到223.255.255.255。C类网络可达209万余个,每个网络能容纳254个主机。 注:C:192-223,其中192.0.0.0和223.255.255.0为保留ip,实际范围是192.0.1.0–223.255.254.0
  • D类地址 用于多点广播(Multicast)。D类IP地址第一个字节以“lll0”开始,它是一个专门保留的地址。它并不指向特定的网络,目前这一类地址被用在多点广播(Multicast)中。多点广播地址用来一次寻址一组计算机,它标识共享同一协议的一组计算机。224.0.0.0到239.255.255.255用于多点广播 。
  • E类IP地址 以“llll0”开始,为将来使用保留。240.0.0.0到255.255.255.254,255.255.255.255用于广播地址。

全零(“0.0.0.0”)地址对应于当前主机。全“1”的IP地址(“255.255.255.255”)是当前子网的广播地址。

RGB 色彩模型

十六进制颜色值:红:FF0000,绿 00FF00,蓝 0000FF,白色 FFFFFF,黑色 000000

表格合并

  • 按行 rowspan=””
  • 按列 colsapn =””

JavaScript

JavaScript(简称“JS”) 是一种具有函数优先的轻量级,解释型或即时编译型的编程语言。虽然它是作为开发 Web 页面的脚本语言而出名,但是它也被用到了很多非浏览器环境中,JavaScript 基于原型编程、多范式的动态 脚本语言,并且支持面向对象、命令式和声明式(如函数式编程)风格。

jQuery

jQuery 是一个快速、简洁的 JavaScript 框架,是继 Prototype 之后又一个优秀的 JavaScript 代码库(或 JavaScript 框架)。jQuery 设计的宗旨是“write Less,Do More”,即倡导写更少的代码,做更多的事情。它封装 JavaScript 常用 的功能代码,提供一种简便的 JavaScript 设计模式,优化 HTML 文档操作、事件处理、动画设计和 Ajax 交互。 jQuery 的核心特性可以总结为:具有独特的链式语法和短小清晰的多功能接口;具有高效灵活的 css 选择器, 并且可对 CSS 选择器进行扩展;拥有便捷的插件扩展机制和丰富的插件。jQuery 兼容各种主流浏览器,如 IE 6.0+、 FF 1.5+、Safari 2.0+、Opera 9.0+等。

Spring Boot

SpringBoot 是由 Pivotal 团队在 2013 年开始研发、2014 年 4 月发布第一个版本的全新开源的轻量级框架。 它基于 Spring4.0 设计,不仅继承了 Spring 框架原有的优秀特性,而且还通过简化配置来进一步简化了 Spring 应 用的整个搭建和开发过程。另外 SpringBoot 通过集成大量的框架使得依赖包的版本冲突,以及引用的不稳定性 等问题得到了很好的解决。

Maven

Maven 是一个项目管理工具,它包含了一个项目对象模型 (Project Object Model),一组标准集合,可以 通过一小段描述信息来管理项目的构建,报告和文档的项目管理工具软件。Maven 这个单词来自于意第绪语 (犹太语),意为知识的积累,最初在 Jakata Turbine 项目中用来简化构建过程。中文名:麦文。

POM

项目对象模型(POM),POM 作为项目对象模型。通过 xml 表示 maven 项目,使用 pom.xml 来实现。主要描 述了项目:包括配置文件;开发者需要遵循的规则,缺陷管理系统,组织和 licenses,项目的 url,项目的依赖 性,以及其他所有的项目相关因素。

MVC

MVC指MVC模式的某种框架,使用MVC应用程序被分成三个核心部件:(Model)模型、(View)视图、(Controller) 控制器。使用 MVC 的目的是将 Model 和 View 的实现代码分离,从而使同一个程序可以使用不同的表现形式。 M 即 Model 模型是指模型表示业务规则。由于应用于模型的代码只需写一次就可以被多个视图重用,所以减少了代码的重复性。 V 即 View 视图是指用户看到并与之交互的界面。比如由 html 元素组成的网页界面,或者软件的客户端界面。C 即 Controller 控制器是指控制器接受用户的输入并调用模型和视图去完成用户的需求,控制器本身不输出 任何东西和做任何处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示 返回的数据。

resources

在 src/main/resources 下面有两个文件夹,static 和 templates springboot 默认 static 中放静态页面,而 templates 中放动态页面。 templates 文件夹中的页面需要引入 thymeleaf 组件之后经过控制器返回才能访问到。

@RestController 与@Controller

@Controller和@RestController的区别: @Controller:在对应的方法上,视图解析器可以解析return 的jsp,html页面,并且跳转到相应页面 若返回json等内容到页面,则需要加@ResponseBody注解 @RestController:相当于@Controller+@ResponseBody两个注解的结合,返回json数据不需要在方法前面加@ResponseBody注解了,但使用@RestController这个注解,就不能返回jsp,html页面,视图解析器无法解析jsp,html页面

thymleaf

thymeleaf 是一个 XML/XHTML/HTML5 模板引擎,可用于 Web 与非 Web 环境中的应用开发。它是一个开源 的 Java 库。thymeleaf 提供了一个用于整合 Spring MVC 的可选模块,在应用开发中它可以完全替代 JSP 。

equals 方法

equals 方法是 java.lang.Object 类的方法,有两种用法说明: 对于字符串变量来说,使用“==”和“equals()”方法比较字符串时,其比较方法不同。

  • “==”比较两个变量本身的值,即两个对象在内存中的首地址。
  • “equals()”比较字符串中所包含的内容是否相同。

@ GetMapping 与 PostMapping

  • GetMapping,处理 get 请求 @GetMapping 是一个作为快捷方式的组合注解,是@RequestMapping(method = RequestMethod.GET)的缩写。该 注解将 HTTP Get 映射到 定的处理方法上。
  • @PostMapping,处理 post 请求 @PostMapping 是一个作为快捷方式的组合注释@RequestMapping(method = RequestMethod.POST)。该注解将将 HTTP POST 请求映射到特定处理程序方法上。

@RequestParam 注解使用

  • 作用: @RequestParam:将请求参数绑定到你控制器的方法参数上(是 springmvc 中接收普通参数的注解)
  • 语法: @RequestParam(value=“参数名”,required=“true/false”,defaultValue=“”) value:参数名,required:是否包含该参数,默认为 true,表示该请求路径中必须包含该参数,如果不包含就 报错。defaultValue:默认参数值,如果设置了该值,required=true 将失效,自动为 false,如果没有传该参数,就使用默认值。

@PathVariable 注解使用

绑定@PathVariable 映射 URL 绑定的占位符

$.post()

这是一个简单的 POST 请求功能以取代复杂 $.ajax 。请求成功时可调用回调函数。如果需要在出错时执行函数,请使用 $.ajax。

$.post("/goregister",{name:name,password:password},function (result){
    if (result==true){
        alert("注册成功");
		window.location.href="/login"
	}else{
		if (pwd1==''){
				alert("密码不能为空,注册失败");
		}else {
				alert("已有同名用户存在,注册失败");
		}
	}
});

$.ajax()

jQuery 底层 AJAX 实现。$.ajax()返回其创建的 XMLHttpRequest 对象。ajax() 方法用于执行 AJAX(异步 HTTP) 请求。所有的 jQuery AJAX 方法都使用 ajax() 方法。 语法:$.ajax({name:value, name:value, ... }),该参数规定 AJAX 请求的一个或多个名称/值对。

$.ajax({
 type: "post",
 dataType: "json",
 url:"/goregister",
 data:{name:name,password:password}
});

声明当前文件是 thymeleaf, 里面可以用 th 开头的属性

<html xmlns:th="http://www.thymeleaf.org">

动态数据的显示

把 name 的值显示在当前 p 里,用的是 th 开头的属性: th:text, 而取值用的是 "${name}"

<p th:text="${name}" >name</p>

用这种方式,就可以把服务端的数据,显示在当前 html 里了。这种写法是完全合法的 html 语法,所以可以直接通过浏览器打开 hello.html,也是可以看到效果的,只不过看到的是 "name",而不是服务端传过来的值。 引入资源文件:

th:href=”@{ }”

<link rel="stylesheet" th:href="@{xxx/css/bootstrap.css}">

Session

在 WEB 开发中,服务器可以为每个用户浏览器创建一个会话对象(session 对象),注意:一个浏览器独占一个 session 对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的 session 中,当用户使用浏览器访问其它程序时,其它程序可以从用户的 session 中取出该用户的数据,为用户服务。 写入:session.setAttribute("currentuser",username); 读出:session.setAttribute("currentuser"); 在 Thymeleaf 中中可以用${session.currentuser}读出

Thymeleaf 中的循环使用

<tr th:each="i:${#numbers.sequence(1,10)}">
 <td th:text="${i}"></td>
 <td th:text="${new java.util.Date().getTime()}"></td>
 <td th:text="${#dates.format(new java.util.Date().getTime(), 'yyyy-MM-dd hh:mm:ss')}"></td>
</tr>

application.properties 与 application.yml

如果工程中同时存在 application.properties 文件和 application.yml文件,yml 文件会先加载,而后加载的properties 文件会覆盖 yml 文件。所以建议工程中,只使用其中一种类型的文件即可。 在 springboot 框架里进行项目开始时,我们在 resource 文件夹里可以存放配置文件,而格式可以有两种, properties 和 yml,前者是扁平的格式,而后者是 yml 的树型结构,我们建议使用后者,因为它的可读性更强。 springboot 官方推荐使用 application.yml 配置文件,yml 文件的好处,天然的树状结构,一目了然。使用的时候需要注意一些细节的地方:原有的 key,例如 spring.jpa.properties.hibernate.dialect,按“.”分割,都变成树状的配置,key 后面的冒号, 后面一定要跟一个空格。 如程序中如果用 application.properties 的写法如下:

server.port=8090
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=
mysql://localhost:3306/mytest?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.messages.basename=i18n.login

YML 是什么

YAML (YAML Ain't a Markup Language)YAML 不是一种标记语言,通常以.yml 为后缀的文件,是一种直观的能够被电脑识别的数据序列化格式,并且容易被人类阅读,容易和脚本语言交互的,可以被支持 YAML 库的不同的编程语言程序导入,一种专门用来写配置文件的语言。可用于如: Java,C/C++, Ruby, Python, Perl, C#, PHP 等。

@Autowired 注解

@Autowired 可以对成员变量、方法和构造函数进行标注,来完成自动装配的工作,@Autowired 标注可以放在成员变量上,也可以放在成员变量的 set 方法上,也可以放在任意方法上表示,自动执行当前方法,如果方法有参 数,会在容器中自动寻找同类型参数为其传值。

JdbcTemplate

JDBC 在操作数据库是比较麻烦的,所以 Spring 为了提高开发的效率,就把 JDBC 封装了一 下,而JdbcTemplate 就是 Spring 对原始 JDBC 封装之后提供的一个操作数据库的工具类。

@Controller 如果需要返回 JSON,则需要在对应的方法上加上@ResponseBody 注解。

javaBean

javaBean 在 MVC 模型设计中是 model,又被称为实体模型层。JavaBean 是 Java 的可重用组件,是一种特殊的Java 类,采用 Java 语言编写,并且遵守 JavaBean API 的规范,为写成 JavaBean,类必须是具体的和公共的,并且具有无参数的构造器。JavaBean 通过提供符合一致性设计模式的公共方法将内部域暴露成员属性,set 和 get 方法获取。

JavaBean 与普通 Java 类的区别:

  • 提供一个默认的无参构造函数;
  • 有一系列可读写属性;
  • 有一系列的 get 或 set 方法;
  • 需要被序列化并且实现了 Serializable 接口;

JPA

JPA(Java Persistence API)是 Sun 官方提出的 Java 持久化规范,用来方便大家操作数据库。真正操作的是Hibernate,TopLink 等实现了 JPA 规范的不同厂商,默认是 Hibernate。

Mybatis 的 MVC 设计模式

  • Model:就是数据模型,本例中建有 model 包下建有 UserBean 类,就是对 tbluser 数据表的描述,有人 用 entity 作用也是一样的。
  • View :视图。就是用户可以看到的东西。view 层与控制层结合比较紧密,需要二者结合起来协同开发。view 层主要负责前台页面的显示。spring boot 里使用 Thymeleaf 模板 -Controller:控制器。负责请求转发,接收页面过来的参数,传给业务处理层,接到返回值,并再次传给页面。 -业务处理层:本例中建立的 service 层即为业务逻辑层,可以理解为对一个或者多个数据访问层进行得再次封装,主要是针对具体的问题的操作,把一些数据层的操作进行组合,间接与数据库打交道(提供操作数据库的方法)。要做这一层的话,要先设计接口文件如本例中的 UserService,再编写实现类文 件如 UserServiceImpl。 -数据访问层:本例中建立的 mapper 层即为数据访问层,数据存储对象,相当于以前的 DAO 层,mapper 层直接与数据库打交道(本例中调用 mapping 中的查询语句),接口提供给 service 层。

分层的优点:高内聚,低耦合。

  • 这样看起来结构是很清晰的,虽然对新人来说确实看起来很复杂;
  • 可扩展性和适应性更加强,比如将来用户的业务逻辑有一定的改变,你需要做的仅仅就是在业务处理层中 多调用一个方法即可,而不需要对代码有太多的改动,或者你将来想换个 MVC 的框架的话,只需要对接收和返回参 数方面做些处理即可,而不需要对业务处理层和数据访问层做改动;
  • 维护更加简单。将来维护的不一定是开发这个系统的人,所以,如果严格按照这种架构来写的话,他们只 需要有这种分层意识,很容易就能够对系统有个很好的掌握,也很容易能够对问题进行排查和修改。 #####@Transactional 事务注解,在数据库中,所谓事务是指一组逻辑操作单元即一组 sql 语句。当这个单元中的一部分操作失败,整个事务回滚,只有全部正确才完成提交。

MyBatis

  • MyBatis 注解方法就是将 SQL 语句直接写在接口上,这种方法相对于比较简单的系统则效率较高,但当 SQL 有变化时就需要重新编译代码。最基本的有@Select、@Insert、@Update、@Delete,上面的任务中在 CommentMapper 中对几个方法进行演示。
  • @Mapper 注解: 作用:在接口类上添加了@Mapper,在编译之后会生成相应的接口实现类 添加位置:接口类上面
@Mapper
public interface UserDAO {
//代码
}

$.post("url",{data},function (result) {})

post 就是 POST 方式发请求,url 就是请求路径,data 就是要传回的数据,类似表单中的输入数据,写成 json 形式就行了,最后一个 function 是回调函数,POST 这个函数发送完请求后会调你这个函数,并且把返回数据传进你这个函数里。就类似于:

function post(url, data, func){
var result = 发送请求(url, data);
func(result); }

return "redirect:/"的作用:

redirect 是会先跳转到后面写的 controller,然后继续找到这个 controller 对应的 RequestMapping 的参数,随后再去跳转页面。

缓存支持

  • 启动类添加缓存支持注解@EnableCaching 使用注解对数据操作方法进缓存管理@Cacheable(CacheNames=“findById”)

安全管理

  • 开启默认安全管理
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
 </dependency>
  • 排除自动注入安全自动配置,后才能执行自己的页面 (exclude = {SecurityAutoConfiguration.class})

拦截器

  • HandlerInterceptor 简介 HandlerInterceptor 是 SpringMVC 中为拦截器提供的接口,类似于 Servlet 开发中的过滤器 Filter,用于处理器进 行预处理和后处理,这个接口中需要有三个方法重写。 preHandle:controller 执行之前 postHandle:controller 执行之后,且页面渲染之前调用 afterCompletion:最终执行方法,一般用于清理资源
  • WebMvcConfigurer 简介 WebMvcConfigurer 配置类其实是 Spring 内部的一种配置方式,采用JavaBean 的形式来代替传统的 xml 配置文 件形式进行针对框架个性化定制,可以自定义一些 HandlerInterceptorViewResolverMessageConverter。基于java-based 方式的 spring mvc 配置,需要创建一个配置类并实现 WebMvcConfigurer 接口,使用步骤:
  • 编写一个拦截器,实现 HandlerInterceptor接口
  • 编写一个类,在该类中激活拦截器,配置拦截规则。具体操作是在该类实现 WebMvcConfigurer 接口,然后重 写 addInterceptors()方法,在方法中调用 InterceptorRegistry.addInterceptor()方法,把刚才编写的那个拦截器对象 作为参数加进去。

打包

  • jar 包:JAR 包是类的归档文件,JAR 文件格式以流行的 ZIP 文件格式为基础。与 ZIP 文件不同的是,JAR 文件不仅用于压缩和发布,而且还用于部署和封装库、组件和插件程序,并可被像编译器和 JVM 这样的工具直接使用。
  • war 包:war 包是 JavaWeb 程序打的包,war 包里面包括写的代码编译成的 class 文件,依赖的包,配置文件,所有的网站页面,包括 html,jsp 等等。一个 war 包可以理解为是一个 web 项目,里面是项目的所有东西。

从解压后的目录结构看:

  • jar 包: ①jar 包里的 com 里放的就是 class 文件,②配置文件,但是没有静态资源的文件,③大多数 JAR 文件包含一个 META-INF 目录,它用于存储包和扩展的配置数据,如安全性和版本信息。
  • war 包: ①WEB-INF 里放的 class 文件和配置文件,②META-INF 和 jar 包作用一样③war 包里还包含静态资源的文件。

总结起来就是有两点不同: 1.war 包和项目的文件结构保持一致,jar 包则不一样。 2.jar 包里没有静态资源的文件(index.jsp)。

从部署项目上看: 部署普通的 spring 项目用 war 包就可以,部署 springboot 项目用 jar 包就可以,因为 springboot 内置 tomcat。

Thymeleaf

Thymeleaf 简介

Thymeleaf 是一款用于渲染 XML/XHTML/HTML5 内容的模板引擎。它与 JSP,Velocity,FreeMaker 等模板引擎类似,也可以轻易地与 Spring MVC 等 Web 框架集成。与其它模板引擎相比,Thymeleaf 最大的特点是,即使不启动 Web 应用,也可以直接在浏览器中打开并正确显示模板页面 。

Thymeleaf 是新一代 Java 模板引擎,与 Velocity、FreeMarker 等传统 Java 模板引擎不同,Thymeleaf 支持 HTML 原型,其文件后缀为“.html”,因此它可以直接被浏览器打开,此时浏览器会忽略未定义的 Thymeleaf 标签属性,展示 thymeleaf 模板的静态页面效果;当通过 Web 应用程序访问时,Thymeleaf 会动态地替换掉静态内容,使页面动态显示。

Thymeleaf 通过在 html 标签中,增加额外属性来达到“模板+数据”的展示方式,示例代码如下。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--th:text 为 Thymeleaf 属性,用于在展示文本-->
<h1 th:text="迎您来到Thymeleaf">欢迎您访问静态页面 HTML</h1>
</body>
</html>

当直接使用浏览器打开时,浏览器展示结果如下。 欢迎您访问静态页面HTML

当通过 Web 应用程序访问时,浏览器展示结果如下。 迎您来到Thymeleaf

Thymeleaf 特点

Thymeleaf 模板引擎具有以下特点:

  • 动静结合:Thymeleaf 既可以直接使用浏览器打开,查看页面的静态效果,也可以通过 Web 应用程序进行访问,查看动态页面效果。

  • 开箱即用:Thymeleaf 提供了 Spring 标准方言以及一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。

  • 多方言支持:它提供了 Thymeleaf 标准和 Spring 标准两种方言,可以直接套用模板实现 JSTL、 OGNL 表达式;必要时,开发人员也可以扩展和创建自定义的方言。

  • 与 SpringBoot 完美整合:SpringBoot 为 Thymeleaf 提供了的默认配置,并且还为 Thymeleaf 设置了视图解析器,因此 Thymeleaf 可以与 Spring Boot 完美整合。

Thymeleaf语法规则

在使用 Thymeleaf 之前,首先要在页面的 html 标签中声明名称空间,示例代码如下。

<html lang="en" xmlns:th="http://www.thymeleaf.org">

在 html 标签中声明此名称空间,可避免编辑器出现 html 验证错误,但这一步并非必须进行的,即使我们不声明该命名空间,也不影响 Thymeleaf 的使用。

Thymeleaf 作为一种模板引擎,它拥有自己的语法规则。Thymeleaf 语法分为以下 2 类:

  • 标准表达式语法

  • th 属性

标准表达式语法

Thymeleaf 模板引擎支持多种表达式:

  • 变量表达式:${...}

  • 选择变量表达式:*{...}

  • 链接表达式:@{...}

  • 国际化表达式:#{...}

  • 片段引用表达式:~{...}

变量表达式

使用 ${} 包裹的表达式被称为变量表达式,该表达式具有以下功能:

获取对象的属性和方法

使用内置的基本对象

使用内置的工具对象


① 获取对象的属性和方法

使用变量表达式可以获取对象的属性和方法,例如,获取 person 对象的 lastName 属性,表达式形式如下:

${person.lastName}

② 使用内置的基本对象

使用变量表达式还可以使用内置基本对象,获取内置对象的属性,调用内置对象的方法。 Thymeleaf 中常用的内置基本对象如下:

#ctx :上下文对象;

#vars :上下文变量;

#locale:上下文的语言环境;

#request:HttpServletRequest 对象(仅在 Web 应用中可用);

#response:HttpServletResponse 对象(仅在 Web 应用中可用);

#session:HttpSession 对象(仅在 Web 应用中可用);

#servletContext:ServletContext 对象(仅在 Web 应用中可用)。


例如,我们通过以下 2 种形式,都可以获取到 session 对象中的 map 属性:

${#session.getAttribute('map')}${session.map}

③ 使用内置的工具对象

除了能使用内置的基本对象外,变量表达式还可以使用一些内置的工具对象。

strings:字符串工具对象,常用方法有:equals、equalsIgnoreCase、length、trim、toUpperCase、toLowerCase、indexOf、substring、replace、startsWith、endsWith,contains 和 containsIgnoreCase 等;

numbers:数字工具对象,常用的方法有:formatDecimal 等;

bools:布尔工具对象,常用的方法有:isTrue 和 isFalse 等;

arrays:数组工具对象,常用的方法有:toArray、length、isEmpty、contains 和 containsAll 等;

lists/sets:List/Set 集合工具对象,常用的方法有:toList、size、isEmpty、contains、containsAll 和 sort 等;

maps:Map 集合工具对象,常用的方法有:size、isEmpty、containsKey 和 containsValue 等;

dates:日期工具对象,常用的方法有:format、year、month、hour 和 createNow 等。


例如,我们可以使用内置工具对象 strings 的 equals 方法,来判断字符串与对象的某个属性是否相等,代码如下。

${#strings.equals('编程帮',name)}

选择变量表达式

选择变量表达式与变量表达式功能基本一致,只是在变量表达式的基础上增加了与 th:object 的配合使用。当使用 th:object 存储一个对象后,我们可以在其后代中使用选择变量表达式(*{...})获取该对象中的属性,其中,“*”即代表该对象。

<div th:object="${session.user}" >    <p th:text="*{fisrtName}">firstname</p></div>

th:object 用于存储一个临时变量,该变量只在该标签及其后代中有效,在后面的内容“th 属性”中我详细介绍。

链接表达式

不管是静态资源的引用,还是 form 表单的请求,凡是链接都可以用链接表达式 (@{...})。

链接表达式的形式结构如下:

无参请求:@{/xxx}

有参请求:@{/xxx(k1=v1,k2=v2)}


例如使用链接表达式引入 css 样式表,代码如下。

<link href="asserts/css/signin.css" th:href="@{/asserts/css/signin.css}" rel="stylesheet">

国际化表达式

消息表达式一般用于国际化的场景。结构如下。

th:text="#{msg}"

注意:此处了解即可,我们会在后面的章节中详细介绍。

片段引用表达式

片段引用表达式用于在模板页面中引用其他的模板片段,该表达式支持以下 2  中语法结构:

推荐:~{templatename::fragmentname}

支持:~{templatename::#id}


以上语法结构说明如下:

templatename:模版名,Thymeleaf 会根据模版名解析完整路径:/resources/templates/templatename.html,要注意文件的路径。

fragmentname:片段名,Thymeleaf 通过 th:fragment 声明定义代码块,即:th:fragment="fragmentname"

id:HTML 的 id 选择器,使用时要在前面加上 # 号,不支持 class 选择器。

th属性

Thymeleaf 还提供了大量的 th 属性,这些属性可以直接在 HTML 标签中使用,其中常用 th 属性及其示例如下表。

- th:id
替换 HTML 的 id 属性
<input  id="html-id"  th:id="thymeleaf-id"  />

- th:text
文本替换,转义特殊字符
<h1 th:text="hello,bianchengbang" >hello</h1>

- th:utext
文本替换,不转义特殊字符
<div th:utext="'<h1>欢迎来到编程帮!</h1>'" >欢迎你</div>

th:object
在父标签选择对象,子标签使用 *{…} 选择表达式选取值。
没有选择对象,那子标签使用选择表达式和 ${…} 变量表达式是一样的效果。
同时即使选择了对象,子标签仍然可以使用变量表达式。
<div th:object="${session.user}" >   
<p th:text="*{fisrtName}">firstname</p>
</div>

th:value
替换 value 属性
<input th:value = "${user.name}" />

th:with
局部变量赋值运算
<div th:with="isEvens = ${prodStat.count}%2 == 0"  th:text="${isEvens}"></div>

th:style
设置样式
<div th:style="'color:#F00; font-weight:bold'">编程帮 www.biancheng.net</div>

th:onclick
点击事件
<td th:onclick = "'getInfo()'"></td>

th:each
遍历,支持 Iterable、Map、数组等。
<table>    
<tr th:each="m:${session.map}">        
<td th:text="${m.getKey()}"></td>        
<td th:text="${m.getValue()}"></td>    
</tr>
</table>

th:if
根据条件判断是否需要展示此标签
<a th:if ="${userId == collect.userId}">

th:unless
和 th:if 判断相反,满足条件时不显示
<div th:unless="${m.getKey()=='name'}" ></div>

th:switch
与 Java 的 switch case语句类似
通常与 th:case 配合使用,根据不同的条件展示不同的内容
<div th:switch="${name}">    
<span th:case="a">编程帮</span>    
<span th:case="b">www.biancheng.net</span>
</div>

th:fragment
模板布局,类似 JSP 的 tag,用来定义一段被引用或包含的模板片段
<footer th:fragment="footer">插入的内容</footer>

th:insert
布局标签;
将使用 th:fragment 属性指定的模板片段(包含标签)插入到当前标签中。
<div th:insert="commons/bar::footer"></div>

th:replace
布局标签;
使用 th:fragment 属性指定的模板片段(包含标签)替换当前整个标签。
<div th:replace="commons/bar::footer"></div>

th:selected
select 
选择框选中
<select>    
<option>---</option>    
<option th:selected="${name=='a'}">        编程帮    </option>    
<option th:selected="${name=='b'}">        www.biancheng.net    </option>
</select>

th:src
替换 HTML 中的 src 属性 
<img  th:src="@{/asserts/img/bootstrap-solid.svg}" src="asserts/img/bootstrap-solid.svg" />

th:inline
内联属性;
该属性有 text、none、javascript 三种取值,
在 <script> 标签中使用时,js 代码中可以获取到后台传递页面的对象。
<script type="text/javascript" th:inline="javascript">    var name = /*[[${name}]]*/ 'bianchengbang';    alert(name)</script>

th:action
替换表单提交地址
<form th:action="@{/user/login}" th:method="post"></form>

Thymeleaf 公共页面抽取

在 Web 项目中,通常会存在一些公共页面片段(重复代码),例如头部导航栏、侧边菜单栏和公共的 js css 等。我们一般会把这些公共页面片段抽取出来,存放在一个独立的页面中,然后再由其他页面根据需要进行引用,这样可以消除代码重复,使页面更加简洁。

1.抽取公共页面
Thymeleaf 作为一种优雅且高度可维护的模板引擎,同样支持公共页面的抽取和引用。我们可以将公共页面片段抽取出来,存放到一个独立的页面中,并使用 Thymeleaf 提供的 th:fragment 属性为这些抽取出来的公共页面片段命名。

示例 1
将公共页面片段抽取出来,存放在 commons.html 中,代码如下。

<div th:fragment="fragment-name" id="fragment-id">    
<span>公共页面片段</span>
</div>

2.引用公共页面
在 Thymeleaf 中,我们可以使用以下 3 个属性,将公共页面片段引入到当前页面中。

th:insert:将代码块片段整个插入到使用了 th:insert 属性的 HTML 标签中;

th:replace:将代码块片段整个替换使用了 th:replace 属性的 HTML 标签中;

th:include:将代码块片段包含的内容插入到使用了 th:include 属性的 HTML 标签中。


使用上 3 个属性引入页面片段,都可以通过以下 2 种方式实现。

~{templatename::selector}:模板名::选择器

~{templatename::fragmentname}:模板名::片段名

通常情况下,~{} 可以省略,其行内写法为 [[~{...}]] 或 [(~{...})],其中  [[~{...}]] 会转义特殊字符,[(~{...})] 则不会转义特殊字符。

示例 2
1. 在页面 fragment.html 中引入 commons.html 中声明的页面片段,可以通过以下方式实现。

<!--th:insert 片段名引入-->
<div th:insert="commons::fragment-name"></div>
<!--th:insert id 选择器引入-->
<div th:insert="commons::#fragment-id"></div>
------------------------------------------------
<!--th:replace 片段名引入-->
<div th:replace="commons::fragment-name"></div>
<!--th:replace id 选择器引入-->
<div th:replace="commons::#fragment-id"></div>
------------------------------------------------
<!--th:include 片段名引入-->
<div th:include="commons::fragment-name"></div>
<!--th:include id 选择器引入-->
<div th:include="commons::#fragment-id"></div>

2. 启动 Spring Boot,使用浏览器访问 fragment.html,查看源码,结果如下。

<!--th:insert 片段名引入-->
<div>
    <div id="fragment-id">
        <span>公共页面片段</span>
    </div>
</div>
<!--th:insert id 选择器引入-->
<div>
    <div id="fragment-id">
        <span>公共页面片段</span>
    </div>
</div>
------------------------------------------------
<!--th:replace 片段名引入-->
<div id="fragment-id">
    <span>公共页面片段</span>
</div><!--th:replace id 选择器引入-->
<div id="fragment-id">
    <span>公共页面片段</span>
</div>
------------------------------------------------
<!--th:include 片段名引入--><div>
    <span>公共页面片段</span>
</div>
<!--th:include id 选择器引入-->
<div>
    <span>公共页面片段</span>
</div>




3.传递参数
Thymeleaf 在抽取和引入公共页面片段时,还可以进行参数传递,大致步骤如下:

传入参数;

使用参数。

3.1传入参数
引用公共页面片段时,我们可以通过以下 2 种方式,将参数传入到被引用的页面片段中:

模板名::选择器名或片段名(参数1=参数值1,参数2=参数值2)

模板名::选择器名或片段名(参数值1,参数值2)

注:

若传入参数较少时,一般采用第二种方式,直接将参数值传入页面片段中;

若参数较多时,建议使用第一种方式,明确指定参数名和参数值,。

示例代码如下:

<!--th:insert 片段名引入-->
<div th:insert="commons::fragment-name(var1='insert-name',var2='insert-name2')"></div>
<!--th:insert id 选择器引入-->
<div th:insert="commons::#fragment-id(var1='insert-id',var2='insert-id2')">
</div>
------------------------------------------------
<!--th:replace 片段名引入-->
<div th:replace="commons::fragment-name(var1='replace-name',var2='replace-name2')"></div>
<!--th:replace id 选择器引入-->
<div th:replace="commons::#fragment-id(var1='replace-id',var2='replace-id2')"></div>
------------------------------------------------
<!--th:include 片段名引入-->
<div th:include="commons::fragment-name(var1='include-name',var2='include-name2')"></div>
<!--th:include id 选择器引入-->
<div th:include="commons::#fragment-id(var1='include-id',var2='include-id2')"></div>
3.2 使用参数
在声明页面片段时,我们可以在片段中声明并使用这些参数,例如:

<!--使用 var1 和 var2 声明传入的参数,并在该片段中直接使用这些参数 -->
<div th:fragment="fragment-name(var1,var2)" id="fragment-id">
    <p th:text="'参数1:'+${var1} + '-------------------参数2:' + ${var2}">...</p>
</div>

启动 Spring Boot,使用浏览器访问 fragment.html,结果如下图。

图片