0%

说明

  • 上一篇文章django-admin 主要介绍了利用admin创建用户,然后结合模板、html等实现了增删改查,本次介绍使用类视图来优化

登录的优化

  • 根目录的url,用系统自带的视图
1
2
3
4
5
6
7
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/logout/', LogoutView.as_view(next_page='/login/'), name='logout'),
path('accounts/login/', LoginView.as_view(template_name='login.html'), name='login'),

]

  • setting加入了登录、退出登录的url
1
2
3
4
5
6
7
8
# 指定登录URL
LOGIN_URL = '/accounts/login/'

# 指定注销后重定向的URL
LOGOUT_REDIRECT_URL = '/login/'

# 指定登录后重定向的URL
LOGIN_REDIRECT_URL = '/book/'

书籍列表

  • 看下应用下的url优化
1
2
3
4
5
6
7
urlpatterns = [
path('book/', BookList.as_view(), name='book_list'),
path('book/add/', BookAdd.as_view(), name='book_add'),
path('book/edit/<int:pk>/', BookEdit.as_view(), name='book_edit'),
path('book/<int:pk>/', BookDetail.as_view(), name='book_detail'),
path('book/delete/<int:pk>/', BookDelete.as_view(), name='book_delete'), # 新增删除路径
]
  • vew代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
lass BookList(LoginRequiredMixin, ListView):
model = Book
template_name = 'index.html'
context_object_name = 'latest_book_list'
paginate_by = 5

def get_queryset(self):
title = self.request.GET.get("title", "")
if title:
return Book.objects.filter(title__contains=title).order_by("-pub_date")
else:
return Book.objects.order_by("-pub_date")

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_term'] = self.request.GET.get("title", "")
return context
  • 使用了LoginRequiredMixin登录视图
  • get_queryset 返回列表,get_context_data返回具体字段

新增书籍

  • 具体view代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14

class BookAdd(LoginRequiredMixin, CreateView):
model = Book
# fields = ['title', 'price']
form_class = BookForm

template_name = 'add.html'
success_url = reverse_lazy('book_list')
# 默认实现了 form_valid() 简单重定向至 success_url
def form_valid(self, form):
book = form.save(commit=False)
book.pub_date = now().strftime("%Y-%m-%d")
book.save()
return super().form_valid(form)
  • html文件
1
2
3
4
5
6
7
8
9
10
11
12
{% extends "base_generic.html" %}

{% block content %}
<h2>Add New Book</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save changes</button>
</form>
<a href="{% url 'book_list' %}">Back to list</a>
{% endblock %}

  • form.as_p 直接根据BookForm 生成了表单内容

  • url进行了修改,之前是跳转到add的html界面,然后提交form表单,现在可以一步完成

1
2
# path('event/add/', EventAdd.as_view(), name='event_add'),
path('book/add/', BookAdd.as_view(), name='book_add'),

删除

1
2
3
4
5
6
7
8
9
10
11
12
13
lass BookDelete(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
"""
权限控制:UserPassesTestMixin 允许你在视图级别进行自定义权限检查。
"""
model = Book
template_name = 'delete.html'
success_url = reverse_lazy('book_list')
# test_func: 这是 UserPassesTestMixin 要求实现的方法,用于定义具体的权限检查逻辑。如果 test_func 返回 True,则允许用户访问视图;否则,返回 False 并抛出 PermissionDenied 异常。
def test_func(self):
book = self.get_object()
# 确保只有书籍的所有者或管理员才能删除书籍
return self.request.user.is_superuser or self.request.user == book.owner

编辑

  • view
1
2
3
4
5
6
7
8
9
10
11
12
class BookEdit(LoginRequiredMixin, UpdateView):
model = Book
form_class = BookForm

template_name = 'edit.html'
success_url = reverse_lazy('book_list')

def form_valid(self, form):
book = form.save(commit=False)
book.pub_date = now().strftime("%Y-%m-%d")
book.save()
return super().form_valid(form)
  • html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>编辑</title>
</head>
<body>
{% extends "base_generic.html" %}

{% block content %}
<h2>Edit Book</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save changes</button>
</form>
<a href="{% url 'book_list' %}">Back to list</a>
{% endblock %}

</body>
</html>

安装

  • 安装django
1
E:\proj>python -m pip install Django
  • 使用django-admin 创建项目
1
django-admin startproject StudyDjangoAdmin
  • 查看路径

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    E:\proj\StudyDjangoAdmin>tree /f
    卷 软件盘 的文件夹 PATH 列表
    卷序列号为 240F-1787
    E:.
    │ manage.py

    └─StudyDjangoAdmin
    asgi.py
    settings.py
    urls.py
    wsgi.py
    __init__.py
    • 在我们创建了Django的项目后,我们在最原始的urls.py中就可以看见关于admin的路径:

image-20250909090838252

  • 启动服务
1
python manage.py runserver 0.0.0.0:8000
  • 如果要访问admin后台我们只需要输入以下网址:
1
http://127.0.0.1:8000/admin/

image-20250909091046888

数据库选择

  • 我们在项目的 settings.py 文件中找到 DATABASES 配置项,将其信息修改为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// E:\proj\StudyDjangoAdmin\StudyDjangoAdmin\settings.py
DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': BASE_DIR / 'db.sqlite3',
# }
'default':
{
'ENGINE': 'django.db.backends.mysql', # 数据库引擎
'NAME': 'runoob1', # 数据库名称
'HOST': '127.0.0.1', # 数据库地址,本机 ip 地址 127.0.0.1
'PORT': 3306, # 端口
'USER': 'root', # 数据库用户名
'PASSWORD': '123456', # 数据库密码
}
}
  • 安装mysql驱动
1
E:\proj\StudyDjango>pip3 install pymysql
  • 在与 settings.py 同级目录下的 __init__.py 中引入模块和进行配置
1
2
3
4
// E:\proj\StudyDjangoAdmin\StudyDjangoAdmin\__init__.py

import pymysql
pymysql.install
  • 默认使用的sqlite,本次选择使用mysql,(win7 mysql安装详解 )

  • 使用heidisql连接本地mysql数据库后,新建一个数据库,名称为:runoob1

  • 生成表结构

1
E:\proj\StudyDjangoAdmin>python manage.py migrate
  • 如果报错,MySQLclient 目前只支持到 Python3.4,因此如果使用的更高版本的 python,把D:\app\Python37\Lib\site-packages\django\db\backends\mysql\base.py 对应报错代码注释

  • 迁移知识点:迁移是非常强大的功能,它能让你在开发过程中持续的改变数据库结构而不需要重新删除和创建表 - 它专注于使数据库平滑升级而不会丢失数据,改变模型需要这三步:

image-20250909091526544

  • 创建超级用户
1
2
3
4
5
6
执行:
python manage.py createsuperuser

-- 提示输入创建的账号: admin
-- 邮箱:可直接回车不输
-- 密码、输两次

image-20250909093243315

修改为中文的界面

找到settings.py里的LANGUAGE_CODE 和TIME_ZONE修改:

1
2
LANGUAGE_CODE = 'zh-Hans'
TIME_ZONE = 'Asia/Shanghai

新增用户

  • 在admin后台新增用户,确保人员状态正确

image-20250911085956081

创建APP

  • Django 规定,如果要使用ORM模型,必须要创建一个 app
1
django-admin startapp TestModel

注册app

  • 接下来在 settings.py 中找到INSTALLED_APPS这一项,加入这个app
1
2
3
4
5
6
7
8
9
10
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'TestModel', # 添加此项

]

模型

  • 我们修改 TestModel/models.py 文件
1
2
3
4
5
6
7
8
from django.db import models


# Create your models here.
class Book(models.Model):
title = models.CharField(max_length=32)
price = models.DecimalField(max_digits=5, decimal_places=2)
pub_date = models.DateField()
  • 生成数据表
1
2
3
4
# 让 Django 知道我们在我们的模型有一些变更
python manage.py makemigrations TestModel
# 创建表结构
python manage.py migrate TestModel

image-20250911092703957

应用实例

  • 书籍的增删改查

  • TestModel/view

1
2
3
4
5
6
7
8
9

from django.http import HttpResponse
from django.template import loader
from TestModel import models
def book_list(request):
latest_book_list = models.Book.objects.order_by("-pub_date")[:5]
template = loader.get_template("index.html")
context = {"latest_book_list": latest_book_list}
return HttpResponse(template.render(context, request))
  • html模板文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
{% if latest_book_list %}
<ul>
{% for book in latest_book_list %}
<li><a href="/book/{{ book.id }}/">{{ book.title }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No book are available.</p>
{% endif %}
</body>
</html>
  • 要为 TestModel 应用定义一个 URLconf,创建一个名为 TestModel/urls.py 的文件,并包含以下内容:
1
2
3
4
urlpatterns = [
path("book/", views.book_list, name="book_list"),

]
  • 把TestModel应用的url引入
1
2
3
4
5
6
7
8
9
10
# StudyDjangoAdmin/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path('admin/', admin.site.urls),
path('', include('TestModel.urls')),

]

  • 手动在book表中插入一些数据后,然后访问book

image-20250914155359641

登录限制

  • TestModel/view 加入登录检验
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
from django.contrib.auth.decorators import login_required


def login(request):
return render(request, "login.html")


def login_action(request):
if request.method == 'POST':
username = request.POST.get("username", "")
password = request.POST.get("password", "")
#接受2个参数,用户名 密码,在用户名密码正确的情况下返回一个user对象。如果不正确,返回None
user = auth.authenticate(username=username, password=password)
if user is not None:
auth.login(request, user) # 登录
request.session['user'] = username # 将session信息记录到浏览器
response = HttpResponseRedirect('/event_manage/')
return response
else:
return render(request, "login.html", {'error': '用户名或者密码错误'})

@login_required
def logout_func(request):
auth.logout(request)
return HttpResponse("退出登录成功,<a href='/login'>重新登录</a>")

@login_required
def event_manage(request):
return render(request, 'event_manage.html')

@login_required
def book_list(request):
latest_book_list = models.Book.objects.order_by("-pub_date")[:5]
template = loader.get_template("index.html")
context = {"latest_book_list": latest_book_list}
return HttpResponse(template.render(context, request))

  • event_manage.html文件
1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html>
<head><title>登录管理</title></head>
<body>
<h1>登录成功</h1>
<p>进入到<a style="font-size:18px" href="/TestModel/book">主页</a><strong</p>
</body>
</html>
  • login.html
1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head><title>Django Page</title></head>
<body><h1>登录</h1>
<form method="POST" action="/login_action/">
<input name="username" type="text" placeholder="username"><br>
<input name="password" type="password" placeholder="password"><br>
{{ error }} <br/>
<button id="btn" type="submit">登录</button>
{% csrf_token %}
</form>
</body>
</html>
  • 书籍主页的html,加入了一个退出登录
1
2
3
<p><a href="/logout">退出登录</a></p>
{% if latest_book_list %}
...
  • 当没有登录的情况下,访问书籍主页,直接就调整到了登录界面

image-20250914141655730

  • 这里登录限制实现后,有个不灵活的地方,就是每次都要手动加上@login_required,之前文章里面实现过,可以参考这里

新增

  • 主页的index.html加入了新增
1
2
3
<div>
<a href="{% url 'event_add' %}">新增</a>
</div>
  • 应用中加入了两个add路由
1
2
3
4
5
6
7
8
urlpatterns = [
path("book/", views.book_list, name="book_list"),
# 新增界面
path("event_add/", views.event_add, name="event_add"),
# 提交新增数据
path("book_add/", views.book_add, name="book_add"),

]
  • view代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@login_required
def event_add(request):
return render(request, 'add.html')

@login_required
def book_add(request):
if request.method == 'POST':
title = request.POST.get("title", "")
price = request.POST.get("price", "")
d_price = float(price)
pub_date = datetime.now().strftime("%Y-%m-%d")
book = models.Book(title=title, price=d_price, pub_date=pub_date)
book.save()
# 保存成功后,调整到首页
return redirect('/book/')

image-20250914175243952

image-20250914175312086

编辑

  • index.html,加入了编辑
