Skip to content

SpringMVC

分层系统

  • 软件层级的发展历程
  • image.png|500

    • 提取不同软件的公共部分,简化软件的开发流程
  • 应用程序的三层结构:

    • 展示层:展示数据、与用户交互
    • 业务逻辑层:处理业务逻辑和决策
    • 数据访问层:负责与数据库或其他持久化存储机制进行通讯

CS 模式

  • 客户机-服务器模式:客户机和服务器都具有一定的计算能力
  • 对三层的划分
    • image.png|500
  • Fat Clients
    • 应用系统是在 Client 端运行的
    • Client 知道 Sever 上的数据组织方式,调用 API 从服务器获取部分信息
    • 用户端程序设计有较大灵活性 ^jr9qnu
    • image.png|500
    • 相对较难管理,因为业务逻辑在终端运行,所以如果需要修改业务逻辑就需要修改每一个客户端的程序
  • Fat Severs(现在使用更为广泛)
    • Sever 通过一组确定的过程提供资源访问,而是提供对资源的直接访问和操作
    • Client 提供 GUI 供用户操作,并通过远程方法调用与 Sever 通信,获得服务
    • 应用系统集中于 Sever ,便于部署和管理
    • image.png|500

BS 模式

  • 三层模式
    • 用户 PC:展示层,用户界面不再需要专门的客户端,使用浏览器来对 html 进行渲染显示
    • 应用服务器:应用服务层
    • 资源管理系统:数据层
    • image.png|500

具体实现:JAVA EE

  • image.png|500
  • CGI:用于生成动态内容
    • 每当 Web 服务器收到一个请求,它就会启动一个新的 CGI 程序(或脚本)实例来处理请求(即一个新进程)。这种处理方式相对效率较低,因为每次请求都需要创建一个新的进程,增加了 CPU 和内存的负担。
    • 无状态
    • HTML内容通常是由脚本语言(如Perl、Python等)动态生成并打印出来的。脚本会执行必要的逻辑处理,并将处理结果以字符串的形式嵌入到HTML标记中,最后将整个HTML内容作为响应输出。
  • Active Page(如 JSP)

    • 支持使用线程池、缓存登记书,并且支持应用状态管理(不同请求之间可以共享信息,如用户会话),效率更高
    • Java代码嵌入到特定的JSP标签中。当页面被请求时,服务器上的JSP引擎会处理这些标签,执行其中的Java代码,生成动态内容,并插入到HTML页面中。
    • CGI 脚本通常是完全独立于 HTML 的,而 JSP 将 Java 代码嵌入HTML 中。
  • Java Servlet 是作为控制器,接受并读取 http 请求,访问数据并生成相应,之后将响应发送给服务器

    • image.png|500
    • 使用的是 CGI 分离模式,在 JAVA 点钟编写相应逻辑而不是在 HTML 中嵌入代码
    • 可维护性较差,混杂了逻辑处理及页面生成等不同功能,不能使用变化
  • Thyme leaf

  • image.png|500

    • 添加 UI 模块负责将生成的结果填入到页面,即 Active Page
    • 即将内容动态填入到 HTML 模板
    • image.png|500
  • 对结构进一步细分,提取业务处理代码

    • image.png|500

MVC

  • image.png|425
    • View:模版引擎->HTML,决定用户界面
    • Controller:处理请求操作了,决定输入的处理方式
    • Model:核心数据功能(实际进行请求的数据计算),进行业务逻辑的处理
  • 多视图:结合设备信息发送不同的视图
    • image.png|400

补充:渲染模式

服务端渲染 SSR
  • 在服务端渲染中,HTML 页面是在服务器上生成的。当用户请求一个页面时,服务器将所有必要的数据集成到 HTML 中,并且完成页面的渲染,然后将完整的页面发送给客户端。客户端(浏览器)接收到完整的 HTML 页面后,直接渲染显示给用户。(上面提到的吗模板引擎就属于这一种)
  • SEO 友好:由于页面是预先渲染的,搜索引擎更容易抓取和索引内容,这对 SEO 非常有利。
  • 首屏加载快:用户能更快地看到完整渲染的页面,因为浏览器不需要等待所有JavaScript下载并执行完成才能显示内容。
  • 兼容性好:SSR生成的页面不依赖于客户端JavaScript,因此即使在禁用JavaScript的环境下也能正常显示。
