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

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


  • 首页

  • 归档

  • 搜索

Django 序列化

发表于 2021-11-14

这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

一、简介

django rest framework 中的序列化组件,可以说是其核心组件,也是我们平时使用最多的组件,它不仅仅有序列化功能,更提供了数据验证的功能(与django中的form类似)。

便于展现的序列化操作,我们需要在model添加外键、多对多情况。以下是新的models(请删除原有的数据库,重新migrate):

models.py

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
Python复制代码from django.db import models

class UserInfo(models.Model):
user_type_choice = (
(1,"普通用户"),
(2,"会员"),
)
user_type = models.IntegerField(choices=user_type_choice)
username = models.CharField(max_length=32,unique=True)
password = models.CharField(max_length=64)
group = models.ForeignKey(to='UserGroup',null=True,blank=True)
roles = models.ManyToManyField(to='Role')


class UserToken(models.Model):
user = models.OneToOneField(to=UserInfo)
token = models.CharField(max_length=64)



class UserGroup(models.Model):
"""用户组"""
name = models.CharField(max_length=32,unique=True)


class Role(models.Model):
"""角色"""
name = models.CharField(max_length=32,unique=True)

二、使用

1.基本使用

在urls.py中添加新的角色url,以前的url为了减少干扰,在这里进行注释:

1
2
3
4
5
6
7
8
9
Python复制代码from django.conf.urls import url
from app01 import views

urlpatterns = [

# url(r'^api/v1/auth', views.AuthView.as_view()),# url(r'^api/v1/order', views.OrderView.as_view()),
url(r'^api/v1/roles', views.RoleView.as_view()), # 角色视图
# url(r'^api/(?P<version>[v1|v2]+)/user', views.UserView.as_view(),name="user_view"),
]

views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Python复制代码from rest_framework import serializers
from rest_framework.views import APIView
from django.shortcuts import HttpResponse
from app01 import models
import json


class RolesSerializer(serializers.Serializer): #定义序列化类
id=serializers.IntegerField() #定义需要提取的序列化字段,名称和model中定义的字段相同
name=serializers.CharField()
class RoleView(APIView):
"""角色"""
def get(self,request,*args,**kwargs):
roles=models.Role.objects.all()
res=RolesSerializer(instance=roles,many=True) #instance接受queryset对象或者单个model对象,当有多条数据时候,使用many=True,单个对象many=Falsereturn HttpResponse(json.dumps(res.data,ensure_ascii=False))

使用浏览器访问http://127.0.0.1:8000/api/v1/roles ,结果如下:

2.自定义序列化字段

当数据模型中有外键或者多对多时候,这时候就需要自定义序列化了

新增用户信息url

1
2
3
4
5
6
7
8
9
10
Python复制代码from django.conf.urls import url
from app01 import views

urlpatterns = [

# url(r'^api/v1/auth', views.AuthView.as_view()),# url(r'^api/v1/order', views.OrderView.as_view()),
url(r'^api/v1/roles', views.RoleView.as_view()),
url(r'^api/v1/userinfo', views.UserinfoView.as_view()), #用户信息
# url(r'^api/(?P<version>[v1|v2]+)/user', views.UserView.as_view(),name="user_view"),
]

UserinfoView和序列化类

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
python复制代码class UserinfoSerializer(serializers.ModelSerializer):
id = serializers.IntegerField() # 定义需要提取的序列化字段,名称和model中定义的字段相同
username=serializers.CharField()
password=serializers.CharField()
#sss=serializers.CharField(source='user_type') #该方法只能拿到user_type的ID
sss=serializers.CharField(source='get_user_type_display') #自定义字段名称,和数据模型不一致,需要指定source本质调用get_user_type_display()方法获取数据
#rl=serializers.CharField(source='roles.all.first.name')
gp=serializers.CharField(source='group.name')
rl=serializers.SerializerMethodField() #多对多序列化方法一
def get_rl(self,obj): #名称固定:get_定义的字段名称
"""
自定义序列化
:param obj:传递的model对象,这里已经封装好的
:return:
"""
roles=obj.roles.all().values() #获取所有的角色

return list(roles) #返回的结果一定有道是json可序列化的对象
class Meta:
model = models.UserInfo
fields = ['id', 'username', 'password', 'sss','rl','gp'] #配置要序列化的字段
# fields = "__all__" 使用model中所有的字段

class UserinfoView(APIView):
"""用户信息"""
def get(self,request,*args,**kwargs):
users=models.UserInfo.objects.all()
res=UserinfoSerializer(instance=users,many=True) #instance接受queryset对象或者单个model对象,当有多条数据时候,使用many=True,单个对象many=False
return HttpResponse(json.dumps(res.data,ensure_ascii=False))

访问http://127.0.0.1:8000/api/v1/userinfo ,查看结果:


除了以上的Serializer,还可以使用ModelSerializer,ModelSerializer继承了serializer,其结果和上面示例一样:

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
python复制代码class UserinfoSerializer(serializers.ModelSerializer):
id = serializers.IntegerField() # 定义需要提取的序列化字段,名称和model中定义的字段相同
username=serializers.CharField()
password=serializers.CharField()
#sss=serializers.CharField(source='user_type') #该方法只能拿到user_type的ID
sss=serializers.CharField(source='get_user_type_display') #自定义字段名称,和数据模型不一致,需要指定source本质调用get_user_type_display()方法获取数据
#rl=serializers.CharField(source='roles.all.first.name')
gp=serializers.CharField(source='group.name')
rl=serializers.SerializerMethodField() #多对多序列化方法一
def get_rl(self,obj): #名称固定:get_定义的字段名称
"""
自定义序列化
:param obj:传递的model对象,这里已经封装好的
:return:
"""
roles=obj.roles.all().values() #获取所有的角色

return list(roles) #返回的结果一定有道是json可序列化的对象
class Meta:
model = models.UserInfo
fields = ['id', 'username', 'password', 'sss','rl','gp'] #配置要序列化的字段
# fields = "__all__" 使用model中所有的字段

class UserinfoView(APIView):
"""用户信息"""
def get(self,request,*args,**kwargs):
users=models.UserInfo.objects.all()
res=UserinfoSerializer(instance=users,many=True) #instance接受queryset对象或者单个model对象,当有多条数据时候,使用many=True,单个对象many=False
return HttpResponse(json.dumps(res.data,ensure_ascii=False))

3.连表序列化以及深度控制

使用depth进行深度控制,越深其序列化的细读越高

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码class UserinfoSerializer(serializers.ModelSerializer):

