5. 用户-注册-业务层
业务(Service):业务是普通用户眼里某1个功能,从开发的角度来看,它可能由多项数据操作组成,例如“注册”就应该至少由“根据用户名查询用户数据”和“插入用户数据”这2种数据操作来组成!在开发时,业务应该负责业务流程和业务逻辑,以保障数据的完整性和安全性!
开发业务层时,应该先考虑当前要实现的功能有没有失败的可能,以当前的“注册”为例,就可能因为“用户名已经被占用”导致注册失败!则业务层应该抛出对应的异常。
为了便于管理自定义的异常类,应该先创建cn.tedu.store.service.ex.ServiceException
,继承自RuntimeException
。
然后,再创建此次注册时可能涉及的“用户名已被占用”的异常类cn.tedu.store.service.ex.UsernameDuplicateException
,继承自ServiceException
。
另外,在插入数据时仍可能出现插入数据失败,所以,还应该创建cn.tedu.store.service.ex.InsertException
,继承自ServiceException
。
然后,在cn.tedu.store
包下创建子级的service
包,并在这个包中创建业务层接口IUserService
,然后在接口中添加“注册”功能的抽象方法,关于抽象方法的声明原则:
-
返回值仅以操作成功为前提来进行设计;另外,使用抛出异常来表示失败;
-
方法名称可以自定义,应该尽量精准的表达所需要实现的功能;
-
参数列表需要保证能够调用持久层的相关功能,并且,这些参数应该是客户端可以提交的数据;
所以,关于“注册”的抽象方法可以设计为:
void reg(User user) throws UsernameDuplicateException;
由于抛出的异常是RuntimeException
的子孙类异常,所以,在声明抽象方法时,也不必显式的使用throws
声明抛出,即抽象方法也可以写为:
void reg(User user);
接下来,还需要自定义实现类来实现以上接口,并重写抽象方法!所以,在cn.tedu.store.service
包下创建子级impl
包,并在这个包中创建UserServiceImpl
,实现IUserService
接口:
public class UserServiceImpl implements IUserService {
@Override
public void reg(User user) {
}
}
后续,在具体实现“注册”功能时,必然需要调用持久层对象的相关方法,所以,需要在类中声明private UserMapper userMapper;
,该属性的值应该是自动装配的,如果需要使用自动装配,首先,就必须使得Spring框架能管理这个类的对象,才会为其中的属性实现自动装配,所以,在类的声明之前需要添加注解,此次应该使用@Service
注解,并在UserMapper
属性的声明之前添加@Autowired
注解:
@Service
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
@Override
public void reg(User user) {
}
}
然后,应该规划如何重写抽象方法:
@Override
public void reg(User user) {
// 从参数user中获取用户名
// 调用userMapper的findByUsername()方法执行查询
// 判断查询结果是否不为null
// 是:查询结果不为null,表示用户名已经被占用,则抛出UsernameDuplicateException
// 准备执行注册
// 调用userMapper的insert()方法执行注册,并获取返回的受影响行数
// 判断受影响的行数是否不为1
// 是:抛出InsertException
}
注意:在编写判断相关代码时,应该以“能够终止方法”的条件作为判断条件,例如什么情况下能抛出异常,则if
就以这种情况为判断标准!
以上代码具体实现为:
@Override
public void reg(User user) {
// 从参数user中获取用户名
String username = user.getUsername();
// 调用userMapper的findByUsername()方法执行查询
User result = userMapper.findByUsername(username);
// 判断查询结果是否不为null
if (result != null) {
// 是:查询结果不为null,表示用户名已经被占用,则抛出UsernameDuplicateException
throw new UsernameDuplicateException();
}
// 准备执行注册
// 调用userMapper的insert()方法执行注册,并获取返回的受影响行数
Integer rows = userMapper.insert(user);
// 判断受影响的行数是否不为1
if (rows != 1) {
// 是:抛出InsertException
throw new InsertException();
}
}
完成后,应该及时执行单元测试!所以,在src/test/java的cn.tedu.store
下创建子级service
包,并在这个包中创建UserServiceTests
测试类,在测试类的声明之前添加必要的注解,并在类中编写并执行单元测试:
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTests {
@Autowired
private IUserService service;
@Test
public void reg() {
try {
User user = new User();
user.setUsername("service");
user.setPassword("1234");
service.reg(user);
System.err.println("OK.");
} catch (ServiceException e) {
System.out.println(e.getClass().getName());
}
}
}
最后,在业务的实现类中添加执行加密的方法:
/**
* 执行密码加密,获取加密后的密码
* @param password 原始密码
* @param salt 盐值
* @return 加密后的密码
*/
private String getMd5Password(String password, String salt) {
// 加密规则:
// 1. 使用“盐 + 密码 + 盐”作为原文
// 2. 三重加密
System.err.println("\t加密-原始密码:" + password);
System.err.println("\t加密-盐值:" + salt);
for (int i = 0; i < 3; i++) {
password = DigestUtils.md5DigestAsHex(
(salt + password + salt).getBytes());
}
System.err.println("\t加密-密文:" + password);
return password;
}
然后,在处理“注册”时,补全数据:
@Override
public void reg(User user) {
// 输出日志
System.err.println("UserServiceImp.reg()");
System.err.println("\t注册数据:" + user);
// 从参数user中获取用户名
String username = user.getUsername();
// 调用userMapper的findByUsername()方法执行查询
User result = userMapper.findByUsername(username);
// 判断查询结果是否不为null
if (result != null) {
// 是:查询结果不为null,表示用户名已经被占用,则抛出UsernameDuplicateException
throw new UsernameDuplicateException();
}
// 准备执行注册
// 补全数据:加密后的密码,盐值
String salt = UUID.randomUUID().toString();
user.setSalt(salt);
String md5Password = getMd5Password(user.getPassword(), salt);
user.setPassword(md5Password);
// 补全数据:isDelete:值为0
user.setIsDelete(0);
// 补全数据:4项日志
Date now = new Date();
user.setCreatedUser(username);
user.setCreatedTime(now);
user.setModifiedUser(username);
user.setModifiedTime(now);
// 调用userMapper的insert()方法执行注册,并获取返回的受影响行数
System.err.println("\t插入数据:" + user);
Integer rows = userMapper.insert(user);
// 判断受影响的行数是否不为1
if (rows != 1) {
// 是:抛出InsertException
throw new InsertException();
}
}
最后,删除原有的测试数据,并重新执行单元测试。
6. 用户-注册-控制器层
应该先创建用于封装响应结果的类,所以,在cn.tedu.store
包下创建子级的util
包,并在这个包中创建JsonResult
类:
public class JsonResult<T> {
private Integer state; // 状态
private String message; // 错误信息
private T data; // 数据
// SET/GET/无参数构造方法
}
在cn.tedu.store
包下创建子级的controller
包,并在这个包中创建UserController
控制器类,在类的声明之前添加@RestController
注解,并添加@RequestMapping("users")
,在类中声明IUserService
类型的业务层对象并自动装配:
@RestController
@RequestMapping("users")
public class UserController {
@Autowired
private IUserService userService;
}
然后,在接口中编写处理“用户提交的注册”请求的方法:
// http://localhost:8080/users/reg?username=controller&password=1234&gender=0&phone=13100131001&email=controller@qq.com
@RequestMapping("reg")
public JsonResult<Void> reg(User user) {
JsonResult<Void> jsonResult = new JsonResult<Void>();
try {
userService.reg(user);
jsonResult.setState(1);
} catch (UsernameDuplicateException e) {
jsonResult.setState(2);
jsonResult.setMessage("注册失败!尝试注册的用户名已经被占用!");
} catch (InsertException e) {
jsonResult.setState(3);
jsonResult.setMessage("注册失败!执行插入数据时出现未知错误!请联系系统管理员!");
}
return jsonResult;
}
然后,就可以通过http://localhost:8080/users/reg?username=controller&password=1234&gender=0&phone=13100131001&email=controller@qq.com
测试控制器是否可以正确的工作!
为了使得为null
的属性不响应在JSON字符串中,可以在application.properties中添加配置:
spring.jackson.default-property-inclusion=non_null
当添加了配置以后,后续响应的任何属性的值只要为null
就不会出现在JSON字符串中!如果一定需要某个属性为null
也会出现在JSON字符串中,可以在这个属性的声明之前添加@JsonInclude(Include.ALWAYS)
注解!
7. 用户-注册-前端页面
8. 用户-登录-持久层
9. 用户-登录-业务层
10. 用户-登录-控制器层
11. 用户-登录-前端页面
---------------------------------------
附1:使用SpringMVC框架统一处理异常
在Java语言中,异常的继承体系结构是:
Throwable
Error
OutOfMemoryError
Exception
IOException
FileNotFoundException
SQLException
RuntimeException
NullPointerException
ClassCastException
ArithmeticException
IndexOutOfBoundsException
ArrayIndexOutOfBoundsException
StringIndexOutOfBoundsException
以上异常中,RuntimeException
及其子孙类异常在语法上没有要求必须处理!而其它异常必须通过try...catch
或throw / throws
进行处理!
使用try...catch
语法,表示“处理”异常,而使用throws
表示“声明抛出”异常!如果当前类适合处理异常,则应该try...catch
,反之,如果不适合,则应该throws
抛出!所谓“适合”处理,首先要明白“什么才叫处理”,处理异常的本质应该是对已经出现的异常进行补救,并且尽量使得后续不要再出现同样的异常!常见的处理方案应该是“明确的给予用户提示信息,使得用户能明确出现异常的问题,从而使得用户可能不再做错误的操作”!
既然“处理”异常的操作是需要给客户端/用户明确的错误提示信息,则控制器就是适合处理异常,因为控制器可以给予客户端响应,而例如业务层就是不适合处理异常的,因为业务层并不负责处理响应结果!
在一个项目中,同样的异常可能在多个功能中都会出现,例如“插入数据异常”,只要向数据库中插入新的数据,都有可能出现,例如注册用户、创建收货地址、生成订单等,或者“密码错误异常”,在登录、修改密码等任何需要验证密码的功能中都有可能出现!
SpringMVC框架就提供了统一处理异常的机制,使得某种类型的异常只需要处理1次,后续再出现相同的异常,在代码中是不需要再处理的!
具体的做法是专门使用1个方法来处理相关的异常(如果若干种异常的处理方式不同,也可以使用多个方法区分处理),关于这个方法:
-
应该使用
public
权限; -
返回值的设计完全参考处理请求的方法;
-
方法的名称可以自定义;
-
参数列表中必须包含异常类型的参数,表示即将被处理的异常,也可以按需添加
HttpServletRequest
、HttpServletResponse
类型的参数,但是,不可以随意添加其它参数; -
必须在方法的声明之前添加
@ExceptionHandler
注解。
所以,可以在UserController
中添加处理异常的方法:
@ExceptionHandler
public JsonResult<Void> handleException(Throwable ex) {
JsonResult<Void> jsonResult = new JsonResult<Void>();
if (ex instanceof UsernameDuplicateException) {
jsonResult.setState(2);
jsonResult.setMessage("注册失败!尝试注册的用户名已经被占用!");
} else if (ex instanceof InsertException) {
jsonResult.setState(3);
jsonResult.setMessage("注册失败!执行插入数据时出现未知错误!请联系系统管理员!");
}
return jsonResult;
}
而处理请求的方法中就不必再处理异常了:
// http://localhost:8080/users/reg?username=controller&password=1234&gender=0&phone=13100131001&email=controller@qq.com
@RequestMapping("reg")
public JsonResult<Void> reg(User user) {
JsonResult<Void> jsonResult = new JsonResult<Void>();
userService.reg(user);
jsonResult.setState(1);
return jsonResult;
}
这样的代码的执行流程是:当SpringMVC框架的DispatcherServlet
接收到reg
路径的请求,就会调用控制器类中的public JsonResult<Void> reg(User user)
方法,如果执行过程中出现异常,但是,该方法并没有try...catch
捕获并处理,相当于将异常抛出,则SpringMVC框架就会捕获异常,并调用handleException()
方法进行处理!
注意:按照目前的项目结构,以上处理异常的方法只能作用于当前控制器类,如果后续添加了更多的控制器类,那些控制器类中抛出的异常是不会被当前控制器类中的方法进行处理的!解决方案参考后续课程!
附2:内存溢出/内存泄漏
出现这样的问题,是存在希望被释放的资源却无法释放,导致内存中存在垃圾数据却无法被清除!
假设存在FileInputStream fis = new FileInputStream("某文件的路径");
,然后,在程序执行过程中出现了意外崩溃,则当前方法就会终止,程序员就不可能通过代码再调用fis
变量去操作这个流对象了!但是,这个fis
变量引用的对象还可能仍连接着硬盘上的某文件,当垃圾回收机制(GC系统)发现这个fis
变量对应的流对象时,由于该对象还与硬盘的文件处理连接状态,就不会将其视为垃圾,则不会回这个对象占用的内存空间!
当这样的数据越来越多,就会导致系统的可用内存越来越少,如果达到峰值,内存就会没有空间可用,如果仍尝试产生新的数据,就会“溢出”!
所以,其实,少量的内存溢出对系统是没有明显的危害的!但是,应该尽可能避免所有可能存在的内存溢出问题,具体的做法就是“资源随时用完随时释放(关闭)”,至少,在有try...catch
的语法中,使用finally
来释放资源!