阿里老司机带你使用Spring框架快速搭建Web工程项目

摘要:Spring 框架是一个开源的 Java 平台,它为容易而快速的开发出耐用的 Java 应用程序提供了全面的基础设施。借助于Spring框架可以快速搭建Web工程项目,本文中阿里巴巴高级开发工程师嵛山就带大家一起入门Spring框架。

视频回顾地址:https://yq.aliyun.com/video/play/1435

PPT下载地址:https://yq.aliyun.com/download/2661


演讲嘉宾简介
吕德庆(花名:嵛山), 阿里巴巴高级开发工程师,武汉大学地信硕士,有丰富的系统开发经验,目前就职于阿里巴巴代码中心团队,负责后端开发。

本文首先将介绍Spring框架的相关概念,其次将借助Spring Web示例工程带大家学习如何快速开发Spring Web应用。

一、Spring介绍
Spring是一个开源的Java企业应用开发框架,其诞生的目标就是简化Java应用开发。下图中是早期Spring版本的模块图,可以看到Spring框架的基座是Spring Core模块,其支撑了Spring的上层模块AOP、DAO、ORM等,而Spring Core的核心就是IOC容器。其实整个Spring框架的核心主要有两个:IOC和AOP。
阿里老司机带你使用Spring框架快速搭建Web工程项目
IOC
IOC,即Inversion of Control,也就是控制反转。控制反转的概念从字面上比较晦涩难懂,而且每个Spring开发工程师对于这个概念也都有不同的理解。想要理解控制反转,首先要弄清楚控制指的是什么的控制,反转又指的反转什么。
阿里老司机带你使用Spring框架快速搭建Web工程项目
首先看正常程序的伪代码,在这段代码中有一个Process进程类,实例化之后还有一个Thread线程类,由于进程必须有一个主线程才能正常运行,所以进程是依赖于线程的,那么就需要在进程中设置主线程。代码中将实例化的线程对象设置进去,这样进程才完整并且能够运行起来。

这里的控制就是通过编码的形式控制了对象的实例化,通过编码主动设置对象的依赖并最终得到一个可以运行的对象。假如在正常的套路里面剧情出现了反转,但是反转的结果还是能够得到一个正常运行的进程,那么能够反转的部分就是实例化的部分以及依赖设置的部分。将实例化以及依赖关系的设置的控制权反转就需要IOC容器接收控制权,让IOC容器来控制对象的实例化以及依赖关系的设置。

上图中还有一段关于IOC的伪代码。首先需要一个IOC容器,通过new去实例化一个,然后告诉容器其所需要控制的类的信息。在Process类里面通过某种配置或者声明的方式告诉IOC容器Process类里面需要依赖于Thread类。当向IOC容器请求一个Process对象的时候,IOC会实例化一个Process,同时也会实例化一个Thread类,根据Process里面声明的依赖将Thread设置到Process里面,并最终给用户一个可以正常运行的Process。

那么IOC是如何知道类的信息以及类之间的依赖关系的呢?现实的做法就是通过配置,配置可以为配置文件,也可以通过Java的注解进行实现。这样只需要定义类并通过配置声明依赖关系,就不用去关心类的对象的串接了,这些统统交由IOC进行处理,这样就简化了开发的工作量,同时通过配置可以灵活地配置对象之间的依赖关系。

这里还涉及到的一个概念就是依赖注入,依赖注入是一个比较易懂的概念。所谓的依赖注入就是将对象之间的依赖关系通过容器进行注入,避免在代码上直接设置依赖。其实依赖注入和控制反转表达的是同样的思想,但是依赖注入更加简洁明了,便于理解。

AOP
AOP,Aspect Oriented Programming,也就是面向切面编程。在AOP的概念里需要理解两个小的概念:切面和切点。
阿里老司机带你使用Spring框架快速搭建Web工程项目
首先,所谓切面就是刀切开之后所看到的平面,而切点则是刀切的位置。那么如何理解面向切面编程呢?在生活中的例子就是“肉夹馍”,在制作肉夹馍时首先需要将馍切开,之后在切面上放上肉并盖好,这样在吃的时候不仅能够吃到馍还能够吃到肉。而在做的时候需要关注这个馍应该怎么切,当然是横着切的,而且其切点位于边上,切完之后还需要选择放什么肉。