客户端渲染 CSR
  • 在客户端渲染中,服务器发送的 HTML 页面最初几乎是空的,所有的内容生成和渲染都在浏览器中通过 JavaScript 完成。当页面加载到浏览器后,JavaScript 代码会运行,从服务器获取数据,然后在客户端动态生成页面内容。(vue、react 都是这种)
  • 富交互性:CSR 非常适合构建交互性强的单页应用(SPA)。JavaScript 框架(如 React、Vue.js)可以提供流畅的用户体验和高效的页面更新。
  • 前后端分离:客户端渲染支持前后端分离的开发模式,前端负责UI和用户交互,后端通过API提供数据服务,这种模式增强了应用的可维护性和扩展性。
  • 减轻服务器负担:由于页面渲染工作转移到了客户端,服务器的负担减轻了,尤其是在高流量的情况下。
静态站点
  • 适用于内容驱动的网站,如博客、文档站点、营销网站。
  • 极快的加载速度,优秀的SEO表现,安全性高。

SpringMVC

例子

  • spring-petclinic
  • image.png|500
    • image.png|275
  • 在控制器和仓库接口之间添加了服务层
    • 让多个控制器可以重用相同代码,实现代码复用,并且简化了控制器的实现
    • 服务层可以进行验证和授权,提高系统的安全性
      //controller与服务层交互
          @PostMapping(value = "/owners/{ownerId}/edit")
          public String processUpdateOwnerForm(@Valid Owner owner, BindingResult result, @PathVariable("ownerId") int ownerId) {
              if (result.hasErrors()) {
                  return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
              }
      
              owner.setId(ownerId);
              this.clinicService.saveOwner(owner);
              return "redirect:/owners/{ownerId}";
          }
          //server与仓库交互
          @Override  
      @Transactional  
      public void saveOwner(Owner owner) {  
          ownerRepository.save(owner);  
      }
      

控制器层

  • 处理指定路径的 GET/POST
    • 并准备需要的数据
      @GetMapping("/signup")//处理的路径
      public String showSignUpForm(User user) {
          return "add-user";
      }
      
      @PostMapping("/adduser")
      public String addUser(@Valid User user, BindingResult result, Model model) {
          if (result.hasErrors()) {
              return "add-user";
          }
          userRepository.save(user);
          return "redirect:/index";
      }
      //添加依赖项,用于模板引擎进行渲染
      GetMapping("/")
          public String pos(Model model) {
              posService.add("PD1", 2);
              //添加依赖
              model.addAttribute("products", posService.products());
              model.addAttribute("cart", posService.getCart());
              return "index";
          }
      

模板引擎

  • 上面 return 的就是一个模板引擎,比如 "add-user" 就对应一个 HTML 模板文件的名字。该文件应该位于 src/main/resources/templates 目录下,并且文件名应该是 add-user.html
    • Spring Boot 配置了Thymeleaf模板引擎作为其默认的模板引擎,所以当返回 "add-user" 时,Spring MVC 会寻找名为 add-user.html 的模板文件,然后渲染这个模板作为 HTTP 响应。
  • 重定义路径:当执行完一个操作后,比如添加或更新数据,将用户重定向到另一个路径,告诉浏览器去请求一个新的 URL。

    • "redirect:/index" 就表示重定位到 /index
  • 模版引擎的内容填充发生在服务器端

    • 解析模板:模板引擎读取模板文件(例如,add-user.html),并解析文件中的 Thymeleaf 语法。
    • 数据绑定:模板中的动态部分(比如,表达式、选择变量)会与控制器传递给视图的模型数据(Model)进行绑定。模型数据通常是在控制器方法中构造的,可以包含各种形式的数据,如实体对象、列表或任何需要在页面上展示的数据。
    • 生成 HTML:模板引擎将填充后的模板渲染为最终的 HTML 页面。这个过程包括替换动态表达式为实际的值,执行循环、条件判断等操作。
    • 发送响应:渲染后的HTML页面作为HTTP响应的一部分发送给客户端(浏览器)。

