构建SpringMVCRestful前后端分离项目实例

构建SpringMVCRestful前后端分离项目实例

啊哈,第一次写博客啊,这次是想梳理总结一下这个假期学习的内容,并为大家提供相关技术的参考。

一、所涉及到的技术

  • SpringMVC(经典流行的JavaWebMVC框架)
  • Spring Data (Spring项目的一个子项目,用于简化数据层开发和为不同数据源提供一致接口)
  • Spring(流行的IOC容器与AOP框架)
  • Redis(流行的NoSQl分布式内存数据库)
  • Nginx (流行的Web服务器,常用于反向代理和负载均衡)
  • Tomcat(流行的应用服务器,JSP/Servlet容器)
  • Linux(最常用的服务器操作系统)
  • Log4J(流行的Java日志组件)

二、开发目标

选用Restful就断的一个重要目的是实现前后端分离。前后端分离是最大好处是前后端可以实现较好的人员分工和并行开发。双方只需要约定数据接口,而不必在代码层面有任何的耦合。传统的JSP或者后端模板方式都存在着较为严重的前后端耦合,前后端人员不仅需要掌握自身所需知识,还需要对对方的领域有一定了解;开发时进度会因为双方的大量沟通而被拖慢,且项目存在缺陷时难以分清是前后端哪一方的责任,调试困难。目前较为流行的解决方案是后端提供纯数据接口,前端使用MVC/MVVM等框架实现数据绑定、界面路由等功能,且未来的发展趋势是后端负责的任务将逐步减少,进入“大前端”时代。

为之后的一些需要团队合作完成项目开发的课程做准备,我在这个假期先是了解了大量关于前后端分离的知识,逐步确定了前后端分别需要掌握的技能树,然后进行了一段时间的集中学习。现将学习成果总结为这个Demo。

三、项目介绍

一个简单的雇员信息的增改删查:
- 分页查看雇员信息
- 添加雇员
- 修改雇员信息
- 删除雇员

恩,高射炮打蚊子系列项目。

四、准备工作

1、云服务器

为了更贴近真实使用场景,购买了腾讯云服务器,由于是学生特惠,所以价格很低(差不多白送…)
如何申请腾讯云服务器请参照:

https://www.qcloud.com/act/camp

注意,第一次申请之后会得到一张云服务器代金券和一个域名代金券,可以直接去买最便宜的一台云服务器和申请一个免费的域名,以后每个月都要去申请一次才能领到当月的云服务器代金券。

2、开发环境:

后端:MyEclipse 2016
MyEclipse并非免费….使用的是破解版(穷=。=)
优点是较Eclipse添加了很多对JavaEE开发的支持,缺点是…比较耗资源,性能稍弱的笔记本有时会假死。

3、其他工具:

-配置工具:maven
-版本控制工具:git

五、环境搭建

1、CentOS安装所需软件

1) 安装FTP服务

需要配置FTP服务器与客户端,vsftp服务器安装如下:
yum install vsftpd
然后编辑vsftp的配置文件
vi /etc/vsftpd/vsftpd.conf
进入到这个文件中,找到如下这几行:
anonymous_enable=YES,将其中的YES换成NO。
这个NO就表示禁止用户匿名登陆,也就是需要账号密码。
local_enbale=YES 确认这一项是YES,意思是允许本地账户进行ftp用户登陆。
然后添加下列几行:

    userlist_enable=YES          
    userlist_deny=YES         
    userlist_file=/etc/vsftpd/user_list 

之后修改user_list文件:
该文件里的用户账户在默认情况下也不能访问FTP服务器,仅当vsftpd .conf配置文件里启用userlist_enable=NO选项时才允许访问。
将root用户那一行开头加一个#,表示禁用
保存,启动ftp服务器,
service vsftpd start
关闭ftp服务器使用命令:
service vsftpd stop

然后可以按照ftp客户端,我使用的是FlashFxp,其他同类软件亦可。
ip是云服务器的公网ip,在腾讯云的管理界面可以查询到
密码是云服务器的密码。
配置好FTP服务器之后,我们就可以从客户端传输文件到云服务器啦,之后所有的文件传输都基于FTP服务器。

2) 安装JDK