1
2
3
4
5
6
7
8
9
{% if latest_book_list %}
<ul>
{% for book in latest_book_list %}
<li>{{book.id}}--<a href="#">{{book.title}}</a>--<a href="{% url 'book_edit' book.id%}">编辑</a></li>
{% endfor %}
</ul>
{% else %}
<p>No book are available.</p>
{% endif %}
  • view代码。如果是get请求,就读取数据并且跳转到edit.html,然后是post请求直接就修改数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@login_required
def book_edit(request, book_id):
if request.method == 'POST':
title = request.POST.get("title", "")
price = request.POST.get("price", "")
d_price = float(price)
pub_date = datetime.now().strftime("%Y-%m-%d")
book = Book.objects.get(pk=book_id)
book.price = d_price
book.pub_date = pub_date
book.title = title
book.save()
return redirect('book_list')
else:
# 处理GET请求,显示编辑表单
book = get_object_or_404(Book, pk=book_id)
return render(request, 'edit.html', {'book': book})
  • url修改
1
2
path("book_edit/<int:book_id>/", views.book_edit, name="book_edit"),

image-20250914195750503

明细页面

  • index.html修改
1
2
3
4
5
6
7
8
9
{% if latest_book_list %}
<ul>
{% for book in latest_book_list %}
<li>{{book.id}}--<a href="{% url 'book_detail' book.id%}">{{book.title}}</a>--<a href="{% url 'book_edit' book.id%}">编辑</a></li>
{% endfor %}
</ul>
{% else %}
<p>No book are available.</p>
{% endif %}
  • view.py
1
2
3
4
5
6
@login_required
def book_detail(request, book_id):
book = get_object_or_404(Book, pk=book_id)
return render(request, 'detail.html', {
'book': book
})
  • url
1
path("book_detail/<int:book_id>/", views.book_detail, name="book_detail"),
  • detail.html明细页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>详情页面</title>
</head>
<body>
<div>
<p>id:{{ book.id }}</p>
<p>标题:{{ book.title }}</p>
<p>价格:{{ book.price }}</p>
<p>日期:{{ book.pub_date }}</p>
</div>
<a href="{% url 'book_list' %}">Back to List</a>

</body>
</html>

删除

  • index.html 加入了删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{% if latest_book_list %}
<ul>
{% for book in latest_book_list %}
<li>{{book.id}}--<a href="{% url 'book_detail' book.id%}">{{book.title}}</a>--<a href="{% url 'book_edit' book.id%}">编辑</a>
<form action="{% url 'book_delete' book.id %}" method="post" style="display:inline;">
{% csrf_token %}
<button type="submit" onclick="return confirm('确定要删除这本书吗?')">删除</button>
</form>
</li>
{% endfor %}
</ul>
{% else %}
<p>No book are available.</p>
{% endif %}
  • view加入删除
1
2
3
4
5
6
@login_required
def book_delete(request,book_id):
if request.method == 'POST':
book = get_object_or_404(Book, pk=book_id)
book.delete()
return redirect('book_list')
  • url
1
path("book_delete/<int:book_id>/", views.book_delete, name="book_delete"),

查询

  • 书籍列表index.html加入搜索
1
2
3
4
5
6
7
8
9
10
11
12
<p><a href="/logout">退出登录</a></p>

<form class="navbar-form" method="get" action="{% url 'book_list' %}">
<div class="form-group">
<input type="text" name="title" value="{{ search_term }}">
</div>
<button type="submit" class="btn btn-success">
搜索
</button>
</form>
{% if latest_book_list %}
....
  • view代码修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@login_required
def book_list(request):
title = request.GET.get("title", "")

if title:
books = Book.objects.filter(
title__contains=title
).order_by("-pub_date")[:5]
else:
books = Book.objects.order_by("-pub_date")[:5]

return render(request, 'index.html', {
'latest_book_list': books,
'search_term': title
})

分页

  • view的book_list加入分页,有自带的Paginator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

@login_required
def book_list(request):
title = request.GET.get("title", "")

if title:
books = Book.objects.filter(
title__contains=title
).order_by("-pub_date")
else:
books = Book.objects.order_by("-pub_date")

# Set up pagination
paginator = Paginator(books, 5) # Show 5 books per page.
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
logger.debug(f"Page number: {page_number}, Page obj: {page_obj}")
return render(request, 'index.html', {
'latest_book_list': page_obj,
'search_term': title
})
  • index.html加入分页样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
!-- Pagination controls -->
<div class="pagination">
<span class="step-links">
{% if latest_book_list.has_previous %}
<a href="?page=1&title={{ search_term }}">« 首页</a>
<a href="?page={{ page_obj.previous_page_number }}&title={{ search_term }}">上一页</a>
{% endif %}

<span class="current">
第 {{ latest_book_list.number }} 页 / 共 {{ latest_book_list.paginator.num_pages }} 页
</span>

{% if latest_book_list.has_next %}
<a href="?page={{ latest_book_list.next_page_number }}&title={{ search_term }}">下一页</a>
<a href="?page={{ latest_book_list.paginator.num_pages }}&title={{ search_term }}">末页 »</a>
{% endif %}
</span>
</div>
  • 查看结果

image-20250914231917746

退出登录

  • index.html
1
2
<p><a href="/logout">退出登录</a></p>

  • 在根目录的url配置
1
2
url(r'^logout/', views.logout_func),

  • 在应用的view中
1
2
3
4
5
@login_required
def logout_func(request):
auth.logout(request)
return HttpResponse("退出登录成功,<a href='/login'>重新登录</a>")

短语收集

  • It is to turn 轮到你付账
  • It is said 据说
  • be getting 比较级
    • she is getting more and more beautiful(她越来越漂亮)

昨天、今天

  • last night 昨天晚上
  • yesterday 昨天白天
  • tonight 今天晚上

现在分词和过去分词

  • 现在分词:令人…,用来表示主动进行的动作。
    • This work is tiring. I need a lie down.(这工作很累人。我需要躺下歇歇。)
    • She’s so interesting. I could listen to her talk for hours!(她真有趣。我可以长时间听她讲话!)
  • 过去式:感到…,表示被动完成的动作
    • I’m tired. I need a lie down.(我累了。我需要躺下休息。)
    • I’m really interested in what she has to say.(我对她说的话很感兴趣。)

都是基于动词,英语中的分词分为以 -ing 结尾的现在分词和多以 -ed 结尾的过去分词

我们通常用以 “-ed” 结尾的过去分词表示人的内在感受,比如:“tired(累的)”、“interested(感兴趣的)”。用 “-ing” 结尾的现在分词强调是谁或什么事情给人造成了这种感受,比如 “tiring(令人感到累的)”、“interesting(有趣的)”

常见句型

  • 主+谓(主语的动作)+宾(动作承受者)+状语(地点,时间。一般时间在地点后面)+宾语补足语+主语补足语

    • Jack plays basketball every morning
      • jack:主语
      • plays:谓语
      • 宾语:basketball
      • 状语:every morning
  • 主+系(am,is,are)+表,表示没有动作就用这个

    • 常见的很,在,是出现,适合这个句型
    • Lily is a little cat.
      • a little cat:表语,说明主语是什么
      • little:定语,形容cat的特征

说明

  • 上篇文章 主要介绍了spring cloud的业务演示,本次主要介绍服务治理

服务治理介绍

前面我们实现了微服务之间的调用,但是我们把服务提供者的网络地址(ip,端口)等硬编码到了代码中,这种做法存在许多问题:

  1. 一旦服务提供者地址变化,就需要手工修改代码
  2. 一旦服务提供者为多个,无法实现负载均衡功能
  3. 一旦服务变的越来越多,人工维护调用关系困难

那么应该则么解决呢?这时候就需要通过注册中心动态的实现服务治理。

什么是服务治理?

服务治理是微服务架构中最核心最基本的模块。用于实现各个微服务的自动化注册和发现。

服务注册: 在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供服务的详细信息。并在注册中心形成一张服务的清单,服务注册中心需要以心跳的方式去监测清单中的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务。

服务发现: 服务调用方向服务注册中心咨询服务,并获取所有服务的服务清单,实现对具体服务实例的访问。

image-20210618154855845

除了微服务,还有一个组件是服务注册中心, 他是微服务架构中非常重要的一个组件,在微服务架构里主要起到了协调者的一个作用。注册中心一般包含如下几个功能:

  1. 服务发现
    1. 服务注册: 保存服务提供者和服务调用者的信息
    2. 服务订阅: 服务调用者订阅服务提供者的信息,注册中心向订阅者推送提供者的信息
  2. 服务配置
    1. 配置订阅:服务提供者和服务调用者订阅微服务相关的配置
    2. 配置下发:主动将配置推送给服务提供者和服务调用者
  3. 服务健康检测
    1. 检测服务提供者的健康情况,如果发现异常,执行服务剔除

常见的注册中心

Zookeeper

  • zookeeper是一个分布式服务框架,是Apache Hadoop的一个子项目,它主要用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务,状态同步服务,集群管理,分布式应用配置项的管理等

Eureka

  • Eureka是Spring Cloud Netflix中的重要组件,主要作用就是做服务注册和发现。

Consul

  • Consul是基于Go语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册,服务发现和配置管理等功能。Consul的功能都很实用,其中包括:服务注册/发现,健康检查,Key/Value存储,多数据中心和分布式一致性保证等特性。Consul本身只是一个二进制的可执行文件,所以安装和部署都非常简单,只需要从官网下载后,执行对应的脚本即可。

https://www.consul.io/

Nacos

  • Nacos 致力于帮助您发现、配置和管理微服务。它是Spring Cloud Alibaba的组件之一

nacos

  • 打开官网下载2.3.1.zip的版本
1
2
3
4
## 进入解压目录,进入bin目录
cd nacos/bin
##命令启动
startup.cmd -m standalone
  • 结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
D:\exe\nacos\bin>startup.cmd -m standalone
"nacos is starting with standalone"

,--.
,--.'|
,--,: : | Nacos 2.3.1
,`--.'`| ' : ,---. Running in stand alone mode, All function modules
| : : | | ' ,'\ .--.--. Port: 8848
: | \ | : ,--.--. ,---. / / | / / ' Pid: 14064
| : ' '; | / \ / \. ; ,. :| : /`./ Console: http://192.168.163.198:8848/nacos/index.html
' ' ;. ;.--. .-. | / / '' | |: :| : ;_
| | | \ | \__\/: . .. ' / ' | .; : \ \ `. https://nacos.io
' : | ; .' ," .--.; |' ; :__| : | `----. \
| | '`--' / / ,. |' | '.'|\ \ / / /`--' /
' : | ; : .' \ : : `----' '--'. /
; |.' | , .-./\ \ / `--'---'
'---' `--`---' `----'

2024-03-27 19:39:12,401 INFO Tomcat initialized with port(s): 8848 (http)

2024-03-27 19:39:12,850 INFO Root WebApplicationContext: initialization completed in 3709 ms

image-20240327194023531

将商品微服务注册到Nacos

  • shop-goods模块中依赖文件加入
1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  • 主类上添加@EnableDiscoveryClient
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package xyz.shi.shop.goods;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class GoodsApp {

public static void main(String[] args) {
SpringApplication.run(GoodsApp.class);
}
}

  • application.yml配置中添加nacos的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
application:
name: shop-goods
datasource:
password: root
username: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
server:
port: 8002

mybatis-plus:
global-config:
db-config:
table-prefix: shop_
  • 启动商品模块的微服务后,查看到数据

image-20240327194825898

将用户微服务注册到Nacos

  • 步骤同上

将订单微服务注册到Nacos

  • 步骤同上

在APP应用服务中添加Nacos

  • 步骤同上

dao修改

使用nacos调用微服务

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
package xyz.shi.shop.app.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

@Bean
@LoadBalanced
public RestTemplate restTemplate(ClientHttpRequestFactory requestFactory){
// return new RestTemplate(requestFactory);
RestTemplate restTemplate = new RestTemplate(requestFactory);
return restTemplate;
}

@Bean
public ClientHttpRequestFactory requestFactory(){
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(5000);//单位为ms
factory.setConnectTimeout(5000);//单位为ms
return factory;
// return new SimpleClientHttpRequestFactory();
}
}

