共计 16752 个字符,预计需要花费 42 分钟才能阅读完成。
1 前言 & 概述
这篇文章是基于此处文章的更新,更新了一些技术栈,更加贴近理论须要,以及修复了若干的谬误。
这是一个前端 Android
+ 后端Java/Kotlin
通过 Servelt
进行后盾数据库(MySQL
)交互的具体步骤以及源码实现,技术栈:
Android
根底- 原生
JDBC
+ 原生Servlet
Tomcat
+MySQL
(Docker
)
当然当初的很多 Java
后端开发都应用了 Spring Boot
而不是原生的 Servlet
,所以应用Spring Boot
实现的能够笔者的另一篇文章。
只管基于 Spring Boot
实现十分的简便,然而应用原生的 Servlet
更能了解底层的原理。另外本篇文章是偏根底向的教程,很多步骤都会比拟具体而且附上了图,好了废话不说,注释开始。
2 环境
Android Studio 4.1.2
IntelliJ IDEA 2020.3
MySQL 8.0.23
Tomcat 10.0
Docker 20.10.1
- 服务器
CentOS 8.1.1911
3 环境筹备
3.1 IDE
筹备
官网装置Android Studio
+IDEA
,这部分就省略了。
3.2 MySQL
3.2.1 装置概述
这里的 MySQL
若无非凡阐明指的是MySQL Community
。
首先,在 Windows
下,MySQL
提供了 exe
安装包:
macOS
下提供了 dmg
安装包:
能够戳这里下载。
Linux
下一般来说 MySQL
装置有如下形式:
- 软件包装置(
apt/apt-get
、yum
、dnf
、pacman
等) - 下载压缩包装置
- 源码编译装置
Docker
装置
其中绝对省事的装置形式为 Docker
装置以及软件包装置,其次是压缩包形式装置,特地不倡议源码装置(当然如果喜爱挑战的话能够参考笔者的一篇编译装置 8.0.19 以及编译装置 8.0.20)。
3.2.2 装置开始
这里笔者本地测试抉择的是应用 Docker
装置,步骤能够查看这里。
另外对于服务器,也能够应用 Docker
装置,如果应用软件包装置的话,这里以笔者的 CentOS8
为例,其余零碎的参考如下:
- Fedroa
- RedHat
- Ubuntu
3.2.2.1 下载并装置
增加仓库:
sudo yum install https://repo.mysql.com/mysql80-community-release-el8-1.noarch.rpm
禁用默认 MySQL
模块(CentOS8
中会蕴含一个默认的 MySQL
模块,不禁用的话没方法应用下面增加的仓库装置):
sudo yum module disable mysql
装置:
sudo yum install mysql-community-server
3.2.2.2 启动服务并查看初始化明码
启动服务:
systemctl start mysqld
查看长期明码:
sudo grep 'temporary password' /var/log/mysqld.log
输出长期明码登录:
mysql -u root -p
批改明码:
alter user 'root'@'localhost' identified by 'PASSWORD'
3.2.2.3 创立内部拜访用户
不倡议在 Java
中间接拜访 root
用户,个别是新建一个对应权限的用户并进行拜访,这里就为了不便就省略了。
3.3 Tomcat
3.3.1 本地Tomcat
Tomcat
装置不难,间接从官网下载即可:
解压:
tar -zxvf apache-tomcat-10.0.0.tar.gz
进入 bin
目录运行startup.sh
:
cd apache-tomcat-10.0.0/bin
./startup.sh
本地拜访localhost:8080
:
这样就算胜利了。对于 Windows
的读者,能够戳这里下载,解压步骤相似,解压后运行 startup.bat
即可拜访localhost:8080
。
3.3.2 服务器Tomcat
服务器的话能够间接应用 wget
装置:
wget https://downloads.apache.org/tomcat/tomcat-10/v10.0.0/bin/apache-tomcat-10.0.0.tar.gz
然而这样速度很慢,倡议下载到本地再应用 scp
上传:
scp apache-tomcat-10.0.0.tar.gz username@xxx.xxx.xxx.xxx:/
一样依照下面的办法解压后运行 startup.sh
,拜访 公网 IP:8080
即可察看是否胜利。
4 建库建表
4.1 用户表
这里应用到的 MySQL
脚本如下:
CREATE DATABASE userinfo;
USE userinfo;
CREATE TABLE user
(
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
name CHAR(30) NULL,
password CHAR(30) NULL
)
4.2 导入
mysql -u root -p < user.sql
5 后端局部
因为是比拟根底向的教程,所以先从创立我的项目开始吧。
5.1 创立我的项目 + 导库
抉择对应 Java Enterprise
,默认是选中了其中的Web application
,构建工具默认Maven
,测试工具JUnit
,如果须要Gradle
或Kotlin
的话自行勾选即可:
2020.3
版本的 IDEA
相比起以前,更加人性化的增加了抉择库的性能,默认是选中了Servlet
,须要其余库的话自行抉择即可。
另外一个要留神的是 JavaEE
曾经更名为JakartaEE
,因而版本这里能够抉择JakartaEE
:
填上对应包名并抉择地位:
创立实现后,这里笔者遇到了一个谬误,找不到对应的 Servlet
包:
在设置中抉择更新核心仓库即可:
创立后的目录如图所示:
接着增加依赖,用到的依赖包含:
MySQL
Jackson
Lombok
增加到 pom.xml
中即可(留神版本,MySQL 不同版本能够查看这里):
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.1</version>
</dependency>
这样第一步就实现了。
5.2 构造
我的项目构造如下:
- 长久层操作:
Dao
- 实体类:
User
- 响应体:
ResponseBody
Servlet
层:SignIn
/SignUp
/Test
- 工具类:
DBUtils
启动类:不须要,因为在Web
服务器中运行
先创立好文件以及目录:
5.3 DBUtils
原生 JDBC
获取连贯工具类:
package com.example.javawebdemo.utils;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DBUtils {
private static Connection connection = null;
public static Connection getConnection() {
try {Class.forName("com.mysql.cj.jdbc.Driver");
final String url = "jdbc:mysql://127.0.0.1:3306/userinfo";
final String username = "root";
final String password = "123456";
connection = DriverManager.getConnection(url, username, password);
} catch (Exception e) {e.printStackTrace();
return null;
}
return connection;
}
public static void closeConnection() {if (connection != null) {
try {connection.close();
} catch (SQLException e) {e.printStackTrace();
}
}
}
}
重点在这四行:
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1:3306/userinfo";
String username = "root";
String password = "123456";
依据集体须要批改,留神 MySQL8
注册驱动与旧版的区别,旧版的是:
Class.forName("com.mysql.jdbc.Driver");
5.4 User
三字段 +@Getter
:
package com.example.javawebdemo.entity;
import lombok.Getter;
@Getter
public class User {
private final String name;
private final String password;
public User(String name, String password) {
this.name = name;
this.password = password;
}
}
5.5 Dao
数据库操作层:
package com.example.javawebdemo.dao;
import com.example.javawebdemo.entity.User;
import com.example.javawebdemo.utils.DBUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class Dao {public boolean select(User user) {final Connection connection = DBUtils.getConnection();
final String sql = "select * from user where name = ? and password = ?";
try {final PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, user.getName());
preparedStatement.setString(2, user.getPassword());
ResultSet resultSet = preparedStatement.executeQuery();
return resultSet.next();} catch (SQLException e) {e.printStackTrace();
return false;
} finally {DBUtils.closeConnection();
}
}
public boolean insert(User user) {final Connection connection = DBUtils.getConnection();
final String sql = "insert into user(name,password) values(?,?)";
try {final PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, user.getName());
preparedStatement.setString(2, user.getPassword());
preparedStatement.executeUpdate();
return preparedStatement.getUpdateCount() != 0;} catch (SQLException e) {e.printStackTrace();
return false;
} finally {DBUtils.closeConnection();
}
}
}
两个操作:
- 查问:存在该用户返回
true
,否则false
- 插入:增加用户
留神插入操作中应用 executeUpdate()
进行插入,同时应用 getUpdateCount() != 0
判断插入的后果,而不能间接应用
return preparedStatement.execute();
一般来说:
select
:executeQuery()
,executeQuery()
返回ResultSet
,示意后果集,保留了select
语句的执行后果,配合next()
应用delete
/insert
/update
:应用executeUpdate()
,executeUpdate()
返回的是一个整数,示意受影响的行数,即delete
/insert
/update
批改的行数,对于drop
/create
操作返回0
create
/drop
:应用execute()
,execute()
的返回值是这样的,如果第一个后果是ResultSet
对象,则返回true
,如果第一个后果是更新计数或者没有后果则返回false
所以在这个例子中
return preparedStatement.execute();
必定返回false
,不能直接判断是否插入胜利。
5.6 响应体
增加一个响应体类不便设置返回码以及数据:
package com.example.javawebdemo.response;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class ResponseBody{
private Object data;
private int code;
}
5.7 Servlet
SingIn
类用于解决登录,调用JDBC
查看数据库是否有对应的用户SignUp
类用于解决注册,把User
增加到数据库中Test
为测试Servlet
,返回固定字符串
先上SignIn.java
package com.example.javawebdemo.servlet;
import com.example.javawebdemo.dao.Dao;
import com.example.javawebdemo.entity.User;
import com.example.javawebdemo.response.ResponseBody;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/sign/in")
public class SignIn extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {req.setCharacterEncoding("utf-8");
resp.setCharacterEncoding("utf-8");
resp.setContentType("application/json;charset=utf-8");
String name = req.getParameter("name");
String password = req.getParameter("password");
Dao dao = new Dao();
User user = new User(name,password);
ObjectMapper mapper = new ObjectMapper();
ResponseBody body = new ResponseBody();
if (dao.select(user)) {body.setCode(200);
body.setData("success");
} else {body.setCode(404);
body.setData("failed");
}
mapper.writeValue(resp.getWriter(), body);
}
}
留神点:
@WebServlet
:定义Servlet
(不加这个注解也是能够的然而须要在web.xml
中手工定义Servlet
),默认的属性为value
,示意Servlet
门路- 编码:
HttpServletRequest
/HttpServletResponse
均设置UTF8
(尽管在这个例子中并不是必要的因为没有中文字符) - 获取参数:
request.getParameter
,从申请中获取参数,传入的参数是键值 - 写响应体:利用
Jackson
,将response.getWriter
以及响应体传入,接着交给mapper.writeValue
进行写响应体
上面是SignUp.java
,大部分代码相似:
package com.example.javawebdemo.servlet;
import com.example.javawebdemo.dao.Dao;
import com.example.javawebdemo.entity.User;
import com.example.javawebdemo.response.ResponseBody;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/sign/up")
public class SignUp extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {req.setCharacterEncoding("utf-8");
resp.setCharacterEncoding("utf-8");
resp.setContentType("application/json;charset=utf-8");
String name = req.getParameter("name");
String password = req.getParameter("password");
Dao dao = new Dao();
User user = new User(name,password);
ResponseBody body = new ResponseBody();
ObjectMapper mapper = new ObjectMapper();
if (dao.insert(user)) {body.setCode(200);
body.setData("success");
} else {body.setCode(500);
body.setData("failed");
}
mapper.writeValue(resp.getWriter(), body);
}
}
测试Servlet
:
package com.example.javawebdemo.servlet;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/test")
public class Test extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {resp.getWriter().print("Hello, Java Web");
}
}
5.8 运行
须要借助 Tomcat
运行,抉择运行配置中的Tomcat Server
:
设置 Tomcat
根目录:
接着在 Deployment
抉择 +
后,抉择第二个带 exploded
的(当然第一个也不是不能够,不过第一个个别是公布到近程版本,是以 WAR
模式的,而第二个是间接将所有文件以当前目录模式复制到 webapps
下,并且在调试模式下反对热部署):
另外能够把这个门路批改为一个比较简单的门路,不便操作:
调试(运行不能进行热部署):
拜访 localhost:8080/demo
(IDEA
应该会主动关上)会呈现如下页面:
拜访门路下的 test
会呈现:
这样后端就解决实现了,上面解决 Android
端。
6 Android
端
6.1 新建我的项目
6.2 依赖 / 权限
依赖如下:
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.12.1'
在 build.gradle
中加上即可,另外,再加上:
buildFeatures{viewBinding = true}
viewBinding
就是视图绑定性能,以前是通过 findViewById
获取对应的组件,前面就有了 Butter Knife,到当初 Butter Knife
过期了,举荐应用view binding
。
另外在 AndroidManifest.xml
中退出网络权限:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
还须要增加 HTTP
的反对,因为这是一个示例 Demo
就不上 HTTPS
了,然而目前 Android
的版本默认不反对,因而须要在 <application>
增加:
android:usesCleartextTraffic="true"
6.3 我的项目构造
四个文件:
MainActivity
:外围Activity
NetworkSettings
:申请URL
,常量NetworkThread
:网络申请线程ResponseBody
:申请体
6.4 ResponseBody
package com.example.androiddemo;
public class ResponseBody {
private int code;
private Object data;
public int getCode() {return code;}
public Object getData() {return data;}
}
响应体,一个返回码字段 + 一个数据字段。
6.5 NetworkSettings
package com.example.androiddemo;
public class NetworkSettings {
private static final String HOST = "192.168.43.35";
private static final String PORT = "8080";
public static final String SIGN_IN = "http://"+ HOST +":"+PORT + "/demo/sign/in";
public static final String SIGN_UP = "http://"+ HOST +":"+PORT + "/demo/sign/up";
}
申请 URL
常量,HOST
请批改为本人的内网 IP
, 留神不能应用localhost
/127.0.0.1
。
能够应用 ip addr
/ifconfig
/ipconfig
等查看本人的内网IP
:
6.6 NetworkThread
package com.example.androiddemo;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Callable;
public class NetworkThread implements Callable<String> {
private final String name;
private final String password;
private final String url;
public NetworkThread(String name, String password, String url) {
this.name = name;
this.password = password;
this.url = url;
}
@Override
public String call(){
try {
// 开启连贯
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
// 拼接数据
String data = "name="+ URLEncoder.encode(name, StandardCharsets.UTF_8.toString())+"&password="+URLEncoder.encode(password,StandardCharsets.UTF_8.toString());
// 设置申请办法
connection.setRequestMethod("POST");
// 容许输入输出
connection.setDoInput(true);
connection.setDoOutput(true);
// 写数据(也就是发送数据)connection.getOutputStream().write(data.getBytes(StandardCharsets.UTF_8));
byte [] bytes = new byte[1024];
// 获取返回的数据
int len = connection.getInputStream().read(bytes);
return new String(bytes,0,len,StandardCharsets.UTF_8);
} catch (IOException e) {e.printStackTrace();
return "";
}
}
}
发送网络申请的线程类,因为是异步操作的线程,实现了 Callable<String>
接口,示意返回的是 String
类型的数据,主线程可通过 get()
阻塞获取返回值。
6.7 MainActivity
package com.example.androiddemo;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.example.androiddemo.databinding.ActivityMainBinding;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.concurrent.FutureTask;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
private final ObjectMapper mapper = new ObjectMapper();
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
}
public void signIn(View view){String name = binding.editTextName.getText().toString();
String password = binding.editTextPassword.getText().toString();
FutureTask<String> signInTask = new FutureTask<>(new NetworkThread(name,password,NetworkSettings.SIGN_IN));
Thread thread = new Thread(signInTask);
thread.start();
try{
//get 获取线程返回值,通过 ObjectMapper 反序列化为 ResponseBody
ResponseBody body = mapper.readValue(signInTask.get(),ResponseBody.class);
// 依据返回码确定提示信息
Toast.makeText(getApplicationContext(),body.getCode() == 200 ? "登录胜利" : "登录失败",Toast.LENGTH_SHORT).show();}catch (Exception e){e.printStackTrace();
}
}
public void signUp(View view){String name = binding.editTextName.getText().toString();
String password = binding.editTextPassword.getText().toString();
FutureTask<String> signUpTask = new FutureTask<>(new NetworkThread(name,password,NetworkSettings.SIGN_UP));
Thread thread = new Thread(signUpTask);
thread.start();
try{ResponseBody body = mapper.readValue(signUpTask.get(),ResponseBody.class);
Toast.makeText(getApplicationContext(),body.getCode() == 200 ? "注册胜利" : "注册失败",Toast.LENGTH_SHORT).show();}catch (Exception e){e.printStackTrace();
}
}
}
说一下 viewBinding
,在onCreate
中:
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
通过 ActivityMainBinding
的静态方法获取 binding
,留神ActivityMainBinding
这个类的类名不是固定的,比方 Android 官网的文档中就是:
6.8 资源文件
两个:
activity_main.xml
strings.xml
别离如下,不细说了:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textViewName"
android:layout_width="45dp"
android:layout_height="38dp"
android:layout_marginStart="24dp"
android:layout_marginTop="92dp"
android:text="@string/name"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/editTextName"
android:layout_width="300dp"
android:layout_height="40dp"
android:layout_marginStart="64dp"
android:layout_marginTop="84dp"
android:autofillHints=""android:inputType="text"app:layout_constraintLeft_toLeftOf="@id/textViewName"app:layout_constraintTop_toTopOf="parent"tools:ignore="LabelFor" />
<TextView
android:id="@+id/textViewPassword"
android:layout_width="45dp"
android:layout_height="36dp"
android:layout_marginStart="24dp"
android:layout_marginTop="72dp"
android:text="@string/password"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="@id/textViewName" />
<EditText
android:id="@+id/editTextPassword"
android:layout_width="300dp"
android:layout_height="40dp"
android:layout_marginStart="64dp"
android:layout_marginTop="72dp"
android:autofillHints=""android:inputType="textPassword"app:layout_constraintLeft_toLeftOf="@id/textViewPassword"app:layout_constraintTop_toTopOf="@id/editTextName"tools:ignore="LabelFor" />
<Button
android:id="@+id/buttonSignUp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="56dp"
android:layout_marginTop="32dp"
android:onClick="signUp"
android:text="@string/signUp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textViewPassword"
tools:ignore="ButtonStyle" />
<Button
android:id="@+id/buttonSignIn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:layout_marginEnd="52dp"
android:onClick="signIn"
android:text="@string/signIn"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/editTextPassword"
tools:ignore="ButtonStyle" />
</androidx.constraintlayout.widget.ConstraintLayout>
<resources>
<string name="app_name">AndroidDemo</string>
<string name="name"> 用户名 </string>
<string name="password"> 明码 </string>
<string name="signUp"> 注册 </string>
<string name="signIn"> 登录 </string>
</resources>
7 测试
7.1 本地测试
首先运行 Java Web
端,应该会主动关上如下界面:
附加 test
后:
运行 Android
端,先输出一个不存在的用户名或明码,提醒登录失败,再进行注册,而后登录胜利:
同时查看后端数据库如下:
7.2 部署测试
首先确保本地数据库的用户名与明码与服务器的用户名与明码统一。同时存在对应的表以及库
部署 Java Web
端之前先在 pom.xml
中退出一个<finalName>
:
在右侧的工具栏先抉择clean
,再抉择编译,最初抉择打包:
之所以这样做是因为如果更新了文件,打包不会把文件更新再打包进去,因而须要先革除原来的字节码文件,再编译最初打包。
实现后会呈现一个 demo.war
位于 target
下:
scp
(或其余工具)上传到服务器,并挪动到 Tomcat
的webapps
(为了不便阐明以下假如服务器的 IP
为8.8.8.8
):
scp demo.war 8.8.8.8/xxx
# 通过 ssh 连贯服务器后
cp demo.war /usr/local/tomcat/webapps
启动Tomcat
:
cd /usr/local/tomcat/bin
./startup.sh
启动后就能够看见在 webapps
下多了一个 demo
的文件夹:
拜访 8.8.8.8/demo
看到本地测试的页面就能够了。接着批改 Android
端的 NetworkSettings
中的 HOST
为8.8.8.8
,如果没问题的话就能失常拜访了:
服务器数据库:
8 注意事项
注意事项比拟琐碎而且有点多,因而另开了一篇博客,戳这里。
如果还有其余问题欢送留言。
9 源码
提供了 Java
+Kotlin
两种语言实现:
- Github
- 码云
- CODE.CHINA
如果感觉文章难看,欢送点赞。
同时欢送关注微信公众号:氷泠之路。