在oracle官网下载最新版本的Linux版本JDK,然后将其传输到云服务器的某个目录下,位置任意,我放到了/usr/java目录下。
使用tar -zxvf 命令将其解压,就算安装成功了。
接下来必要的一步是配置JDK的环境变量(Classpath),这一步非常重要,如果没有配置好那么Tomcat是无法启动的,并且很难弄清楚无法启动的原因….我在这上面耗了2天,就是JDK的环境变量没有配置好造成的。
使用vi命令编辑/etc/profile文件,注意当修改Linux系统的配置文件时最好先copy一份,以避免修改错误导致系统崩溃(血的教训…)
vi /etc/profile
添加如下内容:

JAVA_HOME=/usr/java/jdk1.8.0_111
JRE_HOME=/usr/java/jdk1.8.0_111/jre
PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin
CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib
export JAVA_HOME JRE_HOME PATH CLASSPATH

注意jdk1.8.0xxx要根据实际的JDK版本,不要照抄。
检查环境变量是否配置好的目录是:
echo $JAVA_HOME
如果配置好了,那么会在控制台打印出之前配置的环境变量。

3)安装Tomcat

在Apache Tomcat 官网上下载任意版本(最好是7以上)的Tomcat,这里使用的是8.5版本的,然后将其传输到云服务器上。
同样使用tar -zxvf 命令将其解压。
我这里把tomcat直接放到了根目录/下

启动Tomcat:
进入到Tomcat的bin目录下,使用./startup.sh 命令即可启动;使用./shutdown.sh 命令关闭。
启动后,可以在浏览器中输入http://云服务器ip地址:8080 访问,如果出现Tomcat的欢迎页面,说明启动成功。

无法访问:
1)查看Tomcat是否启动(ps -ef命令或ps -aux命令)
2)检查java环境变量 echo $JAVA_HOME
3)检查服务器的连接情况
在本地 ping 服务器的ip/域名
4)检查端口
Tomcat的conf中的server.xml的端口
lsof -i:端口,查看相应占用端口的进程
netstat –tunlp(查看所有被占用的端口)
5)检查防火墙
一般不是防火墙的问题

4)安装GCC

因为部分软件下载完是源码,需要编译才能安装,所以推荐安装一下GCC。
安装方式非常简单,输入
yum install gcc-c++

5)安装Redis

从Redis的官方网站上下载Redis,传输到云服务器上,拷贝到任意目录(/opt目录比较好,类似于WIndows系统的ProgramFiles文件夹)。
同样,将其解压,然后在安装了GCC前提下,在Redis的目录下输入
make MALLOC=libc,MALLOC注意大小写不能错
然后输入make install
之后是修改Redis的配置文件
构建SpringMVCRestful前后端分离项目实例_第1张图片

把原来的no改为yes,作用是允许Redis以后台服务方式允许。
这里我把Redis配置文件拷贝了一份,放到自建的myredis目录下。
安装Redis之后,其启动程序放到了/usr/local/bin 目录下,原因我也不太了解…
可以启动Redis,启动时可以指定Redis配置文件的位置
/usr/local/bin/redis-server /opt/myredis/redis.conf
这是启动服务器,然后是启动客户端进行测试,
/usr/local/bin/redis-cli -p 6379
6379是Redis默认端口,然后可以输入ping命令进行测试,如果一切正常,那么会返回一个pong =。=
还需要设置一下Redis的密码,在redis.conf中添加一行
requirepass yourpassword
设置密码之后,如果使用Linux客户端访问,那么需要先输入auth yourpassword 之后才能使用。

6)安装Nginx

安装Nginx是参考了这篇文章
https://my.oschina.net/VincentJiang/blog/224993
nginx启动程序放到了 usr/sbin/nginx,配置文件在/etc/nginx/目录下(我也不知道为啥…)
有一个配置文件nginx.conf
在配置文件中添加

    server{
        listen 80;
        server_name 你的域名;
        root /var/www/vueexample;
        location / {
            index index.html index.htm;
        }
    }

稍微解释一下,Nginx会监听80端口,在server_name输入申请的域名,web应用会放到root所指的目录下。
使用这条目录启动Nginx:
/usr/sbin/nginx -c /etc/nginx/nginx.conf
它启动时也需要指定其配置文件。

所有软件的关闭都可以使用ps -ef | grep 软件名,然后kill -9 pid(第二列,是软件的进程号)。
注意配置文件修改后都要重启软件。
好了,Linux服务器配置到此为止,是不是有种要自杀的感觉了…

好吧,配环境只是第一步,下面开始实际的编码。

六、编码

