视频回顾地址: 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。
IOC,即Inversion of Control,也就是控制反转。控制反转的概念从字面上比较晦涩难懂,而且每个Spring开发工程师对于这个概念也都有不同的理解。想要理解控制反转,首先要弄清楚控制指的是什么的控制,反转又指的反转什么。
这里的控制就是通过编码的形式控制了对象的实例化,通过编码主动设置对象的依赖并最终得到一个可以运行的对象。假如在正常的套路里面剧情出现了反转,但是反转的结果还是能够得到一个正常运行的进程,那么能够反转的部分就是实例化的部分以及依赖设置的部分。将实例化以及依赖关系的设置的控制权反转就需要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的概念里需要理解两个小的概念:切面和切点。
那么将Java中的类比喻成馍,那么类可以怎么切呢?其切点又在哪里呢?Java里面已经定义了类是可以切的,其切点只能是方法。将方法作为切点,那么其切面就只能是方法的前和后,那么所谓的肉就是需要添加的代码。那么在执行对象的方法时在进入方法前会执行切入的一段代码,在方法执行之后还会执行切入的另外一段代码,这样面向切面编程的理解就是将代码切入到类的指定方法、指定位置的编程思想。面向切面编程是面向对象编程思想的补充,可以通过面向切面编程将与类不相关的行为提取出来进行共享,并以切点的方式加入到不同的对象当中。一旦行为发生变化只需要修改行为而没有必要修改对象,其典型用法有日志打印、性能统计以及异常处理等。这里的日志打印就是当进入某一个方法的时候希望打印方法的一个参数信息,当退出方法的时候希望在日志中打印方法的返回值;性能统计就是统计方法的执行时间,在方法前记录一个时间戳,在方法后记录一个时间戳,以此得出方法的执行时间来统计方法的性能;异常处理可以定义该方法抛出异常的切面上面应该对于异常进行怎样的处理。
二、Spring Web开发
接下来将通过Spring Web的工程进行学习帮助大家了解如何快速开发Spring Web应用。在这个过程中也会加深大家对于IOC和AOP思想的理解。此外还将了解Web MVC架构模式的使用方式。
首先,示例工程使用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赋予后台的用户数据,在最终的模板文件里将会进行转化,使用真实的数据进行替代并渲染出真实的网页出来。
其实在整个过程中都是在定义一个类,通过注解进行配置,在整个过程中并没有通过类实例化一个对象,这些都是因为控制反转的缘故交由IOC处理了。由于Spring Boot的缘故,也无需定义一个Tomcat,Spring Boot已经默认定义好了一个Tomcat来对外提供服务,这就是Spring Boot所带来的好处,可以帮助我们非常快速地开发Web的应用。
本文由云栖志愿小组贾子甲整理,编辑百见