开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

推荐 9 个 GitHub 上练手项目(在线考试、仿美团、仿

发表于 2021-01-14

GitHub项目推荐

推荐的这几个 GitHub 项目并不是简单的 XX 管理系统,我会从下面这些方向推荐几个入门级别但是不那么 Low 的项目。

01 JavaWeb项目

学之思在线考试系统

这个项目是是一款 Spring 技术栈 + Vue.js 的前后端分离的考试系统,分为学生端、用户端、小程序端,能覆盖到 PC 和移动端。。界面美观、设计友好、代码结构清晰,即使是应届生找工作,这个项目也是不错的练手手项目。

并且支持多种部署方式:集成部署、前后端分离部署、docker部署。

地址:github.com/mindskip/xz…

学生端

img

管理端

img

小程序端

img

在线考试

这个项目后端采用 Spring Boot + JPA + Swagger2 + JWT 技术栈,前端使用 Vue + AntDesign 技术。都是在线考试系统,但这个系统的 UI 界面比上一个项目好看一点。

地址:github.com/19920625lsg/spring-boot-online-exam

登陆

img

首页

img

img

答题

img

考试管理

img

考试列表

img

外卖系统

一个完整的外卖系统,包括手机端,后台管理。基于 Spring Boot 和 Vue 的前后端分离的外卖系统,包含完整的手机端,后台管理功能。

地址:gitee.com/microapp/flash-waimai

img

电影院选座系统

开发技术 : Spring MVC + Spring + MyBatis 框架,MySQL数据库。支付宝沙箱支付 LayUI 百度 Echarts 图表 Redis 缓存中间件。特色:支付、可视化、智能选座等。

地址:gitee.com/bysj2021/ci…

img

02 移动端项目

仿美团外卖点餐

前端用 vue+vuex+vue-router+axios,后端基于nodej.s的框架,数据库采用mongodb。功能涉及登录,定位,浏览商品,加购物车,下订单,支付(微信、支付宝扫码支付),评价,个人信息更改。

项目地址:github.com/zwStar/vue-…

img

精仿今日头条

数据是抓取今日头条App的数据。使用 RxJava + Retrofit + MVP 开发的开源项目。

项目地址:github.com/chaychan/To…

img

下面这两个 GitHub 项目,都是移动端开发者开发的复制版抖音 App,老逛以前也推荐过。

iOS 仿抖音

这个抖音 Demo 适配 iPhone、iPad,同时兼容 iOS 8.0 - iOS 12.0系统。采用 Object-C 语言编写。标星 1.5K Star,项目地址:

github.com/sshiqiao/do…

本项目共分为三个部分:抖音个人主页实现、网络视频相关功能实现、WebSocket 实现 IM 即时聊天功能。

img

img

Android 仿抖音

这个 Demo 涉及的技术要点如下:

  • Recycler + PagerSnapHelper 实现全屏切换播放效果,
  • 使用 Lottie 库加载 Json 动画
  • BottomSheetDialogFragment 实现分享评论弹框功能
  • CoordinatorLayout + AppBarLayout 实现折叠布局。

该项目标星 1K Star,项目地址:

github.com/18380438200…

img

秀视频

这个项目是一个短视频社交小程序,系统包括用户端和后台管理端。用户可以在小程序上发布自己的短视频,并且经过我们的平台加入滤镜或者背景音乐制作出独具特色的短视频。具备点赞、评论、下载、分享、转发等功能。技术栈如下:

前端: H5、CSS、JavaScript 、JQuery、Bootstrap、Themeleaf

后端:Spring Cloud、Spring Boot、Sping、Spring MVC、MyBatis、MySQL、Redis、Shiro

组件:Bootsrap-table、webUploader、PageHelper

项目地址:github.com/RAOE/show-v…

斗鱼直播 APP

flutter 重构的斗鱼直播 APP,首页、娱乐为Material组件;直播间、鱼吧为纯自定义编写。

地址:github.com/yukilzw/dy_…

img

img

仿网易云音乐

基于 flutter 的仿网易云音乐软件,支持 iOS 和 Android。

地址:github.com/boyan01/flu…

img

高仿 B站

基于 react + express 高仿B站 Web 移动端

链接:github.com/code-mcx/re…

img

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

了解一下RPC,为何诞生RPC,和HTTP有什么不同? 了解

发表于 2021-01-14

了解一下RPC,为何诞生RPC,和HTTP有什么不同?

开篇提问

  1. 什么是RPC?
  2. 为什么需要RPC,用来解决什么问题?
  3. RPC与HTTP有什么不同?
  4. 你知道几种RPC?

认识RPC

RPC:Remote Procedure Call,远程过程调用。是指计算机程序使过程在不同的地址空间(通常在共享网络的另一台计算机上)执行时,其编码方式就像是正常的(本地)过程调用,而无需程序员明确为远程交互编码细节。

RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。

RPC是进程间通信(IPC)的一种形式,因为不同的进程具有不同的地址空间:如果在同一主机上,即使物理地址空间相同,它们也具有不同的虚拟地址空间;如果它们位于不同的主机上,则物理地址空间是不同的。许多不同的(通常是不兼容的)技术已被用来实现这一概念。

【简单理解】:两台不同计算机(程序),计算机A有一个约定协议,计算机B想调用计算机A需要通过约定协议来进行通讯调用。

RPC的诞生

其实早在1982年左右RPC就被人用来做分布式系统的通信,最早发明『远程过程调用』这个词语的人是『布鲁斯·杰伊·尼尔森 (Bruce Jay Nelson)』大约是在1981年。

我们所熟知的Java在1.1版本提供了Java版本的RPC框架(RMI),此时在1990年后,基本上RPC被广泛用于系统之间的调用。但是只在后端方向熟知,对于大众更多的还是接触HTTP等协议,也因此RPC更晚让大众了解认知。

RPC与HTTP

先讲讲HTTP

HTTP:Hypertext Transfer Protocol即超文本传输协议。

HTTP协议在1990年才开始作为主流协议出现;之所以被我们所熟知,是因为通常HTTP用于web端,也就是web浏览器和web服务器交互。当ajax和json在前端大行其道的时候,json也开始发挥其自身能力,简洁易用的特性让json成为前后端数据传输主流选择。HTTP协议中以Restful规范为代表,其优势很大。它可读性好,且可以得到防火墙的支持、跨语言的支持。

HTTP的缺点也很快暴露:

  1. 有用信息占比少,HTTP在OSI的第七层,包含了大量的HTTP头等信息
  2. 效率低,因为第七层的缘故,中间有很多层传递
  3. HTTP协议调用远程方法复杂,需要封装各种参数名和参数值以及加密通讯等

所以RPC好在哪?

  1. 都是有用信息
  2. 效率高
  3. 调用简单
  4. 无需关心网络传输或者通讯问题

HTTP和RPC其实有联系

http也是rpc实现的一种方式。

RPC和HTTP一句话说不同

RPC就像地区方言,只有内部知道,双方都需要知道方言,不然没法沟通

HTTP就是普通话,基本都能懂,也会说

RPC一般用于什么地方?

在微服务、分布式已经成为日常的今天,服务通常都部署在不同的服务器,服务器也在不同地区,这时候就存在跨地域跨服务器调用问题,RPC即用于这样类似的情况。

RPC适用于公司内部使用,性能消耗低,传输效率高,服务治理方便,但是不建议传输较大的文本、视频等。

篇末提问

  1. 是否让你理解HTTP和RPC的一些不同了?
  2. RPC用来干嘛了解了吗?
  3. 你有在用RPC吗?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

String还有长度限制?是多少? 前言 String

发表于 2021-01-14

前言

话说Java中String是有长度限制的,听到这里很多人不禁要问,String还有长度限制?是的有,而且在JVM编译中还有规范,而且有的家人们在面试的时候也遇到了。

本人就遇到过面试的时候问这个的,而且在之前开发的中也真实地遇到过这个String长度限制的场景(将某固定文件转码成Base64的形式用字符串存储,在运行时需要的时候在转回来,当时文件比较大),那这个规范限制到底是怎么样的,咱们话不多说先䁖䁖去。关于Java项目还整理了100+Java项目视频+源码+笔记,地址:100+Java项目视频+源码+笔记

String

首先要知道String的长度限制我们就需要知道String是怎么存储字符串的,String其实是使用的一个char类型的数组来存储字符串中的字符的。

那么String既然是数组存储那数组会有长度的限制吗?是的有限制,但是是在有先提条件下的,我们看看String中返回length的方法。

由此我们看到返回值类型是int类型,Java中定义数组是可以给数组指定长度的,当然不指定的话默认会根据数组元素来指定:

1
2
ini复制代码int[] arr1 = new int[10]; // 定义一个长度为10的数组
int[] arr2 = {1,2,3,4,5}; // 那么此时数组的长度为5

整数在java中是有限制的,我们通过源码来看看int类型对应的包装类Integer可以看到,其长度最大限制为2^31 -1,那么说明了数组的长度是0~2^31-1,那么计算一下就是(2^31-1 = 2147483647 = 4GB)

看到这我们尝试通过编码来验证一下上述观点。

以上是我通过定义字面量的形式构造的10万个字符的字符串,编译之后虚拟机提示报错,说我们的字符串长度过长,不是说好了可以存21亿个吗?为什么才10万个就报错了呢?

其实这里涉及到了JVM编译规范的限制了,其实JVM在编译时,如果我们将字符串定义成了字面量的形式,编译时JVM是会将其存放在常量池中,这时候JVM对这个常量池存储String类型做出了限制,接下来我们先看下手册是如何说的。

常量池中,每个 cp_info 项的格式必须相同,它们都以一个表示 cp_info 类型的单字节 “tag”项开头。后面 info[]项的内容 由tag 的类型所决定。

我们可以看到 String类型的表示是 CONSTANT_String ,我们来看下CONSTANT_String具体是如何定义的。

这里定义的 u2 string_index 表示的是常量池的有效索引,其类型是CONSTANT_Utf8_info 结构体表示的,这里我们需要注意的是其中定义的length我们看下面这张图。

在class文件中u2表示的是无符号数占2个字节单位,我们知道1个字节占8位,2个字节就是16位 ,那么2个字节能表示的范围就是2^16- 1 = 65535 。范中class文件格式对u1、u2的定义的解释做了一下摘要:

#这里对java虚拟机规摘要部分

##1、class文件中文件内容类型解释

定义一组私有数据类型来表示 Class 文件的内容,它们包括 u1,u2 和 u4,分别代 表了 1、2 和 4 个字节的无符号数。

每个 Class 文件都是由 8 字节为单位的字节流组成,所有的 16 位、32 位和 64 位长度的数 据将被构造成 2 个、4 个和 8 个 8 字节单位来表示。

##2、程序异常处理的有效范围解释

start_pc 和 end_pc 两项的值表明了异常处理器在 code[]数组中的有效范围。

start_pc 必须是对当前 code[]数组中某一指令的操作码的有效索引,end_pc 要 么是对当前 code[]数组中某一指令的操作码的有效索引,要么等于 code_length 的值,即当前 code[]数组的长度。start_pc 的值必须比 end_pc 小。

当程序计数器在范围[start_pc, end_pc)内时,异常处理器就将生效。即设 x 为 异常句柄的有效范围内的值,x 满足:start_pc ≤ x < end_pc。

实际上,end_pc 值本身不属于异常处理器的有效范围这点属于 Java 虚拟机历史上 的一个设计缺陷:如果 Java 虚拟机中的一个方法的 code 属性的长度刚好是 65535 个字节,并且以一个 1 个字节长度的指令结束,那么这条指令将不能被异常处理器 所处理。

不过编译器可以通过限制任何方法、实例初始化方法或类初始化方法的code[]数组最大长度为 65534,这样可以间接弥补这个 BUG。

注意:这里对个人认为比较重要的点做了标记,首先第一个加粗说白了就是说数组有效范围就是【0-65565】但是第二个加粗的地方又解释了,因为虚拟机还需要1个字节的指令作为结束,所以其实真正的有效范围是【0-65564】,这里要注意这里的范围仅限编译时期,如果你是运行时拼接的字符串是可以超出这个范围的。

接下来我们通过一个小实验来测试一下我们构建一个长度为65534的字符串,看看是否就能编译通过。0期阶段汇总

首先通过一个for循环构建65534长度的字符串,在控制台打印后,我们通过自己度娘的一个在线字符统计工具计算了一下确实是65534个字符,如下:

然后我们将字符复制后以定义字面量的形式赋值给字符串,可以看到我们选择这些字符右下角显示的确实是65534,于是乎运行了一波,果然成功了。

#看到这里我们来总结一下:

##字符串有长度限制吗?是多少?

首先字符串的内容是由一个字符数组 char[] 来存储的,由于数组的长度及索引是整数,且String类中返回字符串长度的方法length() 的返回值也是int ,所以通过查看java源码中的类Integer我们可以看到Integer的最大范围是2^31 -1,由于数组是从0开始的,所以数组的最大长度可以使【0~2^31】通过计算是大概4GB。

但是通过翻阅java虚拟机手册对class文件格式的定义以及常量池中对String类型的结构体定义我们可以知道对于索引定义了u2,就是无符号占2个字节,2个字节可以表示的最大范围是2^16 -1 = 65535。

其实是65535,但是由于JVM需要1个字节表示结束指令,所以这个范围就为65534了。超出这个范围在编译时期是会报错的,但是运行时拼接或者赋值的话范围是在整形的最大范围。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

怎样挑选海外服务器不踩雷?

发表于 2021-01-14

说到海外服务器,大家使用比较多的是韩国服务器、香港服务器、美国服务器。因为这三种海外服务器相较成熟,现在华纳云小编就给大家讲述一下如何选择海外服务器。

1.配置选择:需根据企业本身的业务而定,一般来讲服务器的配置越高,价格越高昂。

2.带宽选择:带宽的大小直接影响网站的访问速度与稳定性,可分为共享带宽和独享带宽。共享是选用与其他用户共有同个机柜,运行速度会受到相互之间的影响。独享宽带稳定性较好,客户资源独立,不受其他用户影响,价格相对较贵。

3.机房区域选择:由于每个国家的兴旺程度不一样,服务器资源各不相同,这就直接导致了不同区域的机房价格相差较远,选购时建议遵循就近原则同时要符合自身企业需求的机房。

4.服务商优惠活动:目前较多服务商会在节假日定期推出各种优惠活动,在租用或续费上都能得到比平时更大的优惠。这需要企业平时多关注各服务商网站,选择一款最适合自己的服务器。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

打印快递鸟电子面单

发表于 2021-01-14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
php复制代码<div id="hh">
打印内容展示
</div>
<button class="btn btn-default btn-xs" type="button" id ="aaa">打印</button>


$(document).ready(function(){
$("#aaa").click(function(){
$.ajax({
type:"post",
url:"{:url('Mall/orderface')}",
data:'number='+$number+'&id='+$id,
dataType:'json',
success:function(data){
if (data.ret == 0){
alert(data.msg);
}else{
$count = data.data.length;
for (var i=0;i<$count;i++){
$('#hh').html(data.data);
$('#hh').show();
$("#hh").printArea();
$('#hh').hide();
}
}
}
});
});
});

public function orderface(){
$id = $_POST['id'];
$number = $_POST['number'];
$config_model = new Config();
$config = $config_model->findOne(['uid'=>UID]);
$mall_order_model = new MallOrder();
$order = $mall_order_model->findOne(['uid' => 2, 'id' => $id]);
//构造电子面单提交信息
//收件人信息
$receiver = [
"Name"=>$order['linkman'],//收件人
"Mobile"=>$order['mobile_phone'],//电话与手机,必填一个
"ProvinceName"=>$order['province'],//收件省
"CityName"=>$order['city'],//收件市
"ExpAreaName"=>$order['area'],//收件区/县
"Address"=>$order['address'],//收件人详细地址
];
//发件人信息
$sender = [
"Name"=>$config['sender_name'],//发件人
"Mobile"=>$config['sender_phone'],//电话与手机,必填一个
"ProvinceName"=>$config['province'],//收件省
"CityName"=>$config['city'],//发件市
"ExpAreaName"=>$config['area'],//发件区/县
"Address"=>$config['sender_address'],//发件人详细地址
];
$commodity = [];
if ($id) {
$order_id = $id;
$mall_order_product_model = new MallOrderProduct();
$order_product = $mall_order_product_model->selectList(['order_id' => array('IN', $order_id)]);
foreach ($order_product as $key => $value) {
$sku_name = $this->skuToName($value['product_sku'], $value['product_id']);
$arr = [
"GoodsName"=>$value['product_name'].$sku_name,
"Goodsquantity"=>$value['number'],
"GoodsPrice"=>$value['product_price']
];
array_push($commodity,$arr);
}
}
$kdiniao_express_model = new KdiniaoExpress();
$code = $kdiniao_express_model->where(['id'=>$order['express_id']])->find();
$eorder = [
"ShipperCode"=>$code['encipher'],//快递公司编码
"OrderCode"=>$order['order_number'],//订单编号(自定义,不可重复)
"PayType"=>1,//邮费支付方式:1-现付,2-到付,3-月结,4-第三方支付(仅SF支持)
"ExpType"=>1,//快递类型:1-标准快件
"Sender"=>$sender,
"Receiver"=>$receiver,
"Commodity"=>$commodity,
"Quantity"=>(int)$number,//包裹数(最多支持30件)
"Remark"=>"",//备注
"IsReturnPrintTemplate"=>1//返回电子面单模板:0-不需要;1-需要
];
$jsonParam = json_encode($eorder, JSON_UNESCAPED_UNICODE);
$jc_api_model = new JcApi();
$result = $jc_api_model->getExterFaceByKdniao($jsonParam);
//解析电子面单返回结果
$result = json_decode($result, true);
if ($result['Success'] == false){
return ajaxFalse($result['Reason']);
}
$PrintTemplate = [
$result['PrintTemplate']
];
if (array_key_exists('SubPrintTemplates',$result)){
foreach($result['SubPrintTemplates'] as $kay => $val){
array_push($PrintTemplate,$val);
}
}
return ajaxSuccess($PrintTemplate);
}
function getExterFaceByKdniao($content)
{
$jc_third_party_config_model = new JcThirdPartyConfig();
$deliver_kdniao_config = $jc_third_party_config_model->getKdniaoDeliveryConfig();
// 测试$deliver_kdniao_config = ['kdniao_user_id'=>'test1597343','kdniao_api_key'=>'07ca9479-e5ff-4e72-81cf-32511eb30000'];
$datas = array(
'EBusinessID' => $deliver_kdniao_config['kdniao_user_id'],//客户id
'RequestType' => '1007',
'RequestData' => urlencode($content) ,
'DataType' => '2',
);
$datas['DataSign'] = $this->encrypt($content, $deliver_kdniao_config['kdniao_api_key']);
// $url = "http://sandboxapi.kdniao.com:8080/kdniaosandbox/gateway/exterfaceInvoke.json";//测试
$url = "https://api.kdniao.com/api/EOrderService";//正式
$result = $this->sendPost($url, $datas);
return $result;
}
//生成签名
function encrypt($data, $appkey)
{
return urlencode(base64_encode(md5($data.$appkey)));
}/** * post提交数据 * @param */
function sendPost($url, $datas) {
$temps = array();
foreach ($datas as $key => $value) {
$temps[] = sprintf('%s=%s', $key, $value);
}
$post_data = implode('&', $temps);
$url_info = parse_url($url);
if(empty($url_info['port']))
{
$url_info['port']=80;
}
$httpheader = "POST " . $url_info['path'] . " HTTP/1.0\r\n";
$httpheader.= "Host:" . $url_info['host'] . "\r\n";
$httpheader.= "Content-Type:application/x-www-form-urlencoded\r\n";
$httpheader.= "Content-Length:" . strlen($post_data) . "\r\n";
$httpheader.= "Connection:close\r\n\r\n";
$httpheader.= $post_data;
$fd = fsockopen($url_info['host'], $url_info['port']);
fwrite($fd, $httpheader);
$gets = "";
$headerFlag = true;
while (!feof($fd)) {
if (($header = @fgets($fd)) && ($header == "\r\n" || $header == "\n")) {
break;
}
}
while (!feof($fd)) {
$gets.= fread($fd, 128);
}
fclose($fd);
return $gets;
}

下载jquery.printarea.js : plugins.jquery.com/PrintArea/

下载快递鸟官方Demo: www.kdniao.com/api-eorder

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

2021年必备的11款VS Code主题

发表于 2021-01-13

再过几个天,2021年就要到来了。

在新的一年,应当给换新一下,也给自己换一种心情。

对于开发者而言,相对容易 满足,一个不错的机械键盘、一个显示屏,甚至,更换一款IDE主题。

本文,就来给大家介绍11款非常惊艳、值得尝试的VS Code主题!

1. Cyberpunk

Cyberpunk SCARLET

img

Cyberpunk UMBRA

img

这款主题的英文名称或许让大家很陌生,但是反映称中文,应该很多人都恍然大悟–赛博朋克,和最近大火的一款游戏同名。

强烈的色彩对比能够让我们的眼睛很快就能检测到颜色的变化,易于我们的大脑处理接收到的信息。

2. In Bed by 7

img

相对于Cyberpunk强烈的色彩对比,它颜色上更加柔和,不会有太过于强烈的感觉。

3. Shades of Purple

img

一个专业的主题,为你的VS Code编辑器和终端精心挑选和大胆的紫色阴影。这是市场上下载次数最多、排名最高的VSCode主题之一。

4. Firefox Theme

Firefox Dark

img

Firefox Light

img

一个基于Mozilla Firefox开发者工具的VS Code主题。

5. Tokyo Night

一个干净、明亮的Visual Studio代码主题。

Tokyo Night

img

Tokyo Night Light

img

6. Nu Disco

Nu Disco是一个由Danny Banks基于Style Dictionary构建的VSCode主题。

Nu Disco Dark

img

Nu Disco Light

img

7. SynthWave ‘84

img

8. Outrun

Outrun Night

img

9. Cobalt 2

img

10. Github

Github Dark

img

GitHub Light

img

11. Bio Dark

img

一个黑暗的VS Code主题代,针对JavaScript(React)、PHP、HTML和Sass/SCSS进行了特殊优化。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

记一次使用AspNet Core WebApi 50+D

发表于 2021-01-13

前言

我可能有三年没怎么碰C#了,目前的工作是在全职搞前端,最近有时间抽空看了一下Asp.net Core,Core版本号都到了5.0了,也越来越好用了,下面将记录一下这几天以来使用Asp.Net Core WebApi+Dapper+Mysql+Redis+Docker的一次开发过程。

项目结构

最终项目结构如下,CodeUin.Dapper数据访问层,CodeUin.WebApi应用层,其中涉及到具体业务逻辑的我将直接写在Controllers中,不再做过多分层。CodeUin.Helpers我将存放一些项目的通用帮助类,如果是只涉及到当前层的帮助类将直接在所在层级种的Helpers文件夹中存储即可。

项目结构

安装环境

MySQL

1
2
3
4
cmd复制代码# 下载镜像
docker pull mysql
# 运行
docker run -itd --name 容器名称 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=你的密码 mysql

如果正在使用的客户端工具连接MySQL提示1251,这是因为客户端不支持新的加密方式造成的,解决办法如下。

1251

1
2
3
4
5
6
7
8
9
10
11
12
13
14
shell复制代码# 查看当前运行的容器
docker ps
# 进入容器
docker exec -it 容器名称 bash
# 访问MySQL
mysql -u root -p
# 查看加密规则
select host,user,plugin,authentication_string from mysql.user;
# 对远程连接进行授权
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
# 更改密码加密规则
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '你的密码';
# 刷新权限
flush privileges;

最后,使用MySQL客户端工具进行连接测试,我使用的工具是Navicat Premium。

MySQL

Redis

1
2
3
4
shell复制代码# 下载镜像
docker pull redis
# 运行
docker run -itd -p 6379:6379 redis

使用Redis客户端工具进行连接测试,我使用的工具是Another Redis DeskTop Manager。

Redis

.NET 环境

服务器我使用的是CentOS 8,使用的NET SDK版本5.0,下面将记录我是如何在CentOS 8中安装.NET SDK和.NET运行时的。

1
2
3
4
shell复制代码# 安装SDK
sudo dnf install dotnet-sdk-5.0
# 安装运行时
sudo dnf install aspnetcore-runtime-5.0

检查是否安装成功,使用dotnet --info命令查看安装信息

SDK

创建项目

下面将实现一个用户的登录注册,和获取用户信息的小功能。

数据服务层

该层设计参考了 玉龙雪山 的架构,我也比较喜欢这种结构,一看结构就知道是要做什么的,简单清晰。

首先,新建一个项目命名为CodeUin.Dapper,只用来提供接口,为业务层服务。

  • Entities
    • 存放实体类
  • IRepository
    • 存放仓库接口
  • Repository
    • 存放仓库接口实现类
  • BaseModel
    • 实体类的基类,用来存放通用字段
  • DataBaseConfig
    • 数据访问配置类
  • IRepositoryBase
    • 存放最基本的仓储接口 增删改查等
  • RepositoryBase
    • 基本仓储接口的具体实现

Dapper

创建BaseModel基类

该类存放在项目的根目录下,主要作用是将数据库实体类中都有的字段独立出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c#复制代码using System;

namespace CodeUin.Dapper
{
/// <summary>
/// 基础实体类
/// </summary>
public class BaseModel
{
/// <summary>
/// 主键Id
/// </summary>
public int Id { get; set; }

/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; }
}
}