class Meta:
model = models.UserInfo
#fields = "__all__" # 使用model中所有的字段
fields = ['id', 'username', 'password', 'group','roles'] # 配置要序列化的字段
depth = 1 #系列化深度,1~10,建议使用不超过3
class UserinfoView(APIView):
"""用户信息"""
def get(self,request,*args,**kwargs):
users=models.UserInfo.objects.all()
res=UserinfoSerializer(instance=users,many=True) #instance接受queryset对象或者单个model对象,当有多条数据时候,使用many=True,单个对象many=False
return HttpResponse(json.dumps(res.data,ensure_ascii=False))

请求http://127.0.0.1:8000/api/v1/userinfo ,结果如下:

4.序列化字段url

urls.py新加入组url

1
2
3
4
5
6
7
8
python复制代码urlpatterns = [

# url(r'^api/v1/auth', views.AuthView.as_view()),# url(r'^api/v1/order', views.OrderView.as_view()),
url(r'^api/v1/roles', views.RoleView.as_view()),
url(r'^api/v1/userinfo', views.UserinfoView.as_view()),
url(r'^api/v1/group/(?P<xxx>\d+)', views.GroupView.as_view(),name='gp'), # 新加入组url
# url(r'^api/(?P<version>[v1|v2]+)/user', views.UserView.as_view(),name="user_view"),
]

views.py

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
python复制代码class UserinfoSerializer(serializers.ModelSerializer):
group=serializers.HyperlinkedIdentityField(view_name='gp',lookup_field='group_id',lookup_url_kwarg='xxx')
#view_name,urls.py目标url的视图别名(name),这里是UserGroup的视图别名
#lookup_field 给url传递的参数,也就是正则匹配的字段
#lookup_url_kwarg,url中正则名称,也就是kwargs中的key
class Meta:
model = models.UserInfo
#fields = "__all__" # 使用model中所有的字段
fields = ['id', 'username', 'password','roles','group'] # 配置要序列化的字段
depth = 1 #系列化深度,1~10,建议使用不超过3
class UserinfoView(APIView):
"""用户信息"""
def get(self,request,*args,**kwargs):
users=models.UserInfo.objects.all()
res=UserinfoSerializer(instance=users,many=True,context={'request': request}) #instance接受queryset对象或者单个model对象,当有多条数据时候,使用many=True,单个对象many=False
#若需生成超链接字段,则需要加context={'request': request}
return HttpResponse(json.dumps(res.data,ensure_ascii=False))

class UserGroupSerializer(serializers.ModelSerializer):
class Meta:
model = models.UserGroup
fields = "__all__"
depth = 0


class GroupView(APIView):
def get(self,request,*args,**kwargs):

group_id=kwargs.get('xxx')
group_obj=models.UserGroup.objects.get(id=group_id)
res=UserGroupSerializer(instance=group_obj,many=False) #instance接受queryset对象或者单个model对象,当有多条数据时候,使用many=True,单个对象many=False
return HttpResponse(json.dumps(res.data,ensure_ascii=False))

此时访问组信息:http://127.0.0.1:8000/api/v1/group/1,结果如下:

在查看用户信息,此时生成的组就是超链接形式了(便于查看json数据,这里用postman发请求):

本文转载自: 掘金

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

一个端口可以让多个进程绑定吗? 前言

发表于 2021-11-14

前言

这是以前找工作时候面试官问我的,但是记不清具体怎么问了。

后来回去研究了一下,但又忙其他事情,没结果,再后来才想起,觉得这么问肯定有道理,又找了些资料。

答案是可以的,一个端口确实可以让多个程序绑定,只不过是在Linux 3.9 上引入了一个特性,称为 SO_REUSEPORT。

我们知道如果一个程序已经在8080上绑定监听了,那么其他程序就不能对8080绑定,否则出异常,但是有个情况特殊,那就是对socket设置过SO_REUSEPORT,这个选项是怎么描述的:允许同一主机上的多个套接字绑定到同一端口,只要第一个服务器在绑定之前设置了这个选项,那么其他任意数量的socket都可以绑定相同的端口,前提是它们也设置了这个选项。

但是如果第一个socket的uid是A,那么其他非A运行的就无法绑定。

Nginx在1.9.1上也引用了这个功能,只需要在listen 后面加上reuseport可以了。

1
2
3
4
c复制代码server{
charset utf-8;
listen 6060 reuseport;
}

之后会创建worker_processes个进程,监听相同的端口。

1
2
3
4
5
6
java复制代码root@meet:/etc/nginx/nginx_configs# netstat -an |grep 6060
tcp 0 0 0.0.0.0:6060 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:6060 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:6060 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:6060 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:6060 0.0.0.0:* LISTEN

在java中貌似还没有直接办法,没有提供设置这个的选项,虽然内部有个SocketOptions类,但里面没有关于SO_REUSEPORT字段的,更何况也无法调用,但是更高级别的jdk不知道有没有办法,但也不是绝对的,如果非要在java中使用这个特性,除了使用jni,还可以通过反射。

首先要了解sun.nio.ch.Net,这是个很重要的类,ServerSocketChannel内部就是使用他,下面是创建一个socket的流程。

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
java复制代码
fun create(): FileDescriptor {
var declaredMethod = Net::class.java.getDeclaredMethod("serverSocket", Boolean.TYPE)
declaredMethod.isAccessible = true
var invoke = declaredMethod.invoke(null, true)
return invoke as FileDescriptor
}

fun bind(fileDescriptor: FileDescriptor) {
Net.bind(fileDescriptor, InetAddress.getLocalHost(), 6666)
}

fun listen(fileDescriptor: FileDescriptor) {
var declaredMethod = Net::class.java.getDeclaredMethod(
"listen",
FileDescriptor::class.java,
Int::class.java
)
declaredMethod.isAccessible = true
var invoke = declaredMethod.invoke(null, fileDescriptor, 50)
}

fun setReusePort(fileDescriptor: FileDescriptor) {
val methodSetIntOption0: Method = Net::class.java.getDeclaredMethod(
"setIntOption0", FileDescriptor::class.java,
Boolean.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE, Boolean.TYPE,
)
methodSetIntOption0.setAccessible(true)
methodSetIntOption0.invoke(
null, fileDescriptor, true, 1,
15, 1, false
);
}

