电子商城逻辑

1. 项目的分析

接手到新的项目,首先,应该大致分析这个项目中有哪些类型的数据,例如:商品、商品分类、用户、收货地址、收藏、购物车、订单……

然后,对这些需要处理的数据排个开发顺序,通常遵守的原则有2个:由简到难,由基础数据开始!所以,以上数据的开发顺序应该是:用户 > 收货地址 > 商品分类 > 商品 > 收藏 > 购物车 > 订单。

接下来,根据需求(也可以根据现有的界面设计)分析每种数据处理时涉及的功能,例如用户数据的相关功能有:注册、登录、修改密码、修改个人资料、上传头像,并对这些功能的开发设计先后顺序,通常遵守增、查、删、改的顺序,可以是:注册 > 登录 > 修改密码 > 修改个人资料 > 上传头像。

针对每个功能,开发顺序应该是:数据库与数据表 > 实体类 > 持久层 > 业务层 > 控制器层 > 前端界面。

做项目之前,一定要把某个数据或功能拆出来,一次只解决一个问题!

					**注册**

2. 用户-注册-数据库与数据表

创建数据库:

CREATE DATABASE store;

使用数据库:

USEstore;

创建用户数据表:

CREATE TABLE user (
	uid INT AUTO_INCREMENT COMMENT '用户id',
	username VARCHAR(20) UNIQUE NOT NULL COMMENT '用户名',
	password CHAR(32) NOT NULL COMMENT '密码',
	salt CHAR(36) COMMENT '盐值',
	gender INT COMMENT '性别,0-女性,1-男性',
	phone VARCHAR(20)  COMMENT '电话',
	email VARCHAR(50) COMMENT '邮箱',
	avatar VARCHAR(50) COMMENT '头像',
	is_delete INT COMMENT '是否删除,0-未删除,1-已删除',
	created_user VARCHAR(20) COMMENT '创建执行人',
	created_time DATETIME COMMENT '创建时间',
	modified_user VARCHAR(20) COMMENT '修改执行人',
	modified_time DATETIME COMMENT '修改时间',
	PRIMARY KEY (uid)
) DEFAULT CHARSET=UTF8;

3. 用户-注册-实体类

打开https://start.spring.io,准备创建SpringBoot项目,勾选上MySQL和MyBatis,将下载的项目导入到Eclipse中,由于添加了数据库相关依赖,首先,必须在application.properties中添加连接数据库的配置,否则运行时会报告错误:

# datasource
spring.datasource.url=jdbc:mysql://localhost:3306/tedu_store?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root

由于4个日志属性在各数据表中都将存在,对应的实体类也都需要添加这些属性,所以,先创建实体类公共的父类cn.tedu.store.entity.BaseEntity用于定义这4个日志属性:

/**
 * 实体类的基类
 */
abstract class BaseEntity implements Serializable {

	private static final long serialVersionUID = -5882064199939706583L;

	private String createdUser;
	private Date createdTime;
	private String modifiedUser;
	private Date modifiedTime;

	// SET/GET/toString
}

由于BaseEntity只在当前cn.soft863.store.entity包中使用,且不需要单独创建对象,所以,可以将访问权限设置为默认(删除public),并添加abstract修饰符。

然后,创建cn.soft863.store.entity.User实体类:

/**
 * 用户数据的实体类
 */
public class User extends BaseEntity {

	private static final long serialVersionUID = 8777086855777796877L;

	private Integer uid;
	private String username;
	private String password;
	private String salt;
	private Integer gender;
	private String phone;
	private String email;
	private String avatar;
	private Integer isDelete;

	// GET/SET/toString

}

4. 用户-注册-持久层

1. 分析SQL语句

增加数据的SQL语句:

INSERT INTO t_user (除了uid以外的字段列表) VALUES (对应的值)

根据用户名查询用户数据的SQL语句:

SELECT uid, password, salt, is_delete FROM t_user WHERE username=?

由于以上查询还可以应用于“登录”功能,所以,查询的字段列表中,还应该添加与“登录”相关的字段。(如果暂时无法考虑得特殊周全,可以后续再补充)

2. 接口与抽象方法

首先,创建持久层接口cn.tedu.store.mapper.UserMapper,并添加抽象方法:

Integer insert(User user);

User findByUsername(String username);

所有的增删改操作,返回值都使用Integer。

由于当前是第1次编写持久层接口,还需要在启动类上添加@MapeprScan("cn.tedu.store.mapper"),用于指定持久层接口所在的包。

3. 配置映射

首先,在resources下创建mappers文件夹,然后,复制粘贴得到UserMapper.xml文件。

<mapper namespace="cn.tedu.store.mapper.UserMapper">

	<!-- 插入用户数据 -->
	<!-- Integer insert(User user) -->
	<insert id="insert">
		INSERT INTO s_user (
			username, password,
			salt, gender,
			phone, email,
			avatar, is_delete,
			created_user, created_time,
			modified_user, modified_time
		) VALUES (
			#{username}, #{password},
			#{salt}, #{gender},
			#{phone}, #{email},
			#{avatar}, #{isDelete},
			#{createdUser}, #{createdTime},
			#{modifiedUser}, #{modifiedTime}
		)
	</insert>
	
	<!-- 根据用户名查询用户数据 -->
	<!-- User findByUsername(String username) -->
	<select id="findByUsername"
		resultType="cn.soft863.store.entity.User">
		SELECT 
			uid, password, 
			salt, 
			is_delete AS isDelete
		FROM 
			s_user 
		WHERE 
			username=#{username}
	</select>
	
</mapper>

由于当前是第1次配置持久层映射,则需要在application.properties中配置mybatis.mapper-locations=classpath:mappers/*.xml,用于指定映射文件所在的位置。

完成后,应该编写并执行单元测试:在src\test\java下创建cn.soft863.store.mapper.UserMapperTestCase类,用于测试UserMapper接口中定义的抽象方法:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserMapperTestCase {

	@Autowired
	public UserMapper mapper;
	
	@Test
	public void insert() {
		User user = new User();
		user.setUsername("root");
		user.setPassword("1234");
		Integer rows = mapper.insert(user);
		System.err.println("rows=" + rows);
	}
	
	@Test
	public void findByUsername() {
		String username = "root";
		User result = mapper.findByUsername(username);
		System.err.println(result);
	}
	
}

5. 用户-注册-业务层

1. 设计异常

在业务层中,应该把所有认为的操作失败(例如注册时用户名被占用、登录时用户名错误、登录时密码错误等等)设计出对应的异常!

则应该创建异常类:

cn.tedu.store.service.ex.ServiceException(继承自RuntimeException)
cn.tedu.store.service.ex.UsernameDuplicateException(继承自ServiceException)
cn.tedu.store.service.ex.InsertException(继承自ServiceException)

凡是自行抛出的异常,都应该是RuntimeException的子孙类异常,同时,为了便于后续的处理,应该自定义某个异常类,然后,当前项目中会抛出的异常都应该是它的子孙类异常。

凡涉及增删改操作都应该判断其返回值(受影响的行数),如果返回值与期望值不同,则抛出异常!

2. 接口与抽象方法

创建cn.soft863.store.service.IUserService接口,并添加抽象方法:

void reg(User user) 
	throws UsernameDuplicateException, 
		InsertException;

返回值:以操作正确(例如注册成功、登录成功等)为前提,如果需要向外(向方法的调用者,也就是控制器,甚至向客户端)提供某些数据,如果需要,则以这个数据的类型作为返回值,如果不需要向外提供数据,则使用void即可;

方法名称:应该与某个业务(在用户来看是某个功能)相对应,例如注册功能的方法名可以使用reg,登录功能的方法名称可以使用login

参数:必须通过该参数能够调用持久层中的那些方法,以注册为例,可能需要调用持久层中的Integer insert(User user)User findByUsername(String username)方法,则当前业务层接口中的抽象方法的参数也能基本一系列的运算能调用这2个方法。

3. 实现

创建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 {
		// TODO Auto-generated method stub

	}

}

在重写抽象方法之前,应该先将持久层接口中的方法复制到业务层实现类中,添加private权限,并实现这些方法。

如果是增删改方法,则应该判断返回值,并在返回值与期望值不相符时抛出对应的异常,方法原本的返回值类型修改为void;如果是查询方法,则直接调用持久层对象完成查询功能即可,并不抛出异常,因为同一个查询,有时查询到数据是正确的,而有时查询不到数据才是正确的,以“根据用户名查询用户数据”为例,在“注册”功能中,只有查询结果为null才能继续注册,即查询不到数据是正确的,但是在“登录”功能中,只有查询到数据才是正确的!

/**
 * 插入用户数据
 * @param user 用户数据
 */