首先介绍配置文件,然后列出一些代码片段作为示例。

1、配置文件

首先需要配置maven,下面直接贴出pom文件的dependencies:


    
        dom4j
        dom4j
        1.6.1
    
    
        org.javassist
        javassist
        3.20.0-GA
    
    
    
        org.aspectj
        aspectjrt
        1.8.9
    
    
        org.aspectj
        aspectjweaver
        1.8.9
    
    
    
        org.springframework
        spring-aop
        4.3.3.RELEASE
    
    
        org.springframework
        spring-context
        4.3.3.RELEASE
    
    
        org.springframework
        spring-test
        4.3.3.RELEASE
    
    
        org.springframework
        spring-context-support
        4.3.3.RELEASE
    
    
        org.springframework
        spring-expression
        4.3.3.RELEASE
    
    
        org.springframework
        spring-web
        4.3.3.RELEASE
    
    
        org.springframework
        spring-webmvc
        4.3.3.RELEASE
    
    
        org.springframework
        spring-core
        4.3.3.RELEASE
    
    
        org.springframework
        spring-beans
        4.3.3.RELEASE
    
    
        org.springframework
        spring-tx
        4.3.3.RELEASE
    
    
    
        org.slf4j
        slf4j-api
        1.7.21
    
    
        commons-logging
        commons-logging
        1.2
    
    
        commons-collections
        commons-collections
        3.2.2
    
    
        commons-lang
        commons-lang
        2.6
    
    
        commons-beanutils
        commons-beanutils
        1.9.3
    
    
        commons-io
        commons-io
        2.5
    
    
        junit
        junit
        4.12
    
    
    
        javax
        javaee-api
        7.0
    
    
        javax.servlet
        jstl
        1.2
    
    
        org.springframework.data
        spring-data-commons
        1.13.0.RELEASE
    
    
        org.springframework.data
        spring-data-jpa
        1.11.0.RELEASE
    
    
        log4j
        log4j
        1.2.17
    
    
        javax.mail
        mail
        1.4.7
    
    
        org.hibernate
        hibernate-validator
        5.4.0.Final
    
    
    com.fasterxml.jackson.core
        jackson-databind
        2.8.6
    
    
    com.fasterxml.jackson.core
        jackson-core
        2.8.6
    

好吧,这应该是只多不少…可能还有一些用不到的。

同时MyEclipse自动帮我生成了这一段:


        
            
                maven-compiler-plugin
                2.3.2
                
                    1.8
                    1.8
                
            
            
                maven-war-plugin
                2.6
                
                    false
                
            
        
    

作用是可以指定JDK版本为1.8,同时可以将项目打成war包进行发布。