如果要用最老的ServerSocket,就是稍微有些麻烦,其中原理是替换掉内部的FileDescriptor,但是这里有个细节,如果使用ServerSocket的有参构造方法,那么创建、绑定、监听socket在一起,没有办法替换,只有使用无参构造,并且在之后调用一次createImpl才行。

但问题是不能直接访问createImpl,他只有一处调用,就是getImpl(),getImpl中判断了如果不为空,才调用createImpl()创建,但问题是getImpl()也不能调用,只能在向上一层找,所以我找了getLocalPort方法,但是TM的getLocalPort()也有限制,就是isBound()返回True才行,所以要通过反射修改一下bound值,之后要修改回来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码fun createNewFd(): FileDescriptor {
var declaredMethod = Net::class.java.getDeclaredMethod("serverSocket", Boolean.TYPE)
declaredMethod.isAccessible = true
var invoke = declaredMethod.invoke(null, true)
var fileDescriptor = invoke as FileDescriptor
setReusePort(fileDescriptor)
return fileDescriptor
}

var serverSocket = ServerSocket()
var boundField = ServerSocket::class.java.getDeclaredField("bound")
boundField.isAccessible=true
boundField.set(serverSocket,true)
serverSocket.localPort
boundField.set(serverSocket,false)
var socketImplField = ServerSocket::class.java.getDeclaredField("impl")
socketImplField.isAccessible = true
var socketImpl= socketImplField.get(serverSocket) as SocketImpl
var field = SocketImpl::class.java.getDeclaredField("fd")
field.isAccessible=true
field.set(socketImpl,createNewFd())
serverSocket.bind(InetSocketAddress(8989))
serverSocket.accept()

但其实使用ServerSocketChannel更方便,ServerSocketChannel很松,原理还是替换内部fd。

1
2
3
4
5
6
7
8
java复制代码var serverSocketChannel = ServerSocketChannel.open()

var declaredField = serverSocketChannel::class.java.getDeclaredField("fd")
declaredField.isAccessible = true
declaredField.set(serverSocketChannel, createNewFd())

serverSocketChannel.bind(InetSocketAddress("127.0.0.1",8080))
serverSocketChannel.socket().accept()

open方法调用后,ServerSocketChannelImpl只会创建一个socket,替换掉即可。

1
2
3
4
5
6
java复制代码ServerSocketChannelImpl(SelectorProvider var1) throws IOException {
super(var1);
this.fd = Net.serverSocket(true);
this.fdVal = IOUtil.fdVal(this.fd);
this.state = 0;
}

这里提到的fd,在Windows下可以理解成句柄,就是描述具体东西的一个id。

下面拿c演示

socket的创建流程就不说了,里面通过setsockopt设置SO_REUSEPORT选项,那么成功后其他程序还是可以绑定8080端口,前提是他们也设置这个选项,JVM最后也会调用下面使用到的函数创建。

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
c复制代码#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>

int main(int argc, char const *argv[]) {

int new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[1024] = {0};


int server_fd = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT,&opt, sizeof(opt));

address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);

if (bind(server_fd, (struct sockaddr *) &address,
sizeof(address)) < 0) {
perror("绑定出错");
exit(EXIT_FAILURE);
}
if (listen(server_fd, 3) < 0) {
perror("监听出错");
exit(EXIT_FAILURE);
}
printf("监听中...\n");
while (1){
new_socket = accept(server_fd, (struct sockaddr *) &address,
(socklen_t *) &addrlen);
printf("accept...\n");
char *hello = "Hello";
send(new_socket, hello, strlen(hello), 0);
}
return 0;
}

编译运行,可以看到两个程序都绑定成功了。

录屏_deepin-terminal_20211114072033.gif

通过netstat查看,确实有两个程序在8080上监听。

1
2
3
4
shell复制代码root@hxl-PC:/home/hxl# netstat -anp |grep 8080
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 22720/./a.out
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 22718/./a.out
root@hxl-PC:/home/hxl#

那么问题就是,客户端连接的时候,那个会做出响应?

我找了很多资料,没有找到具体的说明,但是根据实际情况,大概是随机的,也就是唤醒是不公平的。

录屏_deepin-terminal_20211114073911.gif

哦,对了,我找到了一篇关于SO_REUSEPORT非常好的文章

tech.flipkart.com/linux-tcp-s…

但是最后还有个问题,ServerSocketChannel如果在启用IPV6的情况下,那么最后会调用下面函数创建,但如果其他端不是AF_INET6,是AF_INET,虽然可以进行绑定,但使用AF_INET6的一端将永远收不到请求,全部由AF_INET的负责。

1
c复制代码int server_fd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);

JVM禁用IPV6。

-Djava.net.preferIPv4Stack=true

这个没有找到明确的说明, 但我测试了很多次,确实是这样的。

本文转载自: 掘金

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

SpringBoot定制化错误处理 SpringBoot默认

发表于 2021-11-14

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

SpringBoot默认的错误处理机制

默认效果:

1)、浏览器,返回一个默认的错误页面

浏览器发送请求的请求头:

2)、如果是其他客户端,默认响应一个json数据

原理:

可以参照ErrorMvcAutoConfiguration;错误处理的自动配置;

1、DefaultErrorAttributes:帮我们在页面共享信息;

2、BasicErrorController:处理默认/error请求

3、ErrorPageCustomizer: 系统出现错误以后来到error请求进行处理;(web.xml注册的错误页面规则)

4、DefaultErrorViewResolver:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typescript复制代码@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}

private ModelAndView resolve(String viewName, Map<String, Object> model) {
//默认SpringBoot可以去找到一个页面? error/404
String errorViewName = "error/" + viewName;

//模板引擎可以解析这个页面地址就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
if (provider != null) {
//模板引擎可用的情况下返回到errorViewName指定的视图地址
return new ModelAndView(errorViewName, model);
}
//模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页面 error/404.html
return resolveResource(errorViewName, model);
}

一但系统出现4xx或者5xx之类的错误;ErrorPageCustomizer就会生效(定制错误的响应规则);就会来到/error请求;就会被BasicErrorController处理;

响应页面;去哪个页面是由DefaultErrorViewResolver解析得到的;

自定义错误

为我们要去做一个自定义错误呢?他不是已经给我出现了错误提示吗?

首先,作为开发者我们能看懂错误提示,但是作为用户,他们不知道啊,可能有一些问题是用户在无意间搞出来的,用户可以根据我们的提示,正确的继续使用,而且他还美观,让用户有更好的用户体验

其次,我们自定义错误,能帮我们更方便的找到我们的错误原因