​ @LoadBalanced 表示加上负载均衡

service修改

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
package xyz.shi.shop.app.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import xyz.shi.shop.app.popj.BuyParams;
import xyz.shi.shop.app.service.BuyService;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.common.goods.GoodsBO;
import xyz.shi.shop.common.user.UserBO;
import xyz.shi.shop.order.pojo.Order;

import java.math.BigDecimal;
import java.util.Map;

@Service
public class BuyServiceImpl implements BuyService {
@Autowired
private RestTemplate restTemplate;
private String shopUserName = "shop-user";
private String shopGoodsName = "shop-goods";
private String shopOrderName = "shop-order";

@Override
public Result submitOrder(BuyParams buyParams) {
String userResult = restTemplate.getForObject("http://" + shopUserName + "/user/findUser/" + buyParams.getUserId(), String.class);
Result<UserBO> userBOResult = JSON.parseObject(userResult, new TypeReference<Result<UserBO>>() {
});
if (userBOResult == null || !userBOResult.isSuccess() || userBOResult.getData() == null) {
return Result.fail(10001, "用户不存在");
}
UserBO userBO = userBOResult.getData();

String goodsResult = restTemplate.getForObject("http://" + shopGoodsName + "/goods/findGoods/" + buyParams.getGoodsId(), String.class);
Result<GoodsBO> goodsBOResult = JSON.parseObject(goodsResult, new TypeReference<Result<GoodsBO>>() {
});

if (goodsBOResult == null || !goodsBOResult.isSuccess() || goodsBOResult.getData() == null) {
return Result.fail(10002, "商品不存在");
}
GoodsBO goodsBO = (GoodsBO) goodsBOResult.getData();
Integer goodsStock = goodsBO.getGoodsStock();
if (goodsStock < 0) {
return Result.fail(10003, "商品库存不足");
}
BigDecimal goodsPrice = goodsBO.getGoodsPrice();
BigDecimal account = userBO.getAccount();
if (account.compareTo(goodsPrice) < 0) {
return Result.fail(10004, "余额不足");
}
Order orderParams = new Order();
orderParams.setUserId(userBO.getId());
orderParams.setGoodsId(goodsBO.getId());
orderParams.setOrderPrice(goodsBO.getGoodsPrice());
String orderResult = restTemplate.postForObject("http://" + shopOrderName + "/order/createOrder", orderParams, String.class);
Result<String> orderResultString = JSON.parseObject(orderResult, new TypeReference<Result<String>>() {
});
if (orderResultString == null || !orderResultString.isSuccess()) {
return Result.fail(10005, "下单失败");
}
String orderId = orderResultString.getData();

return Result.success(orderId);
}
}

老的代码:

String userResult = restTemplate.getForObject(“http://localhost:8001/user/findUser/" + buyParams.getUserId(), String.class);

新的代码

String goodsResult = restTemplate.getForObject(“http://“ + shopGoodsName + “/goods/findGoods/“ ..);

  • 通过以上的代码,我们发现各个微服务的地址,不需要硬编码在代码中了,通过在nacos中注册的微服务名称即可访问微服务。

测试

  • 所有的微服务模块全部启动后,查看nacos

image-20240327201224830

  • 测试结果再次如下
1
2
3
4
5
6
7
8
9
10
import requests

header = {"content-type": "application/json",}
data = {"userId": 1, "goodsId": 1111}
t = requests.post("http://localhost:8004/buy/submit", json=data, headers=header)
s = t.text
print(s)


{"success":true,"code":10001,"msg":"用户不存在","data":null}

负载均衡

  • 通俗的讲,负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元(服务器,组件)上进行执行。

  • 根据负载均衡发生位置的不同,一般分为服务端负载均衡客户端负载均衡

    • 服务端负载均衡指的是发生在服务提供者一方,比如常见的nginx负载均衡。

    • 而客户端负载均衡指的是发生在服务请求一方,也就是在发送请求之前已经选好了由哪个实例处理请求。

image-20210619162742384

  • 在微服务调用关系中,一般会选择客户端负载均衡,也就是由服务调用一方来决定由哪个服务来提供执行

基于Feign实现服务调用

  • Feign是Spring Cloud提供的一个声明式的伪Http客户端,它使得调用远程服务就像调用本地服务一样简单,只需要创建一个接口并添加一个注解即可。

  • Nacos很好的集成了Feign,Feign默认集成了Ribbon,所以在Nacos下使用Feign默认就实现了负载均衡的效果

  • shop-app模块,加入Feign的依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 在主类上添加Feign的注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package xyz.shi.shop.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients

public class App {

public static void main(String[] args) {
SpringApplication.run(App.class,args);
}
}
  • 创建各个微服务的feign接口,实现feign的微服务调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package xyz.shi.shop.app.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.common.goods.GoodsBO;

@FeignClient("shop-goods")
public interface GoodsFeign {

//调用路径同http访问路径
@GetMapping("/goods/findGoods/{id}")
public Result<GoodsBO> findGoods(@PathVariable("id") Long id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package xyz.shi.shop.app.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.order.pojo.Order;

@FeignClient("shop-order")
public interface OrderFeign {

//调用路径同http访问路径
@PostMapping("/order/createOrder")
public Result<String> createOrder(@RequestBody Order orderParams);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package xyz.shi.shop.app.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.common.user.UserBO;

@FeignClient("shop-user")
public interface UserFeign {

//调用路径同http访问路径
@GetMapping("/user/findUser/{id}")
public Result<UserBO> findUser(@PathVariable("id") Long id);
}
  • 更改BuyService的实现,使用feign来完成调用
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
package xyz.shi.shop.app.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import xyz.shi.shop.app.feign.GoodsFeign;
import xyz.shi.shop.app.feign.OrderFeign;
import xyz.shi.shop.app.feign.UserFeign;
import xyz.shi.shop.app.popj.BuyParams;
import xyz.shi.shop.app.service.BuyService;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.common.goods.GoodsBO;
import xyz.shi.shop.common.user.UserBO;
import xyz.shi.shop.order.pojo.Order;

import java.math.BigDecimal;

@Service
public class BuyServiceImpl implements BuyService {
@Autowired
private UserFeign userFeign;
@Autowired
private GoodsFeign goodsFeign;
@Autowired
private OrderFeign orderFeign;

@Override
public Result submitOrder(BuyParams buyParams) {
Result<UserBO> userBOResult = userFeign.findUser(buyParams.getUserId());
if (userBOResult == null || !userBOResult.isSuccess() || userBOResult.getData() == null){
return Result.fail(10001,"用户不存在");
}
UserBO userBO = userBOResult.getData();
System.out.println(userBO);
Result<GoodsBO> goodsBOResult = goodsFeign.findGoods(buyParams.getGoodsId());
System.out.println(goodsBOResult);

if (goodsBOResult == null || !goodsBOResult.isSuccess() || goodsBOResult.getData() == null){
return Result.fail(10002,"商品不存在");
}
GoodsBO goodsBO = goodsBOResult.getData();
Integer goodsStock = goodsBO.getGoodsStock();
if (goodsStock < 0){
return Result.fail(10003,"商品库存不足");
}
BigDecimal goodsPrice = goodsBO.getGoodsPrice();
BigDecimal account = userBO.getAccount();
if (account.compareTo(goodsPrice) < 0){
return Result.fail(10004,"余额不足");
}
Order orderParams = new Order();
orderParams.setUserId(userBO.getId());
orderParams.setGoodsId(goodsBO.getId());
orderParams.setOrderPrice(goodsBO.getGoodsPrice());
Result<String> orderResultString = orderFeign.createOrder(orderParams);
System.out.println(orderResultString);

if (orderResultString == null || !orderResultString.isSuccess()){
return Result.fail(10005,"下单失败");
}
String orderId = orderResultString.getData();

return Result.success(orderId);
}
}

老代码

String goodsResult = restTemplate.getForObject(“http://“ + shopGoodsName + “/goods/findGoods/“ ..);

新代码

Result userBOResult = userFeign.findUser(buyParams.getUserId());

  • 重启APP服务,测试,整合上feign之后,我们发现,整个微服务的开发变的开始舒服起来。

说明

  • 上篇文章 主要介绍了spring cloud的基本概念和工程结构,本编开始实践演练

业务场景

下单服务:

  • 前端传递用户id和商品id

  • 判断用户是否存在以及账户是否有足够的金额

  • 判断商品是否存在以及库存是否足够

  • 生成订单

创建APP模块

  • pom.xml
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
<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>
<parent>
<groupId>org.example</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>shop-app</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>shop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

  • 配置文件:src-resource-application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: shop-app
datasource:
password: root
username: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
server:
port: 8004

mybatis-plus:
global-config:
db-config:
table-prefix: shop_xxxxxxxxxx spring: application:   name: shop-app datasource:   password: root   username: 123456   driver-class-name: com.mysql.cj.jdbc.Driver   url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTCserver: port: 8004mybatis-plus: global-config:   db-config:     table-prefix: shop_spring: application:   name: shop-appserver: port: 8004
  • 启动类
1
2
3
4
5
6
7
8
9
10
11
12
package xyz.shi.shop.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

public static void main(String[] args) {
SpringApplication.run(App.class,args);
}
}

用户微服务 提供用户查询服务

dao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package xyz.shi.shop.user.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("xyz.shi.shop.user.mapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}

1
2
3
4
5
6
7
8
package xyz.shi.shop.user.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import xyz.shi.shop.user.pojo.User;

public interface UserMapper extends BaseMapper<User> {
}

service层

1
2
3
4
5
6
7
8
package xyz.shi.shop.user.service;

import xyz.shi.shop.common.Result;

public interface UserService {

Result findUser(Long id);
}
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
package xyz.shi.shop.user.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.common.user.UserBO;
import xyz.shi.shop.user.mapper.UserMapper;
import xyz.shi.shop.user.pojo.User;
import xyz.shi.shop.user.service.UserService;

@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserMapper userMapper;

@Override
public Result findUser(Long id) {
User user = userMapper.selectById(id);
if (user == null){
return Result.success(null);
}
UserBO userBO = new UserBO();
userBO.setAccount(user.getAccount());
userBO.setId(user.getId());
return Result.success(userBO);
}
}
  • common模块下:
1
2
3
4
5
6
7
8
9
package xyz.shi.shop.common.user;
import lombok.Data;
import java.math.BigDecimal;

@Data
public class UserBO {
private Long id;
private BigDecimal account;
}

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package xyz.shi.shop.user.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.user.service.UserService;

@RestController
@RequestMapping("user")
public class UserController {

@Autowired
private UserService userService;

@GetMapping("findUser/{id}")
public Result findUser(@PathVariable("id") Long id) {
return userService.findUser(id);
}
}

商品微服务,提供商品查询服务

dao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package xyz.shi.shop.goods.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("xyz.shi.shop.goods.mapper")
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}

1
2
3
4
5
6
7
package xyz.shi.shop.goods.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import xyz.shi.shop.goods.pojo.Goods;

public interface GoodsMapper extends BaseMapper<Goods> {
}

service层

1
2
3
4
5
6
7
8
package xyz.shi.shop.goods.service;

import xyz.shi.shop.common.Result;

public interface GoodsService {

Result findGoodsById(Long id);
}
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
package xyz.shi.shop.goods.service.impl;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.common.goods.GoodsBO;
import xyz.shi.shop.goods.mapper.GoodsMapper;
import xyz.shi.shop.goods.pojo.Goods;
import xyz.shi.shop.goods.service.GoodsService;

@Service
public class GoodsServiceImpl implements GoodsService {

@Autowired
private GoodsMapper goodsMapper;

@Override
public Result findGoodsById(Long id) {
Goods goods = goodsMapper.selectById(id);
GoodsBO goodsBO = new GoodsBO();
BeanUtils.copyProperties(goods,goodsBO);
return Result.success(goodsBO);
}
}
  • common模块下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package xyz.shi.shop.common.goods;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class GoodsBO {

private Long id;

private String goodsName;

private BigDecimal goodsPrice;

private Integer goodsStock;
}

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package xyz.shi.shop.goods.Controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.goods.service.GoodsService;

@RestController
@RequestMapping("goods")
public class GoodsController {

@Autowired
private GoodsService goodsService;

@GetMapping("findGoods/{id}")
public Result findGoods(@PathVariable("id") Long id){
return goodsService.findGoodsById(id);
}
}

订单微服务,提供生成订单服务

dao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package xyz.shi.shop.order.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("xyz.shi.shop.order.mapper")
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
1
2
3
4
5
6
7
package xyz.shi.shop.order.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import xyz.shi.shop.order.pojo.Order;

public interface OrderMapper extends BaseMapper<Order> {
}

service

1
2
3
4
5
6
7
8
9
10
package xyz.shi.shop.order.service;


import xyz.shi.shop.common.Result;
import xyz.shi.shop.order.pojo.Order;

public interface OrderService {

public Result createOrder(Order orderParams);
}
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
package xyz.shi.shop.order.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.order.mapper.OrderMapper;
import xyz.shi.shop.order.pojo.Order;
import xyz.shi.shop.order.service.OrderService;
import org.apache.commons.lang3.RandomUtils;

@Service
public class OrderServiceImpl implements OrderService {

@Autowired
private OrderMapper orderMapper;
@Override
public Result createOrder(Order orderParams) {
Order order = new Order();
order.setCreateTime(System.currentTimeMillis());
order.setGoodsId(orderParams.getGoodsId());
order.setUserId(orderParams.getUserId());
order.setOrderPrice(orderParams.getOrderPrice());
order.setOrderId(System.currentTimeMillis()+""+orderParams.getUserId()+ ""+RandomUtils.nextInt(1000,9999));
order.setOrderStatus(0);
order.setPayStatus(0);
order.setPayTime(-1L);
this.orderMapper.insert(order);
return Result.success(order.getOrderId());
}
}

下单业务

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>

dao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package xyz.shi.shop.app.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory requestFactory){
return new RestTemplate(requestFactory);
}

@Bean
public ClientHttpRequestFactory requestFactory(){
return new SimpleClientHttpRequestFactory();
}
}

service

1
2
3
4
5
6
7
8
package xyz.shi.shop.app.service;

import xyz.shi.shop.app.popj.BuyParams;
import xyz.shi.shop.common.Result;

public interface BuyService {
Result submitOrder(BuyParams buyParams);
}
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
package xyz.shi.shop.app.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import xyz.shi.shop.app.popj.BuyParams;
import xyz.shi.shop.app.service.BuyService;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.common.goods.GoodsBO;
import xyz.shi.shop.common.user.UserBO;
import xyz.shi.shop.order.pojo.Order;

import java.math.BigDecimal;
import java.util.Map;

@Service
public class BuyServiceImpl implements BuyService {
@Autowired
private RestTemplate restTemplate;

@Override
public Result submitOrder(BuyParams buyParams) {
String userResult = restTemplate.getForObject("http://localhost:8001/user/findUser/" + buyParams.getUserId(), String.class);
Result<UserBO> userBOResult = JSON.parseObject(userResult,new TypeReference<Result<UserBO>>(){});
if (userBOResult == null || !userBOResult.isSuccess() || userBOResult.getData() == null){
return Result.fail(10001,"用户不存在");
}
UserBO userBO = userBOResult.getData();

String goodsResult = restTemplate.getForObject("http://localhost:8002/goods/findGoods/" + buyParams.getGoodsId(), String.class);
Result<GoodsBO> goodsBOResult = JSON.parseObject(goodsResult,new TypeReference<Result<GoodsBO>>(){});

if (goodsBOResult == null || !goodsBOResult.isSuccess() || goodsBOResult.getData() == null){
return Result.fail(10002,"商品不存在");
}
GoodsBO goodsBO = (GoodsBO) goodsBOResult.getData();
Integer goodsStock = goodsBO.getGoodsStock();
if (goodsStock < 0){
return Result.fail(10003,"商品库存不足");
}
BigDecimal goodsPrice = goodsBO.getGoodsPrice();
BigDecimal account = userBO.getAccount();
if (account.compareTo(goodsPrice) < 0){
return Result.fail(10004,"余额不足");
}
Order orderParams = new Order();
orderParams.setUserId(userBO.getId());
orderParams.setGoodsId(goodsBO.getId());
orderParams.setOrderPrice(goodsBO.getGoodsPrice());
String orderResult = restTemplate.postForObject("http://localhost:8003/order/createOrder", orderParams, String.class);
Result<String> orderResultString = JSON.parseObject(orderResult,new TypeReference<Result<String>>(){});
if (orderResultString == null || !orderResultString.isSuccess()){
return Result.fail(10005,"下单失败");
}
String orderId = orderResultString.getData();

return Result.success(orderId);
}
}

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package xyz.shi.shop.app.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.shi.shop.app.popj.BuyParams;
import xyz.shi.shop.app.service.BuyService;
import xyz.shi.shop.common.Result;

@RestController
@RequestMapping("buy")
public class BuyController {

@Autowired
private BuyService buyService;

@PostMapping("submit")
private Result submitOrder(@RequestBody BuyParams buyParams){
return buyService.submitOrder(buyParams);
}
}

  • 实体类
1
2
3
4
5
6
7
8
9
10
11
package xyz.shi.shop.app.popj;

import lombok.Data;

@Data
public class BuyParams {

private Long userId;

private Long goodsId;
}

测试

  • 分别把各自模块下的XXApp.java启动,在idea中就可启动

image-20240327170052020

  • 测试结果如下
1
2
3
4
5
6
7
8
9
10
11
import requests

header = {"content-type": "application/json",}
data = {"userId": 1, "goodsId": 1111}
t = requests.post("http://localhost:8004/buy/submit", json=data, headers=header)
s = t.text
print(s)


{"success":true,"code":10001,"msg":"用户不存在","data":null}

总结

  • 本次代码在这里
  • 从上述的代码 可以看出,下单业务 调用各个微服务的时候 极其不方便。一旦服务提供方 更改ip,端口,接口名称等,服务消费方 也需要跟着变,如果服务提供方有多台服务器呢?服务消费方还得需要维护这些ip信息,甚至自己实现负载均衡。这时候 就需要 服务治理

说明

  • 从本篇文章开始学习微服务的知识
  • 本次文章主要来自于这里

系统架构演变

从互联网兴起到现在,系统架构大致经历了以下几个过程:

  1. 单体应用架构
  2. 垂直应用架构
  3. 分布式应用架构
  4. SOA架构
  5. 微服务架构
  6. Service Mesh(服务网格化)
  7. 无服务化

案例准备

电商系统:搭建用户微服务,商品微服务,订单微服务

技术选型

maven:3.5+

数据库:mysql 5.7

持久层:MybatisPlus

其他:Spring Cloud Alibaba

模块设计

父工程:shop-parent

用户微服务:shop-user

商品微服务:shop-goods

订单微服务: shop-order

公共服务: shop-common

应用服务:shop-app

image-20210609203537573

下单场景

用户下单,会涉及到用户微服务,商品微服务,订单微服务

搭建工程

创建父工程

  • 依赖文件
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
<?xml version="1.0" encoding="UTF-8"?>
<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>

<groupId>org.example</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/>
</parent>
<packaging>pom</packaging>
<properties>
<!-- Spring Cloud -->
<spring.cloud.version>Hoxton.SR9</spring.cloud.version>
<!-- Apache Dubbo -->
<dubbo.version>2.7.8</dubbo.version>
<curator.version>4.0.1</curator.version>
<!-- Apache RocketMQ -->
<rocketmq.starter.version>2.0.2</rocketmq.starter.version>
<spring.cloud.alibaba>2.2.5.RELEASE</spring.cloud.alibaba>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

</project>

<parent> 元素用于继承父级项目的默认配置和依赖管理,而 <dependencyManagement> 元素用于集中管理项目的依赖版本,确保团队中所有成员使用相同的依赖版本。这些元素的使用可以提高项目的组织性、可维护性和一致性。

创建shop-common模块

  • 模块名:shop-common

  • 依赖文件

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
<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>
<parent>
<groupId>org.example</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>shop-common</artifactId>

<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>


  • 统一返回类
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
package xyz.shi.shop.common;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {

private boolean success;

private int code;

private String msg;

private T data;

public static <T> Result<T> success(T data) {
return new Result<>(true,200,"success",data);
}
public static <T> Result<T> fail(int code, String msg) {
return new Result<>( true,code,msg,null);
}
}

创建用户微服务

  • 模块名:shop_user
  • 创建pom依赖文件
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
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop_user</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>shop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

注意这里依赖了shop-common 模块

  • 启动类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package xyz.shi.shop.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UserApp {

public static void main(String[] args) {
SpringApplication.run(UserApp.class,args);
}

}

  • 配置文件,src-resources-application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: shop-user
datasource:
password: root
username: root
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
server:
port: 8001

mybatis-plus:
global-config:
db-config:
table-prefix: shop_
  • shop_user表,在shop数据库中手动创建user表
1
2
3
4
5
6
7
8
CREATE TABLE `shop`.`shop_user`  (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`telephone` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`account` decimal(10, 2) NOT NULL COMMENT '账户',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
  • shop_user表实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package xyz.shi.shop.user.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
//分布式id
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String username;
private String password;
private String telephone;
private BigDecimal account;
}

创建商品微服务

  • 模块名:shop-goods

  • 依赖

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
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop-goods</artifactId>
<!-- <packaging>war</packaging>-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>shop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

注意这里依赖了shop-common 模块

  • 启动类
1
2
3
4
5
6
7
8
9
10
11
12
13
package xyz.shi.shop.goods;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GoodsApp {

public static void main(String[] args) {
SpringApplication.run(GoodsApp.class);
}
}

  • 配置文件 src-resources-application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: shop-goods
datasource:
password: root
username: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
server:
port: 8002

mybatis-plus:
global-config:
db-config:
table-prefix: shop_
  • 商品表
1
2
3
4
5
6
7
8
CREATE TABLE `shop`.`shop_goods`  (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`goods_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '商品名称',
`goods_price` decimal(10, 2) NOT NULL COMMENT '商品价格',
`goods_stock` int(10) NOT NULL COMMENT '商品库存',
`create_time` bigint(20) NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
  • 实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package xyz.shi.shop.goods.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class Goods {

@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String goodsName;
private BigDecimal goodsPrice;
private Integer goodsStock;
private Long createTime;
}

创建订单微服

  • 模块名:shop-order
  • 依赖
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
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop-order</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>shop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
  • 启动类
1
2
3
4
5
6
7
8
9
10
11
12
package xyz.shi.shop.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class OrderApp {

public static void main(String[] args) {
SpringApplication.run(OrderApp.class,args);
}
}
  • 配置文件 sr-resources-application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: shop-order
datasource:
password: root
username: root
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
server:
port: 8003

mybatis-plus:
global-config:
db-config:
table-prefix: shop_
  • 创建订单表
1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `shop`.`shop_order`  (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`user_id` bigint(20) NOT NULL,
`goods_id` bigint(20) NOT NULL,
`order_price` decimal(10, 2) NOT NULL,
`order_status` tinyint(4) NOT NULL,
`pay_status` tinyint(4) NOT NULL,
`pay_time` bigint(20) NOT NULL,
`create_time` bigint(20) NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
  • 实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package xyz.shi.shop.order.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

import java.math.BigDecimal;

@Data
public class Order {

@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String orderId;
private Long userId;
private Long goodsId;
private BigDecimal orderPrice;
private Integer orderStatus;
private Integer payStatus;
private Long payTime;
private Long createTime;
}

总结

  • 整个业务工程搭建已经完成,最终目录如下

image-20240318173650355

  • 后续开展业务场景的模拟

说明

  • 紧接 vue3系列一 文章
  • 环境等信息参考上述的文章

其它Composition API

toRef与toRefs

toRef

  • 作用:创建一个ref对象,其value值指向另一个对象中的某个属性值

  • 语法:const name=toRef(obj,‘name’)

  • 应用:要将响应式对象中的某个属性单独提供给外部使用时

  • 扩展:toRefs与toRef功能一致,但可以批量创建多个ref对象,toRefs(obj)

  • 如下实例:

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
<template>
<h2>toRef与toRefs</h2>
<h2>姓名:{{person.name}}</h2>
<h2>年龄:{{person.age}}</h2>
<h2>薪水:{{person.job.salary}}k</h2>
<button @click="person.name+='!'">修改姓名</button>
<button @click="person.age++">修改年龄</button>
<button @click="person.job.salary++">涨点薪资</button>
</template>

<script>
import {reactive} from 'vue'
export default {
name: "toRef_component",
setup(){
let person=reactive({
name:'二郎神杨杨戬',
age:18,
job:{
salary:20
}
})
return{
person,
}
}
}
</script>

<style scoped>

</style>

示例一里面直接返回person对象,导致每次取值的时候都需要person.xxx,这样既不美观也不优雅,修改一下代码。

  • 修改代码为
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
<template>
<h2>toRef与toRefs</h2>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<h2>薪水:{{salary}}k</h2>
<button @click="name+='!'">修改姓名</button>
<button @click="age++">修改年龄</button>
<button @click="salary++">涨点薪资</button>
</template>

<script>
import {reactive,toRef} from 'vue'
export default {
name: "toRef_component",
setup(){
let person=reactive({
name:'二郎神杨杨戬',
age:18,
job:{
salary:20
}
})
return{
name:toRef(person,'name'),
age:toRef(person,'age'),
salary:toRef(person.job,'salary')
}
}
}
</script>

<style scoped>

</style>

toRefs

  • 代码
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
<template>
<h2>toRef与toRefs</h2>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<h2>薪水:{{job.salary}}k</h2>
<button @click="name+='!'">修改姓名</button>
<button @click="age++">修改年龄</button>
<button @click="job.salary++">涨点薪资</button>
</template>

<script>
import {reactive,toRef,toRefs} from 'vue'
export default {
name: "toRef_component",
setup(){
let person=reactive({
name:'二郎神杨杨戬',
age:18,
job:{
salary:20
}
})
return{
...toRefs(person)
}
}
}
</script>

<style scoped>

</style>

shallowRef和shallowReactive

  • shallowReactive:只处理对象最外层属性的响应式(浅响应式)

  • shallowRef:只处理基本数据类型的响应式,不进行对象的响应式处理

  • 什么时候用

    • 如果有一个对象数据,结构比较深,但变化时只是外层属性变化用shallowReactive

    • 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换用shallowRef

  • 感觉没有什么用,不做演示

readonly和shallowReadonly

  • readonly:让一个响应式数据变为只读的(深只读)

  • shallowReadonly:让一个响应式变为只读的(浅只读)

toRaw和markRaw

  • toRaw

    • 作用:将一个由reactive生成的响应式对象转为普通对象
    • 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新
  • markRow

    • 作用:标记一个对象,使其永远不会再成为响应式对象

      应用场景

    • 有些值不应该被设置为响应式的,例如复杂的第三方类库,

    • 当渲染具有不可变的数据源的大列表时,跳过响应式转换可以提高性能

customRef

  • 作用:创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制,它需要一个工厂函数,该函数接收 track 和 trigger 函数作为参数,并且应该返回一个带有 get 和 set 的对象。
    实现防抖效果:
  • 1、先实现自定义双向绑定
  • 2、实现了双向绑定之后,实现防抖

响应式数据的判断

  • isRef:检查一个值是否为ref对象
  • isReactive:检查一个对象是否由reactive创建的响应式代理
  • isReadonly:检查一个对象是否由readonly创建的只读代理
  • isProxy:检查一个对象是否由reactive或者readonly方法创建的代理

provide和inject

  • 在父子组件传递数据时,通常使用的是 props 和 emit,父传子时,使用的是 props,如果是父组件传孙组件时,就需要先传给子组件,子组件再传给孙组件,如果多个子组件或多个孙组件使用时,就需要传很多次,会很麻烦。
  • 像这种情况,可以使用 provide inject 解决这种问题,不论组件嵌套多深,父组件都可以为所有子组件或孙组件提供数据,父组件使用 provide 提供数据,子组件或孙组件 inject 注入数据。同时兄弟组件之间传值更方便。

img

  • 祖组件

    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
    <template>
    <h2>我是祖组件</h2>
    <h3>汽车信息</h3>
    <p>名称:{{name}}</p>
    <p>价格:{{price}}</p>
    <inject_component></inject_component>
    </template>

    <script>
    import {reactive,toRefs,provide} from 'vue'
    import inject_component from "./inject.vue"

    export default {
    name: "provide_component",
    setup(){
    let car=reactive({
    name:'宝马',
    price:'40w'
    });
    provide('car',car); // 提供provide
    return{
    ...toRefs(car)
    }
    },
    components:{
    inject_component
    }
    }
    </script>

    <style scoped>

    </style>

  • 后代组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<h2>我是孙组件</h2>
<h3>汽车信息</h3>
<p>名称:{{name}}</p>
<p>价格:{{price}}</p>
</template>

<script>
import {inject,toRefs,ref} from 'vue'
export default {
name: "inject_component",
setup(){
let car=inject("car"); //使用inject接收
return{
...toRefs(car)
}
}
}
</script>

<style scoped>

</style>

  • 运行结果

image-20240307151409520

在vue2中

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
//父组件

export default{

provide:{

info:"提供数据"

}

}

//子组件

export default{

inject:['info'],

mounted(){

console.log("接收数据:", this.info) // 接收数据:提供数据

}

}
  • provide / inject 类似于消息的订阅和发布。provide 提供或发送数据, inject 接收数据。

Vue2x和Vue3x的其它变化

1.全局API的转移

vue2.x有许多全局API和配置,例如:全局注册组件、注册全局指令等

  • 注册全局组件
1
2
3
4
5
6
7
Vue.component('MyButton',{
data:()=>{
count:0,
},
template:'<button @click="count++">clicked {{count}} times</button>'
});

  • 注册全局指令,
1
2
3
4
Vue.directive('focus',{
inserted:el=>el.foucus
})

  • Vue3.0中对这些API做出了调整

  • 将全局的API,即Vue.xxx调整到应用实例app上

2.x 全局API(Vue) 3.x 实例API(app)
Vue.config.xxx app.config.xxx
Vue.config.productionTip 移除
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use
Vue.prototype app.config.globalProperties
  • 移除过滤器(filter):过滤器虽然看起来方便,但它需要一个自定义语法,打破大括号内表达式’只是javascript’的假设,这不仅有学习成本,而且有实现成 本, 建议用法调用或者计算属性去替换过滤器

项目搭建

  • node.js 安装16.0以上的版本
1
2
3
4
5
C:\Users\Administrator>node -v
v18.17.0

D:\project>npm -v
9.6.7
  • vite初始化Vue3项目
1
2
3
4
5
6
7
8
9
10
11
12
D:\project>npm init vite-app vue3Demo
Need to install the following packages:
create-vite-app@1.21.0
Ok to proceed? (y) y

cd vue3Demo
npm install
D:\project\vue3Demo>npm run dev
Dev server running at:
> Network: http://192.168.57.198:3000/
> Local: http://localhost:3000/

项目结构分析

  • 这里的项目目录结构分析主要是main.js文件
  • Vue2里面的main.js
1
2
3
4
5
6
new Vue({
el: '#app',
components: {},
template: ''
});

  • Vue3里面的main.js
1
2
3
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
  • 在Vue2里面,通过new Vue({})构造函数创建应用实例对象,而Vue3引入的不再是Vue的构造函数,引入的是一个名为createApp的工厂函数创建应用实例对象。

  • Vue3-devtool获取

Composition API

setp

  • setup是所有Composition API(组合式API)的入口,组件中所用到的数据、方法等等,均要配置在setup里面

  • 组件中所用到的数据、方法等等,均要配置在setup里面

  • setup的执行时机

    • 在beforeCreate之前执行一次,此时this为undefined
  • setup函数的两种返回值

    • 若返回一个对象,则对象中的属性、方法,在模板中均可以直接使用
    • 若返回一个渲染函数,则可以自定义渲染内容
  • setup的参数

    • props:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性
    • context:上下文对象,接收参数 context 内部函数props接受的自定义属性
      • attrs:值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性,相当于this.$attrs
      • slots:收到的插槽内容,相当于this.$slots
      • emit:分发自定义事件的函数,相当于this.$emit

注意事项:

尽量不要与Vue2x的配置使用

  • Vue2x的配置(data、methods、computed)均可以访问到setup中的属性、方法
  • setup中不能访问Vue2x的配置(data、methods、computed)
  • 如果data里面的属性和setup里面的属性有重名,则setup优先

返回值

示例一:setup函数的两种返回值

  • 安装路由依赖
1
npm install vue-router@4 --save
  • src\router\routers.js 手动新建自定义路由
1
2
3
4
5
6
7
8
const routes = [
{
name: "home",
path: "/home",
component: () => import("../components/views/home.vue")
}
]
export default routes;

src\router\index.js 对外暴露路由

1
2
3
4
5
6
7
8
9
10
import { createRouter, createWebHistory } from "vue-router"

// createRouter方法用于创建路由实例对象
// createWebHashHistory方法用于指定路由的工作模式(hash模式
import routes from "./routers"
var router=createRouter({
history:createWebHistory(),
routes
})
export default router
  • src-main.js内容引用路由
1
2
3
4
5
6
7
8
9
10
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import router from "./routers/index.js"

const app = createApp(App)

app.use(router)
app.mount("#app")

  • 编写src-components-views-home.vue
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
<template>
<h2>练习setup相关内容</h2>
<!--<h2>setup返回一个对象,并使用对象中的属性和方法</h2>-->
<!--<p>姓名:{{student.name}}</p>-->
<!--<p>年龄:{{student.age}}</p>-->
<!--<button @click="hello">点击查看控制台信息</button>-->
<hr>
<h2>setup返回一个函数</h2>
</template>

<script>
import {h} from 'vue'
export default {
name: "setupComponent",
setup(){
// 属性
let student={
name:'张三',
age:18,
}
// 方法
function hello() {
console.log(`大家好,我叫${student.name},今年${student.age}`)
}
return{ // 返回一个对象
student,
hello,
}
// return()=>h('h1','你好') // 返回一个函数
}
}
</script>

<style scoped>

</style>


这里需要注意的是setup里面定义的属性和方法均要return出去,否则无法使用

  • 启动测试
1
npm run dev

示例二:setup里面的参数和方法和配置项混合使用

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
<template>
<h2>setup和配置项混用</h2>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<h2>性别:{{sex}}</h2>
<button @click="sayHello">sayHello(Vue3里面的方法)</button>
<button @click="sayWelcome">sayWelcome(Vue2里面的方法)</button>
</template>

<script>
export default {
name: "setup01_component",
data(){
return{
sex:'男',
sum:0,
}
},
methods:{
sayWelcome(){
console.log(`sayWelcome`)
},
},
setup(){
let sum=100;
let name='张三';
let age=18;
function sayHello() {
console.log(`我叫${name},今年${age}`)
}
return{
name,
age,
sayHello,
sum
}
}
}
</script>

<style scoped>

</style>


  • 这段代码是先实现了setup里面的属性和方法,以及Vue2中配置项里面的属性和方法。接下来添加对应的混合方法
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
<template>
<h2>setup和配置项混用</h2>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<h2>性别:{{sex}}</h2>
<button @click="sayHello">sayHello(Vue3里面的方法)</button>
<button @click="sayWelcome">sayWelcome(Vue2里面的方法)</button>
<br>
<br>
<button @click="test01">测试Vue2里面调用Vue3里面的属性和方法</button>
<br>
<br>
<button @click="test02">测试Vue3setup里面调用Vue2里面的属性和方法</button>
<br>
<h2>sum的值是:{{sum}}</h2>
</template>

<script>
export default {
name: "setup01_component",
data(){
return{
sex:'男',
sum:0,
}
},
methods:{
sayWelcome(){
console.log(`sayWelcome`)
},
test01(){
console.log(this.sex); // Vue2里面的属性(data里面的属性)
// setup里面的属性
console.log(this.name);
console.log(this.age);
// setup里面的方法
this.sayHello();
}
},
setup(){
let sum=100;
let name='张三';
let age=18;
function sayHello() {
console.log(`我叫${name},今年${age}`)
}
function test02() {
// setup里面的属性
console.log(name);
console.log(age);

// data里面的属性
console.log(this.sex);
console.log(this.sayWelcome);
}
return{
name,
age,
sayHello,
test02,
sum
}
}
}
</script>

<style scoped>

</style>

测试Vue3setup里面调用Vue2里面的属性和方法时,this.sex无数据

Vue2里面props和slot的使用

讲解setup这里面的两个参数之前,先回顾一下Vue2里面的相关知识

  • props和自定义事件的使用
  • attrs
  • slot(插槽)

示例一:Vue2props和自定义事件的使用

准备两个组件,分别为parent.vue组件和child.vue组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div class="parent">
我是父组件
<child msg="传递信息" name="张三" @sendParentMsg="getMsg"/>
</div>
</template>
<script>
import Child from "./child.vue";
export default {
name: "Parent",
components: {Child},
methods:{
getMsg(msg){
console.log(msg)
}
}
}
</script>
<style scoped>
.parent{
padding: 10px;
background-color: red;
}
</style>
  • child
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
<template>
<div class="child">
<h2>我是子组件</h2>
<p>父组件传递过来的消息是:{{msg}}</p>
<p>父组件传递过来的消息是:{{name}}</p>
<button @click="sendMsg">向父组件的传递信息</button>
</div>
</template>
<script>
export default {
name: "Child",
props:{
msg:{
type:String,
default:''
},
name:{
type:String,
default:''
}
},
mounted(){
console.log(this);
},
methods:{
sendMsg(){
this.$emit("sendParentMsg",'通知父组件更新123')
}
}
}
</script>
<style scoped>
.child{
padding: 10px;
background-color: orange;
}
</style>

vue2_props.gif

总结
  • 子组件通过props接收父组件传递的信息,通过this.$emit()自定义事件向父组件传递信息。当使用props接收数据的时候,attrs里面的数据为空,如果没有使用props接收数据的话,那么props里面就有值。

Vue2里面slot的使用

  • 这两个分类栏里的数据都不一样,但是整体结构是一样的,这就要求组件的结构一样,但是内部 DOM 结构是由使用组件的时候决定的,这就需要插槽

  • 同理准备两个组件,一个Index.vue组件,另一个为MySlot.vue组件

  • Index.vue

    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
    <template>
    <div class="index">
    <h2>我是Index组件</h2>
    <!--写法一-->
    <my-slot>
    <!--插槽里面的内容-->
    <h2>传入的slot参数</h2>
    <h2>传入的slot参数</h2>
    <h2>传入的slot参数</h2>
    <h2>传入的slot参数</h2>
    </my-slot>
    <!--写法二-->
    <my-slot>
    <div slot="header">
    <span>我在header插槽中</span>
    </div>

    <div slot="header">
    <span>我是footer附件</span>
    </div>
    </my-slot>
    </div>
    </template>

    <script>
    import MySlot from "./MySlot.vue";
    export default {
    name: "Index",
    components: {MySlot}
    }
    </script>

    <style scoped>
    .index{
    padding: 10px;
    background: red;
    }
    </style>

  • MySlot

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
<template>
<div class="slot">
<h2>我是MySlot组件1</h2>
<slot></slot>
<br>
<slot name="header">header</slot>
<br>
<slot name="footer">footer</slot>
</div>
</template>

<script>
export default {
name: "MySlot",
mounted(){
console.log(this);
}
}
</script>

<style scoped>
.slot{
padding: 10px;
background: orange;
}
</style>

image-20240229145232356

ref

  • 作用:ref( ) 接受一个内部值,返回一个ref 对象,这个对象是响应式的、可更改的,且只有一个指向其内部值的属性 .value。

  • 语法:const xxx=ref(initValue)

  • 创建一个包含响应式数据的引用对象(reference对象);

  • JS中操作数据:xxx.value=xxx,模板中读取数据:不需要.value,直接:{{xxx}}

  • 备注:

    • 接收的数据可以是:基本类型,也可以是对象类型

    • 基本类型的数据:响应式依然是靠Object.defineProperty()的get和set完成的

    • 对象类型的数据:内部求助了Vue3.0中的一个新函数-reactive函数

  • 测试代码

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
<template>
<h1>ref</h1>
<h2>ref定义基本数据类型</h2>
<p>姓名:{{name}}</p>
<p>年龄:{{age}}</p>
<p>婚否:{{isMarry}}</p>
<h2>ref定义对象类型</h2>
<p>爱好:{{hobby}}</p>
<p>证件类型:{{user.idCard}}</p>
<p>国籍:{{user.nation}}</p>
<button @click="changeName">修改信息</button>
</template>

<script>
import {ref} from 'vue'
export default {
name: "refComponent",
setup(){
// 使用基本数据类型 number,string,boolean,
let name=ref('张三');
let age=ref(18);
let isMarry=ref(false);
// 使用ref定义数组
let hobby=ref(['吃饭','睡觉','打豆豆']);
// 使用ref定义对象
let user=ref({
idCard:'身份证',
nation:['中国','美国','英国','俄罗斯']
})
function changeName() {
// 修改基本数据数据类型
name.value='李四'; // ref定义的响应式数据修改数据时必需要.value
age.value=20;
isMarry.value=true;
// 修改对象数据类型
hobby.value[0]='玩游戏';
user.value.idCard='港澳台居民身份证';
user.value.nation[0]='挪威';
}
return{
name,
age,
isMarry,
changeName,
user,
hobby
}
}
}
</script>

<style scoped>

</style>

image-20240229150642949

点击修改信息数据变化

image-20240229150702474

  • ref定义的响应式数据修改数据时必需要.value
  • ref定义的对象数据类型,内部求助了Vue3.0中的一个新函数-reactive函数(看下面的介绍)
  • 模板中使用数据时不需要.value

reactive

  • 作用:定义一个对象类型的响应式数据(基本类型别用它,用ref函数)

  • const 代理对象=reactive(被代理的对象)接收一个对象(或数组),返回一个代理器对象(Proxy的实例对象,简称Proxy对象)

  • reactive定义的响应式数据是深层次的

  • 内部基于ES6的Proxy实现,通过代理对象的操作源对象的内部数据都是响应式的

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
<template>
<h2>reactive响应式数据</h2>
<p>姓名:{{student.name}}</p>
<p>年龄:{{student.age}}</p>
<p>爱好:{{student.hobbies}}</p>
<button @click="changeStuInfo">改变学生信息</button>
</template>

<script>
import {reactive} from 'vue'
export default {
name: "reactiveComponent",
setup(){
// 数据
let student=reactive({
name:'张三',
age:19,
hobbies:['吃饭','睡觉','打豆豆']
});
console.log(student)
// 方法
function changeStuInfo() {
student.name='李四';
student.age=20;
student.hobbies[0]='做家务'
}
return{
student,
changeStuInfo,
}
}
}
</script>

<style scoped>

</style>

image-20240229153509613

  • 点击改变学生信息后

image-20240229153542699

reactive对比ref

  • 从定义数据的角度对比

    • ref用来定义:基本类型数据
    • reactive用来定义:对象(或数组)类型数据
    • 备注:ref也可以用来定义对象(或数组)类型的数据,它内部会自动通过reactive转为代理对象
  • 从原理角度对比

    • ref通过Object.defineProperty()的get和set实现(响应式)数据劫持
    • reactive通过使用Proxy来实现响应式(数据劫持),并通过Reflect操作源对象内部的数据
  • 从使用角度

    • ref定义的数据:操作数据需要.value,读取数据时模板中直接读取不需要.value
    • reactive定义的数据:操作数据与读取数据均不需要.value

watch和watchEffect

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
   //	attr表示需要监视的属性
// 情况一:监视单个ref定义的响应式数据
watch(attr,(newValue,oldValue)=>{
console.log('attr变化了',newValue,oldValue);
})

// 情况二; 监视多个ref定义的响应式数据
watch([attr1,attr2,....,attrn],(newValue,oldValue)=>{
console.log('attr1或attrn变化了',newValue,oldValue);
})

// obj表示需要监听的对象
// 情况三:监视reactive定义的响应式数据
// 若watch监视的是reactive定义的响应式数据,则无法正确获得oldValue
// 若watch监视的是reactive定义的响应式数据,则强制打开开启了深度监视
watch(obj,(newValue,oldValue)=>{
console.log('obj变化了',newValue,oldValue)

},{immediate:true,deep:false}); // 此处deep配置不在奏效

// 情况四,监视reactive定义的响应式数据中的某个属性
watch(()=>person.job,(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{immediate:true,deep:true})

// 情况五:监视reactive定义的响应式数据中的某一些属性
watch([()=>person.name,()=>person.age],(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
})
// 特殊情况
watch(()=>person.job,(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{deep:false});// 此处由于是监视reactive所定义的对象中的某个属性,所以deep配置有效

  • watch

    • 监视reactive定义的响应式数据时:oldValue无法正确获取、强制开启了深度监视(deep配置失效)

    • 监视reactive定义的响应式数据中某个属性时deep配置有效

示例一:wath监听ref定义的响应式数据

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
<template>
<h2>watch监听ref定义的响应式数据</h2>
<h2>姓名:{{userName}}</h2>
<h2>年龄:{{age}}</h2>
<button @click="userName+='!'">修改姓名</button>
<button @click="age++">修改年龄</button>
<hr>
<h2>姓名:{{user.name}}</h2>
<h2>年龄:{{user.age}}</h2>
<button @click="user.name+='!'">修改姓名</button>
<button @click="user.age++">修改年龄</button>
</template>

<script>
import {ref,watch} from 'vue';
export default {
name: "watch_component01",
setup(){
let userName=ref('张三');
let age=ref(18);
let user=ref({
name:'张三',
age:21,
})
// watch监听ref定义的单个响应式数据
watch(userName,(newValue,oldValue)=>{
console.log(`userName发生了变化,新值是:${newValue},旧值是:${oldValue}`)
});
watch(age,(newValue,oldValue)=>{
console.log(`age发生了变化,新值是:${newValue},旧值是:${oldValue}`);
});

// 如果需要监听多个ref定义的响应式数据的话,代码如下
/**
* newValue和oldValue都是数组的形式,其中数组的第n位表示监听位置的第n位
* 例如:此例子中,监听属性的第一位是userName,那位newValue和oldValue对应的第一位也是
* userName。
* 如果有立即执行,那么最开始的值为[],而不是[undefined,undefined]
*/
watch([userName,age],(newValue,oldValue)=>{
console.log('userName或age中的其中一个发生了变化,',newValue,oldValue)
})

// watch监视ref定义的响应式对象数据
watch(user.value,(newValue,oldValue)=>{
console.log('person发生了变化',newValue,oldValue)
})
watch(user,(newValue,oldValue)=>{
console.log('person发生了变化',newValue,oldValue);
},{deep:false})

return{
userName,
age,
user
}
}
}
</script>

<style scoped>

</style>

image-20240229162030615

  • 点击修改按钮

image-20240229164328671

示例二:watch监听reactive定义的响应式数据

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
<template>
<h1>watch监听reactive定义的响应式数据</h1>
<p>姓名:{{user.name}}</p>
<p>年龄:{{user.age}}</p>
<p>薪水:{{user.job.salary}}K</p>
<button @click="user.name+='!'">改变姓名</button>
<button @click="user.age++">改变年龄</button>
<button @click="user.job.salary++">改变薪水</button>
</template>

<script>
import {watch,reactive} from 'vue'
export default {
name: "watch_component02",
setup(){
let user=reactive({
name:'张三',
age:18,
job:{
salary:20
}
});

// 情况一:监听reactive定义的响应式数据,无法正确获取oldValue
/**
* 此时的newValue和oldValue都是最新的数据
* 默认强制开启深度监视,此时深度监视失效
*/
watch(user,(newValue,oldValue)=>{
console.log(newValue,oldValue);
},{deep:false});


// 情况二,监视reactive定义的响应式数据的单个属性
// watch(()=>user.name,(newValue,oldValue)=>{
// console.log('name发生了变化',newValue,oldValue);
// });
// watch(()=>user.age,(newValue,oldValue)=>{
// console.log('age发生了变化',newValue,oldValue);
// })


// 情况三:监视reactive定义的响应式数据的多个属性
/**
* newValue和oldValue都是数组的形式,其中数组的第n位表示监听位置的第n位
* 例如:此例子中,监听属性的第一位是userName,那位newValue和oldValue对应的第一位也是
* userName,
*/
// watch([()=>user.name,()=>user.age],(newValue,oldValue)=>{ // 写法一
// console.log('name或age中的某个属性发生了变化',newValue,oldValue);
// })
// watch(()=>[user.name,user.age],(newValue,oldValue)=>{ // 写法二
// console.log('name或者age中的某个属性发生了变化',newValue,oldValue)
// })

// 情况四:监视reactive定义的响应式数据的对象的某个属性,此时deep有效
/**
* 注意:此时需要区别是reactive定义的对象还是reactive定义的对象里面的某个属性
* 此时deep有效,关闭了监视
*/
// watch(()=>user.job,(newValue,oldValue)=>{
// console.log(newValue,oldValue);
// },{deep:false});
return{
user
}
}
}
</script>

<style scoped>

</style>

  • watchEffect

    • watch的套路是:既要指明监视的属性,也要指明监视的回调

    • watchEffect的套路是:不用指明监视那个属性,监视的回调中用到那个属性,那就监视那个属性

    • watchEffect有点像computed

      • 但computed注重的是计算出来的值(回调函数的返回值),所以必需要写返回值

      • 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值

  • 代码

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
<template>
<h1>watchEffect监视ref和reactive定义的响应式数据</h1>
<h2>当前求和:{{sum}}</h2>
<button @click="sum++">点我加1</button>
<hr>
<h2>当前的信息:{{msg}}</h2>
<button @click="msg+='!'">修改信息</button>
<hr>
<h2>姓名:{{person.name}}</h2>
<h2>年龄:{{person.age}}</h2>
<h2>薪资:{{person.job.j1.salary}}</h2>
<button @click="person.name+='!'">修改姓名</button>
<button @click="person.age++">修改年龄</button>
<button @click="person.job.j1.salary++">涨薪</button>
</template>

<script>
import {ref,reactive,watchEffect} from 'vue';
export default {
name: "watch_effect_component01",
setup(){
let sum=ref(0);
let msg=ref('你好');
let person=reactive({
name:'张三',
age:18,
job:{
j1:{
salary:100,
}
}
});
/**
* 在watchEffect里面写需要监视的属性,默认会执行一次
* 如果是监视ref定义的响应式书则需要.value
* 如果是监视reactive定义的响应式数据则直接监视
*/
watchEffect(()=>{
let x1=sum.value;
let x2=person.job.j1.salary;
console.log('watchEffect所指定的回调函数执行了');
})

return{
sum,
msg,
person
}
}
}
</script>

<style scoped>

</style>

  • 点我加1和涨薪,就会触发watchEffect监控中的内容

image-20240229175711760

说明

说明

  • Spring Boot让我们的Spring应用变的更轻量化
  • 我们不必像以前那样繁琐的构建项目、打包应用、部署到Tomcat等应用服务器中来运行我们的业务服务。
  • 通过Spring Boot实现的服务,只需要依靠一个Java类,把它打包成jar,并通过java -jar命令就可以运行起来。
  • 依赖说明
1
2
3
4
5
1. Maven  需求:3.5+
2. JDK 需求 8+
3. Spring Framework 5.3.7以上版本
4. Tomcat 9.0
5. Servlet版本 4.0 但是可以部署到Servlet到3.1+的容器中

快速入门

  • 依赖文件
1
2
3
4
5
6
<!--springboot的父工程其中定义了常用的依赖,并且无依赖冲突-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
</parent>

注意上方的parent必须加,其中定义了springboot官方支持的n多依赖,基本上常用的已经有了,所以接下来导入依赖的时候,绝大部分都可以不加版本号。

  • 添加web依赖
1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

添加上方的web依赖,其中间接依赖了spring-web,spring-webmvc,spring-core等spring和springmvc的包,并且集成了tomcat。

  • 编写启动类.src-main-java-对应的包 目录下
1
2
3
4
5
6
7
8
9
10
11
package xyz.shi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HelloApp {

public static void main(String[] args) {
SpringApplication.run(HelloApp.class,args);
}
}

@SpringBootApplication注解标识了HelloApp为启动类,也是Spring Boot的核心。

  • 运行后发现下面的日志,tomcat端口为8080

image-20240222104341082

  • 如果想要修改端口号,可以在resources目录下新建application.properties,然后重写运行即可
1
server.port=8082

编写一个Http接口

  • 创建HelloController类,内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package xyz.shi.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("hello")
public class HelloController {

@GetMapping("boot")
public String hello(){
return "hello spring boot";
}

}
  • 重启程序,使用postman或者直接在浏览器输入http://localhost:8082/hello/boot

image-20240222104901876

单元测试

  • 加入依赖
1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
  • 在src/test/java/xyz.shi 下,编写测试用例
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
package xyz.shi;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import xyz.shi.controller.HelloController;

import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
public class TestHelloController {

private MockMvc mockMvc;

@BeforeEach
public void beforeEach() {
mockMvc = MockMvcBuilders.standaloneSetup(new HelloController()).build();
}

@Test
public void testHello() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/hello/boot")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string(equalTo("hello spring boot")));
}
}

对Controller进行单元测试,需要用到mock

测试类的包需要和启动类HelloApp的包一致

打包为jar运行

  • 添加打包(maven构建springboot)插件
1
2
3
4
5
6
7
8
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
  • 打开idea中的project Structure,选中Artifacts,点击中间的加号(Project Settings->Artifacts->JAR->From modules with dependencies ),如下图所示:

image-20240222171952272

image-20240222172921801

  • 开始打包,点击右侧的Maven Projects,打开LIfecycle,先点击clean,再点击package,生成target文件夹,里面有以项目名命名加版本号的jar文件,至此打包完成。

image-20240222174210640

  • ,IDEA会自动开始把项目所有需要的文件和依赖包到打包到一个jar里面,完成后左侧目录栏里的target文件下会出现两个神奇的文件

image-20240222175201164

  • 进入jar所在的文件夹,使用java -jar命令运行jar,项目就能启动
1
2
3
4
5
6
7
8
9
10
Administrator@WIN-20230710BAT MINGW64 /d/project/StudySpringBoot/target
$ java -jar StudySpringBoot-1.0-SNAPSHOT.jar

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/

  • 测试,使用postman或者直接在浏览器输入http://localhost:8082/hello/boot

  • 得到结果:hello spring boot

  • jar tvf XXX.jar 可以查看包的内容

工程结构

配置文件详解

整合MybatisPlus

  • 依赖文件
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
<dependencies>
<!--web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--mybatis -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

</dependencies>
  • resources-application.properties 配置数据库信息
1
2
3
4
5
6
server.port=8082

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdb
  • 实体类
1
2
3
4
5
6
7
8
9
10
11
package xyz.shi.pojo;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@TableName("users")
@Data
public class User {
private Integer id;
private String name;
private String password;
}
  • mapper
1
2
3
4
5
6
7
package xyz.shi.mapper;

import xyz.shi.pojo.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface UserMapper extends BaseMapper<User> {
}

  • config扫描mapper
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package xyz.shi.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("xyz.shi.mapper")
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}

  • service
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
package xyz.shi.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xyz.shi.mapper.UserMapper;
import xyz.shi.pojo.User;
import java.util.List;

@Service
@Slf4j
public class UserService {

@Autowired
private UserMapper userMapper;

public List<User> findAll() {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
List<User> users = userMapper.selectList(queryWrapper);
return users;
}

//分页
public List<User> findPage(Integer CurrentPage, Integer PageSize) {
System.out.println(CurrentPage);
System.out.println(PageSize);
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
Page page = new Page(CurrentPage, PageSize);
Page<User> userPage = userMapper.selectPage(page, queryWrapper);
log.info("total:{}", userPage.getTotal());
log.info("pages:{}", userPage.getPages());
return userPage.getRecords();
}
}
  • resource-logback-spring.xml 加上日志文件配置
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
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<contextName>logback</contextName>
<!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
<property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" />
<!-- 定义日志存储的路径,不要配置相对路径 -->
<!-- <property name="FILE_PATH" value="/Users/Adm/spring-log.%d{yyyy-MM-dd}.%i.log" />-->
<property name="FILE_PATH" value="d:/log/spring-log.%d{yyyy-MM-dd}.%i.log" />
<!-- 控制台输出日志 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!-- 日志级别过滤INFO以下 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<!-- 按照上面配置的LOG_PATTERN来打印日志 -->
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!--每天生成一个日志文件,保存30天的日志文件。rollingFile用来切分文件的 -->
<appender name="rollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${FILE_PATH}</fileNamePattern>
<!-- keep 15 days' worth of history -->
<maxHistory>30</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- 日志文件的最大大小 -->
<maxFileSize>2MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 超出删除老文件 -->
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- logger节点,可选节点,作用是指明具体的包或类的日志输出级别,
以及要使用的<appender>(可以把<appender>理解为一个日志模板)。
addtivity:非必写属性,是否向上级loger传递打印信息。默认是true-->
<logger name="net.sh.rgface.serive" level="ERROR" />
<!--项目的整体的日志打印级别为info-->
<root level="INFO">
<appender-ref ref="console" />
<appender-ref ref="rollingFile" />
</root>
</configuration>
  • UserController
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
package xyz.shi.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.shi.pojo.User;
import xyz.shi.service.UserService;
import java.util.List;

@RestController
@RequestMapping("user")
public class UserController {

@Autowired
private UserService userService;

@GetMapping("findAll")
public List<User> findAll() {
return userService.findAll();
}

@GetMapping("findPage")
public List<User> findPage(Integer CurrentPage, Integer PageSize) {
return userService.findPage(CurrentPage, PageSize);
}
}
  • 启动后浏览器访问 http://localhost:8082/user/findPage?CurrentPage=1&PageSize=1

image-20240223165729058

  • 日志文件

image-20240223165819078

总结

  • spring系列暂时告一段落,后面开展学习vue方面的知识并且结合springboot

说明

  • 本次搭建环境信息,idea社区版、java1.8、mysql8x,本地tomcat9.0.83

  • idea装好smart tomcat并且关联本地的tomcat

ssm整合

  • s: springMVC,s: spring,m: mybatis,上述三个框架合起来,称为ssm,所谓的ssm整合说的就是将三个框架集成起来

  • 依赖文件

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
<dependencies>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.16.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.16.RELEASE</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<!--mybatis环境-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.3</version>
</dependency>
<!--spring整合mybatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.3</version>
</dependency>
<!--连接池-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<!--分页插件坐标-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.2</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.0</version>
</dependency>
<!--servlet环境-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>

集成spring

  • 在resources下新建applicationContext.xml,做为spring的配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<!--开启bean注解扫描-->
<context:component-scan base-package="xyz.shi"/>

</beans>
  • 新建UserService.java
1
2
3
4
5
6
7
8
9
10
11
package xyz.shi.service;

import org.springframework.stereotype.Service;

@Service
public class UserService {
public void test(){
System.out.println("test....");
}
}

  • 新建APP.java,测试UserService可用
1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import xyz.shi.service.UserService;

public class App {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = context.getBean(UserService.class);
userService.test();
}
}

Spring集成Mybatis

  • 实体类
1
2
3
4
5
6
7
8
9
10
package xyz.shi.pojo;

import lombok.Data;

@Data
public class User {
private int id;
private String name;
private String password;
}
  • 在resources新建jdbc.properties,在spring-dao.xml中引入,将数据库配置提取出来
1
2
3
4
5
6
7
jdbc.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456
jdbc.driverClassName=com.mysql.cj.jdbc.Driver
jdbc.autoCommit=true
jdbc.connectionTimeout=5000
jdbc.idleTimeout=60
  • 在resources下新建spring-dao的xml配置文件
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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath*:*.properties"/>

<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="autoCommit" value="${jdbc.autoCommit}"/>
<property name="connectionTimeout" value="${jdbc.connectionTimeout}" />
<property name="idleTimeout" value="${jdbc.idleTimeout}" />
</bean>
<!--jdbc的xml配置-->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
<constructor-arg name="configuration" ref="hikariConfig"/>
</bean>

<!--mybatis-->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath*:mapper/*.xml"/>
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<props>
<prop key="helperDialect">mysql</prop>
</props>
</property>
</bean>
</array>
</property>
</bean>
<!--配置mapper接口的扫包路径,目的是代理此接口,生成代理类 注册到spring容器中-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="xyz.shi.mapper"/>
</bean>
</beans>
  • 新建UserMapper接口
1
2
3
4
5
6
7
package xyz.shi.mapper;

import xyz.shi.pojo.User;
public interface UserMapper {
User findUser(Integer id);
}

  • 在resources下新建mapper/UserMapper.xml
1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xyz.shi.mapper.UserMapper">

<select id="findUser" parameterType="int" resultType="xyz.shi.pojo.User">
select * from users where id=#{id}
</select>
</mapper>
  • 在UserService中注入UserMapper,实现查询用户的业务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package xyz.shi.service;

import org.springframework.stereotype.Service;
import xyz.shi.mapper.UserMapper;
import xyz.shi.pojo.User;

import javax.annotation.Resource;

@Service
public class UserService {
@Resource
private UserMapper userMapper;
public void test(){
System.out.println("test....");
}
public User getUser(int id){
return userMapper.findUser(id);
}
}

集成SpringMVC

将pom.xml中的package改为war,加入springMVC,就是web工程,同时导入spring-webmvc的依赖

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
<?xml version="1.0" encoding="UTF-8"?>
<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>

<groupId>org.example</groupId>
<artifactId>ssm</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>

<!-- <dependency>-->
<!-- <groupId>org.springframework</groupId>-->
<!-- <artifactId>spring-context</artifactId>-->
<!-- <version>5.2.16.RELEASE</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.16.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.16.RELEASE</version>
</dependency>
  • 在main的下面,新建webapp目录

  • webapp下新建WEB-INF/web.xml文件

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
<?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_3_1.xsd"
version="3.1">
<!--spring配置文件加载-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--解决post请求乱码-->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--springmvc配置文件加载-->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring-mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<!--/代表拦截所有请求,一般也写做*.do代表只匹配.do后缀的请求-->
<url-pattern>/</url-pattern>
</servlet-mapping>

</web-app>
  • 在resources下新建spring-mvc.xml,其中加入mvc相关的配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--这个不能忘,分开加载spring配置文件,扫包也需要单独配置-->
<context:component-scan base-package="xyz.shi.controller" />
<!--开启mvc的注解支持-->
<mvc:annotation-driven/>

<!--扫包的配置,在spring配置文件中 已经定义,保证controller在扫包范围内即可-->

<!--过滤静态文件-->
<mvc:default-servlet-handler />
</beans>
  • 新建UserController.java,调用Service,完成根据id获取用户信息的逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package xyz.shi.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.shi.pojo.User;
import xyz.shi.service.UserService;

@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;

@GetMapping("getUser/{id}")
public User findUser(@PathVariable int id){
return userService.getUser(id);
}
}

  • 本地安装tomcat,我的版本为9.0.83,然后idea中装插件Smart tomcat,然后webapp右键运行
  • http://localhost:8080/ssm/user/getUser/4 打开

image-20240220151657257

添加事务配置

resource-spring-tx.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<tx:annotation-driven proxy-target-class="true"/>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
  • applicationContext.xml中导入
1
<import resource="spring-tx.xml" />
  • usermapper.xml加入新增和查询所有
1
2
3
4
5
6
7
<insert id="save" parameterType="xyz.shi.pojo.User" keyProperty="id" useGeneratedKeys="true">
insert into users(name,password)
value(#{name},#{password})
</insert>
<select id="findAll" resultType="xyz.shi.pojo.User">
select * from users
</select>
  • usermapper代码,加入了新增方法
1
2
3
4
5
public interface UserMapper {
User findUser(int id);
void saveTx(User user);
Page<User> findAll();
}
  • UserService
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
package xyz.shi.service;

import org.springframework.stereotype.Service;
import xyz.shi.mapper.UserMapper;
import xyz.shi.pojo.User;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
public class UserService {
@Resource
private UserMapper userMapper;
public void test(){
System.out.println("test....");
}
public User getUser(int id){
return userMapper.findUser(id);
}
public Page<User> findAll(Integer PageNum, Integer PageSize) {
//在查询之前,设置分页条件 显示第一页,展示3条数据
Page<Object> page = PageHelper.startPage(PageNum, PageSize);
return userMapper.findAll();
}

@Transactional
public void saveTx(User user) {
userMapper.save(user);
int i = 10 / 0; //模拟发生异常
// 加上事务标识后,只有出现异常才不会造成新增数据成功
}

加上事务标识后,只有出现异常才会触发事务

  • UserController
1
2
3
4
5
6
7
@GetMapping("saveTx")
public void saveTx(String name, String password) {
User user = new User();
user.setName(name);
user.setPassword(password);
userService.saveTx(user);
}
  • 打开http://localhost:8080/ssm/user/saveTx?name=123&&password=456 报错了,数据新增不会成功,如果不加Transactional标识,即使报错了新增数据也会成功
  • 分页查询

image-20240221103126485

其他

拦截器

  • 在springmvc文章中已经实践

统一异常处理

为什么要有统一异常呢?

  • 如果是编码过程中没有预料到的异常,比如bug,内存不足,硬件错误等等造成的,会提示给用户一些不友好的信息,如果在controller中处理,那么每个controller都要try catch,又过于繁琐
  • 异常的种类很多,有自定义的业务异常,有系统异常,有空指针异常,有数组越界等,每种异常处理的方式都不一致,比如业务异常,需要提示用户准确信息,系统异常需要提示用户友好信息(系统繁忙,请稍候再试)并且记录错误,空指针异常遇到发送错误日志短信给开发人员等

使用

  • handle包下有分别为异常实体类、自定义异常等
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
package xyz.shi.handler;
import lombok.Data;
//统一结果返回
@Data
public class Result {
private boolean success;
private int code;
private String message;
private Object data;
}

package xyz.shi.handler;
public class BusinessException extends RuntimeException {
private int code;
public BusinessException(){
super();
}
public BusinessException(Integer code,String message){
super(message);
this.code = code;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}

package xyz.shi.handler;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public Result doException(Exception e) {
//记录异常
e.printStackTrace();
Result result = new Result();
result.setCode(-999);
result.setMessage("未知的异常,提示友好信息");
result.setSuccess(false);
return result;
}

@ExceptionHandler(IndexOutOfBoundsException.class)
@ResponseBody
public Result doStack(IndexOutOfBoundsException e) {
//记录异常
e.printStackTrace();
//发送短信给开发人员
// send()
Result result = new Result();
result.setCode(-999);
result.setMessage("数组越界异常");
result.setSuccess(false);
return result;
}

@ExceptionHandler(BusinessException.class)
@ResponseBody
public Result doBusiness(BusinessException e) {
Result result = new Result();
result.setCode(e.getCode());
result.setMessage(e.getMessage());
result.setSuccess(false);
return result;
}
}
  • 控制层修改代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 @GetMapping("getUser/{id}")
public Result findUser(@PathVariable int id){
System.out.println("getUser方法调用...");
if (id == 2){
throw new BusinessException(-999,"对不起,参数不能为2");
}
if (id == 3){
throw new IndexOutOfBoundsException();
}
if (id == 4){
int i = 10/0;
}
User user = userService.getUser(id);
return new Result(true,200,"success",user);
}

image-20240221112245723

  • 获取值正常时,比如:http://localhost:8080/ssm/user/getUser/10,得到结果{"success":true,"code":200,"message":"success","data":{"id":10,"name":"456","password":"123456"}}

数据校验

在进行请求的时候,大多数往往会携带参数,我们总需要对参数进行合法性的校验,比如不能为null,密码长度不能小于8,用户名必须使用大小写字母+数字等等

JSR 303 是 Java 为 Bean 数据合法性校验提供的标准框架,通过 Bean 属性上标注类似于 @NotNull、@Max 等标准的注解指定校验规则,并通过标准的验证接口对 Bean 进行验证。

image-20240221114212311

  • 依赖包
1
2
3
4
5
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.0.Final</version>
</dependency>
  • 实体类修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package xyz.shi.pojo;

import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotNull;

@Data
public class User {
private int id;
@NotNull(message = "用户名不能为空")
private String name;
@Length(min = 5, max = 100, message = "密码长度只能在5-100之间")
private String password;
}
  • 控制层新增一个post新增数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@PostMapping("save")
public Result save( @Valid User user, BindingResult bindingResult) {
System.out.println(user);
if (bindingResult.hasErrors()) {
//获取校验有错误的字段
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
System.out.println(fieldError.getField());
System.out.println(fieldError.getDefaultMessage());
System.out.println("------------------------------");
return new Result(true,-1,fieldError.getDefaultMessage(),null);

}
}
userService.save(user);
return new Result(true,200,"success",user);
}
  • python模拟发一个post请求
1
2
3
4
5
6
data = {"name": "123", "password": "1111"}
t = requests.post("http://localhost:8080/ssm/user/save", data=data)
s = t.text
print(s)
==
{"success":true,"code":-1,"message":"密码长度只能在5-100之间","data":null}

日志

  • 主流使用的日志是logback和log4j2,本次主要演示logback
  • 依赖文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--日志 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.logback-extensions</groupId>
<artifactId>logback-ext-spring</artifactId>
<version>0.1.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.12</version>
</dependency>
  1. 第一个logback-classic包含了logback本身所需的slf4j-api.jarlogback-core.jarlogback-classsic.jar
  2. 第二个logback-ext-spring是由官方提供的对Spring的支持
  3. 第三个jcl-over-slf4j是用来把Spring源代码中大量使用到的commons-logging替换成slf4j,只有在添加了这个依赖之后才能看到Spring框架本身打印的日志–即info文件中打印出的spring启动日志信息,否则只能看到开发者自己打印的日志
  • 在resources下创建logback.xml
1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<!--scan当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。scanPeriod 设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。debug 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。-->
<configuration scan="true" scanPeriod="60 seconds" debug="true">
<!-- 模块名称, 影响日志配置名,日志文件名 -->
<property name="appName" value="mszluSpring"/>
<property name="logMaxSize" valule="100MB"/>
<!--rootPath 日志路径 -->
<property name="rootPath" value="D:/log"/>
<contextName>${appName}</contextName>
...

日志级别标准:ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF

  • 直接运行后,就能在d/log目录下看到相应的日志文件
  • 但是发现一个问题,如果系统报错了error日志没有记录,是因为我们使用了@ControllerAdvice这个异常类,需要在自定义异常类中加入如下代码:log.error
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
package xyz.shi.handler;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
public class MyExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(MyExceptionHandler.class);
@ExceptionHandler(Exception.class)
@ResponseBody
public Result doException(Exception e) {
//记录异常
e.printStackTrace();
log.error("500 Internal Server Error", e);

Result result = new Result();
result.setCode(-999);
result.setMessage("未知的异常,提示友好信息");
result.setSuccess(false);
return result;
}

@ExceptionHandler(IndexOutOfBoundsException.class)
@ResponseBody
public Result doStack(IndexOutOfBoundsException e) {
//记录异常
e.printStackTrace();
log.error("500 Internal Server Error", e);
//发送短信给开发人员
// send()
Result result = new Result();
result.setCode(-999);
result.setMessage("数组越界异常");
result.setSuccess(false);
return result;
}

@ExceptionHandler(BusinessException.class)
@ResponseBody
public Result doBusiness(BusinessException e) {
log.error("500 Internal Server Error", e);
Result result = new Result();
result.setCode(e.getCode());
result.setMessage(e.getMessage());
result.setSuccess(false);
return result;
}
}

纯注解ssm整合

  • 就是去掉web.xml 这些配置,使用配置类的方式,我实践了一下,无法启动成功,可能是我用的idea社区版本

  • 参考步骤