然后是web.xml:



  
  
    contextConfigLocation
    classpath:/applicationContext.xml
  
  
    org.springframework.web.context.ContextLoaderListener
  
  
  
    springDispatcherServlet
    org.springframework.web.servlet.DispatcherServlet
    
      contextConfigLocation
      classpath:/springmvc.xml
    
  
  
    springDispatcherServlet
    /
  

  
    CharacterEncodingFilter
    org.springframework.web.filter.CharacterEncodingFilter
    
      encoding
      UTF-8
    
  
  
    CharacterEncodingFilter
    /*
  
  
    HiddenHttpMethodFilter
    org.springframework.web.filter.HiddenHttpMethodFilter
  
  
    HiddenHttpMethodFilter
    /*
  

有Spring与Web整合配置,SpringMVC整合配置,编码过滤器,以及为支持Restful风格的请求方式过滤器,这些配置项都是必需的。

在src/main/resources目录下放入以下配置文件:
-applicationContext.xml
-springmvc.xml
-log4j.properties
-i18n_zh_CN.properties
-i18n_en_US.properties

下面逐个贴代码。
applicationContext.xml:



    
    
        
        
    

    
    
        
        
        
        
        
        
        
        
        
        
    

    
    
        
        
        
        
         
        
        
        
    

    
    
        
    

    
    
    
    
    
    

其中的Redis的配置项不再解释,详情请深入学习Redis。

然后是springmvc的配置文件:
springmvc.xml



    
        
        
    


    
    
    


注意spring和springmvc的配置文件中对于扫描包的范围是没有交集的,springmvc只负责扫描注解了controller和controlleradvice和类,spring扫描其他类。这样可以避免某些bean被创建两次,节省资源。

然后是log4j的配置文件:
log4j.properties

log4j.rootLogger=INFO,me.newsong.utils.LoggingAspect
            log4j.appender.me.newsong.utils.LoggingAspect=org.apache.log4j.DailyRollingFileAppender
            log4j.appender.me.newsong.utils.LoggingAspect.File=${catalina.home}/logs/SpringDataRedis.log
            log4j.appender.me.newsong.utils.LoggingAspect.DatePattern='.'yyyy-MM-dd
            log4j.appender.me.newsong.utils.LoggingAspect.layout=org.apache.log4j.PatternLayout
            log4j.appender.me.newsong.utils.LoggingAspect.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] %m [%t]%n

该Logger对应的是一个LoggingAspect类,在稍后会进行介绍,
log4j配置项具体含义请查阅log4j的文档。
使用Spring的AOP与Log4j结合可以将服务器的运行信息与异常信息统一到一个类中进行处理,避免了大量业务无关代码的冗余。

下面是两个国际化资源文件,所有国际化资源文件的命名都以basename(i18n)开头,后面跟着语言和国家。
以i18n_zh_CN.properties为例:
构建SpringMVCRestful前后端分离项目实例_第2张图片

下面是包目录结构,是一个标准的分层结构:

构建SpringMVCRestful前后端分离项目实例_第3张图片

2、实体类介绍

首先需要先设计实体类,对实体类要求如下:
1)符合JavaBean规范
2)在类上添加注解@RedisHash,其value值为该类的唯一标识,类似于关系数据库的表名
3)在类的唯一标识属性(比如id)上或其get方法上添加注解@Id,注意是springdata包下的id而不是javax.persistence包下的id。
4)在希望通过此属性进行查询的属性上添加注解@Indexed

这里使用了Employee类和Department类。
以Employee类为例:

@RedisHash("employees")
public class Employee implements Serializable {
    /**
     * 
     */
    private static final long serialVersionUID = 6835815975637187630L;

    private Integer id;
    @NotEmpty
    @Indexed
    private String lastName;
    @Email
    private String email;
    private String gender;
    private Department dept;
    private Date birthday;
    private double salary;

    public Employee() {
    }

    public Employee(Integer id,String lastName, String email, String gender, Department dept, Date birthday, double salary) {
        this.id = id;
        this.lastName = lastName;
        this.email = email;
        this.gender = gender;
        this.dept = dept;
        this.birthday = birthday;
        this.salary = salary;
    }

    @Id
    public Integer getId() {
        return id;
    }

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

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public Department getDept() {
        return dept;
    }

    public void setDept(Department dept) {
        this.dept = dept;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Employee [id=" + id + ", lastName=" + lastName + ", email=" + email + ", gender=" + gender + ", dept="
                + dept + ", birthday=" + birthday + ", salary=" + salary + "]";
    }

}

注意其中的@Empty和@Email等是SpringMVC的validator组件所需要的注解,作用是检查实体类的属性是否满足这些前置条件。

3、数据层介绍

是数据层,使用了SpringData与Redis整合的神奇效果是仅需要声明数据层的接口,其接口的实现完全由Spring负责创建代理对象负责。
为了使用Redis,需要一个Redis的配置类RedisConfig(类名自定义即可)

@Configuration
@EnableRedisRepositories("me.newsong")
public class RedisConfig {
}

其中的EnableRedisRepositories注解的value值为Redis使用的类的扫描包,直接设置为项目根目录即可。

所有数据层的接口都需要继承自CrudRepository接口,该接口提供了一些默认的增改删查的方法,比如findOne,findAll,save等方法。
另外,在SpringData的帮助下,可以根据一些符合SpringData规范的方法名自动生成相应的查询方法,比如List findByLastName(String lastName) 可以根据lastName属性拿到Employee对象的集合,而这个接口是不需要自己去写实现类的。

以EmployeeRepository接口为例:

public interface EmployeeRepository extends CrudRepository{
    Page findAll(Pageable pageable);
    List findByLastName(String lastName);
}

findAll方法中传入一个Pageable对象,表示可以根据该对象实现分页查询。

4、逻辑层介绍

逻辑层是较薄的一层,主要是调用数据层的方法,以EmployeeServiceImpl为例:

@Transactional
@Service
public class EmployeeServiceImpl implements EmployeeService {
    @Autowired
    private EmployeeRepository dao;

    @Override
    public Employee findByID(Integer id) {
        return dao.findOne(id);
    }

