之前我们开发的贷款计算器,属于单机程序,同一时刻只能有一个用户使用,为了支持多用户同时使用贷款计算器,需要改为 web 程序,也就是网络应用程序
网络应用程序,需要采用下面的软件架构方式
这样,多个客户端都可以通过网络连接至服务器,通过【请求】(绿色箭头)将信息传输给服务器,服务器运算后,通过【响应】(蓝色箭头)将计算结果返回给客户端,见下图
这种架构方式称为 Client/Server 架构,为了能支持 C/S 架构,需要引入一个新的开发技术 Spring Boot
它内置了一个软件 Tomcat,可以与客户端进行交互
使用系统自带的浏览器就可以充当架构中的客户端
浏览器客户端要与服务器交互,请求和响应都需要遵从一定的格式,发请求时需要遵从一种 URL 格式
协议://主机[:端口]/路径[?查询参数]
Spring Boot 不光内嵌 Tomcat,省却了搭建服务程序的成本,还提供了方便处理 C/S 开发中输入和输出的类(对服务程序来说,输入就对应着请求,输出就对应着响应),核心类库处理控制台的输入输出还行,但处理 C/S 下的请求、响应就不够看了,Spring boot 是由 VMware 公司维护的一个开源框架,让我们能快速开发独立的、生产级的应用程序。
什么是框架,它包含两方面
主要关注图中红框的几处位置就可以,都设置完了,点击获取代码,它就会根据刚才的选择生成一个压缩包并下载。
骨架代码生成好之后,还有两个准备工作要做一下
刚才也说了,如果是官网生成的骨架,版本不用改,springboot 就用 2.7.0 Java 就用 17,但 aliyun 生成的骨架版本较低,需要修改一下,怎么改呢
maven 到底是干嘛的呢,以后咱们进行 java 开发,会用到很多第三方的压缩 jar 包,例如今天学的 spring boot,spring boot 中的 web 组件等等,都是以这种 jar 包形式提供的,这些 jar 包需要下载到本地才能使用吧,如果让我们人工下载管理的话,显然太过麻烦,maven 就是这么一个工具,能帮我们下载、管理这些第三方的 jar 包。
idea 已经集成了maven 工具,但是由于maven默认会连接国外地址进行下载,不仅下载很慢还容易出错,因此需要用配置将 jar 包下载地址改为国内的
找到 idea 带的 maven 配置文件
D:\ideaIC-2022.1.win\plugins\maven\lib\maven3\conf\settings.xml
在内部加入如下的配置
<mirrors>
<mirror>
<id>central</id>
<mirrorOf>central</mirrorOf>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
<!-- ... -->
</mirrors>
准备工作做好了之后,就可以把刚才的骨架代码加入到当前项目中了。
先到下载目录,把刚刚修改好的 module2 目录,复制到当前项目目录下
切换回 idea 可以看到已经有了,不过 idea 还没有把这个文件夹识别为模块,这时找到 module2 下的 pom.xml 文件,右键点击它,选择【添加为 maven 项目】,就可以了。
骨架代码中,有一个 XxxApplication 类,它其中的 main 方法是程序的入口,但与控制台版的 Hello world 不同,要通过 C/S 的方式处理输入和输出,需要学习一点新的知识
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hi() {
return "hello world";
}
}
这段代码描述的是一个称为控制器的类,用来处理输入、输出。
方法上还有分别跟输入、输出相关的两个注解
这些注解就是框架提供的条条框框
这节课来学习控制器如何处理输入
客户端这边当然可以以查询参数的方式将数据传递给服务器,但这样对于使用者不太友好,因此会用到网页技术,以更友好的方式处理数据。
网页里就可以用下面文本框接收用户的输入,作为查询参数,对用户隐藏了复杂性,提高了用户体验,我现在想做的效果很简单
就是在文本块中输入一个名字 xxx,点击 say hello, 由服务器返回 hello, xxx
页面要放在 static 目录下,页面也需要通过 URL 来访问,格式为 http://localhost:8080/页面名称
双击 hello.html 网页,就看到了它的源代码,这段代码里包含了两种语言,html 语言 和 javascript 语言(简称js),不过这些代码绝大部分也不需要理解,并非我们的重点,我只会讲作用,不会讲语法
其中文本框对应的代码是这一行,<input type="text" name="name" id="n">
,可以接收用户键入的文字
var n = document.querySelector("#n").value;
是页面中用的 js 语言,=右边的代码作用是获取用户在文本框输入的值,var n 是定义了一个变量,用来代表=号后的值。但是输入的值目前还在浏览器这边,并没有通过网络传输给服务器
接下来按格式拼接好 URL,"http://localhost:8080/hello?name=" + n
这是 js 中一种拼接字符串的方法
fetch 处的代码才是真正根据 URL 发送请求,并把服务器返回的内容显示出来,但这部分暂不需要了解
运行,发现总是返回 hello world,这是因为hello控制器这边还未对查询参数做出任何处理,下面就来解决这个问题
hi 如何接收请求中的查询参数呢,一种方式就是提供同名的方法参数
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
String hi(String name) { // ?? 这里的方法参数要和查询参数【名称】一致
return "Hello, " + name;
}
}
加法例子,这回要提供两个参数,两个参数用 int 接收即可,其实请求中的参数类型原始是 String,Spring 会把 String 转换为 int
@Controller
public class AddController {
@RequestMapping("/add")
@ResponseBody
int add(int a, int b) {
return a + b;
}
}
页面上的 URL 拼接有两种方法
`http://localhost:8080/add?a=${a}&b=${b}`
和
"http://localhost:8080/add?a=" + a + "&b=" + b
其中 a 和 b 来自于页面的两个数字框
需求1,计算还款总额,这个其实很简单
@Controller
public class CalController {
@RequestMapping("/cal")
@ResponseBody
String[] cal(double p, int m, double yr) {
double mr = yr / 12 / 100.0;
double pow = Math.pow(1 + mr, m);
double payment = p * mr * pow / (pow - 1);
return NumberFormat.getCurrencyInstance().format(payment * m);
}
}
需求2,计算还款总额,以及利息总额
方法只能返回一个结果,现在要返回两个值,可以使用数组。数组可以容纳多个值。
定义字符串数组、给数组的两个元素赋值
String[] a = new String[2];
a[0] = "hello";
a[1] = "world";
定义并赋值(一步完成)
String[] a = new String[]{"hello", "world"};
后者可以简化为
String[] a = {"hello", "world"};
前端页面要求
数组索引为0的元素,代表还款总额
数组索引为1的元素,代表利息总额
因此代码改写如下
@Controller
public class CalController {
@RequestMapping("/cal")
@ResponseBody
String[] cal(double p, int m, double yr) {
double mr = yr / 12 / 100.0;
double pow = Math.pow(1 + mr, m);
double payment = p * mr * pow / (pow - 1);
// [0]还款总额 [1]总利息
return new String[]{
NumberFormat.getCurrencyInstance().format(payment * m),
NumberFormat.getCurrencyInstance().format(payment * m - p)
};
}
}
数组呢,有一个需要大家注意的地方就是。这个数组一旦它的长度确定了,就不能改了。并且啊,不能越过他的这个大小限制,比如说对于下面的数组
String[] a = {"hello", "world"};
我想访问他的索引2的元素行不行?
即逐一获取数组中的每个元素进行操作
for(int i = 0; i < a.length; i++) {
System.out.println(a[i]); // 获取数组中第 i 个元素
}
整数数组,new int[5],创建了五个元素的数组,并未给元素赋初始值,这时元素默认值是 0
类似的
比如我想表示一张表格
可以用二维数组来表示,就是数组里面再套一个数组
例如上面的表格可以表示为:外层数组长度3,内层数组长度5,内层长度可以不指定
String[][] a2 = new String[3][5];
即
a2[0] = new String[]{"1", "¥33,667.22", "¥33,167.22", "¥500.00", "¥66,832.78"};
a2[1] = new String[]{"2", "¥33,667.22", "¥33,333.06", "¥334.16", "¥33,499.72"};
a2[2] = new String[]{"3", "¥33,667.22", "¥33,499.72", "¥167.50", "¥0.00"};
如何动态生成这张表格呢
回忆一下以前的代码
double mr = yr / 100.0 / 12.0;
double pow = Math.pow((1 + mr), m);
double payment = p * mr * pow / (pow - 1); // 月供
for (int i = 0; i < m; i++) {
double payInterest = p * mr; // 月利息=剩余本金*月利率
double payPrincipal = payment - payInterest; // 月本金=月供-月利息
p -= payPrincipal; // 更新剩余本金
}
只需要把月供、月利息、月本金、剩余本金等作为 row,多个 row 作为二维数组即可。得到最终用二维数组改写的代码
@Controller
public class CalController {
// ...
@RequestMapping("/details")
@ResponseBody
String[][] details(double p, int m, double yr, int type) {
String[][] a2 = new String[m][]; // 二维数组
double mr = yr / 12 / 100.0;
double pow = Math.pow(1 + mr, m);
double payment = p * mr * pow / (pow - 1); // 月供
for (int i = 0; i < m; i++) {
double payInterest = p * mr; // 偿还利息
double payPrincipal = payment - payInterest; // 偿还本金
p -= payPrincipal; // 剩余本金
String[] row = new String[]{ // 一行的数据
(i + 1) + "",
NumberFormat.getCurrencyInstance().format(payment),
NumberFormat.getCurrencyInstance().format(payPrincipal),
NumberFormat.getCurrencyInstance().format(payInterest),
NumberFormat.getCurrencyInstance().format(p)
};
a2[i] = row; // 将每一行放入二维数组
}
return a2;
}
}
增加按等额本金计算的功能
页面新增了还款类型,等额本息的类型为 0,等额本金的类型为1
改写后代码如下
@Controller
public class CalController {
@RequestMapping("/cal")
@ResponseBody
String[] cal(double p, int m, double yr, int type) {
if (type == 0) { // 等额本息
return cal0(p, m, yr);
} else { // 等额本金
return cal1(p, m, yr);
}
}
@RequestMapping("/details")
@ResponseBody
String[][] details(double p, int m, double yr, int type) {
if (type == 0) {
return details0(p, m, yr);
} else {
return details1(p, m, yr);
}
}
static String[] cal0(double p, int m, double yr) {
double mr = yr / 12 / 100.0;
double pow = Math.pow(1 + mr, m);
double payment = p * mr * pow / (pow - 1);
return new String[]{
NumberFormat.getCurrencyInstance().format(payment * m),
NumberFormat.getCurrencyInstance().format(payment * m - p)
};
}
static String[] cal1(double p, int m, double yr) {
double payPrincipal = p / m; // 偿还本金
double backup = p; // 备份本金
double mr = yr / 12 / 100.0;
double payInterestTotal = 0.0; // 总利息
for (int i = 0; i < m; i++) {
double payInterest = p * mr; // 偿还利息
p -= payPrincipal; // 剩余本金
payInterestTotal += payInterest;
}
// [0]还款总额 [1]总利息
return new String[]{
NumberFormat.getCurrencyInstance().format(backup + payInterestTotal),
NumberFormat.getCurrencyInstance().format(payInterestTotal)
};
}
static String[][] details0(double p, int m, double yr) {
String[][] a2 = new String[m][];
double mr = yr / 12 / 100.0;
double pow = Math.pow(1 + mr, m);
double payment = p * mr * pow / (pow - 1); // 月供
for (int i = 0; i < m; i++) {
double payInterest = p * mr; // 偿还利息
double payPrincipal = payment - payInterest; // 偿还本金
p -= payPrincipal; // 剩余本金
String[] row = new String[]{ // 一行的数据
(i + 1) + "",
NumberFormat.getCurrencyInstance().format(payment),
NumberFormat.getCurrencyInstance().format(payPrincipal),
NumberFormat.getCurrencyInstance().format(payInterest),
NumberFormat.getCurrencyInstance().format(p)
};
a2[i] = row;
}
return a2;
}
static String[][] details1(double p, int m, double yr) {
// 等额本金
double payPrincipal = p / m; // 偿还本金
double mr = yr / 12 / 100.0;
String[][] a2 = new String[m][];
for (int i = 0; i < m; i++) {
double payInterest = p * mr; // 偿还利息
p -= payPrincipal; // 剩余本金
double payment = payPrincipal + payInterest; // 月供
String[] row = new String[]{
(i + 1) + "",
NumberFormat.getCurrencyInstance().format(payment),
NumberFormat.getCurrencyInstance().format(payPrincipal),
NumberFormat.getCurrencyInstance().format(payInterest),
NumberFormat.getCurrencyInstance().format(p)
};
a2[i] = row;
}
return a2;
}
}
对输入的数据做合法检查
改写后的代码
@Controller
public class CalController {
@RequestMapping("/cal")
@ResponseBody
String[] cal(double p, int m, double yr, int type) {
check(p, m, yr, type); // 检查通过,才会向下运行
if (type == 0) { // 等额本息
return cal0(p, m, yr);
} else { // 等额本金
return cal1(p, m, yr);
}
}
@RequestMapping("/details")
@ResponseBody
String[][] details(double p, int m, double yr, int type) {
check(p, m, yr, type); // 检查通过,才会向下运行
if (type == 0) {
return details0(p, m, yr);
} else {
return details1(p, m, yr);
}
}
// 检查方法
static void check(double p, int m, double yr, int type) {
if (p <= 0) {
throw new IllegalArgumentException("贷款金额必须>0");
}
if (m < 1 || m > 360) {
throw new IllegalArgumentException("贷款月份必须在 1~360 之间");
}
if (yr < 1.0 || yr > 36.0) {
throw new IllegalArgumentException("年利率必须在 1~36 之间");
}
if (type != 0 && type != 1) {
throw new IllegalArgumentException("不支持的还款类型");
}
}
// ...
}
需要在 application.properties 文件中加入
server.error.include-message=always
这样才能在页面上显示错误详情
Web 程序打包与之前控制台打包操作不一样,参考下图
使用 package 打包
打包成功后,会在 module2 模块下 target 目录下找到这个打好的 jar 包,运行 jar 包的方法是一样的,进入 jar 包所在目录,用终端程序执行
java -jar module2-0.0.1-SNAPSHOT.jar