赞
踩
这篇文章是基于此处文章的更新,更新了一些技术栈,更加贴近实际需要,以及修复了若干的错误。
这是一个前端Android
+后端Java/Kotlin
通过Servelt
进行后台数据库(MySQL
)交互的详细步骤以及源码实现,技术栈:
Android
基础JDBC
+原生Servlet
Tomcat
+MySQL
(Docker
)当然现在的很多Java
后端开发都使用了Spring Boot
而不是原生的Servlet
,所以使用Spring Boot
实现的可以笔者的另一篇文章。
尽管基于Spring Boot
实现非常的简便,但是使用原生的Servlet
更能理解底层的原理。另外本篇文章是偏基础向的教程,很多步骤都会比较详细而且附上了图,好了废话不说,正文开始。
Android Studio 4.1.2
IntelliJ IDEA 2020.3
MySQL 8.0.23
Tomcat 10.0
Docker 20.10.1
CentOS 8.1.1911
IDE
准备官网安装Android Studio
+IDEA
,这部分就省略了。
MySQL
这里的MySQL
若无特殊说明指的是MySQL Community
。
首先,在Windows
下,MySQL
提供了exe
安装包:
macOS
下提供了dmg
安装包:
可以戳这里下载。
Linux
下一般来说MySQL
安装有如下方式:
apt/apt-get
、yum
、dnf
、pacman
等)Docker
安装其中相对省事的安装方式为Docker
安装以及软件包安装,其次是压缩包方式安装,特别不建议源码安装(当然如果喜欢挑战的话可以参考笔者的一篇编译安装8.0.19以及编译安装8.0.20)。
这里笔者本地测试选择的是使用Docker
安装,步骤可以查看这里。
另外对于服务器,也可以使用Docker
安装,如果使用软件包安装的话,这里以笔者的CentOS8
为例,其他系统的参考如下:
添加仓库:
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
启动服务:
systemctl start mysqld
查看临时密码:
sudo grep 'temporary password' /var/log/mysqld.log
输入临时密码登录:
mysql -u root -p
修改密码:
alter user 'root'@'localhost' identified by 'PASSWORD'
不建议在Java
中直接访问root
用户,一般是新建一个对应权限的用户并进行访问,这里就为了方便就省略了。
Tomcat
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
。
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
即可观察是否成功。
这里使用到的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
)
mysql -u root -p < user.sql
因为是比较基础向的教程,所以先从创建项目开始吧。
选择对应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>
这样第一步就完成了。
项目结构如下:
Dao
User
ResponseBody
Servlet
层:SignIn
/SignUp
/Test
DBUtils
Web
服务器中运行先创建好文件以及目录:
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");
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;
}
}
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
,不能直接判断是否插入成功。
添加一个响应体类方便设置返回码以及数据:
package com.example.javawebdemo.response;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class ResponseBody{
private Object data;
private int code;
}
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"); } }
需要借助Tomcat
运行,选择运行配置中的Tomcat Server
:
设置Tomcat
根目录:
接着在Deployment
选择+
后,选择第二个带exploded
的(当然第一个也不是不可以,不过第一个一般是发布到远程版本,是以WAR
形式的,而第二个是直接将所有文件以当前目录形式复制到webapps
下,并且在调试模式下支持热部署):
另外可以把这个路径修改为一个比较简单的路径,方便操作:
调试(运行不能进行热部署):
访问localhost:8080/demo
(IDEA
应该会自动打开)会出现如下页面:
访问路径下的test
会出现:
这样后端就处理完成了,下面处理Android
端。
Android
端依赖如下:
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"
四个文件:
MainActivity
:核心Activity
NetworkSettings
:请求URL
,常量NetworkThread
:网络请求线程ResponseBody
:请求体ResponseBody
package com.example.androiddemo;
public class ResponseBody {
private int code;
private Object data;
public int getCode() {
return code;
}
public Object getData() {
return data;
}
}
响应体,一个返回码字段+一个数据字段。
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
:
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()
阻塞获取返回值。
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官方的文档中就是:
两个:
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>
首先运行Java Web
端,应该会自动打开如下界面:
附加test
后:
运行Android
端,先输入一个不存在的用户名或密码,提示登录失败,再进行注册,然后登录成功:
同时查看后端数据库如下:
首先确保本地数据库的用户名与密码与服务器的用户名与密码一致。同时存在对应的表以及库
部署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
,如果没问题的话就能正常访问了:
服务器数据库:
注意事项比较琐碎而且有点多,因此另开了一篇博客,戳这里。
如果还有其他问题欢迎留言。
提供了Java
+Kotlin
两种语言实现:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。