1)有模板引擎的情况下;error/状态码; 【将错误页面命名为 错误状态码.html 放在模板引擎文件夹里面的 error文件夹下】,发生此状态码的错误就会来到 对应的页面;

我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,精确优先(优先寻找精确的状态码.html);

页面能获取的信息;

  • timestamp:时间戳
  • status:状态码
  • error:错误提示
  • exception:异常对象
  • message:异常消息
  • errors:JSR303数据校验的错误都在这里

2)、没有模板引擎(模板引擎找不到这个错误页面),静态资源文件夹下找;

错误页面:gitee.com/ghllhg/Spri…

3)、以上都没有错误页面,就是默认来到SpringBoot默认的错误提示页面;

最先是需要我们的一个异常类

1
2
3
4
5
scala复制代码public class UserNotExistException extends RuntimeException{
public UserNotExistException() {
super("用户不存在");
}
}

我们创建一个人造错误

1
2
3
4
5
6
7
8
less复制代码 	@ResponseBody
@RequestMapping("/hello")
public String hello(@RequestParam("user") String user){
if(user.equals("aaa")){
throw new UserNotExistException();
}
return "hello World";
}

我们新建一个类,自定义我们的异常返回信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
less复制代码@ControllerAdvice
/**
*浏览器客户端返回的是json
*/
public class MyExceptionHandler {

@ResponseBody
@ExceptionHandler(UserNotExistException.class)
public Map<String,Object> handleException(Exception e){
Map<String,Object> map = new HashMap<>(10);
map.put("code","user.notexist");
map.put("message",e.getMessage());
return map;


}
}

转发到/error进行自适应响应效果处理

1
2
3
4
5
6
7
arduino复制代码	@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e){
Map<String,Object> map = new HashMap<>(10);
map.put("code","user.notexist");
map.put("message",e.getMessage());

return "forward:error";

但是这个样子他又回到了之前的样子,而且状态码显示是200,这是不对的

1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e,HttpServletRequest request){
Map<String,Object> map = new HashMap<>(10);
request.setAttribute("javax.servlet.error.status_code",400);
map.put("code","user.notexist");
map.put("message",e.getMessage());
request.setAttribute("ext",map);
return "forward:error";

}
}

我们通过自己设置状态码的方式,跳转到我们的错误页面

但这样我们的定制信息又没,反反复复

出现错误以后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的数据是由getErrorAttributes得到的(是AbstractErrorController(ErrorController)规定的方法);

1、完全来编写一个ErrorController的实现类【或者是编写AbstractErrorController的子类】,放在容器中;

2、页面上能用的数据,或者是json返回能用的数据都是通过errorAttributes.getErrorAttributes得到;

容器中DefaultErrorAttributes.getErrorAttributes();默认进行数据处理的;

自定义ErrorAttributes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
/**
*
* @param webRequest
* @param options
* @return
* 返回值的map就是页面和json能获取的所有字段的值
*/
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> map = super.getErrorAttributes(webRequest, options);
map.put("company","ll");
//异常处理器携带的数据
Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
map.put("ext",ext);
return map;
}
}

本文转载自: 掘金

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

动态规划攻略之:最大子序和

发表于 2021-11-14

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」

题目

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例1:

1
2
3
ini复制代码输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例2:

1
2
ini复制代码输入:nums = [1]
输出:1

示例3:

1
2
ini复制代码输入:nums = [0]
输出:0

示例4:

1
2
ini复制代码输入:nums = [-1]
输出:-1

解题思路

首先对数组进行遍历,当前最大连续子序列和为 sum,结果为 ans;如果 sum > 0,则说明 sum 对结果有增益效果,则 sum 保留并加上当前遍历数字,如果 sum <= 0,则说明 sum 对结果无增益效果,需要舍弃,则 sum 直接更新为当前遍历数字;每次比较 sum 和 ans的大小,将最大值置为ans,遍历结束返回结果

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码class Solution {
public int maxSubArray(int[] nums) {
int ans = nums[0];
int sum = 0;
for(int num: nums) {
if(sum > 0) {
sum += num;
} else {
sum = num;
}
ans = Math.max(ans, sum);
}
return ans;
}
}

最后

时间复杂度: O(n)

空间复杂度:O(1)

往期文章:

  • 二叉树刷题总结:二叉搜索树的属性
  • 二叉树总结:二叉树的属性
  • 二叉树总结:二叉树的修改与构造
  • StoreKit2 有这么香?嗯,我试过了,真香
  • 看完这篇文章,再也不怕面试官问我如何构造二叉树啦!
  • 那帮做游戏的又想让大家氪金,太坏了!
  • 手把手带你撸一个网易云音乐首页 | 适配篇
  • 手把手带你撸一个网易云音乐首页(三)
  • 手把手带你撸一个网易云音乐首页(二)
  • 手把手带你撸一个网易云音乐首页(一)
  • 代码要写注释吗?写你就输了
  • Codable发布这么久我就不学,摸鱼爽歪歪,哎~就是玩儿
  • iOS 优雅的处理网络数据,你真的会吗?不如看看这篇
  • UICollectionView 自定义布局!看这篇就够了

请你喝杯 ☕️ 点赞 + 关注哦~

  1. 阅读完记得给我点个赞哦,有👍 有动力
  2. 关注公众号— HelloWorld杰少,第一时间推送新姿势

最后,创作不易,如果对大家有所帮助,希望大家点赞支持,有什么问题也可以在评论区里讨论😄~

本文转载自: 掘金

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

Java循环结构的一些总结

发表于 2021-11-14

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战」

前言

  大家好,又见面了,作为技术开发人员,相信大家都是在不短信学习新的技术和新的框架,有时候学习了新的知识的同时,可能老旧的知识就会陌生,甚至忘记怎么使用,所以在学习的过程中需要不断的回顾和总结,加深基础知识的理解。下面本次将把自己学习的Java循环结构总结一下。

什么是循环

  循环是指事物反复的连续的按照某种规律进行运行。循环结构就是指在程序中反复的连续的运行结构,循环结构不同于顺序结构,顺序结构在在程序中只运行一次,而循环结构运行一下或多次。Java 循环结构常用的方式有for、 while 、 do…while、还有Iterator迭代器等方式。下满针对这些方式进行介绍。

for循环

  使用for循环使一些循环结构变得更加简单。不同于while和do…while循环结构体,for循环执行的次数是在执行前就确定的,

普通for循环

第一种是普通的for循环,其语法结构如下:

1
2
3
js复制代码for(初始化; 布尔表达式; 循环变量更新) {
//业务代码语句
}

  通过普通for循环的语法结构可以看出,for循环中有初始值、表达式、循环变量,在程序块运行时首先检测布尔表达式的值。如果为 true,循环体被执行。如果为false,循环终止,开始执行循环体后面的语句。
示例:

1
2
3
js复制代码for (int i = 0; i < 10; i++) {
System.out.println("输出值为:" + i);
}

for循环执行结果如下图
图片.png

增强for循环

  for循环还有一种增强for循环,其主要用于集合的增强型 for 循环。其语法格式如下:

1
2
3
4
js复制代码for(数据类型 属性 : 集合体)
{
//业务代码句子
}

  其中声明语句是集合中的元素,可以是引用类型,也可以是基本数据类型,还可以是对象,作用域限定在循环语句块,其值与此时集合元素的值相等。表达式一般指将要循环的几何体。
示例

1
2
3
4
js复制代码List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
for(Integer i: list){
System.out.println("list输出值为:" + i);
}

增强for循环执行结果如下图
图片.png
可以看到下图中定义了两个边路i,都是在各自的循环代码块中起作用。
图片.png

while 循环

  while循环是最基本的循环,也是结构最简单的循环,只包含一个布尔表达式,只要布尔表达式为 true,循环就会一直执行下去。其表达式为:

1
2
3
js复制代码while( 布尔表达式 ) {
//业务代码句子
}

示例和普通for循环一样输出0-9的数字。如下:

1
2
3
4
5
js复制代码int i = 0;
while (i < 10) {
System.out.println("while输出值为:" + i);
i++;
}

while 循环其执行结果为:

图片.png

do…while 循环

  do…while 循环和while循环基本类似,其中do…while循环至少会执行一次,然后判断while中的表达式是否满足条件,如果为true则继续执行循环体,如果为false则不再执行循环体跳出循环,循环结束。其语法结构如下:

1
2
3
js复制代码do {
//代码语句
}while(布尔表达式);

  可以看到语法结构与while不同,在while中多了一个do的代码块,则表示先执行一次,再进行逻辑条件判断。
示例

1
2
3
4
5
js复制代码int i = 0;
do {
System.out.println("dowhile输出值为:" + i);
i++;
}while(i < 10);

do…while循环执行结果如下图:

图片.png

forEach 循环

在java8中有一个新特性的就是Stream流,Stream流中的常用方法forEach也可以实现循环的功能,其于for循环类似,但其语法结构更加简单。语法结构为:

1
js复制代码list.stream().forEach(执行方法);

示例如下:

1
2
3
js复制代码
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
list.stream().forEach(System.out::println);

forEach循环执行结果如下:

图片.png

结语

  好了,以上就是Java循环结构的一些总结,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

  作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

本文转载自: 掘金

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

【Java入门100例】09数组中的最小值——一维数组

发表于 2021-11-14

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

🌲本文收录于专栏《Java入门练习100例》——试用于学完「Java基础语法」后的巩固提高及「LeetCode刷题」前的小试牛刀。

点赞再看,养成习惯。微信搜索【一条coding】关注这个在互联网摸爬滚打的程序员。

本文收录于技术专家修炼,里面有我的学习路线、系列文章、面试题库、自学资料、电子书等。欢迎star⭐️

题目描述

难度:简单

输出一维整型数组中的值最小的那个元素及其下标。

知识点

  • 一维数组
  • 排序

解题思路

1.什么是数组

所谓的数组指的就是一组相关类型的变量集合,并且这些变量可以按照统一的方式进行操作。

定义数组

1
2
java复制代码int data[] = new int[3];
// 数组的长度为3,超过会报下标越界异常,且下标从0开始

添加元素

1
2
3
java复制代码data[0] = 10; // 第一个元素
data[1] = 20; // 第二个元素
data[2] = 30; // 第三个元素

循环打印

1
2
3
java复制代码for(int x = 0; x < data.length; x++) {
System.out.println(data[x]); //通过循环控制索引
}

2.排序算法

其实严格来说我们并没有用到排序算法,但有一些思想在里面,想提前了解可以看这篇。

冒泡排序

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码/**
* 输出一维整型数组中的值最小的那个元素及其下标。
*/
public class question_09 {
public static void main(String args[]) {
int a[] = { 12, 24, 6, 37, 3, 22, 64 };
int min = 0;
for (int i = 1; i < a.length; i++) {
if (a[min] > a[i]) {
min = i;
}
}
System.out.println("a[" + min + "] = " + a[min]);
}
}

输出结果

扩展总结

本节练习了一维数组的操作,下一节练习二维数组。

最后

独脚难行,孤掌难鸣,一个人的力量终究是有限的,一个人的旅途也注定是孤独的。当你定好计划,怀着满腔热血准备出发的时候,一定要找个伙伴,和唐僧西天取经一样,师徒四人团结一心才能通过九九八十一难。
所以,

如果你想学好Java

想进大厂

想拿高薪

想有一群志同道合的伙伴

请加入技术交流

本文转载自: 掘金

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

若依系统分页工具学习-PageHelper篇二

发表于 2021-11-14

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

昨天我们讲到若依系统中使用PageHelper来实现分页,从而实现了无需编写分页代码而实现了分页。

但是我们并没有阐述PageHelper的具体逻辑。今天我们来学习一下PageHelper是如何实现的?

PageHelper介绍

PageHelper是与Mybatis密不可分的。其网址为pagehelper.github.io/,在首页的大标题就是:MyBatis分页插件PageHelper。并且在Github与Gitee上均有源代码部署。

Github:github.com/pagehelper/…

Gitee:gitee.com/free/Mybati…

因为国内访问Github不稳定,我们通过Gitee来查看介绍内容与源码。

startPage()

我们最关心的仍然是startPage函数,下载PageHelper源码后,首先我们查找startPage函数的源码:

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
java复制代码/**
* 基础分页方法
*
* @author liuzh
*/
public abstract class PageMethod {
////其他属性与参数

/**
* 开始分页
*
* @param pageNum 页码
* @param pageSize 每页显示数量
* @param count 是否进行count查询
* @param reasonable 分页合理化,null时用默认配置
* @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
*/
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page<E> page = new Page<E>(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
//当已经执行过orderBy的时候
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page);
return page;
}

////其他函数

}

方法的前三行,构造了一个Page<E>对象,对象page中包含了分页相关参数,如pageSize与pageNum。