    @Override
    public void update(Employee employee) {
        dao.save(employee);
    }

    @Override
    public void delete(Integer id) {
        dao.delete(id);
    }

    @Override
    public void save(Employee employee) {
        dao.save(employee);
    }

    @Override
    public Page findAll(int pageNum, int pageSize) {
        return dao.findAll(new PageRequest(pageNum, pageSize));
    }

    @Override
    public boolean isLastNameValid(String lastName) {
        if(dao.findByLastName(lastName).size() == 0){
            return true;
        }
        return false;
    }

}

在类上添加@Transactional注解表示为该类的每个方法都添加@Transaction注解,使用了Spring的声明式事务。

5、控制器介绍

这个Demo中最重要的是Controller,以EmployeeController为例:

@CrossOrigin
@RestController
public class EmployeeRestController {
    @Autowired
    private EmployeeService employeeService;

    // 显示所有员工信息
    @RequestMapping(value = "/emps", method = RequestMethod.GET)
    public Page findEmployeesByPage(
            @RequestParam(value = "pageNum", required = false, defaultValue = "0") String pageNum) {
        int page = 0;
        try {
            page = Integer.parseInt(pageNum);
        } catch (NumberFormatException e) {
        }
        if (page < 0) {
            page = 0;
        }
        return employeeService.findAll(page, 5);
    }

    // 添加
    @RequestMapping(value = "/emp", method = RequestMethod.POST)
    public void add(@RequestBody @Valid Employee employee, BindingResult result, Locale locale) {
        System.out.println(employee);
        if (!validateLastName(employee.getLastName())) {
            throw new UsernameExistedException(employee.getLastName(), locale);
        } else if (result.hasErrors()) {
            throw new ValidationException(result.getFieldErrors());
        }
        System.out.println("add:" + employee);
        employeeService.save(employee);
    }

    // 删除
    @RequestMapping(value = "/emp/{id}", method = RequestMethod.DELETE)
    public void delete(@PathVariable("id") Integer id, Locale locale) {
        if (employeeService.findByID(id) == null) {
            throw new EmployeeNotFoundException(locale);
        }
        System.out.println("delete:" + id);
        employeeService.delete(id);
    }

    // 更新
    @RequestMapping(value = "/emp", method = RequestMethod.PUT)
    public void update(@RequestBody @Valid Employee employee, BindingResult result, Locale locale) {
        if (result.hasErrors()) {
            throw new ValidationException(result.getFieldErrors());
        }
        System.out.println("update:" + employee);
        employeeService.update(employee);
    }

    // 验证用户名是否合法
    @RequestMapping(value = "/emp/{lastName}", method = RequestMethod.GET)
    public boolean validateLastName(@PathVariable("lastName") String lastName) {
        if (employeeService.isLastNameValid(lastName)) {
            System.out.println("用户名可用");
            return true;
        } else {
            System.out.println("用户名不可用");
            return false;
        }
    }
}

@CrossOrigin是为了处理Ajax跨域问题而添加的注解,其value值是哪些域来的请求是可以被接收的,如果不指定value值,那么就是所有域来的请求都接收。
@RestController是SpringMVC为支持Restful风格提供的注解,当为类添加这个注解后,不仅表示该类是控制器,并且为该类的每个方法添加@ResponseBody,配合Jackson(Json解析器)可以将返回值转为Json格式返回到前端页面。

每个方法都有一个URL(由@RequestMapping的value指定)和一个method(由@RequestMapping的method指定),它们共同确定一个唯一的访问路径,不可重复。
按照Restful风格规定,增改删查分别对应着POST,PUT,DELETE,GET方法,由SpringMVC提供的Method解析器将POST请求转为PUT和DELETE请求。

6、工具类介绍

LoggingAspect:该类使用了Spring的AOP技术,负责整个项目的日志管理。

@Aspect
@Component
public class LoggingAspect {
    private static final Logger logger = Logger.getLogger(LoggingAspect.class.getName());

    @Pointcut("execution(* me.newsong.service.impl.*.*(..))||execution(* me.newsong.web.*.*(..))")
    public void declareJoinPointExpression() {
    }

    @Before("declareJoinPointExpression()")
    public void beforeMethod(JoinPoint joinPoint) {// 连接点
        Object[] args = joinPoint.getArgs();// 取得方法参数
        logger.info("The method [" + joinPoint.getSignature() + " ] begins with Parameters: " + Arrays.toString(args));
    }