private void insert(User user) {
	Integer rows = userMapper.insert(user);
	if (rows != 1) {
		throw new InsertException();
	}
}

/**
 * 根据用户名查询用户数据
 * @param username 用户名
 * @return 匹配的用户数据,如果没有匹配的数据,则返回null
 */
private User findByUsername(String username) {
	return userMapper.findByUsername(username);
}

然后,重写抽象方法:

@Override
public void reg(User user) 
	throws UsernameDuplicateException, 
		InsertException {
	// 根据尝试注册的用户名查询用户数据
	String username = user.getUsername();
	User result = findByUsername(username);
	// 检查用户名是否被占用:如果查询到数据,则表示被占用,如果查询结果为null,则表示用户名没有被占用
	if (result == null) {
		// 未占用:执行注册
		insert(user);
	} else {
		// 已占用:抛出UsernameDuplicateException
		throw new UsernameDuplicateException();
	}
}

可以看到,重写的方法中,并不直接调用持久层对象来实现增删改查,而是调用自身的私有方法,间接的调用到持久层对象来实现数据访问。

完成后,应该编写并执行单元测试:在src\test\java下创建cn.tedu.store.service.UserServiceTestCase类,用于测试IUserService接口中定义的抽象方法:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTestCase {

	@Autowired
	public IUserService service;
	
	@Test
	public void reg() {
		try {
			User user = new User();
			user.setUsername("mybatis");
			user.setPassword("1234");
			user.setGender(1);
			user.setPhone("18738213922");
			service.reg(user);
			System.err.println("OK");
		} catch (ServiceException e) {
			System.err.println(e.getClass().getName());
			System.err.println(e.getMessage());
		}
	}
	
}

注册后,通过查询数据表,可以发现,仍有一部分数据没有值,例如is_delete、created_user字段等,所以,业务层的实现类还有一项任务,就是“保障数据的完整性”,那些不由用户(客户端)提交的数据,应该在业务层中来生成,所以,需要调整业务层实现类中的代码:

@Override
public void reg(User user) 
	throws UsernameDuplicateException, 
		InsertException {
	// 根据尝试注册的用户名查询用户数据
	String username = user.getUsername();
	User result = findByUsername(username);
	// 检查用户名是否被占用:如果查询到数据,则表示被占用,如果查询结果为null,则表示用户名没有被占用
	if (result == null) {
		// 设置is_delete
		user.setIsDelete(0);
		
		// 设置4项日志
		Date now = new Date();
		user.setCreatedUser(username);
		user.setCreatedTime(now);
		user.setModifiedUser(username);
		user.setModifiedTime(now);
		
		// TODO 密码加密
		
		// 执行注册
		insert(user);
	} else {
		// 已占用:抛出UsernameDuplicateException
		throw new UsernameDuplicateException(
			"注册失败!您尝试注册的用户名(" + username + ")已经被占用!");
	}
}

【附】 常见错误

【错误描述】 Caused by: com.mysql.cj.exceptions.WrongArgumentException: No timezone mapping entry for ‘Asia/Shanghaispring.datasource.username’

【错误原因】 数据库连接字符串有误

【错误描述】 Caused by: java.sql.SQLException: Access denied for user ‘root’@‘localhost’ (using password: YES)

【错误原因】 数据库密码错误

【错误描述】 NoSuchBeanDefinitionException … ‘cn.tedu.store.mapper.UserMapper’ … expected at least 1 bean …

【错误原因】 没有找到UserMapper类型的对象,可能是因为在启动类(StoreApplication)上没有添加@MapperScanner注解,或注解中填写的包名是错误的,

【错误描述】 java.lang.NullPointerException at cn.tedu.store.mapper.UserMapperTestCase.findByUsername(UserMapperTestCase.java:29)