那么将Java中的类比喻成馍,那么类可以怎么切呢?其切点又在哪里呢?Java里面已经定义了类是可以切的,其切点只能是方法。将方法作为切点,那么其切面就只能是方法的前和后,那么所谓的肉就是需要添加的代码。那么在执行对象的方法时在进入方法前会执行切入的一段代码,在方法执行之后还会执行切入的另外一段代码,这样面向切面编程的理解就是将代码切入到类的指定方法、指定位置的编程思想。面向切面编程是面向对象编程思想的补充,可以通过面向切面编程将与类不相关的行为提取出来进行共享,并以切点的方式加入到不同的对象当中。一旦行为发生变化只需要修改行为而没有必要修改对象,其典型用法有日志打印、性能统计以及异常处理等。这里的日志打印就是当进入某一个方法的时候希望打印方法的一个参数信息,当退出方法的时候希望在日志中打印方法的返回值;性能统计就是统计方法的执行时间,在方法前记录一个时间戳,在方法后记录一个时间戳,以此得出方法的执行时间来统计方法的性能;异常处理可以定义该方法抛出异常的切面上面应该对于异常进行怎样的处理。

二、Spring Web开发
接下来将通过Spring Web的工程进行学习帮助大家了解如何快速开发Spring Web应用。在这个过程中也会加深大家对于IOC和AOP思想的理解。此外还将了解Web MVC架构模式的使用方式。
阿里老司机带你使用Spring框架快速搭建Web工程项目
在正式开始之前需要设计代码层级的结构,整个Spring Web工程将主要分为5个模块:view、controller、service、model和AOP,这些模块是具有层级关系的,view最靠近浏览器端,所以在层级上面属于最上层,浏览器发送请求并由controller响应请求并调用底下的service,由service层去操作数据,controller将得到service的返回结果并将数据传递给view层,最终view层输出给浏览器,而AOP将作为通用模块使用。

首先,示例工程使用Maven管理,这里首先定义了Spring Boot的依赖。Spring Boot是集成了Spring框架的开发套件,它将帮助开发者更快地开发Spring Web的应用。同时在依赖中还可以看到Web依赖、AOP依赖以及模板引擎Thymeleaf的依赖。工程的POM文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>java.alibaba</groupId>
    <artifactId>spring-boot-web-demo</artifactId>
    <version>1.0-SNAPSHOT</version>


    <dependencies>

        <!-- Spring boot 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.0.1.RELEASE</version>
        </dependency>


        <!-- Web 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.0.1.RELEASE</version>
        </dependency>

        <!-- AOP 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>2.0.1.RELEASE</version>
        </dependency>

        <!-- 模板引擎 Thymeleaf 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>2.0.1.RELEASE</version>
        </dependency>
    </dependencies>

</project>
示例工程下主要有aop包、controller包、model包和service包,这与前面所提到的代码层级模式是一致的。


以下所展现的是WebApplication主程序类,在WebApplication中存在一个main函数,而main函数中只有一句代码就是SpringApplication.run(),然后将WebApplication类的信息作为参数传递进去。那么SpringApplication.run()的背后就是Spring的核心,即IOC容器的初始化以及其他工作。在IOC容器初始化完成之后,将收到WebApplication类的信息,同时因为@SpringBootApplication注解,IOC容器将开启对demo包以及其下面的子包中所有类的扫描,通过扫描IOC容器将发现很多需要用到的类并将这些类注入进去。

package demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Spring boot 启动类
 *
 * @author yushan.ldq
 * @date 2018/04/22
 */
@SpringBootApplication()
public class WebApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class, args);
    }
}

如下代码所示的是UserController类,在其上面可以看到@Controller的注解,因为这个注解,UserController的信息将被注入到IOC容器中。在UserController中可以看到UserService的一个成员变量,这个成员变量在整个UserController并没有得到初始化。因为有一个@Autowired注解,这个UserController就可以在初始化之后会产生由于@Autowired注解声明的依赖,这个依赖将由IOC容器负责注入。其所注入的是UserService接口的实现。

UserController类:

package demo.controller;

import demo.model.User;
import demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 *
 * @author yushan.ldq
 * @date 2018/04/22
 */
@Controller
@RequestMapping(value = "/users")
public class UserController {

    @Autowired
    @Qualifier("userServiceImpl")
    private UserService userService;

    @RequestMapping(method = RequestMethod.GET)
    public String getUsersPage(ModelMap modelMap) {
        modelMap.addAttribute("userList", userService.findAll());
        return "users";
    }

