1 前言&概述
这篇文章是基于这篇文章的更新,主要是更新了一些技术栈以及开发工具的版本,还有修复了一些Bug。
本文是SpringBoot+Android+MySQL的增删查改的简单实现,用到的技术包括Jackson、OkHttp、bouncycastle、Spring Data JPA。
2 环境
Android 4.1.2
IDEA 2020.3.1
Spring Boot 2.4.2
MySQL 8.0.23
OpenJDK 11
环境准备就略过了,需要的可以参考这里。
3 后端
3.1 新建项目
依赖:
3.2 项目结构
3.3 实体类
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String name;
private String password;
}
基本的Lombok注解+JPA中的两个注解:
@Id:标识主键
@GeneratedValue:主键生成策略,包括四个
主键生成策略如下:
GenerationType.TABLE:使用一个特定的数据库表格来保存主键,不依赖外部环境和数据库的具体实现,但是不能充分利用数据库特性,一般不会优先使用,且一般配合@TableGenerator使用
GenerationType.SEQUENCE:一些数据库不支持主键自增(如Oracle),这时就可以使用SEQUENCE,只有部分(Oracle/DB2/PostgreSQL)支持序列对象,一般不用于其他数据库
GenerationType.IDENTITY:一般意义上的主键自增长,插入数据时自动给主键复制,比如MySQL中的auto_increment
GenerationType.AUTO:主键生成策略交给持久化引擎,持久化引擎会根据数据库在以上三种主键策略中选择其中一种,这是JPA默认的主键生成策略
3.4 持久层
继承CrudRepository,T为实体类,ID为主键类型:
@Repository
public interface UserRepository extends CrudRepository {
boolean existsByName(String name);
User findByNameAndPassword(String name,String password);
}
一个需要注意的点是CrudRepository继承了Repository,而后者有一个叫查询方法的特性,就是说能根据一些方法中指定的关键字去生成对应的SQL,比如第一个方法existsByName,就根据name判断用户是否存在,参数为一个String name,返回boolean,具体的关键字以及例子参考如下:
3.5 业务层
@Transactional
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserService {
private final UserRepository repository;
public boolean exists(User user){
return repository.existsByName(user.getName());
}
public User findByNameAndPassword(User user){
return repository.findByNameAndPassword(user.getName(),user.getPassword());
}
public boolean insert(User user){
repository.save(user);
return true;
}
public boolean update(User user){
if(repository.findById(user.getId()).isEmpty()){
return false;
}
repository.save(user);
return true;
}
public boolean deleteById(int id){
if(!repository.existsById(id)){
return false;
}
repository.deleteById(id);
return true;
}
}
注解解释如下:
@Transactional:
@Service:标识为业务层,实际效果等价于@Component
@RequiredArgsConstructor:Lombok中的一个注解,主要是为了解决如下的警告:
其他一些根据方法名就知道含义的方法就不解释了。
3.6 控制层
@RestController
@RequestMapping("/")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserController {
private final UserService service;
@PostMapping("sign/in/up")
public ResponseBody signInUp(@RequestBody User user) {
if (service.exists(user)) {
User u = service.findByNameAndPassword(user);
return new ResponseBody(u != null ? ResponseCode.SIGN_IN_SUCCESS : ResponseCode.SIGN_IN_FAILED, u != null ? u.getId() : "");
}
return new ResponseBody(service.insert(user) ? ResponseCode.SIGN_UP_SUCCESS : ResponseCode.SIGN_UP_FAILED, "");
}
@PutMapping("update")
public ResponseBody update(@RequestBody User user) {
return new ResponseBody(service.update(user) ? ResponseCode.UPDATE_SUCCESS : ResponseCode.UPDATE_FAILED, "");
}
@DeleteMapping("delete")
public ResponseBody deleteByName(@RequestParam int id) {
return new ResponseBody(service.deleteById(id) ? ResponseCode.DELETE_SUCCESS : ResponseCode.DELETE_FAILED, "");
}
@GetMapping("test")
public String test() {
return "test";
}
}
注解解释如下:
@RestController:等价于@ResponseBody+@Controller,@RepsonseBody是直接返回数据的注解(不是默认的视图名字),而@Controller与@Service类似,查看源码可知道都是@Component的别名
@RequestMapping:表示该类中的方法中包含的Mapping都以此值开头
@PostMapping/@PutMapping/@DeleteMapping/@GetMapping:标识处理POST/PUT/Delete/GET请求的路径,如果类上添加了@RequestMapping,则把路径拼接在@RequestMapping的后面,比如这里的@GetMapping("test")相当于/test
3.7 响应体+响应码
响应体:
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class ResponseBody {
private int code;
private Object data;
}
响应码:
public class ResponseCode {
public static final int SIGN_IN_SUCCESS = 2000;
public static final int SIGN_UP_SUCCESS = 2001;
public static final int UPDATE_SUCCESS = 2002;
public static final int DELETE_SUCCESS = 2003;
public static final int SIGN_IN_FAILED = 3000;
public static final int SIGN_UP_FAILED = 3001;
public static final int UPDATE_FAILED = 3002;
public static final int DELETE_FAILED = 3003;
}
3.8 配置文件
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/userinfo
jpa:
open-in-view: false
hibernate:
ddl-auto: update
数据库名字以及用户名密码请根据自己需要修改,open-in-view这个选项在JPA默认为true,设置为false是为了抑制一个警告,开启它的含义是在事务外也可以访问懒加载的数据,这样可能会引起手动数据源切换失败的问题,因此设置为false。
ddl-auto: update表示更新数据表,原有数据保留,而且能在没有创建表的情况下自动创建表。该参数一共有5个设置选项:update、create、create-drop、validate、none,具体区别可以查看这里。
3.9 测试
运行后可以访问本地的localhost:8080/test会看到如下页面:
这样就没问题了,剩下的需要配合Android端测试。
4 Android端
4.1 新建项目
Android Q+Java。
4.2 依赖+权限
依赖:
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.1'
implementation 'org.bouncycastle:bcprov-jdk15to18:1.68'
implementation "org.projectlombok:lombok:1.18.16"
annotationProcessor 'org.projectlombok:lombok:1.18.16'
权限:
android:usesCleartextTraffic="true"
开启viewBinding:
buildFeatures{
viewBinding = true
}
4.3 项目结构
4.4 实体类
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class User {
private int id;
private String name;
private String password;
public User(String name, String password) {
this.name = name;
this.password = password;
}
}
4.5 工具类
public class Utils {
private static final Keccak.Digest512 digest512 = new Keccak.Digest512();
public static String encrypt(String origin) {
return new String(Hex.encode(digest512.digest(origin.getBytes(StandardCharsets.UTF_8))));
}
public static String getResponseMessage(int code) {
String message = "";
switch (code) {
case ResponseCode.SIGN_IN_SUCCESS:
message = "登录成功";
break;
case ResponseCode.SIGN_UP_SUCCESS:
message = "注册成功";
break;
case ResponseCode.SIGN_IN_FAILED:
message = "用户名或密码错误";
break;
case ResponseCode.SIGN_UP_FAILED:
message = "注册失败";
break;
case ResponseCode.DELETE_FAILED:
message = "删除失败";
break;
case ResponseCode.DELETE_SUCCESS:
message = "删除成功,自动退出";
break;
case ResponseCode.UPDATE_SUCCESS:
message = "更新成功";
break;
case ResponseCode.UPDATE_FAILED:
message = "更新失败";
break;
case ResponseCode.EMPTY_RESPONSE:
message = "响应体为空";
break;
case ResponseCode.SERVER_ERROR:
message = "服务器错误";
break;
case ResponseCode.JSON_SERIALIZATION:
message = "JSON序列化错误";
break;
case ResponseCode.EXIT_SUCCESS:
message = "退出成功";
break;
case ResponseCode.REQUEST_FAILED:
message = "请求发送失败";
break;
case ResponseCode.UNCHANGED_INFORMATION:
message = "未修改信息";
break;
}
return message;
}
public static void showMessage(Context context, Message message) {
Toast.makeText(context, getResponseMessage(message.what), Toast.LENGTH_SHORT).show();
}
}
工具类有三个方法,分别是:
加密:将密码进行SHA3-512加密,加密后的密码再发送到后端
获取对应信息:根据Message获取对应的提示信息
展示信息:利用Toast展示信息
4.6 响应体+响应码
响应体:
@NoArgsConstructor
@Setter
@Getter
public class RestResponse {
private int code;
private Object data;
}
响应码:
public class ResponseCode {
public static final int SIGN_IN_SUCCESS = 2000;
public static final int SIGN_UP_SUCCESS = 2001;
public static final int UPDATE_SUCCESS = 2002;
public static final int DELETE_SUCCESS = 2003;
public static final int SIGN_IN_FAILED = 3000;
public static final int SIGN_UP_FAILED = 3001;
public static final int UPDATE_FAILED = 3002;
public static final int DELETE_FAILED = 3003;
public static final int EMPTY_RESPONSE = 4000;
public static final int SERVER_ERROR = 4001;
public static final int REQUEST_FAILED = 4002;
public static final int JSON_SERIALIZATION = 4003;
public static final int EXIT_SUCCESS = 4004;
public static final int UNCHANGED_INFORMATION = 4005;
}
4.7 请求URL常量
public class NetworkSettings {
private static final String HOST = "192.168.1.8";
private static final String PORT = "8080";
public static final String SIGN_IN_UP = "http://"+ HOST +":"+PORT + "/sign/in/up";
public static final String UPDATE = "http://"+ HOST +":"+PORT + "/update";
public static final String DELETE = "http://"+ HOST +":"+PORT + "/delete";
}
4.8 MainActivity
上一部分代码吧,剩下的大部分类似,看源码链接即可。
public void signInUp(View view) {
try {
String name = binding.name.getText().toString();
//SHA3-512加密
String password = Utils.encrypt(binding.password.getText().toString());
//构造OkHttp请求Request
Request request = new Request.Builder().url(NetworkSettings.SIGN_IN_UP).post(
//请求体类型为application/json;charset=utf-8,利用了Jackson序列化为JSON
RequestBody.create(mapper.writeValueAsString(new User(name, password)), mediaType)
).build();
//异步POST操作,传入一个Callback回调
client.newCall(request).enqueue(new Callback() {
//若失败
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
//请求失败信息
message.what = ResponseCode.REQUEST_FAILED;
//展示对应信息,注意不能直接使用Toast.make(getApplicationContext(),"message",Toast.LENGTH_SHORT).show()
//因为不是同一个线程,需要使用Handler提交,也就是post()方法,参数为一个线程
handler.post(()->Utils.showMessage(getApplicationContext(),message));
e.printStackTrace();
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
//如果成功
if (response.isSuccessful()) {
//获取请求体
ResponseBody body = response.body();
//如果响应体不为空
if (body != null) {
//反序列化为响应体,包含了一个响应码以及数据字段
RestResponse restResponse = mapper.readValue(body.string(), RestResponse.class);
//设置Message
message.what = restResponse.getCode();
//如果登录成功
if(message.what == ResponseCode.SIGN_IN_SUCCESS){
handler.post(()->{
//存储用户id
signInId = (int)restResponse.getData();
//更新UI
binding.update.setVisibility(View.VISIBLE);
binding.delete.setVisibility(View.VISIBLE);
binding.signInUp.setText("退出");
binding.signInUp.setOnClickListener(v->signOut(false));
//保存旧用户名以及旧密码在更新的时候使用
oldName = binding.name.getText().toString();
oldPassword = binding.password.getText().toString();
});
}
} else {
//空响应体
message.what = ResponseCode.EMPTY_RESPONSE;
Log.e("RESPONSE_BODY_EMPTY", response.message());
}
} else {
//服务器错误
message.what = ResponseCode.SERVER_ERROR;
Log.e("SERVER_ERROR", response.message());
}
//根据Message提示对应信息
handler.post(()->Utils.showMessage(getApplicationContext(),message));
}
});
} catch (JsonProcessingException e) {
message.what = ResponseCode.JSON_SERIALIZATION;
Utils.showMessage(getApplicationContext(),message);
e.printStackTrace();
}
}
这部分是登录注册的代码,还有更新用户信息以及删除用户的代码,大部分类似。
5 测试
6 注意事项
如果出现了问题某些功能不能正常实现可以参考此处的一些注意事项以及解决方案。
7 源码
提供了Java+Kotlin两种实现:
8 参考链接
如果觉得文章好看,欢迎点赞。
同时欢迎关注微信公众号:氷泠之路。