第5行代码Page<E> oldPage = getLocalPage();,我们来看看做了什么?

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
java复制代码/**
* 基础分页方法
*
* @author liuzh
*/
public abstract class PageMethod {
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

/**
* 设置 Page 参数
*
* @param page
*/
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}

/**
* 获取 Page 参数
*
* @return
*/
public static <T> Page<T> getLocalPage() {
return LOCAL_PAGE.get();
}
}

pageHelper中使用ThreadLocal来实现将分页数据保存在当前线程中。ThreadLocal本篇暂时不展开讲述,大家可理解为一个线程本地变量。

如此,我们便知道PageHelper是通过ThreadLocal将分页变量保存在当前线程中,以便后续查询获取。

那么后续执行时又是如何通过PageHelper将分页sql插入原sql的呢?

题外话

TheadLocal

这个类我最初是在阿里巴巴的Java编码规范中看到的,当时是讲述Java中时间的格式化问题容易引发多线程并发问题导致时间格式化出错,从而引入说使用ThreadLocal可以规避这一问题。

DateFormat是线程非安全的, 一般在多线程环境下, 必须为每一次日期时间的转换创建一个DateFormat

另外在格式时间时,发现编辑器推荐了两种很“优雅”的日期格式化方式:

1
2
3
4
5
6
7
8
9
java复制代码public static final ThreadLocal<DateFormat> df_Ch_yyyy_MM_dd_HH_mm = 
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy年MM月dd日HH:mm"));

public static final ThreadLocal<DateFormat> df_year_month_day_hour_minutes = new ThreadLocal<DateFormat>(){
@Override
protected DateFormat initialValue(){
return new SimpleDateFormat("yyyy-MM-dd HH:mm");
}
};

代码简洁漂亮!

本文转载自: 掘金

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

SpringBoot实现热部署两种方式!

发表于 2021-11-14

前言

  小宅作为一个Java程序员,在日常的工作中,经常需要修改代码,然后重启服务,在验证代码是否生效。如果是小项目还好,重启速度比较快,等待时间比较短。但是随着项目逐渐变大,并且被拆分成多个服务时,改动一些代码,可能需要重启多个服务才能生效。这样下来就耗费了大量的时间在等待服务重启。

  这样肯定是不行的,极大的影响了我的开发效率,那么是否有方式能够实现,修改完代码之后,能够不重启项目呢?

那肯定是有的,要不然这篇文章咋来的😁。

热部署(Hot Swap)

  从Java1.4起,JVM引入了HotSwap,能够在Debug的时候更新类的字节码。所以使用热部署,可以实现修改代码后,无须重启服务就可以加载修改的代码,但是它只能用来更新方法体。作为神器的IDEA自然是支持这一技术的。

配置IDEA

  点击当前运行的服务,再点击Edit Configurations。

  点击要配置的程序,找到 On ‘Update’ action 和 On frame deactivation选择 Update classes and resources。点击OK就可以实现热部署了。

  经过以上配置,在修改代码以后。只需要点击小锤子或者使用快捷键Command + F9重新编译一下,就可以让改动的代码生效了。并且还会提示有多少个class被重新读取了。

   虽然到这里已经能实现热部署的功能了。但是Java的虚拟机只能实现方法体的修改热部署,对于整个类的结构修改,仍然需要重启虚拟机,对类重新加载才能完成更新操作。

测试

初始状态

方法体修改

类结构变更

  由于热部署只支持修改方法体,所以类结构变更时会报错,并提示是否需要重启。

DevTools

  前面虽然通过配置IDEA实现了简单的热部署,但是有很明显的缺点,只能实现方法体的修改热部署。很明显无法满足日常的需求的,所以这个时候就需要使用DevTools来替代了。

   DevTools是Spring为开发者提供了一个名为spring-boot-devtools的模块,来使Spring Boot应用支持热部署,提高开发者的开发效率,无需手动重启Spring Boot应用。使用起来非常简单,只需要将下面的依赖引入项目里面就可以了。

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>

触发重启

   DevTools严格意义上其实不算热部署,而是快速重启。为什么这样说呢?DevTools的实现原理是:使用两个类加载器,一个是base classloader来加载不会被更改的类(例如,来自第三方的Jar),还有一个是restart classloader用来加载当前正在开发的类。所以当应用程序重新启动时,restart classloader将被丢弃,并创建一个新的类加载器。也就意味着应用程序重新启动通常比“冷启动”快得多,因为base classloader已经填充好了并且是可用的。

  简而言之就是:通过监控类路径资源,当类路径上的文件发生更改时,自动重新启动应用程序,由于只需要重新读取被修改的类,所以要比冷启动快。

  那么问题来了,该如何更新类路径来触发自动重启呢?其实这个取决于你使用的 IDE:

  1. 在 Eclipse中,保存修改后的文件会导致更新类路径并触发重新启动。
  2. 在 IntelliJ IDEA中,需要点击Build按钮Command + F9构建项目来实现。

配置自动重启

  这时候可能有小伙伴想问了,难道IDEA没有类似于Eclipse中保存文件自动触发重启的功能嘛。那肯定是有的,只需要进行下面两步的配置就可以实现了。

注意:需要将前面的设置,全部还原。

  1. 开启Build project automatically 。

  2. 使用快捷键:Ctrl + Alt + Shift + / 调出 Registry 窗口,勾选 compiler.automake.allow.when.app.running 选项。

新版本如下图所示:

总结

 IDEA只能实现方法体的修改热部署,无法满足日常的使用要求,所以更推荐使用DevTools。但是如果你觉得重新启动对你来说还不够快。你可以考虑使用JRebel插件。

结尾

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!

本文转载自: 掘金

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

SpringSecurity学习-表单登录的入门案例

发表于 2021-11-14

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」

作者:汤圆

个人博客:javalover.cc

简介

SpringSecurity的认证机制有多种,比如基于用户名/密码的认证,基于OAuth2.0的认证(OAuth已废弃)。。。

而基于用户名/密码的认证方式,又分多种,比如:

  • Form Login,表单登录认证(单体应用,比如SpringMVC)
  • Basic Authentication,基本的http认证(前后端分离应用)
  • 【已废弃】Digest Authentication,数字认证(已废弃,不再使用这种认证方式,因为它的加密方式不安全,比如md5加密等;现在比较安全的加密方式有BCrypt等)

本节介绍的就是第一种:表单登录的认证方式

目录

  1. maven配置
  2. security配置
  3. controller控制器
  4. web界面
  5. 启动运行