【错误原因】 所有的NullPointerException都是因为某个为null的值调用了属性或方法,应该根据下一行错误提示找到对应的代码,例如User result = mapper.findByUsername(username);,在这一行代码中,找到.左侧的对象,极有可能它是null值!如果一行代码中有多个.,则每个.的左侧都有可能是null值,甚至在某个方法的调用中,参数为null也会导致NullPointerException。如果某个值应该是自动装配的,则检查它有没有添加@Autowired注解,或它所在的类是否被Spring所管理。

【错误描述】 BadSqlGrammarException

【错误原因】 尝试执行的SQL语句存在语法错误,可以通过进一步的提示找出错误,如果进一步的提示中包含near关键字,则找near提示的代码的左侧的SQL语句部分,也可以直接去检查SQL语句

【错误描述】 There is no getter for property named …

【错误原因】 在配置映射时,应该填写类中的属性名时,所填写的名称在类中并不存在

【附】关于配置MyBatis映射没有代码提示的解决方案

5. 用户-注册-业务层【续】

密码加密的必要性:如果直接并密码使用明文方式存储,可能存在内部泄露或入侵盗取的风险。

通常,对密码进行加密时,并不会使用通用的加密算法,因为这些加密算法都是可以被逆向运算的,即:只要能够获得加密过程中的所有参数,就可以将密码逆向运算得到原始密码。

真正应用于密码加密的是“消息摘要算法”,消息摘要算法的特性:

  1. 原文相同,则摘要数据一定相同;

  2. 摘要算法不变,则摘要数据的长度不变,无视原文长度;

  3. 原文不同,则摘要数据几乎不会相同。

常见的消息摘要算法有:SHA-128、SHA-256、SHA-384、SHA-512、MD2、MD4、MD5……

关于MD5的破解:

  • 消息摘要算法不存在逆向运算,所谓的消息摘要算法的破解,是如何找到2个或更多不同的原文,却可以得到相同的摘要,以MD5为例,理论上运算2的128次方的次数就可以找到,而破解的核心在于运算更少的次数来找到这样的数据;

  • 在线破解:本质是记录了原文与摘要数据的对应关系,当输入摘要数据时,查询得到原文,这些在线破解只能“破解”原文比较简单的数据。

进一步加强密码的安全性:

  1. 加强原始密码的复杂程度;

  2. 使用位数更长的加密算法;

  3. 加盐;

  4. 多重加密;

  5. 综合以上用法。

在实际应用中,应该在业务层添加一个用于加密的方法,后续在注册、登录及其它需要验证密码的场合都可以直接调用该方法:

/**
 * 获取执行MD5加密后的密码
 * @param password 原密码
 * @param salt 盐值
 * @return 加密后的密码
 */
private String getMd5Password(
		String password, String salt) {
	// 加密规则:使用“盐+密码+盐”作为原始数据,执行5次加密
	String result = salt + password + salt;
	for (int i = 0; i < 5; i++) {
		result = DigestUtils
			.md5DigestAsHex(result.getBytes()).toUpperCase();
	}
	return result;
}

并且,在注册过程中,需要执行加密,并且,将加密后的密码、生成的盐值都封装到执行注册的user对象中,以将这些数据插入到数据表中:

// 生成随机盐
String salt = UUID.randomUUID().toString().toUpperCase();
// 执行密码加密,得到加密后的密码
String md5Password = getMd5Password(user.getPassword(), salt);
// 将盐和加密后的密码封装到user中
user.setPassword(md5Password);
user.setSalt(salt);

注意:由于应用了新的密码功能,所以,此前产生的测试数据已经无法使用,应该将此前产生的测试数据全部删除,避免后续使用时出现错误。

6. 用户-注册-控制器层

创建控制器类cn.soft863.store.controller.UserController,添加@RestController@RequestMapping("/users")注解,在类中添加业务层对象@Autowired private IUserService userService;

@RestController
@RequestMapping("/users")
public class UserController {

	@Autowired
	private IUserService userService;
	
}

1. 设计请求

请求路径:/users/reg
请求参数:User
请求方式:POST
响应数据:无

2. 处理请求

首先,应该创建用于响应操作结果的cn.soft863.store.util.ResponseResult类:

/**
 * 用于向客户端响应操作结果的类型
 * @param <T> 操作结果中包含的数据的类型
 */
public class ResponseResult<T> implements Serializable {

