Project(2)
1、分析项目
当需要开发某个项目时,首先,应该分析这个项目中,需要处理哪些种类的数据!例如:用户、商品、商品类别、收藏、订单、购物车、收货地址…
然后,将以上这些种类的数据的处理排个顺序,即先处理哪种数据,后处理哪种数据!通常,应该先处理基础数据,再处理所相关的数据,例如需要先处理商品数据,才可以处理订单数据,如果多种数据之间没有明显的关联,则应该先处理简单的,再处理较难的!
则以上这些数据的处理顺序应该是:用户 > 收货地址 > 商品类别 > 商品 > 收藏 > 购物车 > 订单
当确定了数据处理顺序后,就应该分析某个用户对应的功能有哪些,以“用户”数据为例,相关功能有:注册、登录、修改密码、修改资料、上传头像…
然后,还是需要确定以上功能的开发顺序,通常,遵循“增 > 查 > 删 > 改”的顺序,则以上功能的开发顺序应该是:注册 > 登录 > 修改密码 > 修改资料 > 上传头像。
每个功能的开发都应该遵循 创建数据表 > 创建实体类 > 持久层 > 业务层 > 控制器层 > 前端页面
一次只解决一个问题
大问题拆成小问题
2、用户 - 注册 - 创建数据表
3、用户 - 注册 - 创建实体类
4、用户 - 注册 - 持久层
4.1.规划SQL语句
4.2.接口与抽象方法
4.3.配置映射
5、用户 - 注册 - 业务层
5.1.业务层的基本定位
在MVC设计理念中,M表示的是Model,即数据模型,它由持久层和业务层共同构成!持久层负责完成数据操作,即增删改查,业务层负责组织业务流程和管理业务逻辑,业务更多在表现为用户能操作的某1个“功能”,但是,对于程序员来说,可能是更多的细小的功能来组成的!之所以需要业务层,是因为需要通过业务层来保证数据的安全,(数据必须经过业务层的各种流程和逻辑才产生或发生变化!)
5.2.规划异常
考虑当前的“注册”功能可能会抛出哪些异常!
在“注册”时,需要先检查用户名是否被占用,如果已经被占用,则不允许注册,就是一种“操作失败”,则应该有“用户名占用异常”被抛出!
如果用户名没有被占用,则允许注册,将执行数据表的INSERT
操作,凡是数据表的增、删、改操作,都是有可能出现操作失败的(磁盘已满、MySQL服务器崩了等等)!则应该抛出“插入数据异常”。
在业务层,一旦视为“操作失败”,对异常的处理都应该是“抛出”,而不是自行处理!因为业务层不适合进行处理!因为业务层不会直接和用户交互,直接和用户交互的是控制器,所以应该异常抛出,抛给调用业务层的控制器,由控制器处理或继续抛出。
此次,需要创建2个异常类:
cn.tedu.store.service.ex.UsernameDuplicateException
和
cn.tedu.store.service.ex.InsertException
另外还需创建这两个异常的公共父级异常类cn.tedu.store.service.ex.ServiceException
他们都应该是RuntimeException
的子孙类(所有的自定义异常类都应该是RuntimeException
的子孙类):
RuntimeException
ServiceException
UsernameDuplicateException
InsertException
......
package cn.tedu.store.service.ex;
/**
* 业务异常,是业务层抛出的异常的基类
*/
public class ServiceException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ServiceException() {
super();
}
public ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
public ServiceException(String message) {
super(message);
}
public ServiceException(Throwable cause) {
super(cause);
}
}
/**
* 用户名冲突异常,如:用户名已经被占用
*/
public class UsernameDuplicateException extends ServiceException {
//实现序列化接口版本号
//实现父类的五个构造方法,同上
}
/**
* 插入数据异常
*/
public class InsertException extends ServiceException {
//实现序列化接口版本号
//实现父类的五个构造方法,同上
}
5.3.接口与抽象方法
在设计业务层时,应该先有业务层的接口,并在接口中定义抽象方法,后续,外界(当前Model以外,例如controller或其它service等)调用时,是基于接口来声明对象,并调用方法的,所以,此处使用接口是一种解耦的做法!
先创建cn.tedu.store.service.IUserService
业务层接口,并添加抽象方法:
//一般RuntimeException及其子孙类异常不必在方法中声明出来,而其他的IO类异常必须声明。
//此处为了更直观地了解到reg方法可能抛出的异常,所以声明了出来。
void reg(User user) throws UsernameDuplicateException, InsertException;
/**
* 处理用户数据的业务层接口
*/
public interface IUserService {
/**
* 用户注册
* @param user 用户数据
* @throws UsernameDuplicateException 用户名冲突异常
* @throws InsertException 插入数据异常
*/
void reg(User user) throws UsernameDuplicateException, InsertException;
}
关于业务层的抽象方法的设计原则:
- 返回值:仅以操作成功为前提来设计返回值(因为通常失败会以异常的方式返回给用户,不必考虑失败的返回值);
- 方法名:尽量体现业务例如使用
reg
或regist
或register
表示注册,使用login
表示登录; - 参数列表:一定是客户端可以提供的数据,或来自于Session中的数据,且足够调用持久层的各方法;
- 异常:把用户操作失败的可能,都设计成各种异常,并把这些异常都添加到方法的声明中。
5.4.实现类与重写方法
创建cn.tedu.store.service.impl.UserServiceImpl
业务层实现类,
实现以上IUserService
接口,
添加@Service
注解,
并在类中声明@Autowired private UserMapper userMapper
持久层对象:
@Service
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
}
然后,重写接口中的抽象方法:
@Override
public void reg(User user) throws UsernameDuplicateException, InsertException {
// 根据参数user对象中的username属性查询数据
// userMapper.findByUsername
// 判断查询结果是否不为null(用户名已存在)
// 是--用户名已被占用(抛出UsernameDuplicateException)
// TODO 得到盐值
// TODO 基于参数user对象中的password进行加密,得到加密后的密码
// TODO 将加密后的密码和盐值封装到user中
// 将user中的isDelete设置为0
// 封装user中的4个日志属性
// 执行注册:userMapper.insert(user)
}
代码实现:
@Service
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
@Override
public void reg(User user) throws UsernameDuplicateException, InsertException {
// 根据参数user对象中的username属性查询数据
// userMapper.findByUsername
String username = user.getUsername();
User result = userMapper.findByUsername(username);
// 判断查询结果是否不为null(用户名已存在)
if (result != null) {
// 是--用户名已被占用(抛出UsernameDuplicateException)
throw new UsernameDuplicateException("注册失败!用户名已经被占用!");
}
System.err.println("reg() > password = " + user.getPassword());
// 得到盐值
String salt = UUID.randomUUID().toString();
// 基于参数user对象中的password进行加密,得到加密后的密码
String md5Password = getMd5Password(user.getPassword(), salt);
// 将加密后的密码和盐值封装到user中
user.setPassword(md5Password);
user.setSalt(salt);
System.err.println("reg() > salt = " + salt);
System.err.println("reg() > md5Password = " + md5Password);
// 将user中的isDelete设置为0
user.setIsDelete(0);
// 封装user中的4个日志属性
Date now = new Date();
user.setCreatedUser(username);
user.setCreatedTime(now);
user.setModifiedUser(username);
user.setModifiedTime(now);
// 执行注册:userMapper.insert(user)
Integer rows = userMapper.insert(user);
if (rows != 1) {
throw new InsertException("注册失败!出现未知错误!请联系管理员!");
}
}
/**
* 对密码进行加密
*
* @param password 原始密码
* @param salt 盐值
* @return 加密后的密码
*/
String getMd5Password(String password, String salt) {
// 规则:对 原始密码+盐值 3重加密
String str = password + salt;
for (int i = 0; i < 3; i++) {
str = DigestUtils.md5Hex(str.getBytes());
}
return str;
}
}
注意:特别是在执行插入数据时,业务层除了需要保证业务流程和业务逻辑,还需要保证数据的完整性!
完成后,在src/test/java下创建cn.tedu.store.service.UserServiceTests
测试类,编写并执行单元测试:
@SpringBootTest
public class UserServiceTests {
@Autowired
private IUserService service;
@Test
public void testReg() {
try {
User user = new User();
user.setUsername("Tom3");
user.setPassword("123");
service.reg(user);
System.err.println("OK");
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
}
6、用户 - 注册 - 控制器层
6.1.处理异常
首先,需要创建响应结果的数据类型cn.tedu.store.util.JsonResult
:
package cn.tedu.store.util;
/**
* 向客户端响应操作结果的数据类型
*
* @param <T> 向客户端响应的数据的类型
*/
public class JsonResult<T> {
private Integer state;
private String message;
private T data;
public Integer getState() {
return state;
}
public void setState(Integer state) {
this.state = state;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public JsonResult(Integer state) {
this.state = state;
}
public JsonResult() {
}
}
在编写处理请求的控制器及方法之前,还应该对异常进行统一处理!
在统一处理异常时,相关代码只能作用于当前控制器类,为了使得整个项目都可以使用这个统一处理异常的机制,应该把相应代码添加在控制器类的基类cn.tedu.store.controller.BaseController
中,所以:
/**
* 控制器类的基类,实现统一处理异常
* @author DELL
*
*/
public abstract class BaseController {
/**
* 操作结果的“成功”状态
*/
public static final Integer SUCCESS = 2000;
//只处理ServiceException及其子孙类异常,避免异常过度处理
@ExceptionHandler(ServiceException.class)
public JsonResult<Void> handleException(Throwable e){
JsonResult<Void> jr = new JsonResult<Void>();
jr.setMessage(e.getMessage());
if(e instanceof UsernameDuplicateException) {
jr.setState(4000);
}else if(e instanceof InsertException) {
jr.setState(5000);
}
return jr;
}
}
6.2.设计请求
需要事先规划用户的请求是什么样的,例如用户向哪个URL发出请求表示“注册”,请求方式是哪种,是否需要提交某些请求参数等,以及最终服务器端向客户端响应什么样的数据:
请求路径:/users/reg
请求参数:User user
请求类型:POST
响应数据:JsonResult<Void>
6.3.处理请求
创建cn.tedu.store.controller.UserController
控制器类,添加@RestController
和@RequestMapping("users")
注解,并在类中添加@Autowired private IUserService userService;
业务层对象。
关于@RestController
注解:
SpringBoot框架推荐服务器端不必关心页面的处理,在处理请求时,应该响应正文,在对控制器类添加注解时,可以使用@RestController
,这个注解既能起到@Controller
的作用,还能表示该控制器类所有的方法都是响应正文的!也就是说,使用了这个注解,该类中每个方法都相当于添加了@ResponseBody
注解!如果一定要转发或重定向,则不可以使用@RestController
,只能继续使用@Controller
注解!
并在类中添加处理请求的方法:
/**
* 处理用户数据相关请求的控制器类
* @author DELL
*
*/
@RestController
@RequestMapping("users")
public class UserController extends BaseController {
@Autowired
private IUserService userService;
@RequestMapping("reg")
public JsonResult<Void> reg(User user){
// 执行注册,失败时会抛出异常,异常会在BaseController类中进行统一处理
// 此方法只处理返回成功的情况。
userService.reg(user);
//返回成功
return new JsonResult<Void>(SUCCESS);
}
}
处理请求的方法中添加@RequestMapping("reg")
注解是为了方便在地址栏中进行测试:
打开浏览器,输入http://localhost:8080/users/reg?username=Tom4&password=123
进行测试。
7、用户 - 注册 - 前端页面
<script type="text/javascript">
// 页面加载完成后执行的方法(函数)
$(document).ready(function() {
// 在button控件上添加id:btn-reg
$("#btn-reg").click(function() {
$.ajax({
// url: 请求交到哪里去
// data: 请求提交的参数,以上的form表单中,在需要提交的参数的控件中添加name属性;
// form表单上添加id属性
// type: 请求方式
// dataType: 服务器端响应的结果的类型
// success: 响应成功时的处理函数
"url" : "/users/reg",
"data" : $("#form-reg").serialize(),
"type" : "post",
"dataType" : "json",
"success" : function(json) {
if (json.state == 2000) {
alert("注册成功!");
//跳转到某个页面
} else {
alert(json.message);
}
}
});
});
});
</script>
8、用户 - 登录 - 持久层
9、用户 - 登录 - 业务层
10、用户 - 登录 - 控制器层
11、用户 - 登录 - 前端页面
-------------------------------------------------------------------------------
附1:密码加密
加密算法并不适合对密码进行加密!因为,所以的加密算法都是可以逆运算的,如果使用的算法和加密时使用的参数是已知的,则可以轻松推算出原始数据!而密码安全问题主要源自内部泄密!也就是说,数据库的数据能泄密,可以认为算法和加密参数也是有泄密的可能的!
对于密码安全问题而言,最好的做法就是将密码进行加密,却任何人都无法解密!
则,在密码加密这个问题上,所有的加密算法都是不使用的!只能使用摘要算法!
摘要算法的全称是“消息摘要算法”!这种算法的特征有:
- 消息原文相同时,得到的摘要是相同的;
- 使用的摘要算法没有发生变化时,无论原文长度是多少,摘要的长度是固定的;
- 消息原文不同时,得到的摘要几乎不会相同!
在消息摘要领域中,一定存在N种不同的原文,可以计算得到完全相同的摘要!
使用MD5(Message Digest 5)算法执行摘要运算时,得到的结果都是32位的十六进制数。转换成二进制就需要128位,所以,MD5算法也成为128位算法!
如果有2个不同的原文,可以得到相同的摘要,这种状况称之为“碰撞”,在有限长度的原文中,得到相同的摘要的概率是极低,甚至就是不可能的!
所以,真正用于对密码加密的算法,都是摘要算法,主要原因是因为它不可逆,且在有限长度的原文的基础之上,几乎不可能发生碰撞!
常见的摘要算法有MD5家族和SHA系列,例如:MD2、MD4、MD5、SHA1、SHA128、SHA256、SHA384、SHA512.….其中,MD家族的都是128的,SHA系列名称后有位数的,就是对应的位数。
关于MD5或相关摘要算法的破解研究是存在的,主要是针对算法的碰撞攻击,而并不是尝试执行逆运算来得到原始数据!
另外,还有许多网站号称可以在线破解MD5,只需要将摘要结果填进去,就可以查到原始数据,例如填入e10adc3949ba59abbe56e057f20f883e
就可以查出123456
,事实上,这些网站是记录了大量的原始数据与MD5摘要数据的对应关系,如果原始数据比较简单,或者是常用密码值,被这些网站收录的可能性就非常大!所以,为了提升密码的安全性,保证密码不被这些网站“破解”,可以采取的做法:
- 增强原始密码的安全强度,例如从组成元素、长度方面提出更高要求;
- 反复执行加密,即多重加密;
- 加盐;
- 综合以上所有的做法。
SELECT 原始数据 FROM 表 WHERE 摘要数据=?
要实现MD5算法可以使用spring自带的工具类,也可以添加新的依赖:
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
MD5算法:
@Test
public void messageDigest() {
String password = "000000";
//随机盐值
String salt = UUID.randomUUID().toString();
System.err.println(salt);
password = password + salt;
//md5算法:
String md5 = DigestUtils.md5DigestAsHex(password.getBytes());
System.err.println(md5);
}
"123456"
e10adc3949ba59abbe56e057f20f883e
"1"
c4ca4238a0b923820dcc509a6f75849b
"1111111111111111111111111111111111"
67cc4ac459440ed68504c334a03baa7f