正文

在开始之前,需要先了解两个词

  • Authenticate认证:就是通过用户名/密码等方式,登入到系统,这个过程就是认证;类似于进入景区的大门
  • Authorize授权:就是登入到系统之后,校验用户是否有权限操作某个模块,这个过程就是授权;类似于进入景区后,各个收费区域,只有交了钱(有权限),才能进入指定区域;

项目背景:Spring Boot + SpringMVC + Thymeleaf

项目结构如下:

image-20210603141803128

1.maven配置:

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
xml复制代码<?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">
<parent>
<artifactId>spring-boot-demo</artifactId>
<groupId>com.jalon</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>demo-spring-security-login-form</artifactId>
<properties>
<java.version>1.8</java.version>
<spring.boot.version>2.4.3</spring.boot.version>
<lombok.version>1.18.16</lombok.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

2.security配置

这里面主要包含两部分:

  • authenticate 认证配置:主要配置用户名,密码,角色(这里基于内存来保存,为了简化)
  • authorize 授权配置:主要配置各个角色的权限,即可以访问哪些页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
java复制代码@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

// 认证相关操作
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// 数据没有持久化,只是保存在内存中
auth.inMemoryAuthentication()
.withUser("javalover").password(passwordEncoder().encode("123456")).roles("USER")
.and()
.withUser("admin").password(passwordEncoder().encode("123456")).roles("ADMIN");
}

// 授权相关操作
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// admin页面,只有admin角色可以访问
.antMatchers("/admin").hasRole("ADMIN")
// home 页面,ADMIN 和 USER 都可以访问
.antMatchers("/home").hasAnyRole("USER", "ADMIN")
// login 页面,所有用户都可以访问
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
// 自定义登录表单
.formLogin().loginPage("/login")
// 登录成功跳转的页面,第二个参数true表示每次登录成功都是跳转到home,如果false则表示跳转到登录之前访问的页面
.defaultSuccessUrl("/home", true)
// 失败跳转的页面(比如用户名/密码错误),这里还是跳转到login页面,只是给出错误提示
.failureUrl("/login?error=true")
.and()
.logout().permitAll()
.and()
// 权限不足时跳转的页面,即访问一个页面时没有对应的权限,会跳转到这个页面
.exceptionHandling().accessDeniedPage("/accessDenied");
}

// 定义一个密码加密器,这个BCrypt也是Spring默认的加密器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

}

3. controller控制器

控制器主要任务就是处理请求,下面就是典型的MVC模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
java复制代码@Controller
@Slf4j
public class SecurityController {

@RequestMapping("/login")
public String login(){
log.info("=== login ===");
return "login";
}

@RequestMapping("/home")
public String home(Model model){
model.addAttribute("user", getUsername());
model.addAttribute("role", getAuthority());
return "home";
}

@RequestMapping("/admin")
public String admin(Model model){
model.addAttribute("user", getUsername());
model.addAttribute("role", getAuthority());
return "admin";
}

// 权限不足
@RequestMapping("/accessDenied")
public String accessDenied(Model model){
model.addAttribute("user", getUsername());
model.addAttribute("role", getAuthority());
return "access_denied";
}

// 获取当前登录的用户名
private String getUsername(){
return SecurityContextHolder.getContext().getAuthentication().getName();
}

// 获取当前登录的用户角色:因为有可能一个用户有多个角色,所以需遍历
private String getAuthority(){
Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
ArrayList<String> list = new ArrayList<>();
for(GrantedAuthority authority: authorities){
list.add(authority.getAuthority());
}
log.info("=== authority:" + list);
return list.toString();
}
}

4. web界面

界面有4个:

  • login.html: 登录界面,所有人都可以访问
  • home.html: 主页面,普通用户和管理员可以访问
  • admin.html: 管理员页面,只有管理员可以访问
  • access_denied.html: 访问被拒绝页面,权限不足时会跳转到该页面;比如普通用户访问admin.html时

login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
html复制代码<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Spring Security</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out
</div>
<form action="/login" method="post">
<input name="username" placeholder="用户名">
<input name="password" placeholder="密码">
<button type="submit">登录</button>
</form>
</body>
</html>

home.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
html复制代码<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Spring Security Home</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
</head>
<body>
欢迎<span th:text="${user}"></span>
你的权限是<span th:text="${role}"></span>
<a href="admin">admin页面</a>
<a href="logout">退出</a>
</body>
</html>

admin.html

1
2
3
4
5
6
7
8
9
10
11
12
13
html复制代码<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Spring Security Admin</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
</head>
<body>
欢迎<span th:text="${user}"></span>
你的权限是<span th:text="${role}"></span>
<a href="logout">退出</a>
</body>
</html>

access_denied.html

1
2
3
4
5
6
7
8
9
10
11
12
13
html复制代码<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Access Denied</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
</head>
<body>
<span th:text="${user}"></span>没有权限访问页面
你的权限是<span th:text="${role}"></span>
<a href="logout">退出</a>
</body>
</html>

5. 启动运行

访问 http://localhost:8088,会自动跳转到login界面,如下:

image-20210603143352317

这里先用普通用户的身份来登录,javalover/123456,登陆后进入主页:可以看到,权限是普通用户

image-20210603143453932

这时点击admin页面就会提示权限不足,如下:

image-20210603143531292

此时点击退出,又重新回到登录界面:并附有提示【已退出登录】

image-20210603143631810

最后用管理账户登录,admin/123456,登录进入主页:可以看到,权限是管理员

image-20210603143901708

这时点击admin页面,就会正常显示:

image-20210603144003914

总结

SpringSecurity的表单登录认证,总的来说代码不是很多,因为很多功能SpringSecurity都是自带的(比如登录、登出、权限不足等),我们只需要根据自己的需求来修改一些配置就可以了

源码地址:demo-spring-security-login-form

本文转载自: 掘金

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

Django中的超级用户和自己创建app原来这么简单!

发表于 2021-11-14

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

系列文章第一篇:快速创建一个Django项目,Python环境也给你安排了

创建一个admin账户

首先在vscode中我们启动调试项目,然后打开终端,进入到对应项目目录下,执行命令pipenv shell即可进入虚拟环境,然后再执行下面命令创建一个admin账户,

1
bash复制代码python manage.py createsuperuser


创建成功后,我们可以浏览器访问下方网址,(确保vscode中已经启动项目了)

1
bash复制代码http://127.0.0.1:8000/admin

会自动跳转到admin登录界面,

