1 前言&概述

这篇文章是基于此处文章的更新,更新了一些技术栈,更加贴近理论须要,以及修复了若干的谬误。

这是一个前端Android+后端Java/Kotlin通过Servelt进行后盾数据库(MySQL)交互的具体步骤以及源码实现,技术栈:

  • Android根底
  • 原生JDBC+原生Servlet
  • Tomcat+MySQLDocker

当然当初的很多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-getyumdnfpacman等)
  • 下载压缩包装置
  • 源码编译装置
  • 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,如果须要GradleKotlin的话自行勾选即可:

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;@Getterpublic 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();

一般来说:

  • selectexecuteQuery()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@Getterpublic 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/demoIDEA应该会主动关上)会呈现如下页面:

拜访门路下的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(或其余工具)上传到服务器,并挪动到Tomcatwebapps(为了不便阐明以下假如服务器的IP8.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中的HOST8.8.8.8,如果没问题的话就能失常拜访了:

服务器数据库:

8 注意事项

注意事项比拟琐碎而且有点多,因而另开了一篇博客,戳这里。

如果还有其余问题欢送留言。

9 源码

提供了Java+Kotlin两种语言实现:

  • Github
  • 码云
  • CODE.CHINA

如果感觉文章难看,欢送点赞。

同时欢送关注微信公众号:氷泠之路。