创建DataBaseConfig类

该类存放在项目的根目录下,我这里使用的是MySQL,需要安装以下依赖包,如果使用的其他数据库,自行安装对应的依赖包即可。

依赖

该类具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c#复制代码using MySql.Data.MySqlClient;
using System.Data;

namespace CodeUin.Dapper
{
public class DataBaseConfig
{
private static string MySqlConnectionString = @"Data Source=数据库地址;Initial Catalog=codeuin;Charset=utf8mb4;User ID=root;Password=数据库密码;";

public static IDbConnection GetMySqlConnection(string sqlConnectionString = null)
{
if (string.IsNullOrWhiteSpace(sqlConnectionString))
{
sqlConnectionString = MySqlConnectionString;
}
IDbConnection conn = new MySqlConnection(sqlConnectionString);
conn.Open();
return conn;
}
}
}

创建IRepositoryBase类

该类存放在项目的根目录下,存放常用的仓储接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
c#复制代码using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace CodeUin.Dapper
{
public interface IRepositoryBase<T>
{
Task<int> Insert(T entity, string insertSql);

Task Update(T entity, string updateSql);

Task Delete(int Id, string deleteSql);

Task<List<T>> Select(string selectSql);

Task<T> Detail(int Id, string detailSql);
}
}

创建RepositoryBase类

该类存放在项目的根目录下,是IRepositoryBase类的具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
c#复制代码using Dapper;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;

namespace CodeUin.Dapper
{
public class RepositoryBase<T> : IRepositoryBase<T>
{
public async Task Delete(int Id, string deleteSql)
{
using (IDbConnection conn = DataBaseConfig.GetMySqlConnection())
{
await conn.ExecuteAsync(deleteSql, new { Id });
}
}

public async Task<T> Detail(int Id, string detailSql)
{
using (IDbConnection conn = DataBaseConfig.GetMySqlConnection())
{
return await conn.QueryFirstOrDefaultAsync<T>(detailSql, new { Id });
}
}

public async Task<List<T>> ExecQuerySP(string SPName)
{
using (IDbConnection conn = DataBaseConfig.GetMySqlConnection())
{
return await Task.Run(() => conn.Query<T>(SPName, null, null, true, null, CommandType.StoredProcedure).ToList());
}
}

public async Task<int> Insert(T entity, string insertSql)
{
using (IDbConnection conn = DataBaseConfig.GetMySqlConnection())
{
return await conn.ExecuteAsync(insertSql, entity);
}
}

public async Task<List<T>> Select(string selectSql)
{
using (IDbConnection conn = DataBaseConfig.GetMySqlConnection())
{
return await Task.Run(() => conn.Query<T>(selectSql).ToList());
}
}

public async Task Update(T entity, string updateSql)
{
using (IDbConnection conn = DataBaseConfig.GetMySqlConnection())
{
await conn.ExecuteAsync(updateSql, entity);
}
}
}
}