视图层

数据绑定

  • 用户表单注册
    <form action="#" th:action="@{/adduser}" th:object="${user}" method="post">
        <label for="name">Name</label>
        <input type="text" th:field="*{name}" id="name" placeholder="Name">
        <span th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span>
    
        <label for="email">Email</label>
        <input type="text" th:field="*{email}" id="email" placeholder="Email">
        <span th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></span>
    
        <input type="submit" value="Add User">   
    </form>
    
  • th:action="@{/adduser}" 指定表单提交的 URL,Thymeleaf 会解析 @{/adduser} 为应用的相对路径。
  • th:object="${user}" 将表单与后端的User对象绑定。
  • th:field="*{name}" 将输入框绑定到User对象的name属性,实现数据的双向绑定。
  • th:if="${#fields.hasErrors('name')}" th:errors="*{name}" 如果 name 字段有验证错误,这些错误会被显示出来。
  • 设置动态路径 @{/update/{id}(id=${user.id})}

  • 用户列表展示

    <div th:switch="${users}">
        <h2 th:case="null">No users yet!</h2>
        <div th:case="*">
            <h2>Users</h2>
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Email</th>
                        <th>Edit</th>
                        <th>Delete</th>
                    </tr>
                </thead>
                <tbody>
                    <tr th:each="user : ${users}">
                        <td th:text="${user.name}">Name</td>
                        <td th:text="${user.email}">Email</td>
                        <td><a th:href="@{/edit/{id}(id=${user.id})}">Edit</a></td>
                        <td><a th:href="@{/delete/{id}(id=${user.id})}">Delete</a></td>
                    </tr>
                </tbody>
            </table>
        </div>      
        <p><a href="/signup">Add a new user</a></p>
    </div>
    

  • th:each="user : ${users}" 遍历所有用户,并为每个用户创建一个表格行。
  • th:text="${user.name}"th:text="${user.email}" 分别显示用户的姓名和电子邮箱。
  • th:href="@{/edit/{id}(id=${user.id})}"th:href="@{/delete/{id}(id=${user.id})}" 分别为编辑和删除操作提供动态链接,链接中包含用户的 ID。

    • 只适用于 a 标签,可以实现跳转@{} 专门用于拼接 URL
  • 实现按钮的点击事件

仓库层(存储)

使用 h2 数据库

  • h2 数据库无需本地配置,通常用于开发以及测试环境
  • maven 配置
    <dependency>  
        <groupId>com.h2database</groupId>  
        <artifactId>h2</artifactId>  
    </dependency>
    
  • 在 application. properties 中配置基本信息
    //非持久存储
    spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    //持久存储(需要指示存储目录)
    spring.datasource.url=jdbc:h2:file:~/testdb;DB_CLOSE_ON_EXIT=FALSE
    spring.datasource.driverClassName=org.h2.Driver
    spring.datasource.username=sa
    spring.datasource.password=
    spring.h2.console.enabled=true
    spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
    spring.h2.console.path=/h2-console
    

数据库的使用

  • 使用 SQL 脚本:Spring Boot 会在启动时自动执行 schema.sqldata.sql 文件(如果它们存在于 src/main/resources 目录中)。schema.sql 用于创建数据库结构(表、视图等),data.sql 用于插入初始数据。
  • 使用 JPA 实体自动创建数据库结构:需要将要存储的对象转化为 JPA 实体