    @RequestMapping(value = "/add", method = RequestMethod.GET)
    public String createUserPage(ModelMap modelMap) {
        modelMap.addAttribute("user", new User());
        return "userCreate";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String createUser(@ModelAttribute User user) {
        userService.create(user);
        return "redirect:/users";
    }


    @RequestMapping(value = "/{id}/delete", method = RequestMethod.GET)
    public String deleteUser(@PathVariable Integer id) {
        userService.delete(id);
        return "redirect:/users";
    }
}

对于UserService接口而言,其具有一个实现类,在实现类中有一个@Service注解,因为这个注解,UserService的实现类将被注入到IOC容器中,最终IOC容器可以初始化UserService的实现类的一个对象,并将对象注入到UserController里面,这样就可以通过IOC拿到一个完整的UserController对象,并且依赖已经自动装配好了。这也就是IOC的作用。在所有的代码中都没有UserService的实例化,这些都是由SpringBoot和IOC共同完成的。

UserService接口:

package demo.service;

import java.util.List;

import demo.model.User;

/**
 * @author yushan.ldq
 * @date 2018/04/22
 */
public interface UserService {

    User findById(Integer id);

    User create(User user);

    User update(User user);

    User delete(Integer id);

    List<User> findAll();
}


UserServiceImpl实现类:

package demo.service.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import demo.model.User;
import demo.service.UserService;
import org.springframework.stereotype.Service;

/**
 * @author yushan.ldq
 * @date 2018/04/22
 */
@Service()
public class UserServiceImpl implements UserService{

    private Map<Integer, User> userDb = new HashMap<Integer, User>();

    public User findById(Integer id) {
        return null;
    }

    public User create(User user) {
        user.setId(userDb.size() + 1);
        return userDb.put(user.getId(), user);
    }

    public User update(User user) {
        return null;
    }

    public User delete(Integer id) {
        return userDb.remove(id);
    }

    public List<User> findAll() {
        return new ArrayList<User>(userDb.values());
    }
}
在aop包下的LogAspect类里面可以看到面向切面的编程思想。首先在log()函数之前可以看到一个名为@Pointcut的注解,因为这个注解,这个log()函数将代表一个切点,同时可以看到在@Pointcut注解里面有一个表达式,该表达式标明了切点的位置,其将把demo.controller包下面的所有类以及所有公开的方法都作为切点进行切面编程。在doBefore()方法上面有一个函数注解,该注解就定义了一个切面,该切面位于切点之前,也就是在执行切点之前需要执行这段代码。同时还有一个切面是位于函数执行之后,将打印函数返回值输出的代码。

因为LogAspect的存在,将导致后续代码在执行controller里面的所有公有方法的时候都会在之前进行方法参数打印,以及方法后返回值的输出。这就是AOP面向切面编程思想所带来的好处。

LogAspect类:

package demo.aop;

import java.util.Arrays;

import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * @author yushan.ldq
 * @date 2018/04/24
 */
@Aspect
@Component
public class LogAspect {
    @Pointcut("execution(public * demo.controller.*.*(..))") //切点表达式
    public void log(){}

    @Before("log()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 记录下请求内容
        System.out.println("URL : " + request.getRequestURL().toString());
        System.out.println("HTTP_METHOD : " + request.getMethod());
        System.out.println("IP : " + request.getRemoteAddr());
        System.out.println("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
    }

    @AfterReturning(returning = "ret", pointcut = "log()")
    public void doAfterReturning(Object ret) {
        // 处理完请求,返回内容
        System.out.println("方法的返回值 : " + ret);
    }
}

接下来将与大家分享WebMVC这种架构模式的使用,首先需要理解MVC中的M、V、C分别都代表什么。M指的是Model,也就是数据模型。如下图所示的User类,Model就是数据的载体。

User 类:

package demo.model;

/**
 * @author yushan.ldq
 * @date 2018/04/22
 */
public class User {
    private Integer id;
    private String name;
    private Integer age;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

V指的是View视图,在这里的视图是templates文件夹下定义的两个html模板文件,这些模板文件最终将由引入的依赖Thymeleaf模板引擎进行读。在这里其实可以将模板文件理解为Java的一个类。

userCreate.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>创建用户</title>
</head>
<body>
<form th:action="@{/users}" method="post" >

    <div>
        <label for="user_name" class="col-sm-2 control-label">用户名:</label>
        <input type="text" id="user_name" name="name" th:field="*{user.name}"/>
    </div>
    <div >
        <label for="user_age" >年龄:</label>
        <input type="text" id="user_age" name="age" th:field="*{user.age}"/>
    </div>
    <div >
        <input type="submit" value="提交"/>
    </div>
</form>

</body>
</html>


user.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Users</title>
</head>
<body>
<table>