    @AfterReturning(value = "declareJoinPointExpression()", returning = "result")
    public void afterMethodReturn(JoinPoint joinPoint, Object result) {
        logger.info("The method [" + joinPoint.getSignature() + "] ends with Result: " + result);
    }

    @AfterThrowing(value = "declareJoinPointExpression()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        logger.error("Error happened in method: [" + joinPoint.getSignature()+"]");
        logger.error("Parameters: "+Arrays.toString(joinPoint.getArgs()));
        logger.error("Exception StackTrace: ",e);
    }
}

InternationalizeUtil:该类涉及了Spring的静态注入,作用是处理国际化问题。

@Component
public class InternationalizeUtil {
    @Autowired
    private ResourceBundleMessageSource ms;
    private static InternationalizeUtil util;

    @PostConstruct
    public void init() {
        util = this;
        util.ms = this.ms;
    }

    public static String getMessage(String message,Locale locale){
        return util.ms.getMessage(message, null, locale);
    }
}

七、异常处理

关于Restful的异常处理是参考了大神们的做法之后自己思考出来的,并且配合了SpringMVC自身的异常处理,实现了一定程度的复用。
controller中检查前置条件和后置结果之后,如果发现存在问题,则抛出一个自定义的异常。其中检查前置条件有一部分是自己进行检验, 有一部分是由SpringMVC的JSR303风格的数据校验完成的,尤其是POJO类型的对象的格式校验。
抛出异常之后,由注解了ControllerAdvice的异常处理类ExceptionHandler进行捕捉,并将异常所对应的异常对象以Json格式传输到前端,并附带了Http错误状态码。
整个异常体系由以下几个关键类组成:
RestExceptionHandler:

@ControllerAdvice
public class RestExceptionHandler {

    @ExceptionHandler(BaseRestException.class)
    public ResponseEntity handle(BaseRestException e) {
        return new ResponseEntity(new RestError(e.getStatus(), e.getCode(), e.getErrors(), ""), e.getStatus());
    }
}

其中的RestError、BaseRestException在后面介绍,这里主要是将异常对象内部的异常信息取出放到异常对象,然后将其连带着Http异常状态码返回到前端。
RestError:异常对象,是直接面向前端的

public class RestError {
    private HttpStatus status;
    private int code;
    private List fieldErrors;
    private String moreinfoURL;

    public RestError() {
    }

    public RestError(HttpStatus status, int code, List fieldErrors, String moreinfoURL
            ) {
        this.status = status;
        this.code = code;
        this.fieldErrors = fieldErrors;
        this.moreinfoURL = moreinfoURL;
    }


    public HttpStatus getStatus() {
        return status;
    }

    public void setStatus(HttpStatus status) {
        this.status = status;
    }

    public int getCode() {
        return code;
    }

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

    public List getFieldErrors() {
        return fieldErrors;
    }

    public void setFieldErrors(List fieldErrors) {
        this.fieldErrors = fieldErrors;
    }

    public String getMoreinfoURL() {
        return moreinfoURL;
    }

    public void setMoreinfoURL(String moreinfoURL) {
        this.moreinfoURL = moreinfoURL;
    }

    @Override
    public String toString() {
        return "RestError [status=" + status + ", code=" + code + ", fieldErrors=" + fieldErrors + ", moreinfoURL="
                + moreinfoURL + "]";
    }
}

其中封装了Http异常状态码,code是指自己内部定义的异常状态码,5位数字,前三位是Http异常状态吗,后两位是自己定义的;并且持有了一组RestFieldError,每个对象封装了一个属性的错误信息。
RestFieldError:属性异常对象,封装了一个属性的错误信息

public class RestFieldError {
    private String field;
    private Object rejectedValue;
    private String message;

    public RestFieldError(FieldError error) {
        this.field = error.getField();
        this.rejectedValue = error.getRejectedValue();
        this.message = error.getDefaultMessage();
    }

    public RestFieldError(String field, Object rejectedValue, String message) {
        super();
        this.field = field;
        this.rejectedValue = rejectedValue;
        this.message = message;
    }

    public String getField() {
        return field;
    }

    public void setField(String field) {
        this.field = field;
    }

    public Object getRejectedValue() {
        return rejectedValue;
    }

