# 簡介
這一篇文章主要是練習從零開始創建用戶,使用 JPA+寫 Service+ 重點使用 Junit 寫 Unit Test。
唯一要實現的簡單功能是 注冊用戶
。
# 創建 Spring Boot Project 並且加入依賴
在 pom.xml
主要加入 spring web, spring data jpa, mysql driver 的依賴
- 這次使用 MySql 數據庫
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-data-jpa</artifactId> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-web</artifactId> | |
</dependency> | |
<dependency> | |
<groupId>mysql</groupId> | |
<artifactId>mysql-connector-java</artifactId> | |
</dependency> |
# 修改 application.properties
在 application.propeties
裡定義一下 MySQL 的配置
spring.datasource.url=jdbc:mysql://localhost:3306/junit | |
spring.datasource.username=root | |
spring.datasource.password=123456 | |
spring.jpa.database=mysql | |
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect |
# 數據庫
User
table:
-- auto-generated definition | |
create table user | |
( | |
user_id int auto_increment comment 'user id' | |
primary key, | |
username varchar(256) null comment 'user name', | |
user_account varchar(256) not null comment 'user account number | |
', | |
user_password varchar(500) null comment 'user password', | |
user_email varchar(1000) null comment 'user email', | |
create_time datetime default CURRENT_TIMESTAMP null comment 'user created time', | |
update_time datetime default CURRENT_TIMESTAMP null comment 'user info modified/ update time', | |
user_status int null comment 'user status 0-active 1-inactive', | |
is_deleted int null comment '1 - user is delete' | |
) | |
comment 'user information'; |
# 代碼
Domain 裡面的 > User
class
package com.example.junit_restapi.domain; | |
import javax.persistence.*; | |
import java.sql.Timestamp; | |
@Entity | |
@Data | |
@AllArgsConstructor | |
@NoArgsConstructor | |
public class User { | |
@GeneratedValue(strategy = GenerationType.IDENTITY) | |
@Id | |
@Column(name = "user_id") | |
private int userId; | |
@Basic | |
@Column(name = "username") | |
private String username; | |
@Basic | |
@Column(name = "user_account") | |
private String userAccount; | |
@Basic | |
@Column(name = "user_password") | |
private String userPassword; | |
@Basic | |
@Column(name = "user_email") | |
private String userEmail; | |
@Basic | |
@Column(name = "create_time") | |
private Timestamp createTime; | |
@Basic | |
@Column(name = "update_time") | |
private Timestamp updateTime; | |
@Basic | |
@Column(name = "user_status") | |
private Integer userStatus; | |
@Basic | |
@Column(name = "is_deleted") | |
private Integer isDeleted; | |
@Override | |
public boolean equals(Object o) { | |
// 省略 | |
// 都是自己生成的(Generate persistence mapping) | |
} | |
@Override | |
public int hashCode() { | |
// 省略 | |
// 都是自己生成的(Generate persistence mapping) | |
} |
# 命名 bug
之前創建表的時候,每個 field 命名的時候都是用 camel case,比如 userAccount
而沒有用 _
的格式寫 user_account
。
然後在 user
entity 裡面 寫的也都是用 camel case,按道理來說應該是對 Map 上數據的,但最後出現這個 bug:
java.sql.SQLSyntaxErrorException: Unknown column "create_time" in 'field list'
這裡寫 unknow column "create_time" , 原本應該是 createTime
的。
結果是我忘了,hibernate 會把 camel 的字轉成 underscores,但我並沒有在 user
entity class 裡面用 @Column()
去表示是對應哪一列。
實際上應該要寫成這樣的:
@Column(name = "user_account") | |
private String userAccount; | |
@Column(name = "create_time") | |
private Timestamp createTime; |
所以後來我都直接把數據庫裡的 field 的名稱都改成 underscore 去寫了,然後直接 generate persistence mapping,懶得自己再寫一遍 User
class 了。
# 注冊用戶功能
在這裡就很簡單的實現注冊用戶這一個功能,實現的邏輯:
- 前端輸入帳號、用戶名、和密碼
- 校驗輸入
- 把密碼加密
- 把數據存到數據庫裡
帳號密碼的期望規範
# 檢查帳號用戶名密碼
因為只是簡單寫寫,下面代碼都沒有優化的,也沒有寫 throw exceptions 就直接返回 -1 了。
//check if it's null value | |
if (StringUtils.isAnyBlank(userAccount,username, userPassword)) { | |
log.error("counldn't save user - null value"); | |
return -1; | |
} | |
//check the user account pattern and length | |
String acct_pattern = "^[a-zA-Z0-9]([._-](?![._-])|[a-zA-Z0-9]){3,18}[a-zA-Z0-9]$"; | |
if (userAccount.length() < 4 || !userAccount.matches(acct_pattern)) { | |
log.error("counldn't save user - acct name or password length problem"); | |
return -1; | |
} | |
// username length should > 4 | |
if (username.length() < 4 || !username.matches(acct_pattern)) { | |
log.error("username length should be greater or equal to 4"); | |
return -1; | |
} | |
//Check that the password is in compliance 1. correct pattern 2. length < 8 | |
String password_pattern = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$"; | |
if (!userPassword.matches(password_pattern) || userPassword.length() < 8 ) { | |
log.error("it didn't save user - password problem"); | |
return -1; | |
} | |
//check if the username is already exist | |
List<User> user_result_list = userRepo.findUserByUsername(username); | |
if(user_result_list.size() > 0) { | |
log.error("counldn't save user - user_result_list"); | |
return -1; | |
} |
# 正則表達式
# 密碼:
^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,}$
^ # start-of-string | |
(?=.*[0-9]) # a digit must occur at least once | |
(?=.*[a-z]) # a lower case letter must occur at least once | |
(?=.*[A-Z]) # an upper case letter must occur at least once | |
(?=.*[@#$%^&+=]) # a special character must occur at least once | |
(?=\S+$) # no whitespace allowed in the entire string | |
.{8,} # anything, at least eight places though | |
$ # end-of-string |
# user_account & username :
^[a-zA-Z0-9]([._-](?![._-])|[a-zA-Z0-9]){3,18}[a-zA-Z0-9]$
# StringUtils
項目裡面用到 StringUtils.isAnyBlank()
,如果要測下面這種代碼,用 StringUtils
看上去比較易懂點...
if(username == null || password == null || checkPassword == null) |
在 google 裡搜 → Maven repository → apache commons utils (用最新最多 usage 的
https://mvnrepository.com/artifact/org.apache.commons/commons-lang3/3.12.0
然後把裡面的 dependency 加入到 pom.xml 配置
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --> | |
<dependency> | |
<groupId>org.apache.commons</groupId> | |
<artifactId>commons-lang3</artifactId> | |
<version>3.12.0</version> | |
</dependency> |
就可以用 StringUtils.isAnyBlank () 去測是否為 null ,並且代碼更加易懂
public long testUser(String username, String password, String checkPassword) { | |
if(StringUtils.isAnyBlank(username, password, checkPassword)){ | |
return -1; | |
} | |
return 0; | |
} |
# 加密密碼
在這邊只是用最簡單的方法加密,沒有用 JWT 或其他方法密碼。
//encrypt password | |
String salt = "tiff"; | |
String new_password = DigestUtils.md5DigestAsHex((salt + userPassword).getBytes(StandardCharsets.UTF_8)); |
# 創建 User 並存到數據庫
//insert user into database | |
User user = new User(); | |
user.setUsername(username); | |
user.setUserAccount(userAccount); | |
user.setUserPassword(new_password); | |
userRepo.save(user); |
# 完整代碼
UserRepository
@Repository | |
public interface UserRepository extends JpaRepository<User, Long> { | |
@Query(value = "select u from User u where u.username = ?1") | |
List<User> findUserByUsername(String username); | |
} |
UserService
@Service | |
public interface UserService { | |
long registerUser(String userAccount, String username, String userpassword); | |
} |
UserServiceImpl
@Slf4j | |
@Service | |
public class UserServiceImpl implements UserService{ | |
private final UserRepository userRepo; | |
@Autowired | |
public UserServiceImpl(UserRepository userRepo) { | |
this.userRepo = userRepo; | |
} | |
@Override | |
public long registerUser(String userAccount, String username, String userPassword) { | |
//check if it's null value | |
if (StringUtils.isAnyBlank(userAccount,username, userPassword)) { | |
log.error("counldn't save user - null value"); | |
return -1; | |
} | |
//check the user account pattern and length | |
String acct_pattern = "^[a-zA-Z0-9]([._-](?![._-])|[a-zA-Z0-9]){3,18}[a-zA-Z0-9]$"; | |
if (userAccount.length() < 4 || !userAccount.matches(acct_pattern)) { | |
log.error("counldn't save user - acct name or password length problem"); | |
return -1; | |
} | |
// username length should > 4 | |
if (username.length() < 4 || !username.matches(acct_pattern)) { | |
log.error("username length should be greater or equal to 4"); | |
return -1; | |
} | |
//Check that the password is in compliance 1. correct pattern 2. length < 8 | |
String password_pattern = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$"; | |
if (!userPassword.matches(password_pattern) || userPassword.length() < 8 ) { | |
log.error("it didn't save user - password problem"); | |
return -1; | |
} | |
//check if the username is already exist | |
List<User> user_result_list = userRepo.findUserByUsername(username); | |
if(user_result_list.size() > 0) { | |
log.error("counldn't save user - user_result_list"); | |
return -1; | |
} | |
//encrypt password | |
String salt = "tiff"; | |
String new_password = DigestUtils.md5DigestAsHex((salt + userPassword).getBytes(StandardCharsets.UTF_8)); | |
//insert user into database | |
User user = new User(); | |
user.setUsername(username); | |
user.setUserAccount(userAccount); | |
user.setUserPassword(new_password); | |
userRepo.save(user); | |
log.info("========User has been created-======"); | |
return user.getUserId(); | |
} | |
} |
# 單元測試
@Test | |
void registerUser() { | |
User user = new User(); | |
user.setUsername("1"); | |
user.setUserAccount("testing123"); | |
user.setUserPassword("Ki_12352@932"); | |
user.setCreateTime(new Timestamp(new java.util.Date().getTime())); | |
user.setUpdateTime(new Timestamp(new java.util.Date().getTime())); | |
Assertions.assertAll("testing error", | |
() -> { // username length should not <= 4 | |
long result = userService.registerUser(user.getUserAccount(), user.getUsername(), user.getUserPassword()); | |
Assertions.assertEquals(-1, result); | |
}, | |
() -> { // username pattern check | |
user.setUsername("user4$@-^"); | |
long result = userService.registerUser(user.getUserAccount(), user.getUsername(), user.getUserPassword()); | |
Assertions.assertEquals(-1, result); | |
}, | |
() -> { // test password length less than 4 | |
user.setUsername("tiffany"); | |
user.setUserPassword("11"); | |
long result = userService.registerUser(user.getUserAccount(), user.getUsername(), user.getUserPassword()); | |
Assertions.assertEquals(-1, result); | |
}, | |
() -> { // test password doesn't include special char | |
user.setUserPassword("24359832759"); | |
long result = userService.registerUser(user.getUserAccount(), user.getUsername(), user.getUserPassword()); | |
Assertions.assertEquals(-1, result); | |
}, | |
() -> { // test already have existed user in the db | |
user.setUserPassword("Jj@12345678"); | |
user.setUsername("Alice"); | |
long result = userService.registerUser(user.getUserAccount(), user.getUsername(), user.getUserPassword()); | |
Assertions.assertEquals(-1, result); | |
}, | |
() -> { // account has special char, | |
user.setUserAccount("test@^&2"); | |
long result = userService.registerUser(user.getUserAccount(), user.getUsername(), user.getUserPassword()); | |
Assertions.assertEquals(-1, result, "account shouldn't have special char!"); | |
}, | |
() -> { // account length check : < 4 | |
user.setUserAccount("123"); | |
long result = userService.registerUser(user.getUserAccount(), user.getUsername(), user.getUserPassword()); | |
Assertions.assertEquals(-1, result, "account shouldn't have special char!"); | |
} | |
); | |
// test the successful user | |
User successUser = new User(); | |
successUser.setUsername("junituser"); | |
successUser.setUserAccount("junituser001"); | |
successUser.setUserPassword("k_8Uj72882@5"); | |
successUser.setCreateTime(new Timestamp(new java.util.Date().getTime())); | |
successUser.setUpdateTime(new Timestamp(new java.util.Date().getTime())); | |
long success_result = userService.registerUser(successUser.getUserAccount(), successUser.getUsername(), successUser.getUserPassword()); | |
Assertions.assertTrue(success_result > 0); |
# 測試完的數據庫長這樣
Alice 是本身就有了
經過測試,最後只加上 junituser 的那一 row
之後再練習下用 Mockito,這次先寫到這裡~