    <thead>
    <tr>
        <th>用户id</th>
        <th>名字</th>
        <th>年龄</th>
    </tr>
    </thead>
    <tbody>
    <tr th:each="user : ${userList}">
        <th scope="row" th:text="${user.id}"></th>
        <td th:text="${user.name}"></td>
        <td th:text="${user.age}"></td>
        <td><a th:href="@{/users/{id}/delete(id=${user.id})}">删除</a></td>
    </tr>
    </tbody>
</table>

<div><a href="/users/add" >新增用户</a></div>
</body>
</html>

C也就是Controller控制器,如下代码所示的UserController(前文中也有具体代码)。
/**
 *
 * @author yushan.ldq
 * @date 2018/04/22
 */
@Controller
@RequestMapping(value = "/users")
public class UserController {

    @Autowired
    @Qualifier("userServiceImpl")
    private UserService userService;

    @RequestMapping(method = RequestMethod.GET)
    public String getUsersPage(ModelMap modelMap) {
        modelMap.addAttribute("userList", userService.findAll());
        return "users";
    }

    @RequestMapping(value = "/add", method = RequestMethod.GET)
    public String createUserPage(ModelMap modelMap) {
        modelMap.addAttribute("user", new User());
        return "userCreate";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String createUser(@ModelAttribute User user) {
        userService.create(user);
        return "redirect:/users";
    }


    @RequestMapping(value = "/{id}/delete", method = RequestMethod.GET)
    public String deleteUser(@PathVariable Integer id) {
        userService.delete(id);
        return "redirect:/users";
    }
}
接下来分享视图层、控制器的作用。首先,视图层的主要作用是将视图最终转化成一个包含真实数据的页面,并返回给浏览器由浏览器展现这个结果。控制器则主要是对请求进行响应,其会根据请求的地址和请求方式不同将请求分发到不同的方法上面,由方法执行对应的代码,然后对于请求做出对应的响应。

可以通过运行main函数执行整个Spring Boot程序,如下所示Spring Boot便已启动成功。Spring Boot在启动的时候就已经同时启动了一个Tomcat,Tomcat作为Web的容器,将可以对外提供Web服务,其将监听8080端口。根据端口以及UserController定义的地址可以通过浏览器进行访问。通过localhost:8080/users地址发起一个请求,在后台中由于做了切面编程,可以看到后台中UserController中的getUsers这个方法被执行了。而且这个方法的返回值是users。之所以会执行getUsers方法是因为在Controller中配置的@RequestMapping注解,当使用GET请求/users这个地址的时候,请求就会转发到这个方法中,这个方法会操作底层的users数据并返回,通过modelMap将数据和userList这个Key进行绑定并最终返回绑定的字符串users。由于框架的缘故,其会触发模板引擎来解释模板文件。同时由于modelMap的关系,它将给userList赋予后台的用户数据,在最终的模板文件里将会进行转化,使用真实的数据进行替代并渲染出真实的网页出来。
阿里老司机带你使用Spring框架快速搭建Web工程项目
接下来可以尝试创建用户,在前端页面点击“创建用户”,后台将会调用相应的用户创建方法,其将有一个modelMap并将返回userCreate,由于模板的关系,其将会将modelMap中的数据带到模板中来,这个模板就是添加用户的页面,然后可以实现数据值的录入。
阿里老司机带你使用Spring框架快速搭建Web工程项目
通过提交按钮将以POST方式请求,这样之后数据就添加成功了。
阿里老司机带你使用Spring框架快速搭建Web工程项目
同时页面也已经重定向了用户列表的页面了。
阿里老司机带你使用Spring框架快速搭建Web工程项目
可以看下后台的实现,首先调用了createUser方法,createUser方法将拿到由浏览器返回的录入的数据,并进行user创建。在具体进行创建的时候会插入一个具体的数据。在创建完成之后通过网页的重定向,让网页重新定向到users页面,进而触发一个新的请求并由getUsersPage重新响应,并返回users数据,并由模板引擎进行渲染进而显示一个真实的数据。整个请求的路由以及后端的MVC实现就是如上述所分析的。
阿里老司机带你使用Spring框架快速搭建Web工程项目

其实在整个过程中都是在定义一个类,通过注解进行配置,在整个过程中并没有通过类实例化一个对象,这些都是因为控制反转的缘故交由IOC处理了。由于Spring Boot的缘故,也无需定义一个Tomcat,Spring Boot已经默认定义好了一个Tomcat来对外提供服务,这就是Spring Boot所带来的好处,可以帮助我们非常快速地开发Web的应用。

本文由云栖志愿小组贾子甲整理,编辑百见

上一篇:学习python对文本文档的操作


下一篇:Day22死锁、线程通信、单例模式