    public void setRejectedValue(Object rejectedValue) {
        this.rejectedValue = rejectedValue;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @Override
    public String toString() {
        return "RestFieldError [field=" + field + ", rejectedValue=" + rejectedValue + ", message=" + message + "]";
    }
}

field是属性名,rejectedValue是错误属性值,message是经过国际化处理的提示信息,是直接显示给用户看的。


异常对象
BaseRestException:这个类继承自RuntimeException,是所有自定义异常的基类,所以自定义异常均需要继承自该类,并且提供Http异常状态码,内部异常状态码和一组错误的RestFieldError。

public class BaseRestException extends RuntimeException {
    /**
     * 
     */
    private static final long serialVersionUID = 1330458449080010936L;
    private HttpStatus status;
    private int code;
    private List error;

    public BaseRestException() {
    }

    public BaseRestException(HttpStatus status, int code, Locale locale, String field, Object rejectedValue) {
        this.status = status;
        this.code = code;
        this.error = Arrays.asList(new RestFieldError(field, rejectedValue,
                InternationalizeUtil.getMessage("i18n." + this.getMessageKey(), locale)));
    }

    public BaseRestException(HttpStatus status, int code, List error) {
        this.status = status;
        this.code = code;
        this.error = error;
    }

    public static List toRestFieldErrorList(List errors) {
        List fieldErrors = new ArrayList<>(errors.size());
        for (FieldError error : errors) {
            fieldErrors.add(new RestFieldError(error));
        }
        return fieldErrors;
    }

    public List getErrors() {
        return error;
    }

    public void setErrors(List error) {
        this.error = error;
    }

    public HttpStatus getStatus() {
        return status;
    }

    public int getCode() {
        return code;
    }

    private String getMessageKey() {
        String simpleName = this.getClass().getSimpleName();
        return simpleName.substring(0, simpleName.lastIndexOf("Exception"));
    }
}

下面列举两个自定义异常作为示例,一个是由自己判断的异常,一个是由SpringMVC帮我们检查的异常。
这个是抛出异常的地方,可以看出UsernameExistedException是由我们自己检查的,ValidationException是由SpringMVC的Validator帮我们检查的。

    // 添加
    @RequestMapping(value = "/emp", method = RequestMethod.POST)
    public void add(@RequestBody @Valid Employee employee, BindingResult result, Locale locale) {
        System.out.println(employee);
        if (!validateLastName(employee.getLastName())) {
            throw new UsernameExistedException(employee.getLastName(), locale);
        } else if (result.hasErrors()) {
            throw new ValidationException(result.getFieldErrors());
        }
        System.out.println("add:" + employee);
        employeeService.save(employee);
    }

UsernameExistedException:

public class UsernameExistedException extends BaseRestException {
    private static final HttpStatus STATUS = HttpStatus.CONFLICT;
    private static final int CODE = 42201;

    /**
     * 
     */
    private static final long serialVersionUID = 6915629262486503046L;

    public UsernameExistedException(String lastName, Locale locale) {
        super(STATUS, CODE, locale, "lastName", lastName);
    }

}

所有自定义异常均需要自己指定Http异常状态码和内部异常状态码。

ValidationException:

public class ValidationException extends BaseRestException {
    private static final HttpStatus STATUS = HttpStatus.BAD_REQUEST;
    private static final int CODE = 40001;

    /**
     * 
     */
    private static final long serialVersionUID = 5495053837578511264L;