	private static final long serialVersionUID = -5368505763231357265L;

	private Long state;
	private String message;
	private T data;

	// SET/GET

}

然后在控制器类中添加处理请求的方法:

@GetMapping("/reg")
public ResponseResult<Void> reg(User user) {
	ResponseResult<Void> rr
		= new ResponseResult<Void>();
	
	try {
		userService.reg(user);
		rr.setState(1);
	} catch (ServiceException e) {
		rr.setState(2);
		rr.setMessage(e.getMessage());
	}
	
	return rr;
}

完成后,启动项目,打开浏览器,通过http://localhost:8080/users/reg?username=root&password=1234执行测试。

测试完成后,将@GetMapping调整为@PostMapping

[附] 异常

异常的体系结构:

Throwable
	Error
		OutOfMemoryError
	Exception
		IOException
			FileNotFoundException
		RuntimeException
			NullPointerException
			NumberFormatException
			ClassCastException
			ArithmeticException
			IndexOutOfBoundsException
				ArrayIndexOutOfBoundsException

关于异常的处理:从语法上,可以通过抛出(在方法体中使用throw抛出异常对象,并在方法签名中使用throws声明抛出)或捕获(使用try...catch语法包裹相关代码)这两种方式对异常进行处理!

抛出:在方法体中如果抛出了异常对象,在方法的签名中必须声明抛出,并且,如果是重写的方法,不可以抛出更多的异常;

捕获:捕获(catch)时,如果有多个异常,在捕获时可以不区分先后顺序,如果多个异常有继承关系,必须先捕获子级异常,然后再捕获父级异常。

如果需要处理的异常是RuntimeException或其子孙类异常,则不受以上处理的语法约束!主要因为:[1] 这些异常出现的频率可能极高;[2] 这些异常都可以通常事先的判断等操作,杜绝异常的发生。

关于异常的处理:首先,无论怎么处理,其实,该发生的异常已经发生了!所谓的处理,应该是“对已经发生的异常的补救”,通过这种处理,希望后续不再出现类似的“问题”。

在实际应用中,处理异常可能表现为“向用户提示错误信息”,因为,如果程序出现异常,类似于NullPointerException这种的信息是普通用户看不懂的,而专业人员可能从中分析出程序的流程或某些数据,所以,这类信息是不应该向外暴露给任何人的!所以,任何异常都是需要处理的!处理时,应该先考虑当前类或当前方法是否适合甚至能够try...catch进行处理,如果不行,则应该抛出!

基于SpringMVC框架的异常处理

可以自定义一个专门用于处理异常的方法,该方法要求:

  1. 访问权限应该是public

  2. 返回值与普通处理请求的方法相同;

  3. 方法的名称可以自定义;

  4. 方法的参数必须包含1个异常类型,例如ThrowableException等,表示将会捕获到的异常对象;

  5. 必须添加@ExceptionHandler注解,该注解要求SSM环境中添加<mvc:annotation-driven />

然后,在方法之前使用@ExceptionHandler注解,并在注解参数中定义需要处理的异常的类型:

@ExceptionHandler(ServiceException.class)
public ResponseResult<Void> handleException(Throwable e) {
	ResponseResult<Void> rr
		= new ResponseResult<Void>();
	rr.setMessage(e.getMessage());
	
	if (e instanceof UsernameDuplicateException) {
		// 400-用户名冲突
		rr.setState(400);
	} else if (e instanceof InsertException) {
		// 500-插入数据异常
		rr.setState(500);
	}
	
	return rr;
}

这种处理异常的方法,只能作用于当前控制器类中的处理请求的方法,即当前类中的代码出现异常才可以被处理!为了统一处理,应该创建BaseController基类,把处理异常的方法放在基类中,然后,当前项目中所有的控制器类都应该继承自这个基类!

7. 用户-注册-前端界面

					**登录**

8. 用户-登录-持久层

1. 分析SQL语句

登录应该是根据用户名查询用户数据,且根据查询结果中的密码和用户输入的密码进行对比,以判断登录成功与否。

SELECT
	uid, username, password, salt, avatar, is_delete
FROM 
	s_user 
WHERE 
	username=?