Spring Data JPA
  • 配置数据库架构生成行为

    • 生产环境下通常使用 spring.jpa.hibernate.ddl-auto=update Hibernate 会根据实体类的定义更新数据库表结构,这包括添加新的表、列和约束等,但不会删除任何现有的表、列或数据。
  • 使用注解标记 JPA 实体以及主键,,得到 JPA 实体

    @Entity
    public class Product {
        @Id
        //设置主键自增生成,数据库在插入记录时会自动分配(忽视id的现值)
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private String id;
        private String name;
        private double price;
        private String image;
    }
    @Entity
    @Data
    public class Item {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        //多个Item共享Product因此是多对一
        @ManyToOne
        private Product product;
    
        private int quantity;
    }
    @Entity
    @Data
    public class Cart {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        //注意这里是一个列表,因此是一对多
        //当`Cart`实体被更新或删除时,相关联的`Item`实体也会被相应地更新或删除
        @OneToMany(cascade = CascadeType.ALL)
        private List<Item> items = new ArrayList<>();
    
        public boolean addItem(Item item) {
            return items.add(item);
        }
    }
    

  • 创建 Repository 接口

    • 对于每个实体,创建一个继承自 JpaRepository 的接口。这将提供基本的 CRUD 操作和 JPA 查询能力。
    • 定义在独立文件内,如 ProductRepository.java
      @Repository
      public interface ProductRepository extends JpaRepository<Product, String> {
      }
      @Repository
      public interface ItemRepository extends JpaRepository<Item, Long> {
      }
      @Repository
      public interface CartRepository extends JpaRepository<Cart, Long> {
          // 这里可以添加特定于Cart的方法,比如根据状态或用户查找购物车
      }
      
  • 在 Repository 接口中自定义查询方法:无需实现这些方法的具体逻辑,Spring Data JPA 能够根据方法名自动解析并生成查询
    • 方法名的构成:方法名通常以 findreadquerycount 开头,后面跟 By 来指示查询条件的开始。之后的部分表达了查询条件,可以通过属性名来直接指定。Spring Data JPA 根据这个命名约定来构造查询。
    • 条件关键字:可以使用 AndOr 来组合查询条件,使用属性名来指定要查询的字段。也可以使用比较关键字如 GreaterThanLessThanLike 等来表达更复杂的查询条件。
      List<Product> findByName(String name); // 通过名字查找产品
      
      List<Product> findByPriceGreaterThanEqual(Double price); // 查找价格大于等于指定值的产品
      
      List<Product> findByCategoryAndPriceLessThan(String category, Double price); // 查找特定分类且价格小于指定值的产品
      
      List<Product> findByNameContaining(String keyword); // 查找名称中包含关键字的产品
      
      List<Product> findByCategoryOrderByPriceAsc(String category); // 查找特定分类的产品,并按价格升序排序
      
  • 也可以使用 @Query 注解,手动给出 SQL

    @Query("SELECT p FROM Product p WHERE p.category = :category")
    List<Product> findByCategory(@Param("category") String category);
    
    @Query(value = "SELECT * FROM product WHERE price < :price", nativeQuery = true)
    List<Product> findByPriceLowerThan(@Param("price") Double price);
    

  • 使用数据接口(数据库)

    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private CartRepository cartRepository;
    
    //添加项
    cartRepository.save(new Cart());
    //saveall可以一次添加一个列表
    productRepository.saveAll(products);
    //获取全部项
    productRepository.findAll();
    

REST 架构

  • 引入原因
    • 之前的使用模版引擎的版本展示层页面与计算逻辑混杂(将逻辑代码部分放入页面)
    • 并且控制层的接口自定义名称,接口不够规范
  • 什么是 REST

    • 资源(Resources):网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一个服务等。在 RESTful 架构中,每个资源都由其 URI 唯一标识。
    • 表现层(Representation):资源的某种表现形式,比如,一个资源可以有 JSON 或 XML 等多种格式的表现层。客户端和服务器之间的交互在传递这些表现层时进行。
    • 状态转移(State Transfer):在客户端和服务器交互时,通过 HTTP 动词(如 GET、POST、PUT、DELETE)表达对资源的操作,使得资源的状态发生转移
  • 优点

    • 使用 restgul 将展示层完全解耦合,并且自动生成规范化接口。使用 REST,可以通过使用标准的 HTTP 方法如 GET、POST、PUT、DELETE 等来进行资源的创建、读取、更新和删除操作。
    • 松耦合:前后端通过定义清晰的API接口交互,各自独立开发、测试和部署,降低了前后端的耦合度,提高了开发效率。