好了,基础类基本已经定义完成。下面将新建一个Users类,并定义几个常用的接口。

创建Users实体类

该类存放在Entities文件夹中,该类继承BaseModel。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
c#复制代码namespace CodeUin.Dapper.Entities
{
/// <summary>
/// 用户表
/// </summary>
public class Users : BaseModel
{
/// <summary>
/// 用户名
/// </summary>
public string UserName { get; set; }

/// <summary>
/// 密码
/// </summary>
public string Password { get; set; }

/// <summary>
/// 盐
/// </summary>
public string Salt { get; set; }

/// <summary>
/// 邮箱
/// </summary>
public string Email { get; set; }

/// <summary>
/// 手机号
/// </summary>
public string Mobile { get; set; }

/// <summary>
/// 性别
/// </summary>
public int Gender { get; set; }

/// <summary>
/// 年龄
/// </summary>
public int Age { get; set; }

/// <summary>
/// 头像
/// </summary>
public string Avatar { get; set; }

/// <summary>
/// 是否删除
/// </summary>
public int IsDelete { get; set; }
}
}

创建IUserRepository类

该类存放在IRepository文件夹中,继承IRepositoryBase,并定义了额外的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c#复制代码using CodeUin.Dapper.Entities;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace CodeUin.Dapper.IRepository
{
public interface IUserRepository : IRepositoryBase<Users>
{
Task<List<Users>> GetUsers();

Task<int> AddUser(Users entity);

Task DeleteUser(int d);

Task<Users> GetUserDetail(int id);

Task<Users> GetUserDetailByEmail(string email);
}
}

创建UserRepository类

该类存放在Repository文件夹中,继承RepositoryBase, IUserRepository ,是IUserRepository类的具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
c#复制代码using CodeUin.Dapper.Entities;
using CodeUin.Dapper.IRepository;
using Dapper;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;

namespace CodeUin.Dapper.Repository
{
public class UserRepository : RepositoryBase<Users>, IUserRepository
{
public async Task DeleteUser(int id)
{
string deleteSql = "DELETE FROM [dbo].[Users] WHERE Id=@Id";
await Delete(id, deleteSql);
}


public async Task<Users> GetUserDetail(int id)
{
string detailSql = @"SELECT Id, Email, UserName, Mobile, Password, Age, Gender, CreateTime,Salt, IsDelete FROM Users WHERE Id=@Id";
return await Detail(id, detailSql);
}

public async Task<Users> GetUserDetailByEmail(string email)
{
string detailSql = @"SELECT Id, Email, UserName, Mobile, Password, Age, Gender, CreateTime, Salt, IsDelete FROM Users WHERE Email=@email";

using (IDbConnection conn = DataBaseConfig.GetMySqlConnection())
{
return await conn.QueryFirstOrDefaultAsync<Users>(detailSql, new { email });
}
}

public async Task<List<Users>> GetUsers()
{
string selectSql = @"SELECT * FROM Users";
return await Select(selectSql);
}

public async Task<int> AddUser(Users entity)
{
string insertSql = @"INSERT INTO Users (UserName, Gender, Avatar, Mobile, CreateTime, Password, Salt, IsDelete, Email) VALUES (@UserName, @Gender, @Avatar, @Mobile, now(),@Password, @Salt, @IsDelete,@Email);SELECT @id= LAST_INSERT_ID();";
return await Insert(entity, insertSql);
}
}
}

大功告成,接下来需要手动创建数据库和表结构,不能像使用EF那样自动生成了,使用Dapper基本上是要纯写SQL的,如果想像EF那样使用,就要额外的安装一个扩展 Dapper.Contrib。