在SQL语句中,不区分英文大小写,所以,在查询时,不应该把密码作为查询条件之一,而是应该把密码作为查询结果的一部分,后续通过Java程序的equals()方法来对比密码!

2. 接口与抽象方法

由于此前设计“注册”时,已经定义了相关方法,所以,无需再次开发!

3. 配置映射

由于此前设计“注册”时,已经配置了相关映射,所以,只需要添加查询的字段列表即可!

<!-- 根据用户名查询用户数据 -->
<!-- User findByUsername(String username) -->
<select id="findByUsername"
	resultType="cn.tedu.store.entity.User">
	SELECT 
		uid, username,
		password, salt, 
		avatar,
		is_delete AS isDelete
	FROM 
		t_user 
	WHERE 
		username=#{username}
</select>

由于修改了程序代码,应该重新执行单元测试(无需重新编写),以测试功能是否正常。

9. 用户-登录-业务层

1. 设计异常

此次“登录”操作可能出现的异常有:用户名不存在、用户数据已被标记为删除、密码错误。

则应该在cn.soft863.store.service.ex创建异常类:

UserNotFoundException
PasswordNotMatchException

以上2个异常都应该继承自ServiceException

2. 接口与抽象方法

IUserService接口中添加抽象方法:

User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException;

3. 实现

UserServiceImpl中重写以上抽象方法:

public User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException {
	// 根据参数username查询用户:User findByUsername(String username)
	// 判断查询结果是否为null
	// 是:抛出UserNotFoundException

	// 判断is_delete是否标记为已删除:isDelete属性值是否为1
	// 是:抛出UserNotFoundException

	// 从查询结果中获取盐值
	// 对参数password执行加密
	// 判断查询结果中的密码与刚加密结果是否一致
	// 是:
	// -- 返回查询结果
	// 否:抛出PasswordNotMatchException
}

具体的实现为:

@Override
public User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException {
	// 根据参数username查询用户:User findByUsername(String username)
	User result = findByUsername(username);
	// 判断查询结果是否为null
	if (result == null) {
		// 是:抛出UserNotFoundException
		throw new UserNotFoundException(
			"登录失败!尝试登录的用户不存在!");
	}

	// 判断is_delete是否标记为已删除:isDelete属性值是否为1
	if (result.getIsDelete().equals(1)) {
		// 是:抛出UserNotFoundException
		throw new UserNotFoundException(
			"登录失败!尝试登录的用户不存在!");
	}

	// 从查询结果中获取盐值
	String salt = result.getSalt();
	// 对参数password执行加密
	String md5Password = getMd5Password(password, salt);
	// 判断查询结果中的密码与刚加密结果是否一致
	if (result.getPassword().equals(md5Password)) {
		// 是:准备返回结果,先去除部分不需要对外使用的数据
		result.setPassword(null);
		result.setSalt(null);
		result.setIsDelete(null);
		// 返回查询结果
		return result;
	} else {
		// 否:抛出PasswordNotMatchException
		throw new PasswordNotMatchException(
			"登录失败!错误密码!");
	}
}

完成后,应该在UserServiceTestCase中编写并执行单元测试:

@Test
public void login() {
	try {
		String username = "root";
		String password = "1234";
		User data = service.login(username, password);
		System.err.println(data);
	} catch (ServiceException e) {
		System.err.println(e.getClass().getName());
		System.err.println(e.getMessage());
	}
}

10. 用户-登录-控制器层

1. 处理异常

此次的业务抛出了2种新的异常,分别是UserNotFoundExceptionPasswordNotMatchException,则应该在BaseController中添加对这2个异常的处理!

2. 设计请求

请求路径:/users/login
请求参数:String username(*), String password(*)
请求方式:POST
响应数据:User

3. 处理请求

@GetMapping("/login")
public ResponseResult<User> login(
	@RequestParam("username") String username,
	@RequestParam("password") String password) {
	User data = userService.login(username, password);
	return new ResponseResult<>(SUCCESS, data);
}

完成后,可以通过http://localhost:8080/users/login?username=java&password=123456进行测试,完成后,将方法之前的注解调整为@PostMapping

###【附】内存溢出 / 内存泄漏 / Leak

内存溢出的原因主要是:程序已经正常或非正常(出现异常导致崩溃)结束,但是相关连接对象(

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值