    public ValidationException(List errors) {
        super(STATUS, CODE,  toRestFieldErrorList(errors));
    }
}

经过一系列的转换,所有类型的异常最终都会转为一个RestError对象传递给前端。
关于国际化:
错误信息是需要进行国际化的,如果是SpringMVC的Validator帮助校验的,其错误信息是已经被国际化了的。而我们自己检查的错误信息是需要自己进行国际化的。
按照BaseRestException的逻辑,所以自定义的异常类的名字均需要以Exception结尾,异常类名去掉后面的Exception,前面加上i18n.,得到国际化的键,其值需要自己指定,示例如下:

i18n.UsernameExisted=用户名已存在

UsernameExistedException对应的异常信息的键为i18n.UsernameExisted,其值的中文版本就是RestFieldError中的message对象。
假如后端抛出异常,那么就会传递给前端RestError异常对象和相应的Http异常状态码,常用的异常状态码参考这篇文章:

http://www.ruanyifeng.com/blog/2011/09/restful.html

八、项目部署

这个Demo完全是后端部分,前端部分可以选用任意一个MVC/MVVM前端框架来构建,比如React、Vue、Angular等框架。
由于是前后端分离的模式,前端模块部署到Nginx服务器,将项目通过的方式打包后放到Nginx配置文件的server结点中指定的root目录下;后端模块使用maven的打包功能之后放到Tomcat的webapps目录下。
当项目运行时,客户端通过Nginx服务器访问前端应用,前端应用通过Tomcat服务器获取后端的数据,全程使用Json数据格式传输。前后端分别构建,分别部署。
当然还有一部分任务是申请域名和将域名与云服务器绑定,这个域名在配置Nginx服务器时也会用到。这个十分简单,百度相关资料即可。

九、项目演示

由于我只搭建了后端部分,而我目前对前端技术不甚了解,所以在这里使用一个Rest请求测试工具postman来进行测试。这
这个工具可以在chrome的插件管理中找到,安装之后需要选择安装其app版本。
界面如下:
构建SpringMVCRestful前后端分离项目实例_第4张图片

左侧可以选择请求方法,输入URL,并且可以选择是否输入请求参数,点击send之后可以在下方看到响应头和响应体。
以添加员工为例:
构建SpringMVCRestful前后端分离项目实例_第5张图片

在Body中设置:
这里写图片描述

查看返回信息:
这里写图片描述

十、文档管理

API文档最好是使用一个结构化的方式进行管理,查询一些信息之后,发现比较好的Rest文档管理平台是swagger,不过其编写文档并不十分方便,学习成本较高。还找到国内的一个中文平台,相比之后虽然功能稍弱,但是使用起来非常容易。

http://www.xiaoyaoji.com.cn

编写好之后的文档示例:
构建SpringMVCRestful前后端分离项目实例_第6张图片
以比较清晰的结构展示给前端开发人员,降低沟通成本。

十一、项目总结

源码分享

这个Demo虽然业务十分简单,但整合了我在整个假期所学的几乎全部知识,涵盖技术面较广,使用的也是业界比较成熟和流行的技术,欢迎大家参考和指正,后端项目放到了Github上,地址:
https://github.com/songxinjianqwe/SpringDataRedis.git

关于技术

Spring Data

我最欣赏的是SpringData这个项目,为开发人员提高数据层的开发效率有很大的帮助,而且基于注解的形式也方便掌握和使用。

Redis

Redis作为一个NoSQL数据库,也值得长期接受RDBMS的我们去了解和掌握。在这个项目中是作为一个全部的数据提供者,实际项目中往往并非如此,而是作为RDBMS的补充,比如存储某些热点数据等,提高一部分数据的访问效率。当然由于Redis自身也提供了持久化解决方案,并非仅可以作为缓存,在一些小型项目中也堪一用。

Spring

Spring的IOC和AOP非常好用,我想不出任何理由不去使用Spring来实现接口隔离原则。

Spring MVC

SpringMVC在传统的Web项目开发中更占有优势,但在Restful领域我更倾向于实现了JAX-RS的一些框架,比如Jersery。可能下一个项目我会尝试使用更Restful的框架来作为Web后端框架。

题外话

最后想说几句题(du)外(ji)话(tang)吧,恩…,真的觉得大学能做好一件事就很成功了。牛人当然有很多,但这并不是自暴自弃的借口;可能身边有一些人开始就很厉害,那是人家比你早很多年努力的成果;往者不可谏,来者犹可追,虽然高中读文,也不太擅长数学物理,但这都是过去,并不是现在对自己要求低的借口。
文转工,换专业,有学数学学到吐血的时候,也有调试了好久的代码终于成功运行的时候,还是这句话吧,只要肯坚持,Keep practicing,仍然可以做好一件事的,哪怕不是做到最好,也要做到足够好,好到让自己感觉,哪怕其他的事做的不好,但又怎么样呢?
希望各位初心不负,砥砺前行。

预告

我的下一篇博客可能会在2017年的暑期发布(如果这个学期没有被搞死的话…),可能会涉及到我比较感兴趣的一些技术,比如Restful框架Jersery,消息中间件如ActiveMQ,安全框架如Apache Shiro,搜索引擎框架Lucene,快速开发框架Spring Boot,Mysql与Redis多数据源整合,mybatis数据层框架,甚至大数据相关框架如Spark等,谢谢大家。

你可能感兴趣的:(后端)