数据库表结构如下,比较简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sql复制代码DROP TABLE IF EXISTS `Users`;
CREATE TABLE `Users` (
`Id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`Email` varchar(255) DEFAULT NULL COMMENT '邮箱',
`UserName` varchar(20) DEFAULT NULL COMMENT '用户名称',
`Mobile` varchar(11) DEFAULT NULL COMMENT '手机号',
`Age` int(11) DEFAULT NULL COMMENT '年龄',
`Gender` int(1) DEFAULT '0' COMMENT '性别',
`Avatar` varchar(255) DEFAULT NULL COMMENT '头像',
`Salt` varchar(255) DEFAULT NULL COMMENT '加盐',
`Password` varchar(255) DEFAULT NULL COMMENT '密码',
`IsDelete` int(2) DEFAULT '0' COMMENT '0-正常 1-删除',
`CreateTime` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`Id`),
UNIQUE KEY `USER_MOBILE_INDEX` (`Mobile`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';

好了,数据访问层大概就这样子了,下面来看看应用层的具体实现方式。

应用程序层

创建一个WebApi项目,主要对外提供Api接口服务,具体结构如下。

  • Autofac
    • 存放IOC 依赖注入的配置项
  • AutoMapper
    • 存放实体对象映射关系的配置项
  • Controllers
    • 控制器,具体业务逻辑也将写在这
  • Fliters
    • 存放自定义的过滤器
  • Helpers
    • 存放本层中用到的一些帮助类
  • Models
    • 存放输入/输出/DTO等实体类

WebApi

好了,结构大概就是这样。错误优先,先处理程序异常,和集成日志程序吧。

自定义异常处理

在Helpers文件夹中创建一个ErrorHandingMiddleware中间件,添加扩展方法ErrorHandlingExtensions,在Startup中将会使用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
c#复制代码using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Threading.Tasks;

namespace CodeUin.WebApi.Helpers
{
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate next;
private readonly ILogger<ErrorHandlingMiddleware> _logger;

public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger)
{
this.next = next;
_logger = logger;
}

public async Task Invoke(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
_logger.LogError(ex.Message);

var statusCode = 500;

await HandleExceptionAsync(context, statusCode, ex.Message);
}
finally
{
var statusCode = context.Response.StatusCode;
var msg = "";

if (statusCode == 401)
{
msg = "未授权";
}
else if (statusCode == 404)
{
msg = "未找到服务";
}
else if (statusCode == 502)
{
msg = "请求错误";
}
else if (statusCode != 200)
{
msg = "未知错误";
}
if (!string.IsNullOrWhiteSpace(msg))
{
await HandleExceptionAsync(context, statusCode, msg);
}
}
}

// 异常错误信息捕获,将错误信息用Json方式返回
private static Task HandleExceptionAsync(HttpContext context, int statusCode, string msg)
{
var result = JsonConvert.SerializeObject(new { Msg = msg, Code = statusCode });

context.Response.ContentType = "application/json;charset=utf-8";

return context.Response.WriteAsync(result);
}
}

// 扩展方法
public static class ErrorHandlingExtensions
{
public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ErrorHandlingMiddleware>();
}
}
}

然后在 Startup 的 Configure 方法中添加 app.UseErrorHandling() ,当程序发送异常时,会走我们的自定义异常处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c#复制代码public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();

// 请求错误提示配置
app.UseErrorHandling();

app.UseRouting();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}

日志程序

我这里使用的是NLog,需要在项目中先安装依赖包。

Nlog

首先在项目根目录创建一个 nlog.config 的配置文件,具体内容如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
xml复制代码<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
internalLogLevel="Info"
internalLogFile="c:\temp\internal-nlog.txt">

<!-- enable asp.net core layout renderers -->
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
</extensions>

<!-- the targets to write to -->
<targets>

<target xsi:type="File" name="allfile" fileName="${currentdir}\logs\nlog-all-${shortdate}.log"
layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${aspnet-request-ip}|${logger}|${message} ${exception:format=tostring}" />

<target xsi:type="Console" name="ownFile-web"
layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${aspnet-request-ip}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}" />
</targets>
<!-- rules to map from logger name to target -->
<rules>
<!--All logs, including from Microsoft-->
<logger name="*" minlevel="Info" writeTo="allfile" />

<!--Skip non-critical Microsoft logs and so log only own logs-->
<logger name="Microsoft.*" maxlevel="Info" final="true" />
<!-- BlackHole without writeTo -->
<logger name="*" minlevel="Info" writeTo="ownFile-web" />
</rules>
</nlog>

更多配置信息可以直接去官网查看 nlog-project.org

最后,在 Program 入口文件中集成 Nlog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
c#复制代码using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NLog.Web;

namespace CodeUin.WebApi
{
public class Program
{
public static void Main(string[] args)
{
NLogBuilder.ConfigureNLog("nlog.config");
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.UseNLog();
}
}

现在,我们可以直接使用NLog了,使用方法可以查看上面的 ErrorHandlingMiddleware 类中有使用到。

依赖注入

将使用 Autofac 来管理类之间的依赖关系,Autofac 是一款超级赞的.NET IoC 容器 。首先我们需要安装依赖包。

Autofac

在 项目根目录的 Autofac 文件夹中新建一个 CustomAutofacModule 类,用来管理我们类之间的依赖关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c#复制代码using Autofac;
using CodeUin.Dapper.IRepository;
using CodeUin.Dapper.Repository;

namespace CodeUin.WebApi.Autofac
{
public class CustomAutofacModule:Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<UserRepository>().As<IUserRepository>();
}
}
}

在 Startup 类中添加方法

1
2
3
4
5
c#复制代码public void ConfigureContainer(ContainerBuilder builder)
{
// 依赖注入
builder.RegisterModule(new CustomAutofacModule());
}

实体映射

将使用 Automapper 帮我们解决对象映射到另外一个对象中的问题,比如这种代码。

1
2
3
4
5
6
7
8
9
c#复制代码// 如果有几十个属性是相当的可怕的
var users = new Users
{
Email = user.Email,
Password = user.Password,
UserName = user.UserName
};
// 使用Automapper就容易多了
var model = _mapper.Map<Users>(user);

先安装依赖包

Automapper

在项目根目录的 AutoMapper 文件夹中 新建 AutoMapperConfig 类,来管理我们的映射关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c#复制代码using AutoMapper;
using CodeUin.Dapper.Entities;
using CodeUin.WebApi.Models;

namespace CodeUin.WebApi.AutoMapper
{
public class AutoMapperConfig : Profile
{
public AutoMapperConfig()
{
CreateMap<UserRegisterModel, Users>().ReverseMap();
CreateMap<UserLoginModel, Users>().ReverseMap();
CreateMap<UserLoginModel, UserModel>().ReverseMap();
CreateMap<UserModel, Users>().ReverseMap();
}
}
}

在 Startup 文件的 ConfigureServices 方法中 添加 services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()) 即可。

使用JWT

下面将集成JWT,来处理授权等信息。首先,需要安装依赖包。

JWT

修改 appsttings.json 文件,添加 Jwt 配置信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c#复制代码{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"Jwt": {
"Key": "e816f4e9d7a7be785a", // 这个key必须大于16位数,非常生成的时候会报错
"Issuer": "codeuin.com"
}
}

最后,在 Startup 类的 ConfigureServices 方法中添加 Jwt 的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c#复制代码     services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5), //缓冲过期时间 默认5分钟
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});

好了,最终我们的 Startup 类是这样子的,关于自定义的参数验证后面会讲到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
c#复制代码using Autofac;
using AutoMapper;
using CodeUin.WebApi.Autofac;
using CodeUin.WebApi.Filters;
using CodeUin.WebApi.Helpers;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Text;

namespace CodeUin.WebApi
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureContainer(ContainerBuilder builder)
{
// 依赖注入
builder.RegisterModule(new CustomAutofacModule());
}

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5), //缓冲过期时间 默认5分钟
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});

services.AddHttpContextAccessor();

// 使用AutoMapper
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

// 关闭参数自动校验
services.Configure<ApiBehaviorOptions>((options) =>
{
options.SuppressModelStateInvalidFilter = true;
});

// 使用自定义验证器
services.AddControllers(options =>
{
options.Filters.Add<ValidateModelAttribute>();
}).
AddJsonOptions(options =>
{
// 忽略null值
options.JsonSerializerOptions.IgnoreNullValues = true;
});
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();

// 请求错误提示配置
app.UseErrorHandling();

// 授权
app.UseAuthentication();

app.UseRouting();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}

新建实体类

我将新建三个实体类,分别是 UserLoginModel 用户登录,UserRegisterModel 用户注册,UserModel 用户基本信息。

UserLoginModel 和 UserRegisterModel 将根据我们在属性中配置的特性自动验证合法性,就不需要在控制器中单独写验证逻辑了,极大的节省了工作量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
c#复制代码using System;
using System.ComponentModel.DataAnnotations;

namespace CodeUin.WebApi.Models
{
/// <summary>
/// 用户实体类
/// </summary>
public class UserModel
{
public int Id { get; set; }

public string Email { get; set; }
public string UserName { get; set; }

public string Mobile { get; set; }

public int Gender { get; set; }

public int Age { get; set; }

public string Avatar { get; set; }
}

public class UserLoginModel
{
[Required(ErrorMessage = "请输入邮箱")]
public string Email { get; set; }

[Required(ErrorMessage = "请输入密码")]
public string Password { get; set; }
}

public class UserRegisterModel
{
[Required(ErrorMessage = "请输入邮箱")]
[EmailAddress(ErrorMessage = "请输入正确的邮箱地址")]
public string Email { get; set; }

[Required(ErrorMessage = "请输入用户名")]
[MaxLength(length: 12, ErrorMessage = "用户名最大长度不能超过12")]
[MinLength(length: 2, ErrorMessage = "用户名最小长度不能小于2")]
public string UserName { get; set; }

[Required(ErrorMessage = "请输入密码")]
[MaxLength(length: 20, ErrorMessage = "密码最大长度不能超过20")]
[MinLength(length: 6, ErrorMessage = "密码最小长度不能小于6")]
public string Password { get; set; }
}
}

验证器

在项目根目录的 Filters 文件夹中 添加 ValidateModelAttribute 文件夹,将在 Action 请求中先进入我们的过滤器,如果不符合我们定义的规则将直接输出错误项。

具体代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
c#复制代码using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Linq;

namespace CodeUin.WebApi.Filters
{
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var item = context.ModelState.Keys.ToList().FirstOrDefault();

//返回第一个验证参数错误的信息
context.Result = new BadRequestObjectResult(new
{
Code = 400,
Msg = context.ModelState[item].Errors[0].ErrorMessage
});
}
}
}
}

添加自定义验证特性

有时候我们需要自己额外的扩展一些规则,只需要继承 ValidationAttribute 类然后实现 IsValid 方法即可,比如我这里验证了中国的手机号码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c#复制代码using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;

namespace CodeUin.WebApi.Filters
{
public class ChineMobileAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
if (!(value is string)) return false;

var val = (string)value;

return Regex.IsMatch(val, @"^[1]{1}[2,3,4,5,6,7,8,9]{1}\d{9}$");
}
}
}

实现登录注册

我们来实现一个简单的业务需求,用户注册,登录,和获取用户信息,其他的功能都大同小异,无非就是CRUD!。

接口我们在数据服务层已经写好了,接下来是处理业务逻辑的时候到了,将直接在 Controllers 中编写。

新建一个控制器 UsersController ,业务很简单,不过多介绍了,具体代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
c#复制代码using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using AutoMapper;
using CodeUin.Dapper.Entities;
using CodeUin.Dapper.IRepository;
using CodeUin.Helpers;
using CodeUin.WebApi.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;

namespace CodeUin.WebApi.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
[Authorize]
public class UsersController : Controller
{
private readonly ILogger<UsersController> _logger;
private readonly IUserRepository _userRepository;
private readonly IMapper _mapper;
private readonly IConfiguration _config;
private readonly IHttpContextAccessor _httpContextAccessor;

public UsersController(ILogger<UsersController> logger, IUserRepository userRepository, IMapper mapper, IConfiguration config, IHttpContextAccessor httpContextAccessor)
{
_logger = logger;
_userRepository = userRepository;
_mapper = mapper;
_config = config;
_httpContextAccessor = httpContextAccessor;
}

[HttpGet]
public async Task<JsonResult> Get()
{
var userId = int.Parse(_httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value);

var userInfo = await _userRepository.GetUserDetail(userId);

if (userInfo == null)
{
return Json(new { Code = 200, Msg = "未找到该用户的信息" });
}

var outputModel = _mapper.Map<UserModel>(userInfo);

return Json(new { Code = 200, Data = outputModel }); ;
}

[HttpPost]
[AllowAnonymous]
public async Task<JsonResult> Login([FromBody] UserLoginModel user)
{
// 查询用户信息
var data = await _userRepository.GetUserDetailByEmail(user.Email);

// 账号不存在
if (data == null)
{
return Json(new { Code = 200, Msg = "账号或密码错误" });
}

user.Password = Encrypt.Md5(data.Salt + user.Password);

// 密码不一致
if (!user.Password.Equals(data.Password))
{
return Json(new { Code = 200, Msg = "账号或密码错误" });
}

var userModel = _mapper.Map<UserModel>(data);

// 生成token
var token = GenerateJwtToken(userModel);

// 存入Redis
await new RedisHelper().StringSetAsync($"token:{data.Id}", token);

return Json(new
{
Code = 200,
Msg = "登录成功",
Data = userModel,
Token = token
});
}

[HttpPost]
[AllowAnonymous]
public async Task<JsonResult> Register([FromBody] UserRegisterModel user)
{
// 查询用户信息
var data = await _userRepository.GetUserDetailByEmail(user.Email);

if (data != null)
{
return Json(new { Code = 200, Msg = "该邮箱已被注册" });
}

var salt = Guid.NewGuid().ToString("N");

user.Password = Encrypt.Md5(salt + user.Password);

var users = new Users
{
Email = user.Email,
Password = user.Password,
UserName = user.UserName
};

var model = _mapper.Map<Users>(user);

model.Salt = salt;

await _userRepository.AddUser(model);

return Json(new { Code = 200, Msg = "注册成功" });
}

/// <summary>
/// 生成Token
/// </summary>
/// <param name="user">用户信息</param>
/// <returns></returns>
private string GenerateJwtToken(UserModel user)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

var claims = new[] {
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(JwtRegisteredClaimNames.Gender, user.Gender.ToString()),
new Claim(ClaimTypes.NameIdentifier,user.Id.ToString()),
new Claim(ClaimTypes.Name,user.UserName),
new Claim(ClaimTypes.MobilePhone,user.Mobile??""),
};

var token = new JwtSecurityToken(_config["Jwt:Issuer"],
_config["Jwt:Issuer"],
claims,
expires: DateTime.Now.AddMinutes(120),
signingCredentials: credentials);

return new JwtSecurityTokenHandler().WriteToken(token);
}
}
}

最后,来测试一下我们的功能,首先是注册。

先来验证一下我们的传入的参数是否符合我们定义的规则。

输入一个错误的邮箱号试试看!

注册

ok,没有问题,和我们在 UserRegisterModel 中 添加的验证特性返回结果一致,最后我们测试一下完全符合规则的情况。

注册成功

最后,注册成功了,查询下数据库也是存在的。

user

我们来试试登录接口,在调用登录接口之前我们先来测试一下我们的配置的权限验证是否已经生效,在不登录的情况下直接访问获取用户信息接口。

未授权

直接访问会返回未授权,那是因为我们没有登录,自然也就没有 Token,目前来看是没问题的,但要看看我们传入正确的Token 是否能过权限验证。

现在,我们需要调用登录接口,登录成功后会返回一个Token,后面的接口请求都需要用到,不然会无权限访问。

先来测试一下密码错误的情况。

密码错误

返回正确,符合我们的预期结果,下面将试试正确的密码登录,看是否能够返回我们想要的结果。

登录成功

登录成功,接口也返回了我们预期的结果,最后看看生成的 token 是否按照我们写的逻辑那样,存一份到 redis 当中。

redis

也是没有问题的,和我们预想的一样。

下面将携带正确的 token 请求获取用户信息的接口,看看是否能够正确返回。

获取用户信息的接口不会携带任何参数,只会在请求头的 Headers 中 添加 Authorization ,将我们正确的 token 传入其中。

获取用户信息

能够正确获取到我们的用户信息,也就是说我们的权限这一块也是没有问题的了,下面将使用 Docker 打包部署到 Linux 服务器中。

打包部署

在项目的根目录下添加 Dockerfile 文件,内容如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
sql复制代码#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build
WORKDIR /src
COPY ["CodeUin.WebApi/CodeUin.WebApi.csproj", "CodeUin.WebApi/"]
COPY ["CodeUin.Helpers/CodeUin.Helpers.csproj", "CodeUin.Helpers/"]
COPY ["CodeUin.Dapper/CodeUin.Dapper.csproj", "CodeUin.Dapper/"]
RUN dotnet restore "CodeUin.WebApi/CodeUin.WebApi.csproj"
COPY . .
WORKDIR "/src/CodeUin.WebApi"
RUN dotnet build "CodeUin.WebApi.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "CodeUin.WebApi.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "CodeUin.WebApi.dll"]

在 Dockerfile 文件的目录下运行打包命令

1
2
3
4
5
6
cmd复制代码# 在当前文件夹(末尾的句点)中查找 Dockerfile
docker build -t codeuin-api .
# 查看镜像
docker images
# 保存镜像到本地
docker save -o codeuin-api.tar codeuin-api

最后,将我们保存的镜像通过上传的服务器后导入即可。

通过 ssh 命令 连接服务器,在刚上传包的目录下执行导入命令。

1
2
3
4
5
6
cmd复制代码# 加载镜像
docker load -i codeuin-api.tar
# 运行镜像
docker run -itd -p 8888:80 --name codeuin-api codeuin-api
# 查看运行状态
docker stats

到此为止,我们整个部署工作已经完成了,最后在请求服务器的接口测试一下是否ok。

服务器请求

最终的结果也是ok的,到此为止,我们所有基础的工作都完成了,所有的代码存储在 github.com/xiazanzhang… 中,如果对你有帮助的话可以参考一下。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

有了这 4 款工具,老板再也不怕我写烂SQL了

发表于 2021-01-13

你对于正在运行的mysql 性能如何?参数设置的是否合理?账号设置的是否存在安全隐患?是否了然于胸?

俗话说工欲善其事,必先利其器,定期对你的MYSQL数据库进行一个体检,是保证数据库安全运行的重要手段。

今天和大家分享几个mysql 优化的工具,你可以使用它们对你的mysql进行一个体检,生成awr报告,让你从整体上把握你的数据库的性能情况。

在这里插入图片描述

1、mysqltuner.pl

这是mysql一个常用的数据库性能诊断工具,主要检查参数设置的合理性包括日志文件、存储引擎、安全建议及性能分析。针对潜在的问题,给出改进的建议,是mysql优化的好帮手。

在上一版本中,MySQLTuner支持MySQL / MariaDB / Percona Server的约300个指标。

项目地址:github.com/major/MySQL…

1.1 下载

1
2
ruby复制代码[root@localhost ~]#wget https:
//raw.githubusercontent.com/major/MySQLTuner-perl/master/mysqltuner.pl

1.2 使用

1
2
3
4
5
6
7
8
ruby复制代码[root@localhost ~]# ./mysqltuner.pl --socket /var/lib/mysql/mysql.sock
>> MySQLTuner1.7.4- MajorHayden<major@mhtx.net>
>> Bug reports, feature requests, and downloads at http://mysqltuner.com/
>> Runwith'--help'for additional options and output filtering
[--] Skipped version check forMySQLTuner script
Please enter your MySQL administrative login: root
Please enter your MySQL administrative password: [OK] Currently running supported MySQL version 5.7.
[OK] Operating on 64-bit architecture

1.3、报告分析

1)重要关注[!!](中括号有叹号的项)例如[!!] Maximum possible memory usage: 4.8G (244.13% of installed RAM),表示内存已经严重用超了。

图片

2)关注最后给的建议“Recommendations ”。

图片

2、tuning-primer.sh

这是mysql的另一个优化工具,针于mysql的整体进行一个体检,对潜在的问题,给出优化的建议。

项目地址:github.com/BMDan/tunin…

目前,支持检测和优化建议的内容如下:

在这里插入图片描述

2.1 下载

1
2
bash复制代码[root@localhost ~]#wget https:
//launchpad.net/mysql-tuning-primer/trunk/1.6-r1/+download/tuning-primer.sh

2.2 使用

1
2
3
4
sql复制代码[root@localhost ~]# [root@localhost dba]# ./tuning-primer.sh

-- MYSQL PERFORMANCE TUNING PRIMER --
- By: MatthewMontgomery-

2.3 报告分析

重点查看有红色告警的选项,根据建议结合自己系统的实际情况进行修改,例如:

图片

3、pt-variable-advisor

pt-variable-advisor 可以分析MySQL变量并就可能出现的问题提出建议。

3.1 安装

www.percona.com/downloads/p…

1
2
less复制代码[root@localhost ~]#wget https://www.percona.com/downloads/perconatoolkit/3.0.13/binary/redhat/7/x86_64/pecona-toolkit-3.0.13-re85ce15-el7-x86_64-bundle.tar
[root@localhost ~]#yum install percona-toolkit-3.0.13-1.el7.x86_64.rpm

3.2 使用

pt-variable-advisor是pt工具集的一个子工具,主要用来诊断你的参数设置是否合理。

1
css复制代码[root@localhost ~]# pt-variable-advisor localhost --socket /var/lib/mysql/mysql.sock

3.3 报告分析

重点关注有WARN的信息的条目,例如:

图片

4、pt-qurey-digest

pt-query-digest 主要功能是从日志、进程列表和tcpdump分析MySQL查询。

4.1安装

具体参考3.1节

4.2使用

pt-query-digest主要用来分析mysql的慢日志,与mysqldumpshow工具相比,py-query_digest 工具的分析结果更具体,更完善。

1
csharp复制代码[root@localhost ~]# pt-query-digest /var/lib/mysql/slowtest-slow.log

4.3 常见用法分析

1)直接分析慢查询文件:

1
c复制代码pt-query-digest /var/lib/mysql/slowtest-slow.log > slow_report.log

2)分析最近12小时内的查询:

1
c复制代码pt-query-digest --since=12h/var/lib/mysql/slowtest-slow.log > slow_report2.log

3)分析指定时间范围内的查询:

1
matlab复制代码pt-query-digest /var/lib/mysql/slowtest-slow.log --since '2017-01-07 09:30:00'--until'2017-01-07 10:00:00'> > slow_report3.log

4)分析指含有select语句的慢查询

1
c复制代码pt-query-digest --filter '$event->{fingerprint} =~ m/^select/i'/var/lib/mysql/slowtest-slow.log> slow_report4.log

5)针对某个用户的慢查询

1
c复制代码pt-query-digest --filter '($event->{user} || "") =~ m/^root/i'/var/lib/mysql/slowtest-slow.log> slow_report5.log

6)查询所有所有的全表扫描或full join的慢查询

1
swift复制代码pt-query-digest --filter '(($event->{Full_scan} || "") eq "yes") ||(($event->{Full_join} || "") eq "yes")'/var/lib/mysql/slowtest-slow.log> slow_report6.log

4.4 报告分析

  • 第一部分:总体统计结果 Overall:总共有多少条查询 Time range:查询执行的时间范围 unique:唯一查询数量,即对查询条件进行参数化以后,总共有多少个不同的查询 total:总计 min:最小 max:最大 avg:平均 95%:把所有值从小到大排列,位置位于95%的那个数,这个数一般最具有参考价值 median:中位数,把所有值从小到大排列,位置位于中间那个数
  • 第二部分:查询分组统计结果 Rank:所有语句的排名,默认按查询时间降序排列,通过–order-by指定 Query ID:语句的ID,(去掉多余空格和文本字符,计算hash值) Response:总的响应时间 time:该查询在本次分析中总的时间占比 calls:执行次数,即本次分析总共有多少条这种类型的查询语句 R/Call:平均每次执行的响应时间 V/M:响应时间Variance-to-mean的比率 Item:查询对象
  • 第三部分:每一种查询的详细统计结果 ID:查询的ID号,和上图的Query ID对应 Databases:数据库名 Users:各个用户执行的次数(占比) Query_time distribution :查询时间分布, 长短体现区间占比。Tables:查询中涉及到的表 Explain:SQL语句

作者 | 爱码士人员

来源 | urlify.cn/fQBNnq

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

springboot集成轻量级权限认证框架sa-token

发表于 2021-01-13

sa-token是什么?

sa-token是一个JavaWeb轻量级权限认证框架,主要解决项目中登录认证、权限认证、Session会话等一系列由此衍生的权限相关业务。相比于其他安全性框架较容易上手。

  • github: github.com/click33/sa-…
  • 官网文档: sa-token.dev33.cn/

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
scss复制代码sa-token的API调用非常简单,有多简单呢?以登录验证为例,你只需要:
// 在登录时写入当前会话的账号id
StpUtil.setLoginId(10001);
// 然后在任意需要校验登录处调用以下API --- 如果当前会话未登录,这句代码会抛出 `NotLoginException`异常
StpUtil.checkLogin();

如果上面的示例能够证明sa-token的简单,那么以下API则可以证明sa-token的强大

StpUtil.setLoginId(10001); // 标记当前会话登录的账号id
StpUtil.getLoginId(); // 获取当前会话登录的账号id
StpUtil.isLogin(); // 获取当前会话是否已经登录, 返回true或false
StpUtil.logout(); // 当前会话注销登录
StpUtil.logoutByLoginId(10001); // 让账号为10001的会话注销登录(踢人下线)
StpUtil.hasRole("super-admin"); // 查询当前账号是否含有指定角色标识, 返回true或false
StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权限, 返回true或false
StpUtil.getSession(); // 获取当前账号id的Session
StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session
StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值
StpUtil.setLoginId(10001, "PC"); // 指定设备标识登录
StpUtil.logoutByLoginId(10001, "PC"); // 指定设备标识进行强制注销 (不同端不受影响)
StpUtil.switchTo(10044); // 将当前会话身份临时切换为其它账号

框架涵盖功能

与SpringBoot集成具体API

sa-token.dev33.cn/doc/index.h…

登录验证具体API

sa-token.dev33.cn/doc/index.h…

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
scss复制代码  StpUtil.setLoginId(Object loginId)
标记当前会话登录的账号id
建议的参数类型:long | int | String, 不可以传入复杂类型,如:User、Admin等等

StpUtil.logout()
当前会话注销登录

StpUtil.isLogin()
获取当前会话是否已经登录,返回true=已登录,false=未登录

StpUtil.checkLogin()
检验当前会话是否已经登录, 如果未登录,则抛出异常:NotLoginException
扩展:NotLoginException 对象可通过 getLoginKey() 方法获取具体是哪个 StpLogic 抛出的异常
扩展:NotLoginException 对象可通过 getType() 方法获取具体的场景值,详细参考章节:未登录场景值

StpUtil.getLoginId()
获取当前会话登录id, 如果未登录,则抛出异常:NotLoginException
类似API还有:![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/86e5e3863c6740dabe03d44976540c92~tplv-k3u1fbpfcp-watermark.image)
StpUtil.getLoginIdAsString() 获取当前会话登录id, 并转化为String类型
StpUtil.getLoginIdAsInt() 获取当前会话登录id, 并转化为int类型
StpUtil.getLoginIdAsLong() 获取当前会话登录id, 并转化为long类型

StpUtil.getLoginId(T defaultValue)
获取当前会话登录id, 如果未登录,则返回默认值 (defaultValue可以为任意类型)
类似API还有:
StpUtil.getLoginIdDefaultNull() 获取当前会话登录id, 如果未登录,则返回null

StpUtil.getLoginIdByToken(String tokenValue)
获取指定token对应的登录id,如果未登录,则返回 null

StpUtil.getTokenName()
获取当前StpLogic的token名称

权限验证查看具体API

sa-token.dev33.cn/doc/index.h…

写在最后

源码开源,作者不易,如果你喜欢这个框架麻烦你随手点一颗小星星哦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

笔记-Java-JavaWeb JavaWeb

发表于 2021-01-13

JavaWeb

学习总结b站up“狂神说Java”的“JavaWeb”笔记,侵删

1、基本概念

1.1、前言

web开发:

  • web,网页的意思 , www.baidu.com
  • 静态web
    • html,css
    • 提供给所有人看的数据始终不会发生变化!
  • 动态web
    • 淘宝,几乎是所有的网站;
    • 提供给所有人看的数据始终会发生变化,每个人在不同的时间,不同的地点看到的信息各不相同!
    • 技术栈:Servlet/JSP,ASP,PHP

在Java中,动态web资源开发的技术统称为JavaWeb;

1.2、web应用程序

web应用程序:可以提供浏览器访问的程序;

  • a.html、b.html……多个web资源,这些web资源可以被外界访问,对外界提供服务;
  • 你们能访问到的任何一个页面或者资源,都存在于这个世界的某一个角落的计算机上。
  • URL
  • 这个统一的web资源会被放在同一个文件夹下,web应用程序–>Tomcat:服务器
  • 一个web应用由多部分组成 (静态web,动态web)
    • html,css,js
    • jsp,servlet
    • Java程序
    • jar包
    • 配置文件 (Properties)

web应用程序编写完毕后,若想提供给外界访问:需要一个服务器来统一管理;

1.3、静态web

  • *.htm, *.html,这些都是网页的后缀,如果服务器上一直存在这些东西,我们就可以直接进行读取。通络;

1567822802516

  • 静态web存在的缺点
    • Web页面无法动态更新,所有用户看到都是同一个页面
      • 轮播图,点击特效:伪动态
      • JavaScript [实际开发中,它用的最多]
      • VBScript
    • 它无法和数据库交互(数据无法持久化,用户无法交互)

1.4、动态web

页面会动态展示: “Web的页面展示的效果因人而异”;

1567823191289

缺点:

  • 加入服务器的动态web资源出现了错误,我们需要重新编写我们的后台程序,重新发布;
    • 停机维护

优点:

  • Web页面可以动态更新,所有用户看到都不是同一个页面
  • 它可以与数据库交互 (数据持久化:注册,商品信息,用户信息……..)

1567823350584

新手村:–魔鬼训练(分析原理,看源码)–> PK场

2、web服务器

2.1、技术讲解

ASP:

  • 微软:国内最早流行的就是ASP;
  • 在HTML中嵌入了VB的脚本, ASP + COM;
  • 在ASP开发中,基本一个页面都有几千行的业务代码,页面极其换乱
  • 维护成本高!
  • C#
  • IIS
1
2
3
4
5
6
7
8
9
10
11
12
13
html复制代码<h1>
<h1><h1>
<h1>
<h1>
<h1>
<h1>
<%
System.out.println("hello")
%>
<h1>
<h1>
<h1><h1>
<h1>

php:

  • PHP开发速度很快,功能很强大,跨平台,代码很简单 (70% , WP)
  • 无法承载大访问量的情况(局限性)

**JSP/Servlet : **

B/S:浏览和服务器

C/S: 客户端和服务器

  • sun公司主推的B/S架构
  • 基于Java语言的 (所有的大公司,或者一些开源的组件,都是用Java写的)
  • 可以承载三高问题带来的影响;
  • 语法像ASP , ASP–>JSP , 加强市场强度;

…..

2.2、web服务器

服务器是一种被动的操作,用来处理用户的一些请求和给用户一些响应信息;

IIS

微软的; ASP…,Windows中自带的

Tomcat

1567824446428

面向百度编程;

Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,最新的Servlet 和JSP 规范总是能在Tomcat 中得到体现,因为Tomcat 技术先进、性能稳定,而且免费,因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可,成为目前比较流行的Web 应用服务器。

Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选。对于一个Java初学web的人来说,它是最佳的选择

Tomcat 实际上运行JSP 页面和Servlet。Tomcat最新版本为9.0。

….

工作3-5年之后,可以尝试手写Tomcat服务器;

下载tomcat:

  1. 安装 or 解压
  2. 了解配置文件及目录结构
  3. 这个东西的作用

3、Tomcat

3.1、 安装tomcat

tomcat官网:tomcat.apache.org/

1567825600842

1567825627138

3.2、Tomcat启动和配置

文件夹作用:

1567825763180

启动。关闭Tomcat

1567825840657

访问测试:http://localhost:8080/

可能遇到的问题:

  1. Java环境变量没有配置
  2. 闪退问题:需要配置兼容性
  3. 乱码问题:配置文件中设置

3.3、配置

1567825967256

可以配置启动的端口号

  • tomcat的默认端口号为:8080
  • mysql:3306
  • http:80
  • https:443
1
2
3
xml复制代码<Connector port="8081" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />

可以配置主机的名称

  • 默认的主机名为:localhost->127.0.0.1
  • 默认网站应用存放的位置为:webapps
1
2
xml复制代码  <Host name="www.qinjiang.com"  appBase="webapps"
unpackWARs="true" autoDeploy="true">

高难度面试题:

请你谈谈网站是如何进行访问的!

  1. 输入一个域名;回车
  2. 检查本机的 C:\Windows\System32\drivers\etc\hosts配置文件下有没有这个域名映射;
1. 有:直接返回对应的ip地址,这个地址中,有我们需要访问的web程序,可以直接访问



1
java复制代码127.0.0.1       www.qinjiang.com
2. 没有:去DNS服务器找,找到的话就返回,找不到就返回找不到;![1567827057913](https://gitee.com/songjianzaina/juejin_p10/raw/master/img/b6916f5fb748115d8ec6e879d75322ee6f6e22dea94482392fe9a6b4c215739d)
  1. 可以配置一下环境变量(可选性)

3.4、发布一个web网站

不会就先模仿

  • 将自己写的网站,放到服务器(Tomcat)中指定的web应用的文件夹(webapps)下,就可以访问了

网站应该有的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码--webapps :Tomcat服务器的web目录
-ROOT
-kuangstudy :网站的目录名
- WEB-INF
-classes : java程序
-lib:web应用所依赖的jar包
-web.xml :网站配置文件
- index.html 默认的首页
- static
-css
-style.css
-js
-img
-.....

HTTP协议 : 面试

Maven:构建工具

  • Maven安装包

Servlet 入门

  • HelloWorld!
  • Servlet配置
  • 原理

4、Http

4.1、什么是HTTP

HTTP(超文本传输协议)是一个简单的请求-响应协议,它通常运行在TCP之上。

  • 文本:html,字符串,~ ….
  • 超文本:图片,音乐,视频,定位,地图…….
  • 80

Https:安全的

  • 443

4.2、两个时代

  • http1.0
+ HTTP/1.0:客户端可以与web服务器连接后,只能获得一个web资源,断开连接
  • http2.0
+ HTTP/1.1:客户端可以与web服务器连接后,可以获得多个web资源。‘

4.3、Http请求

  • 客户端—发请求(Request)—服务器

百度:

1
2
3
4
java复制代码Request URL:https://www.baidu.com/   请求地址
Request Method:GET get方法/post方法
Status Code:200 OK 状态码:200
Remote(远程) Address:14.215.177.39:443
1
2
3
4
5
java复制代码Accept:text/html  
Accept-Encoding:gzip, deflate, br
Accept-Language:zh-CN,zh;q=0.9 语言
Cache-Control:max-age=0
Connection:keep-alive

1、请求行

  • 请求行中的请求方式:GET
  • 请求方式:Get,Post,HEAD,DELETE,PUT,TRACT…
    • get:请求能够携带的参数比较少,大小有限制,会在浏览器的URL地址栏显示数据内容,不安全,但高效
    • post:请求能够携带的参数没有限制,大小没有限制,不会在浏览器的URL地址栏显示数据内容,安全,但不高效。

2、消息头

1
2
3
4
5
6
java复制代码Accept:告诉浏览器,它所支持的数据类型
Accept-Encoding:支持哪种编码格式 GBK UTF-8 GB2312 ISO8859-1
Accept-Language:告诉浏览器,它的语言环境
Cache-Control:缓存控制
Connection:告诉浏览器,请求完成是断开还是保持连接
HOST:主机..../.

4.4、Http响应

  • 服务器—响应—–客户端

百度:

1
2
3
4
java复制代码Cache-Control:private    缓存控制
Connection:Keep-Alive 连接
Content-Encoding:gzip 编码
Content-Type:text/html 类型

1.响应体

1
2
3
4
5
6
7
8
java复制代码Accept:告诉浏览器,它所支持的数据类型
Accept-Encoding:支持哪种编码格式 GBK UTF-8 GB2312 ISO8859-1
Accept-Language:告诉浏览器,它的语言环境
Cache-Control:缓存控制
Connection:告诉浏览器,请求完成是断开还是保持连接
HOST:主机..../.
Refresh:告诉客户端,多久刷新一次;
Location:让网页重新定位;

2、响应状态码

200:请求响应成功 200

3xx:请求重定向

  • 重定向:你重新到我给你新位置去;

4xx:找不到资源 404

  • 资源不存在;

5xx:服务器代码错误 500 502:网关错误

常见面试题:

当你的浏览器中地址栏输入地址并回车的一瞬间到页面能够展示回来,经历了什么?

5、Maven

我为什么要学习这个技术?

  1. 在Javaweb开发中,需要使用大量的jar包,我们手动去导入;
  2. 如何能够让一个东西自动帮我导入和配置这个jar包。

由此,Maven诞生了!

5.1 Maven项目架构管理工具

我们目前用来就是方便导入jar包的!

Maven的核心思想:约定大于配置

  • 有约束,不要去违反。

Maven会规定好你该如何去编写我们的Java代码,必须要按照这个规范来;

5.2 下载安装Maven

官网;maven.apache.org/

1567842350606

下载完成后,解压即可;

小狂神友情建议:电脑上的所有环境都放在一个文件夹下,方便管理;

5.3 配置环境变量

在我们的系统环境变量中

配置如下配置:

  • M2_HOME maven目录下的bin目录
  • MAVEN_HOME maven的目录
  • 在系统的path中配置 %MAVEN_HOME%\bin

1567842882993

测试Maven是否安装成功,保证必须配置完毕!

5.4 阿里云镜像

1567844609399

  • 镜像:mirrors
    • 作用:加速我们的下载
  • 国内建议使用阿里云的镜像
1
2
3
4
5
6
xml复制代码<mirror>
<id>nexus-aliyun</id>
<mirrorOf>*,!jeecg,!jeecg-snapshots</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>

5.5 本地仓库

在本地的仓库,远程仓库;

**建立一个本地仓库:**localRepository

1
xml复制代码<localRepository>D:\Environment\apache-maven-3.6.2\maven-repo</localRepository>

5.6、在IDEA中使用Maven

  1. 启动IDEA
  2. 创建一个MavenWeb项目

1567844785602

1567844841172

1567844917185

1567844956177

1567845029864
3. 等待项目初始化完毕

1567845105970

1567845137978
4. 观察maven仓库中多了什么东西?
5. IDEA中的Maven设置

注意:IDEA项目创建成功后,看一眼Maven的配置

1567845341956

1567845413672
6. 到这里,Maven在IDEA中的配置和使用就OK了!

5.7、创建一个普通的Maven项目

1567845557744

1567845717377

这个只有在Web应用下才会有!

1567845782034

5.8 标记文件夹功能

1567845910728

1567845957139

1567846034906

1567846073511

5.9 在 IDEA中配置Tomcat

1567846140348

1567846179573

1567846234175

1567846369751

解决警告问题

必须要的配置:为什么会有这个问题:我们访问一个网站,需要指定一个文件夹名字;

1567846421963

1567846546465

1567846559111

1567846640372

5.10 pom文件

pom.xml 是Maven的核心配置文件

1567846784849

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
xml复制代码<?xml version="1.0" encoding="UTF-8"?>

<!--Maven版本和头文件-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<!--这里就是我们刚才配置的GAV-->
<groupId>com.kuang</groupId>
<artifactId>javaweb-01-maven</artifactId>
<version>1.0-SNAPSHOT</version>
<!--Package:项目的打包方式
jar:java应用
war:JavaWeb应用
-->
<packaging>war</packaging>


<!--配置-->
<properties>
<!--项目的默认构建编码-->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!--编码版本-->
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<!--项目依赖-->
<dependencies>
<!--具体依赖的jar包配置文件-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
</dependencies>

<!--项目构建用的东西-->
<build>
<finalName>javaweb-01-maven</finalName>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

1567847410771

maven由于他的约定大于配置,我们之后可以能遇到我们写的配置文件,无法被导出或者生效的问题,解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xml复制代码<!--在build中配置resources,来防止我们资源导出失败的问题-->
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>

5.12 IDEA操作

1567847630808

1567847662429

5.13 解决遇到的问题

  1. Maven 3.6.2

解决方法:降级为3.6.1

1567904721301
2. Tomcat闪退
3. IDEA中每次都要重复配置Maven
在IDEA中的全局默认配置中去配置

1567905247201

1567905291002
4. Maven项目中Tomcat无法配置
5. maven默认web项目中的web.xml版本问题

1567905537026
6. 替换为webapp4.0版本和tomcat一致

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0"
metadata-complete="true">



</web-app>
  1. Maven仓库的使用

地址:mvnrepository.com/

1567905870750

1567905982979

1567906017448

1567906039469

6、Servlet

6.1、Servlet简介

  • Servlet就是sun公司开发动态web的一门技术
  • Sun在这些API中提供一个接口叫做:Servlet,如果你想开发一个Servlet程序,只需要完成两个小步骤:
    • 编写一个类,实现Servlet接口
    • 把开发好的Java类部署到web服务器中。

把实现了Servlet接口的Java程序叫做,Servlet

6.2、HelloServlet

Serlvet接口Sun公司有两个默认的实现类:HttpServlet,GenericServlet

  1. 构建一个普通的Maven项目,删掉里面的src目录,以后我们的学习就在这个项目里面建立Moudel;这个空的工程就是Maven主工程;
  2. 关于Maven父子工程的理解:

父项目中会有

1
2
3
xml复制代码    <modules>
<module>servlet-01</module>
</modules>

子项目会有

1
2
3
4
5
xml复制代码    <parent>
<artifactId>javaweb-02-servlet</artifactId>
<groupId>com.kuang</groupId>
<version>1.0-SNAPSHOT</version>
</parent>

父项目中的java子项目可以直接使用

1
java复制代码son extends father
  1. Maven环境优化
1. 修改web.xml为最新的
2. 将maven的结构搭建完整
  1. 编写一个Servlet程序

1567911804700

1. 编写一个普通类
2. 实现Servlet接口,这里我们直接继承HttpServlet



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class HelloServlet extends HttpServlet {

//由于get或者post只是请求实现的不同的方式,可以相互调用,业务逻辑都一样;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//ServletOutputStream outputStream = resp.getOutputStream();
PrintWriter writer = resp.getWriter(); //响应流
writer.print("Hello,Serlvet");
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
  1. 编写Servlet的映射

为什么需要映射:我们写的是JAVA程序,但是要通过浏览器访问,而浏览器需要连接web服务器,所以我们需要再web服务中注册我们写的Servlet,还需给他一个浏览器能够访问的路径;

1
2
3
4
5
6
7
8
9
10
xml复制代码    <!--注册Servlet-->
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>com.kuang.servlet.HelloServlet</servlet-class>
</servlet>
<!--Servlet的请求路径-->
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
  1. 配置Tomcat

注意:配置项目发布的路径就可以了
7. 启动测试,OK!

6.3、Servlet原理

Servlet是由Web服务器调用,web服务器在收到浏览器请求之后,会:

1567913793252

6.4、Mapping问题

  1. 一个Servlet可以指定一个映射路径
1
2
3
4
xml复制代码    <servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
  1. 一个Servlet可以指定多个映射路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码    <servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello2</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello3</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello4</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello5</url-pattern>
</servlet-mapping>
  1. 一个Servlet可以指定通用映射路径
1
2
3
4
xml复制代码    <servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello/*</url-pattern>
</servlet-mapping>
  1. 默认请求路径
1
2
3
4
5
xml复制代码    <!--默认请求路径-->
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
  1. 指定一些后缀或者前缀等等….
1
2
3
4
5
6
7
8
xml复制代码<!--可以自定义后缀实现请求映射
注意点,*前面不能加项目映射的路径
hello/sajdlkajda.qinjiang
-->
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>*.qinjiang</url-pattern>
</servlet-mapping>
  1. 优先级问题
    指定了固有的映射路径优先级最高,如果找不到就会走默认的处理请求;
1
2
3
4
5
6
7
8
9
xml复制代码<!--404-->
<servlet>
<servlet-name>error</servlet-name>
<servlet-class>com.kuang.servlet.ErrorServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>error</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>

6.5、ServletContext

web容器在启动的时候,它会为每个web程序都创建一个对应的ServletContext对象,它代表了当前的web应用;

1、共享数据

我在这个Servlet中保存的数据,可以在另外一个servlet中拿到;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

//this.getInitParameter() 初始化参数
//this.getServletConfig() Servlet配置
//this.getServletContext() Servlet上下文
ServletContext context = this.getServletContext();

String username = "秦疆"; //数据
context.setAttribute("username",username); //将一个数据保存在了ServletContext中,名字为:username 。值 username

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class GetServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext context = this.getServletContext();
String username = (String) context.getAttribute("username");

resp.setContentType("text/html");
resp.setCharacterEncoding("utf-8");
resp.getWriter().print("名字"+username);

}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
XML复制代码    <servlet>
<servlet-name>hello</servlet-name>
<servlet-class>com.kuang.servlet.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>


<servlet>
<servlet-name>getc</servlet-name>
<servlet-class>com.kuang.servlet.GetServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>getc</servlet-name>
<url-pattern>/getc</url-pattern>
</servlet-mapping>

测试访问结果;

2、获取初始化参数

1
2
3
4
5
xml复制代码    <!--配置一些web应用初始化参数-->
<context-param>
<param-name>url</param-name>
<param-value>jdbc:mysql://localhost:3306/mybatis</param-value>
</context-param>
1
2
3
4
5
java复制代码protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext context = this.getServletContext();
String url = context.getInitParameter("url");
resp.getWriter().print(url);
}

3、请求转发

1
2
3
4
5
6
7
8
java复制代码@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext context = this.getServletContext();
System.out.println("进入了ServletDemo04");
//RequestDispatcher requestDispatcher = context.getRequestDispatcher("/gp"); //转发的请求路径
//requestDispatcher.forward(req,resp); //调用forward实现请求转发;
context.getRequestDispatcher("/gp").forward(req,resp);
}

1567924457532

4、读取资源文件

Properties

  • 在java目录下新建properties
  • 在resources目录下新建properties

发现:都被打包到了同一个路径下:classes,我们俗称这个路径为classpath:

思路:需要一个文件流;

1
2
properties复制代码username=root12312
password=zxczxczxc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class ServletDemo05 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

InputStream is = this.getServletContext().getResourceAsStream("/WEB-INF/classes/com/kuang/servlet/aa.properties");

Properties prop = new Properties();
prop.load(is);
String user = prop.getProperty("username");
String pwd = prop.getProperty("password");

resp.getWriter().print(user+":"+pwd);

}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}

访问测试即可ok;

6.6、HttpServletResponse

web服务器接收到客户端的http请求,针对这个请求,分别创建一个代表请求的HttpServletRequest对象,代表响应的一个HttpServletResponse;

  • 如果要获取客户端请求过来的参数:找HttpServletRequest
  • 如果要给客户端响应一些信息:找HttpServletResponse

1、简单分类

负责向浏览器发送数据的方法

1
2
java复制代码ServletOutputStream getOutputStream() throws IOException;
PrintWriter getWriter() throws IOException;

负责向浏览器发送响应头的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码    void setCharacterEncoding(String var1);

void setContentLength(int var1);

void setContentLengthLong(long var1);

void setContentType(String var1);

void setDateHeader(String var1, long var2);

void addDateHeader(String var1, long var2);

void setHeader(String var1, String var2);

void addHeader(String var1, String var2);

void setIntHeader(String var1, int var2);

void addIntHeader(String var1, int var2);

响应的状态码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码    int SC_CONTINUE = 100;
int SC_SWITCHING_PROTOCOLS = 101;
int SC_OK = 200;
int SC_CREATED = 201;
int SC_ACCEPTED = 202;
int SC_NON_AUTHORITATIVE_INFORMATION = 203;
int SC_NO_CONTENT = 204;
int SC_RESET_CONTENT = 205;
int SC_PARTIAL_CONTENT = 206;
int SC_MULTIPLE_CHOICES = 300;
int SC_MOVED_PERMANENTLY = 301;
int SC_MOVED_TEMPORARILY = 302;
int SC_FOUND = 302;
int SC_SEE_OTHER = 303;
int SC_NOT_MODIFIED = 304;
int SC_USE_PROXY = 305;
int SC_TEMPORARY_REDIRECT = 307;
int SC_BAD_REQUEST = 400;
int SC_UNAUTHORIZED = 401;
int SC_PAYMENT_REQUIRED = 402;
int SC_FORBIDDEN = 403;
int SC_NOT_FOUND = 404;
int SC_METHOD_NOT_ALLOWED = 405;
int SC_NOT_ACCEPTABLE = 406;
int SC_PROXY_AUTHENTICATION_REQUIRED = 407;
int SC_REQUEST_TIMEOUT = 408;
int SC_CONFLICT = 409;
int SC_GONE = 410;
int SC_LENGTH_REQUIRED = 411;
int SC_PRECONDITION_FAILED = 412;
int SC_REQUEST_ENTITY_TOO_LARGE = 413;
int SC_REQUEST_URI_TOO_LONG = 414;
int SC_UNSUPPORTED_MEDIA_TYPE = 415;
int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
int SC_EXPECTATION_FAILED = 417;
int SC_INTERNAL_SERVER_ERROR = 500;
int SC_NOT_IMPLEMENTED = 501;
int SC_BAD_GATEWAY = 502;
int SC_SERVICE_UNAVAILABLE = 503;
int SC_GATEWAY_TIMEOUT = 504;
int SC_HTTP_VERSION_NOT_SUPPORTED = 505;

2、下载文件

  1. 向浏览器输出消息 (一直在讲,就不说了)
  2. 下载文件
    1. 要获取下载文件的路径
    2. 下载的文件名是啥?
    3. 设置想办法让浏览器能够支持下载我们需要的东西
    4. 获取下载文件的输入流
    5. 创建缓冲区
    6. 获取OutputStream对象
    7. 将FileOutputStream流写入到buffer缓冲区
    8. 使用OutputStream将缓冲区中的数据输出到客户端!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 要获取下载文件的路径
String realPath = "F:\\班级管理\\西开【19525】\\2、代码\\JavaWeb\\javaweb-02-servlet\\response\\target\\classes\\秦疆.png";
System.out.println("下载文件的路径:"+realPath);
// 2. 下载的文件名是啥?
String fileName = realPath.substring(realPath.lastIndexOf("\\") + 1);
// 3. 设置想办法让浏览器能够支持(Content-Disposition)下载我们需要的东西,中文文件名URLEncoder.encode编码,否则有可能乱码
resp.setHeader("Content-Disposition","attachment;filename="+URLEncoder.encode(fileName,"UTF-8"));
// 4. 获取下载文件的输入流
FileInputStream in = new FileInputStream(realPath);
// 5. 创建缓冲区
int len = 0;
byte[] buffer = new byte[1024];
// 6. 获取OutputStream对象
ServletOutputStream out = resp.getOutputStream();
// 7. 将FileOutputStream流写入到buffer缓冲区,使用OutputStream将缓冲区中的数据输出到客户端!
while ((len=in.read(buffer))>0){
out.write(buffer,0,len);
}

in.close();
out.close();
}

3、验证码功能

验证怎么来的?

  • 前端实现
  • 后端实现,需要用到 Java 的图片类,生产一个图片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
java复制代码package com.kuang.servlet;

import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;

public class ImageServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

//如何让浏览器3秒自动刷新一次;
resp.setHeader("refresh","3");

//在内存中创建一个图片
BufferedImage image = new BufferedImage(80,20,BufferedImage.TYPE_INT_RGB);
//得到图片
Graphics2D g = (Graphics2D) image.getGraphics(); //笔
//设置图片的背景颜色
g.setColor(Color.white);
g.fillRect(0,0,80,20);
//给图片写数据
g.setColor(Color.BLUE);
g.setFont(new Font(null,Font.BOLD,20));
g.drawString(makeNum(),0,20);

//告诉浏览器,这个请求用图片的方式打开
resp.setContentType("image/jpeg");
//网站存在缓存,不让浏览器缓存
resp.setDateHeader("expires",-1);
resp.setHeader("Cache-Control","no-cache");
resp.setHeader("Pragma","no-cache");

//把图片写给浏览器
ImageIO.write(image,"jpg", resp.getOutputStream());

}

//生成随机数
private String makeNum(){
Random random = new Random();
String num = random.nextInt(9999999) + "";
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 7-num.length() ; i++) {
sb.append("0");
}
num = sb.toString() + num;
return num;
}


@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}

4、实现重定向

1567931587955

B一个web资源收到客户端A请求后,B他会通知A客户端去访问另外一个web资源C,这个过程叫重定向

常见场景:

  • 用户登录
1
java复制代码void sendRedirect(String var1) throws IOException;

测试:

1
2
3
4
5
6
7
8
9
java复制代码@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

/*
resp.setHeader("Location","/r/img");
resp.setStatus(302);
*/
resp.sendRedirect("/r/img");//重定向
}

面试题:请你聊聊重定向和转发的区别?

相同点

  • 页面都会实现跳转

不同点

  • 请求转发的时候,url不会产生变化
  • 重定向时候,url地址栏会发生变化;

1567932163430

5、简单实现登录重定向

1
2
3
4
5
6
7
8
jsp复制代码<%--这里提交的路径,需要寻找到项目的路径--%>
<%--${pageContext.request.contextPath}代表当前的项目--%>

<form action="${pageContext.request.contextPath}/login" method="get">
用户名:<input type="text" name="username"> <br>
密码:<input type="password" name="password"> <br>
<input type="submit">
</form>
1
2
3
4
5
6
7
8
9
10
11
JAVA复制代码    @Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//处理请求
String username = req.getParameter("username");
String password = req.getParameter("password");

System.out.println(username+":"+password);

//重定向时候一定要注意,路径问题,否则404;
resp.sendRedirect("/r/success.jsp");
}
1
2
3
4
5
6
7
8
xml复制代码  <servlet>
<servlet-name>requset</servlet-name>
<servlet-class>com.kuang.servlet.RequestTest</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>requset</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
1
2
3
4
5
6
7
8
9
10
11
jsp复制代码<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>

<h1>Success</h1>

</body>
</html>

6.7、HttpServletRequest

HttpServletRequest代表客户端的请求,用户通过Http协议访问服务器,HTTP请求中的所有信息会被封装到HttpServletRequest,通过这个HttpServletRequest的方法,获得客户端的所有信息;

1567933996830

1567934023106

获取参数,请求转发

1567934110794

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

req.setCharacterEncoding("utf-8");
resp.setCharacterEncoding("utf-8");

String username = req.getParameter("username");
String password = req.getParameter("password");
String[] hobbys = req.getParameterValues("hobbys");
System.out.println("=============================");
//后台接收中文乱码问题
System.out.println(username);
System.out.println(password);
System.out.println(Arrays.toString(hobbys));
System.out.println("=============================");


System.out.println(req.getContextPath());
//通过请求转发
//这里的 / 代表当前的web应用
req.getRequestDispatcher("/success.jsp").forward(req,resp);

}

面试题:请你聊聊重定向和转发的区别?

相同点

  • 页面都会实现跳转

不同点

  • 请求转发的时候,url不会产生变化 307
  • 重定向时候,url地址栏会发生变化; 302

7、Cookie、Session

7.1、会话

会话:用户打开一个浏览器,点击了很多超链接,访问多个web资源,关闭浏览器,这个过程可以称之为会话;

有状态会话:一个同学来过教室,下次再来教室,我们会知道这个同学,曾经来过,称之为有状态会话;

你能怎么证明你是西开的学生?

你 西开

  1. 发票 西开给你发票
  2. 学校登记 西开标记你来过了

一个网站,怎么证明你来过?

客户端 服务端

  1. 服务端给客户端一个 信件,客户端下次访问服务端带上信件就可以了; cookie
  2. 服务器登记你来过了,下次你来的时候我来匹配你; seesion

7.2、保存会话的两种技术

cookie

  • 客户端技术 (响应,请求)

session

  • 服务器技术,利用这个技术,可以保存用户的会话信息? 我们可以把信息或者数据放在Session中!

常见常见:网站登录之后,你下次不用再登录了,第二次访问直接就上去了!

7.3、Cookie

1568344447291

  1. 从请求中拿到cookie信息
  2. 服务器响应给客户端cookie
1
2
3
4
5
6
java复制代码Cookie[] cookies = req.getCookies(); //获得Cookie
cookie.getName(); //获得cookie中的key
cookie.getValue(); //获得cookie中的vlaue
new Cookie("lastLoginTime", System.currentTimeMillis()+""); //新建一个cookie
cookie.setMaxAge(24*60*60); //设置cookie的有效期
resp.addCookie(cookie); //响应给客户端一个cookie

cookie:一般会保存在本地的 用户目录下 appdata;

一个网站cookie是否存在上限!聊聊细节问题

  • 一个Cookie只能保存一个信息;
  • 一个web站点可以给浏览器发送多个cookie,最多存放20个cookie;
  • Cookie大小有限制4kb;
  • 300个cookie浏览器上限

删除Cookie;

  • 不设置有效期,关闭浏览器,自动失效;
  • 设置有效期时间为 0 ;

编码解码:

1
2
java复制代码URLEncoder.encode("秦疆","utf-8")
URLDecoder.decode(cookie.getValue(),"UTF-8")

7.4、Session(重点)

1568344560794

什么是Session:

  • 服务器会给每一个用户(浏览器)创建一个Seesion对象;
  • 一个Seesion独占一个浏览器,只要浏览器没有关闭,这个Session就存在;
  • 用户登录之后,整个网站它都可以访问!–> 保存用户的信息;保存购物车的信息…..

1568342773861

Session和cookie的区别:

  • Cookie是把用户的数据写给用户的浏览器,浏览器保存 (可以保存多个)
  • Session把用户的数据写到用户独占Session中,服务器端保存 (保存重要的信息,减少服务器资源的浪费)
  • Session对象由服务创建;

使用场景:

  • 保存一个登录用户的信息;
  • 购物车信息;
  • 在整个网站中经常会使用的数据,我们将它保存在Session中;

使用Session:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
java复制代码package com.kuang.servlet;

import com.kuang.pojo.Person;

import javax.servlet.ServletException;
import javax.servlet.http.*;
import java.io.IOException;

public class SessionDemo01 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

//解决乱码问题
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=utf-8");

//得到Session
HttpSession session = req.getSession();
//给Session中存东西
session.setAttribute("name",new Person("秦疆",1));
//获取Session的ID
String sessionId = session.getId();

//判断Session是不是新创建
if (session.isNew()){
resp.getWriter().write("session创建成功,ID:"+sessionId);
}else {
resp.getWriter().write("session以及在服务器中存在了,ID:"+sessionId);
}

//Session创建的时候做了什么事情;
// Cookie cookie = new Cookie("JSESSIONID",sessionId);
// resp.addCookie(cookie);

}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}

//得到Session
HttpSession session = req.getSession();

Person person = (Person) session.getAttribute("name");

System.out.println(person.toString());

HttpSession session = req.getSession();
session.removeAttribute("name");
//手动注销Session
session.invalidate();

会话自动过期:web.xml配置

1
2
3
4
5
xml复制代码<!--设置Session默认的失效时间-->
<session-config>
<!--15分钟后Session自动失效,以分钟为单位-->
<session-timeout>15</session-timeout>
</session-config>

1568344679763

8、JSP

8.1、什么是JSP

Java Server Pages : Java服务器端页面,也和Servlet一样,用于动态Web技术!

最大的特点:

  • 写JSP就像在写HTML
  • 区别:
    • HTML只给用户提供静态的数据
    • JSP页面中可以嵌入JAVA代码,为用户提供动态数据;

8.2、JSP原理

思路:JSP到底怎么执行的!

  • 代码层面没有任何问题
  • 服务器内部工作

tomcat中有一个work目录;

IDEA中使用Tomcat的会在IDEA的tomcat中生产一个work目录

1568345873736

我电脑的地址:

1
java复制代码C:\Users\Administrator\.IntelliJIdea2018.1\system\tomcat\Unnamed_javaweb-session-cookie\work\Catalina\localhost\ROOT\org\apache\jsp

发现页面转变成了Java程序!

1568345948307

浏览器向服务器发送请求,不管访问什么资源,其实都是在访问Servlet!

JSP最终也会被转换成为一个Java类!

JSP 本质上就是一个Servlet

1
2
3
4
5
6
7
8
9
java复制代码//初始化
public void _jspInit() {

}
//销毁
public void _jspDestroy() {
}
//JSPService
public void _jspService(.HttpServletRequest request,HttpServletResponse response)
  1. 判断请求
  2. 内置一些对象
1
2
3
4
5
6
7
8
java复制代码final javax.servlet.jsp.PageContext pageContext;  //页面上下文
javax.servlet.http.HttpSession session = null; //session
final javax.servlet.ServletContext application; //applicationContext
final javax.servlet.ServletConfig config; //config
javax.servlet.jsp.JspWriter out = null; //out
final java.lang.Object page = this; //page:当前
HttpServletRequest request //请求
HttpServletResponse response //响应
  1. 输出页面前增加的代码
1
2
3
4
5
6
7
8
9
java复制代码response.setContentType("text/html");       //设置响应的页面类型
pageContext = _jspxFactory.getPageContext(this, request, response,
null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
_jspx_out = out;
  1. 以上的这些个对象我们可以在JSP页面中直接使用!

1568347078207

在JSP页面中;

只要是 JAVA代码就会原封不动的输出;

如果是HTML代码,就会被转换为:

1
java复制代码out.write("<html>\r\n");

这样的格式,输出到前端!

8.3、JSP基础语法

任何语言都有自己的语法,JAVA中有,。 JSP 作为java技术的一种应用,它拥有一些自己扩充的语法(了解,知道即可!),Java所有语法都支持!

JSP表达式

1
2
3
4
5
jsp复制代码  <%--JSP表达式
作用:用来将程序的输出,输出到客户端
<%= 变量或者表达式%>
--%>
<%= new java.util.Date()%>

jsp脚本片段

1
2
3
4
5
6
7
8
jsp复制代码  <%--jsp脚本片段--%>
<%
int sum = 0;
for (int i = 1; i <=100 ; i++) {
sum+=i;
}
out.println("<h1>Sum="+sum+"</h1>");
%>

脚本片段的再实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
jsp复制代码  <%
int x = 10;
out.println(x);
%>
<p>这是一个JSP文档</p>
<%
int y = 2;
out.println(y);
%>

<hr>


<%--在代码嵌入HTML元素--%>
<%
for (int i = 0; i < 5; i++) {
%>
<h1>Hello,World <%=i%> </h1>
<%
}
%>

JSP声明

1
2
3
4
5
6
7
8
9
10
11
jsp复制代码  <%!
static {
System.out.println("Loading Servlet!");
}

private int globalVar = 0;

public void kuang(){
System.out.println("进入了方法Kuang!");
}
%>

JSP声明:会被编译到JSP生成Java的类中!其他的,就会被生成到_jspService方法中!

在JSP,嵌入Java代码即可!

1
2
3
4
5
jsp复制代码<%%>
<%=%>
<%!%>

<%--注释--%>

JSP的注释,不会在客户端显示,HTML就会!

8.4、JSP指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
jsp复制代码<%@page args.... %>
<%@include file=""%>

<%--@include会将两个页面合二为一--%>

<%@include file="common/header.jsp"%>
<h1>网页主体</h1>

<%@include file="common/footer.jsp"%>

<hr>


<%--jSP标签
jsp:include:拼接页面,本质还是三个
--%>
<jsp:include page="/common/header.jsp"/>
<h1>网页主体</h1>
<jsp:include page="/common/footer.jsp"/>

8.5、9大内置对象

  • PageContext 存东西
  • Request 存东西
  • Response
  • Session 存东西
  • Application 【SerlvetContext】 存东西
  • config 【SerlvetConfig】
  • out
  • page ,不用了解
  • exception
1
2
3
4
java复制代码pageContext.setAttribute("name1","秦疆1号"); //保存的数据只在一个页面中有效
request.setAttribute("name2","秦疆2号"); //保存的数据只在一次请求中有效,请求转发会携带这个数据
session.setAttribute("name3","秦疆3号"); //保存的数据只在一次会话中有效,从打开浏览器到关闭浏览器
application.setAttribute("name4","秦疆4号"); //保存的数据只在服务器中有效,从打开服务器到关闭服务器

request:客户端向服务器发送请求,产生的数据,用户看完就没用了,比如:新闻,用户看完没用的!

session:客户端向服务器发送请求,产生的数据,用户用完一会还有用,比如:购物车;

application:客户端向服务器发送请求,产生的数据,一个用户用完了,其他用户还可能使用,比如:聊天数据;

8.6、JSP标签、JSTL标签、EL表达式

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<!-- JSTL表达式的依赖 -->
<dependency>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>jstl-api</artifactId>
<version>1.2</version>
</dependency>
<!-- standard标签库 -->
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>

EL表达式: ${ }

  • 获取数据
  • 执行运算
  • 获取web开发的常用对象

JSP标签

1
2
3
4
5
6
7
8
9
10
jsp复制代码<%--jsp:include--%>

<%--
http://localhost:8080/jsptag.jsp?name=kuangshen&age=12
--%>

<jsp:forward page="/jsptag2.jsp">
<jsp:param name="name" value="kuangshen"></jsp:param>
<jsp:param name="age" value="12"></jsp:param>
</jsp:forward>

JSTL表达式

JSTL标签库的使用就是为了弥补HTML标签的不足;它自定义许多标签,可以供我们使用,标签的功能和Java代码一样!

格式化标签

SQL标签

XML 标签

核心标签 (掌握部分)

1568362473764

JSTL标签库使用步骤

  • 引入对应的 taglib
  • 使用其中的方法
  • 在Tomcat 也需要引入 jstl的包,否则会报错:JSTL解析错误

c:if

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
jsp复制代码<head>
<title>Title</title>
</head>
<body>


<h4>if测试</h4>

<hr>

<form action="coreif.jsp" method="get">
<%--
EL表达式获取表单中的数据
${param.参数名}
--%>
<input type="text" name="username" value="${param.username}">
<input type="submit" value="登录">
</form>

<%--判断如果提交的用户名是管理员,则登录成功--%>
<c:if test="${param.username=='admin'}" var="isAdmin">
<c:out value="管理员欢迎您!"/>
</c:if>

<%--自闭合标签--%>
<c:out value="${isAdmin}"/>

</body>

c:choose c:when

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
jsp复制代码<body>

<%--定义一个变量score,值为85--%>
<c:set var="score" value="55"/>

<c:choose>
<c:when test="${score>=90}">
你的成绩为优秀
</c:when>
<c:when test="${score>=80}">
你的成绩为一般
</c:when>
<c:when test="${score>=70}">
你的成绩为良好
</c:when>
<c:when test="${score<=60}">
你的成绩为不及格
</c:when>
</c:choose>

</body>

c:forEach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
jsp复制代码<%

ArrayList<String> people = new ArrayList<>();
people.add(0,"张三");
people.add(1,"李四");
people.add(2,"王五");
people.add(3,"赵六");
people.add(4,"田六");
request.setAttribute("list",people);
%>


<%--
var , 每一次遍历出来的变量
items, 要遍历的对象
begin, 哪里开始
end, 到哪里
step, 步长
--%>
<c:forEach var="people" items="${list}">
<c:out value="${people}"/> <br>
</c:forEach>

<hr>

<c:forEach var="people" items="${list}" begin="1" end="3" step="1" >
<c:out value="${people}"/> <br>
</c:forEach>

9、JavaBean

实体类

JavaBean有特定的写法:

  • 必须要有一个无参构造
  • 属性必须私有化
  • 必须有对应的get/set方法;

一般用来和数据库的字段做映射 ORM;

ORM :对象关系映射

  • 表—>类
  • 字段–>属性
  • 行记录—->对象

people表

id name age address
1 秦疆1号 3 西安
2 秦疆2号 18 西安
3 秦疆3号 100 西安
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码class People{
private int id;
private String name;
private int id;
private String address;
}

class A{
new People(1,"秦疆1号",3,"西安");
new People(2,"秦疆2号",3,"西安");
new People(3,"秦疆3号",3,"西安");
}
  • 过滤器
  • 文件上传
  • 邮件发送
  • JDBC 复习 : 如何使用JDBC , JDBC crud, jdbc 事务

10、MVC三层架构

什么是MVC: Model view Controller 模型、视图、控制器

10.1、早些年

1568423664332

用户直接访问控制层,控制层就可以直接操作数据库;

1
2
3
4
5
6
7
8
9
10
java复制代码servlet--CRUD-->数据库
弊端:程序十分臃肿,不利于维护
servlet的代码中:处理请求、响应、视图跳转、处理JDBC、处理业务代码、处理逻辑代码

架构:没有什么是加一层解决不了的!
程序猿调用
|
JDBC
|
Mysql Oracle SqlServer ....

10.2、MVC三层架构

1568424227281

Model

  • 业务处理 :业务逻辑(Service)
  • 数据持久层:CRUD (Dao)

View

  • 展示数据
  • 提供链接发起Servlet请求 (a,form,img…)

Controller (Servlet)

  • 接收用户的请求 :(req:请求参数、Session信息….)
  • 交给业务层处理对应的代码
  • 控制视图的跳转
1
java复制代码登录--->接收用户的登录请求--->处理用户的请求(获取用户登录的参数,username,password)---->交给业务层处理登录业务(判断用户名密码是否正确:事务)--->Dao层查询用户名和密码是否正确-->数据库

11、Filter (重点)

Filter:过滤器 ,用来过滤网站的数据;

  • 处理中文乱码
  • 登录验证….

1568424858708

Filter开发步骤:

  1. 导包
  2. 编写过滤器
1. 导包不要错


![1568425162525](https://gitee.com/songjianzaina/juejin_p10/raw/master/img/645646616bab1e52c44f7f6a0186f53c23618ed1ddd28cc3626dabec2c551465)


实现Filter接口,重写对应的方法即可



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码public class CharacterEncodingFilter implements Filter {

//初始化:web服务器启动,就以及初始化了,随时等待过滤对象出现!
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("CharacterEncodingFilter初始化");
}

//Chain : 链
/*
1. 过滤中的所有代码,在过滤特定请求的时候都会执行
2. 必须要让过滤器继续同行
chain.doFilter(request,response);
*/
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=UTF-8");

System.out.println("CharacterEncodingFilter执行前....");
chain.doFilter(request,response); //让我们的请求继续走,如果不写,程序到这里就被拦截停止!
System.out.println("CharacterEncodingFilter执行后....");
}

//销毁:web服务器关闭的时候,过滤会销毁
public void destroy() {
System.out.println("CharacterEncodingFilter销毁");
}
}
  1. 在web.xml中配置 Filter
1
2
3
4
5
6
7
8
9
10
xml复制代码<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>com.kuang.filter.CharacterEncodingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<!--只要是 /servlet的任何请求,会经过这个过滤器-->
<url-pattern>/servlet/*</url-pattern>
<!--<url-pattern>/*</url-pattern>-->
</filter-mapping>

12、监听器

实现一个监听器的接口;(有N种)

  1. 编写一个监听器

实现监听器的接口…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
java复制代码//统计网站在线人数 : 统计session
public class OnlineCountListener implements HttpSessionListener {

//创建session监听: 看你的一举一动
//一旦创建Session就会触发一次这个事件!
public void sessionCreated(HttpSessionEvent se) {
ServletContext ctx = se.getSession().getServletContext();

System.out.println(se.getSession().getId());

Integer onlineCount = (Integer) ctx.getAttribute("OnlineCount");

if (onlineCount==null){
onlineCount = new Integer(1);
}else {
int count = onlineCount.intValue();
onlineCount = new Integer(count+1);
}

ctx.setAttribute("OnlineCount",onlineCount);

}

//销毁session监听
//一旦销毁Session就会触发一次这个事件!
public void sessionDestroyed(HttpSessionEvent se) {
ServletContext ctx = se.getSession().getServletContext();

Integer onlineCount = (Integer) ctx.getAttribute("OnlineCount");

if (onlineCount==null){
onlineCount = new Integer(0);
}else {
int count = onlineCount.intValue();
onlineCount = new Integer(count-1);
}

ctx.setAttribute("OnlineCount",onlineCount);

}


/*
Session销毁:
1. 手动销毁 getSession().invalidate();
2. 自动销毁
*/
}
  1. web.xml中注册监听器
1
2
3
4
xml复制代码<!--注册监听器-->
<listener>
<listener-class>com.kuang.listener.OnlineCountListener</listener-class>
</listener>
  1. 看情况是否使用!

13、过滤器、监听器常见应用

监听器:GUI编程中经常使用;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码public class TestPanel {
public static void main(String[] args) {
Frame frame = new Frame("中秋节快乐"); //新建一个窗体
Panel panel = new Panel(null); //面板
frame.setLayout(null); //设置窗体的布局

frame.setBounds(300,300,500,500);
frame.setBackground(new Color(0,0,255)); //设置背景颜色

panel.setBounds(50,50,300,300);
panel.setBackground(new Color(0,255,0)); //设置背景颜色

frame.add(panel);

frame.setVisible(true);

//监听事件,监听关闭事件
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
super.windowClosing(e);
}
});


}
}

用户登录之后才能进入主页!用户注销后就不能进入主页了!

  1. 用户登录之后,向Sesison中放入用户的数据
  2. 进入主页的时候要判断用户是否已经登录;要求:在过滤器中实现!
1
2
3
4
5
6
7
8
java复制代码HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;

if (request.getSession().getAttribute(Constant.USER_SESSION)==null){
response.sendRedirect("/error.jsp");
}

chain.doFilter(request,response);

14、JDBC

什么是JDBC : Java连接数据库!

1568439601825

需要jar包的支持:

  • java.sql
  • javax.sql
  • mysql-conneter-java… 连接驱动(必须要导入)

实验环境搭建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sql复制代码CREATE TABLE users(
id INT PRIMARY KEY,
`name` VARCHAR(40),
`password` VARCHAR(40),
email VARCHAR(60),
birthday DATE
);

INSERT INTO users(id,`name`,`password`,email,birthday)
VALUES(1,'张三','123456','zs@qq.com','2000-01-01');
INSERT INTO users(id,`name`,`password`,email,birthday)
VALUES(2,'李四','123456','ls@qq.com','2000-01-01');
INSERT INTO users(id,`name`,`password`,email,birthday)
VALUES(3,'王五','123456','ww@qq.com','2000-01-01');


SELECT * FROM users;

导入数据库依赖

1
2
3
4
5
6
xml复制代码<!--mysql的驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>

IDEA中连接数据库:

1568440926845

JDBC 固定步骤:

  1. 加载驱动
  2. 连接数据库,代表数据库
  3. 向数据库发送SQL的对象Statement : CRUD
  4. 编写SQL (根据业务,不同的SQL)
  5. 执行SQL
  6. 关闭连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码public class TestJdbc {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
//配置信息
//useUnicode=true&characterEncoding=utf-8 解决中文乱码
String url="jdbc:mysql://localhost:3306/jdbc?useUnicode=true&characterEncoding=utf-8";
String username = "root";
String password = "123456";

//1.加载驱动
Class.forName("com.mysql.jdbc.Driver");
//2.连接数据库,代表数据库
Connection connection = DriverManager.getConnection(url, username, password);

//3.向数据库发送SQL的对象Statement,PreparedStatement : CRUD
Statement statement = connection.createStatement();

//4.编写SQL
String sql = "select * from users";

//5.执行查询SQL,返回一个 ResultSet : 结果集
ResultSet rs = statement.executeQuery(sql);

while (rs.next()){
System.out.println("id="+rs.getObject("id"));
System.out.println("name="+rs.getObject("name"));
System.out.println("password="+rs.getObject("password"));
System.out.println("email="+rs.getObject("email"));
System.out.println("birthday="+rs.getObject("birthday"));
}

//6.关闭连接,释放资源(一定要做) 先开后关
rs.close();
statement.close();
connection.close();
}
}

预编译SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码public class TestJDBC2 {
public static void main(String[] args) throws Exception {
//配置信息
//useUnicode=true&characterEncoding=utf-8 解决中文乱码
String url="jdbc:mysql://localhost:3306/jdbc?useUnicode=true&characterEncoding=utf-8";
String username = "root";
String password = "123456";

//1.加载驱动
Class.forName("com.mysql.jdbc.Driver");
//2.连接数据库,代表数据库
Connection connection = DriverManager.getConnection(url, username, password);

//3.编写SQL
String sql = "insert into users(id, name, password, email, birthday) values (?,?,?,?,?);";

//4.预编译
PreparedStatement preparedStatement = connection.prepareStatement(sql);

preparedStatement.setInt(1,2);//给第一个占位符? 的值赋值为1;
preparedStatement.setString(2,"狂神说Java");//给第二个占位符? 的值赋值为狂神说Java;
preparedStatement.setString(3,"123456");//给第三个占位符? 的值赋值为123456;
preparedStatement.setString(4,"24736743@qq.com");//给第四个占位符? 的值赋值为1;
preparedStatement.setDate(5,new Date(new java.util.Date().getTime()));//给第五个占位符? 的值赋值为new Date(new java.util.Date().getTime());

//5.执行SQL
int i = preparedStatement.executeUpdate();

if (i>0){
System.out.println("插入成功@");
}

//6.关闭连接,释放资源(一定要做) 先开后关
preparedStatement.close();
connection.close();
}
}

事务

要么都成功,要么都失败!

ACID原则:保证数据的安全。

1
2
3
4
5
6
7
8
9
10
java复制代码开启事务
事务提交 commit()
事务回滚 rollback()
关闭事务

转账:
A:1000
B:1000

A(900) --100--> B(1100)

Junit单元测试

依赖

1
2
3
4
5
6
xml复制代码<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>

简单使用

@Test注解只有在方法上有效,只要加了这个注解的方法,就可以直接运行!

1
2
3
4
java复制代码@Test
public void test(){
System.out.println("Hello");
}

1568442261610

失败的时候是红色:

1568442289597

搭建一个环境

1
2
3
4
5
6
7
8
9
sql复制代码CREATE TABLE account(
id INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(40),
money FLOAT
);

INSERT INTO account(`name`,money) VALUES('A',1000);
INSERT INTO account(`name`,money) VALUES('B',1000);
INSERT INTO account(`name`,money) VALUES('C',1000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
java复制代码    @Test
public void test() {
//配置信息
//useUnicode=true&characterEncoding=utf-8 解决中文乱码
String url="jdbc:mysql://localhost:3306/jdbc?useUnicode=true&characterEncoding=utf-8";
String username = "root";
String password = "123456";

Connection connection = null;

//1.加载驱动
try {
Class.forName("com.mysql.jdbc.Driver");
//2.连接数据库,代表数据库
connection = DriverManager.getConnection(url, username, password);

//3.通知数据库开启事务,false 开启
connection.setAutoCommit(false);

String sql = "update account set money = money-100 where name = 'A'";
connection.prepareStatement(sql).executeUpdate();

//制造错误
//int i = 1/0;

String sql2 = "update account set money = money+100 where name = 'B'";
connection.prepareStatement(sql2).executeUpdate();

connection.commit();//以上两条SQL都执行成功了,就提交事务!
System.out.println("success");
} catch (Exception e) {
try {
//如果出现异常,就通知数据库回滚事务
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}finally {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…737738739…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%