资源-URI 统一资源标识符

  • URI 的的设计原则
    • image.png|475
  • 使用动词明确表示目标动作
    • GET:获取资源
    • POST:创建资源
    • PUT:更新/替换现有资源
    • DELETE:
  • 而 url 就不应该再表示动作,只需要表示资源名称
    • image.png|450

2-表现/表述

  • 资源是一种信息实体,有多种外在表现形式
  • URI 只只代表资源的实体,不代表资源的形式,因此 URI 后也不必要有. html 因为这个具体表示格式是表现层的范围而不是资源范围
  • 客户端可以通过 accept 头请求特定格式的表述,服务端通过 content-type 告诉客户端的表述形式
    • image.png|475
  • 资源链接:
    • 页面上可能返回超链接,将不同页面链接起来
    • 比如再创建订单之后通过链接引导客户端去付款
    • image.png|475

状态转移

  • 状态分为应用状态和资源状态,客户端维护应用状态 (如购物车),服务器维护资源状态(如库存)
  • 客户端与服务端的交互是无状态的,客户端每一次请求应该包含处理请求需要的一切信息
    • 也就是说服务器不应该保存用户的应用状态,这样多次请求不依赖相同的服务器,可以使吸纳高可扩展性和高可用性服务端
  • 另一个例子:比如返回多页结果,服务器不知道用户正在看第一页,应该由客户端维护当前访问的界面,根据自身的应用状态向服务器请求资源
  • 客户端应用状态在服务端提供的超媒体的指引下发生变迁,服务端的超媒体高速客户端哪些后序状态可以进入。

重写控制器

  • 传统的 Spring MVC 应用中,控制器方法通常返回一个字符串,表示的是视图名称(View Name),然后通过视图解析器(View Resolver)找到相应的视图模板进行渲染,最后将渲染后的 HTML 返回给客户端。这种方式主要用于生成动态的 HTML 页面。
  • 而在 Spring REST 中,控制器的主要职责转变为处理 HTTP 请求并返回数据本身,而不是返回视图名称。这里的数据通常是JSON或 XML 格式,直接返回给客户端,供客户端应用(如 Web 应用的前端、移动应用等)使用。这种方式更加适合于构建 API 和服务导向的应用。
    • 这也是 Vue 等前端框架的工作模式

编程实现

  • 在 REST 风格的控制器中,方法应直接返回对象或数据集合,而不是视图名称
  • 直接返回对象(Spring 框架可以自动机将对象转化为 JSON 以及其他响应格式)
    @GetMapping("/api/data")
        public List<MyData> getData() {
            // 返回数据列表
            return someDataService.findAll();
        }
    
  • 如果想要返回更加详细的信息,可以使用 ResponseEntity,可以用于表示整个 HTTP 响应(包含状态码,头部信息以及响应体)
    • 这是一个泛型类 ResponseEntity<TYPE>
  • 静态创建 return new ResponseEntity<>(body, HttpStatus.OK);
  • 链式构建
    @GetMapping("/api/users/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        User user = userService.findById(id);
        if (user == null) {
        //创建404的响应
            return ResponseEntity.notFound().build();
        } else {
        //创建200的正确响应
            return ResponseEntity.ok(user);
        }
    }
    
  • 添加更多头部信息
    @GetMapping("/api/download")
    public ResponseEntity<Resource> downloadFile() {
        Resource file = fileService.getFileAsResource();
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"");
    
        return ResponseEntity.ok()
            .headers(headers)
            .contentLength(file.contentLength())
       .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .body(file);
    }