输入我们刚刚创建的账号和密码即可登录django自带的管理后台,

默认有两个Groups(默认是空的),Users(里面有我们自己创建的admin账号,我们还可以在这里直接创建其他管理员账号)。

settings.py基本介绍


在vscode(当然你也可以用pycharm或者其他代码开发工具)打开settings.py文件你可以看到里面的源码,通过部分英文注释你基本可以了解一些代码的含义,这里我挑选了一些需要修改的、常用的配置,给大家解释一下,我认为你是完全小白,所以还是介绍一下(当然你也可以跳过)。

1
2
3
4
5
6
bash复制代码# 也可以前往官网查看相关介绍(推荐)
For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
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
bash复制代码# True 表示会在终端输出调试信息,在生产环境中需要设置为False
DEBUG = True

# 可以设置允许访问的ip地址,默认是 127.0.0.1
ALLOWED_HOSTS = []

# app配置说明,每个新建的app都要在这里安装
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

# 模版配置,默认是DjangoTemplates,也可以改成大家熟知的Jinja2
# 这里我们需要设置一下DIRS,添加一个根目录下的templates文件
# APP_DIRS设置为True,设置为False后无法正常使用Django admin后台
# 所有的模板我们都放到根目录下的templates文件,统一管理
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

# 数据库配置,默认使用sqlite3
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}

# 项目语言
LANGUAGE_CODE = 'en-us'
# 时间时区
TIME_ZONE = 'UTC'

# 静态资源文件路径
STATIC_URL = '/static/'

通过上面注释解释,首先我们可以修改一下项目语言和时区,改为中文,时区也改成我们这边的~

1
2
3
bash复制代码LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Asia/Shanghai'

修改后保存,项目是以调试模式开启的,所以保存后系统会自动更新,无需我们再次启动项目,我们直接刷新前面访问的管理后台页面,就会发现页面内容变成中文了。

创建blog app

首先在vscode中我们启动调试项目,然后打开终端,进入到对应项目目录下,执行命令pipenv shell即可进入虚拟环境,然后再执行下面命令创建一个admin账户,

1
bash复制代码python manage.py startapp blog

运行后,我们看目录结构,会发现多了一个blog目录,也就是我们刚刚创建的一个app,

这里需要说明下myblog是我们创建项目时的目录,可以理解为程序的入口,包含了配置文件、系统路由和web服务网关接口配置。


关于新创建的app blog目录下的文件简介(简单介绍):

  • migrations 迁移文件,主要存放从models迁移到数据库的数据库操作语句;
  • admin.py 注册模型文件,注册后的模型可以在admin管理后台显示;
  • apps.py 可以在里面额外设置程序配置,并应用配置;
  • models.py 数据模型文件,数据库设计主要在这里,直接创建类对象即可;
  • tests.py 测试文件,主要用于app测试;
  • views.py 视图文件,主要写功能实现函数;
  • urls.py(需要自己创建) 路由文件,注意声明路由关系。

在settings中注册新创建的app

创建好app,首先需要在settings中的INSTALLED_APPS中添加上新创建的app,

1
2
3
4
5
bash复制代码INSTALLED_APPS = [
'django.contrib.admin',
...
'blog', # 博客app
]

创建Article Models

进入到blog/models.py,我们先创建一个Article模型,文章模型主要包括文章作者、文章标题、文章概要和文章正文这四个属性,其他还应该添加属性有:标题图、文章标签、浏览量等,方便入门学习,其余属性后期再添加:

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
python复制代码from django.db import models
from django.contrib.auth.models import User

# Create your models here.
class Article(models.Model):
"""
创建一个基本的文章对象
包括:作者、标题、概要、正文
后期需要添加:标题图、文章标签、浏览量等
"""
# 文章作者,author 通过 models.ForeignKey 外键与系统自带的 User 模型关联在一起
# 参数 on_delete 用于指定数据删除的方式,避免两个关联表的数据不一致。
author = models.ForeignKey(User, on_delete=models.CASCADE)
# 文章标题
title = models.CharField(max_length=255, null=False, blank=False)
# 文章概要
summary = models.CharField(max_length=255, null=False, blank=False)
# 文章正文
content = models.TextField()

# 内部类 class Meta 用于给 model 定义元数据
class Meta:
# ordering 指定模型返回的数据的排列顺序
# '-title' 表明数据列表显示按标题名称降序排列
ordering = ('-title',)
# db_table 歉意映射到数据库后的表名
db_table = 'tb_article'
# django后台管理系统显示名称
verbose_name = '文章管理'
verbose_name_plural = verbose_name

# 函数 __str__ 定义当调用对象的 str() 方法时的返回值内容
# 它最常见的就是在Django管理后台中做为对象的显示值。因此应该总是为 __str__ 返回一个友好易读的字符串
def __str__(self):
# 将文章标题返回,django后台管理系统显示条目名称
return str(self.author) + '-' + self.title

创建好模块后,我们需要将Article模型注册到blog/admin.py中,这样才会在后台管理系统中显示,

1
2
3
4
5
6
python复制代码from django.contrib import admin
from blog.models import Article

# Register your models here.
# 注册模型
admin.site.register(Article)

另外我们还需要在终端执行迁移命令,这样系统就会自动根据Article模型中的属性帮我们在数据库中创建对应的表了。

1
2
3
bash复制代码# 进入虚拟环境后执行以下命令
python manage.py makemigrations
python manage.py migrate


完成上述设置后,我们完成了模型的创建和注册,以及迁移映射到数据库,接下来我们可以启动程序后直接浏览器访问http://127.0.0.1:8000/admin,如果需要登录,就直接用之前创建的admin账号登录即可,登录之后我们可以看到我们新建的文章管理模块。

点击进入文章管理模块,我们可以对文章进行增删查改的操作,默认是空的,下面我已经新增了一篇。

我们可以点击增加 文章管理,就可以新建一篇文章了,我们选择作者(和系统中的user是联系起来的,所以只能直接选择),输入文章标题、文章概要、文章内容,天后点击保存即可,也可以点击保存并增加另一个或者保存并继续编辑。

保存后,会自动跳转到文章管理页面,显示目前数据库中所有的文章,以列表显示。

随便点击一篇,就可以进入到对应文章的编辑界面了,可以对内容进行修改或者删除等。

下期见,我是爱猫爱技术的老表,如果觉得本文对你学习有所帮助,欢迎点赞、评论、关注我!

本文转载自: 掘金

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

1…346347348…956

开发者博客

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