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

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


  • 首页

  • 归档

  • 搜索

如何正确的使用 Spring Boot ?

发表于 2021-11-17

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

通过上一课时的学习,相信你已经掌握了环境搭建与配置,那么接下我们就一起来探讨如何正确的使用 Spring Boot 开发我们应用。

Spring Boot 是什么?能做什么?

Spring Boot 是一个快速开发的脚手架,那么它可以做什么呢?Spring Boot 在传统 Spring 框架的基础上做了创新和优化,将开发人员从以往烦琐的配置工作中解放出来,并提供了大量即插即用的集成化组件,从而解决了各种组件之间复杂的整合过程,大大提高了开发效率,降低了维护成本。用通俗的说法解释一下,就是 Spring Boot 是一个基于 Spring 的用来提升效率的一个框架,它可以让编码更加的简单,让你的配置管理更加的简单,让部署更加的简单。

那么是 Spring Boot 有哪些特性呢?

  • 创建独立的 Spring 应用程序。
  • 嵌入的 Tomcat、Jetty 服务器运行 Spring Boot 程序,无需部署WAR文件。
  • 简化 Maven 配置。
  • 自动配置 Spring。
  • 提供生产就绪型功能,如指标,健康检查和外部配置。
  • 基本可以完全不使用 XML 配置文件,采用注解配置。

下面我们还是快速创建一个 Spring Boot的应用。

编写第一个 Spring Boot 应用

针对一个基于 Spring Boot 开发的 Web 应用程序,其代码组织方式需要遵循一定的项目结构。如果不做特殊说明,我们都将使用 Maven 来管理项目工程中的结构和包依赖。一个典型的 Web 应用程序的项目结构如下图所示:

image.png

在上图中,有几个地方需要特别注意,我也在图中做了专门的标注,分别是包依赖、启动类、控制器类以及配置,让我们讲此部分内容分别做一些展开。

需求

我们编写的第一个 Spring Boot 希望它满足两点需求。

第一,整合 Spring MVC。

第二,有一个叫 test 的路径。这里说明一下,从现在开始,我们的课程会把路径称为端点。这样做主要是为了和官方文档以及市面上的文章接轨。工作中总是避免不了要查阅官方文档或者别人的技术博客。如果别人使用的术语你都看不懂,那就尴尬了。

怎么样创建 Spring Boot应用呢?

Spring Boot 官方提供了一个叫 Spring Initializr 的工具,通过这个工具可以快速的创建 Spring Boot 应用。

new -> Module

image.png

在左边有一个 Spring Initializr 的选项。

image.png

在右边,有很多项目基本信息:

Server URL 使用默认值 start.spring.io。可以通过直接设定,比如写入阿里云地址 start.aliyun.com, 这里需要连接 spring 官方服务器,因为网络原因经常无法访问,所以采用国内阿里云镜像生成工程初始代码。这里我们采用默认的即可。start.spring.io 是什么,后面在介绍。

name 项目名称,这个根据自己的项目的具体情况填写。

Location 项目的路径,也就是本地存储路径。

Type 项目构建类型,一般会使用 Maven 或者 Gradle 构建 Spring Boot。有同学可能不了解 Maven 或者是 Gradle,这是 Java 圈子里面最常用的构建工具。可以用来管理 jar 版本,下载 jar 包,编译以及打包等等。不了解 Maven 或者是 Gradle ,学习本门课程并没有太大的障碍,这点大家可以不用担心,本课程是用的是 Maven。

Language 开发语言。用什么语言去开发 Spring Boot,Spring Boot 支持用多种语言开发,你可以任选一种语言开发,这里采用 Java 语言开发。

Group 表示组织名称,一般是团队的唯一标尺。比如你公司域名是 itbbfx.com,那么组织名称就是 com.itbbfx。

Artifact 模块名称,也是项目的 Artifact 的 Id,一般是项目的唯一标识。公司下面肯定会开发好多项目,那么就需要根据 Artifact 来区别,这是约定俗成的规范,防止包名冲突。

Package name 项目的包 。

Project SDK JDK 版本。

Java 是 Java 版本,使用什么 Java 大版本开发 Spring Boot 应用程序。

Packaging 最终生成包的类型,就是你这个项目的打包方式,Spring Boot 的支持打成 jar 包或者是 war 包。使用 jar 打包的方式是 Spring Boot 更加推介的一种打包方式。

各项基本信息输入完成后即可点击“Next”进入下一步。

可以选择spring boot 的版本,需要注意的是带有后缀这种版本都不是正式的,我们选择当前最新的正式版 2.5.0。由于我们的应用需要整合 Spring MVC,需要选择 Spring Web。

image.png

各项基本信息输入完成后即可点击“finish”创建项目。这样子,我们的 Spring Boot的应用就已经建好了,是不是非常的傻瓜化。

start.spring.io 是干什么的呢?我们来揭晓谜底。其实是一个在线版的 Spring Initializr,这使用方式和刚刚是类似的。完成项目基本信息。

image.png

最后点击 GENEGENERATE CTRL + 一下 ,然后就可以下载一个压缩包。你把这个压缩包解压,然后导入到 IDEA 中, 就可以开发 Spring Boot 应用了。

这个在线版的 Spring Initializr 有什么用呢?比方所使用的 ide 没有整个 Spring Initializr,但是你又希望用 Spring Initializr 快速的创建 Spring Boot 应用,这个时候就非常的实用了。

Maven 依赖

项目工程创建成功,打开 pom.xml 文件,确认 Maven 依赖 spring-boot-starter-web 这个应用比利整合了 Spring MVC 。同时引入了测试相关依赖以及 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
xml复制代码<dependencies>
<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>
</dependencies>


<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.5.0</version>
</plugin>
</plugins>
</build>

Spring Boot 提供了一系列 starter 工程来简化各种组件之间的依赖关系。以开发 Web 服务为例,我们需要引入 spring-boot-starter-web 这个工程,而这个工程中并没有具体的代码,只是包含了一些 pom 依赖。

image.png

可以看到,这里包括了传统 Spring MVC 应用程序中会使用到的 spring-web 和 spring-webmvc 组件,因此 Spring Boot 在底层实现上还是基于这两个组件完成对 Web 请求响应流程的构建。如果我们使用 Spring Boot 2.5.0 版本,你会发现它所依赖的 Spring 组件都升级到了 5.X 版本。

值得一提的是,Spring Boot 应用一旦引入 Spring-Web 依赖。因为 Spring Boot 一般是通过 RESTful API 对外暴露接口,增加 Spring-Web 会在应用中内嵌 Tomcat,使 Spring Boot 应用具备 HTTP 响应能力。

一旦 spring-boot-starter-web 组件引入完毕,我们就可以充分利用 Spring Boot 提供的自动配置机制开发 Web 应用程序。

启动类

使用 Spring Boot 的最重要的一个步骤是创建一个 Bootstrap 启动类。Bootstrap 类结构简单且比较固化,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript复制代码import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


@SpringBootApplication
public class SpringBootDemoApplication {


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


}

显然,这里引入了一个全新的注解 @SpringBootApplication。在 Spring Boot 中,添加了该注解的类就是整个应用程序的入口,一方面会启动整个 Spring 容器,另一方面也会自动扫描代码包结构下的 @Component、@Service、@Repository、@Controller 等注解并把这些注解对应的类转化为 Bean 对象全部加载到 Spring 容器中。

编写测试

我们来写 test 的端点。Bootstrap 类为我们提供了 Spring Boot 应用程序的入口,相当于应用程序已经有了最基本的骨架。接下来我们就可以添加 HTTP 请求的访问入口,表现在 Spring Boot 中也就是一系列的 Controller 类。这里的 Controller 与 Spring MVC 中的 Controller 在概念上是一致的,一个典型的 Controller 类如下所示:

1
2
3
4
5
6
7
8
9
kotlin复制代码@RestController
public class TestController {


@GetMapping("/test")
public String test(){
return "test";
}
}

请注意,以上代码中包含了 @RestController 和 @GetMapping 这两个注解。其中,@GetMapping 的作用等同于指定了 GET 请求的 @RequestMapping 注解,而 @RestController 注解是传统 Spring MVC 中所提供的 @Controller 注解的升级版,相当于就是 @Controller 和 @ResponseEntity 注解的结合体,会自动使用 JSON 实现序列化/反序列化操作。

配置文件

我们注意到,在 src/main/resources 目录下存在一个 application.properties 文件,这就是 Spring Boot 中的主配置文件。例如,我们可以将如下所示的端口、服务名称以及数据库访问等配置信息添加到这个配置文件中:

1
ini复制代码server.port=8080

事实上,Spring Boot 提供了强大的自动配置机制,如果没有特殊的配置需求,开发人员完全可以基于 Spring Boot 内置的配置体系完成诸如数据库访问相关配置信息的自动集成。

建议良好的习惯,在启动应用之前, 执行下mvn clean install,确保能够构建成功再去启动。这主要是为了防止有 jar 包没有下载完整,导致启动失败,或者说应用各种报错,确保构建成功再去启动,一定要养成这样的习惯。

static

这个目录是用来做什么的呢?他是用来存放静态文件的。什么是静态文件啊,就比 js、css 、和HTML 以及图片等等,这就叫静态文件。

templates

这是用来存放模板文件的。什么是模板文件的?大家知道 Spring MVC 是支持多种视图模式的,比如我们可以用 jsp、freemarker 等等。那么 jsp、 freemarker 就是模板引擎。

那么在 Spring Boot 里面支持哪些模板引擎呢?我们可以看官方文档,它主要支持 Thymeleaf、FreeMarker 以及 Groovy 模板引擎。现在越来越多的应用使用前后端分离的方式开发,模板引擎用的越来越少了。并且我们的课程中也不会使用到这里面的任何一款模板引擎,所以具体我们就不介绍。

怎么样启动应用呢?

找到 SpringBootDemoApplication 类。这类上有个 main 方法启动,点击运行即可。

image.png

访问项目

我们的 Spring Boot 应用启动成功之后,通过打开浏览器,在地址栏输入 http://localhost:8080/test 就可以访问的应用了。

image.png

到目前为止,是不是说明我们两个需求都已经达到了。

这是在 idea 上面的启动。那我们实际项目中怎么样部署呢?我们可以看到这个 target 目录里面有个 jar,这个 jar 包是一个可以执行的,jar 可以直接用来部署。

image.png

怎么样启动这个 jar 包呢?执行命令是:

1
复制代码java -jar xxx.jar

通过测试,我们 Spring Boot 依然可以运行,可以访问。

Spring Boot VS 传统开发

我们可以对比一下 Spring Boot 的开发应用和以前的传统的 Web 开发应用的方式,它是不一样的。

我们以前开发应用是不是有一大堆的配置文件,比如说什么 web.xml、application.xml、spring-web.xml 等等,使用 Spring Boot 开发应用,这些配置就不需要了。

外我们以前部署应用的时候,是不是把应用打成 war 包,然后i放到 Tomcat 里面去,或者说其他的 web 服务器。然后再去把 Tomcat 或者说其他的服务器启动起来。那现在呢,现在直接一个 jar 包就直接启动我们的应用,是不是非常的方便啊?

总结

案例编写是掌握一个框架应用方式的最好方法。本课是是一款以案例驱动的 Spring Boot 应用程序开发课程,我们主要针对一个典型的 Spring Boot Web 应用程序的组织结构和开发方式进行了详细介绍。

本文转载自: 掘金

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

01 被k8s弃用的docker还值得学吗?

发表于 2021-11-17

首先抛出答案:Docker依然值得学习。

我们先来聊聊Docker当前被用到哪些地方:

  • 作为k8s的容器运行时
  • 作为开发者日常开发、运行程序、打包镜像使用

即然k8s在新版本中已经准备弃用Docker, 那么在容器运行时这一应用场景下,已经可以放弃Docker了。但是Docker依然还是一款优秀的开发者工具,开发人员和运维人员可以方便的使用Dockerfile打包应用程序镜像,并且Docker镜像依然可以正常运行于k8s集群之上。

另外基本上所有的容器技术实现也是大同小异的,可以说没有用到什么新技术,无非是利用了存在已久的 Namespace和Cgroup技术来做资源的隔离和限制,掌握Docker后,如果以后出现更流行的容器技术,拿过来使用也不会有太大壁垒,基本可以平滑过度。

为什么k8s会弃用Docker作为其容器运行时? Docker在设计之初,并不是为了运行在k8s上的,它是一个功能完备的开发者工具,实际上k8s运行时依赖的是Docker中的containerd组件,即然如此把containerd单独拿出来就可以了,而不需要Docker额外的组件,虽然containerd被集成在Docker中,但是k8无法直接调用Docker中的containerd,而是需要通过一个叫Dockershim的组件,这个组件也是需要额外的开发维护成本的,我们知道在传统运维中,服务器上装的东西越少,那么出问题的概率越小,系统越稳定易于维护,如此看来k8s弃用Docker其实是使整个系统更为精简了,是好事。

为什么用Docker打包的镜像依然可以在k8s上使用? 我们在上面说到Docker的核心利用了存在已久的Namespace和Cgroup技术,这并不是Docker的创新,但镜像绝对是Docker的一项重要创新, Docker镜像解决了应用程序的分发问题,并制定了统一的镜像标准:opencontainers.org/

所以依据此标准制作的镜像,都可以在k8s上使用。

从Docker被k8s弃用,可以得到哪些启发? 没有持久的技术,只有持久的创新,微创新。为什么说是微创新呢?因为从冯诺依曼提出计算机基本结构以后,计算机史上还有比这更大的创新与进步吗?新技术名词层出不穷,技术人员大喊学不动,但这些所谓的新技术,无非都基于那些最基础的计算机知识,所以与其被各种新技术搞的晕头转向,不如夯实基础,练好内功,看透它们的本质。

你同意我的说法吗?你有补充或疑问吗?欢迎和我交流。

本文转载自: 掘金

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

CentOS 8 安装并加载 Nginx 模组 ngx_br

发表于 2021-11-17

项目地址:Github 仓库地址

ngx_brotli

Brotli是一种通用无损压缩算法,它结合了LZ77算法的现代变体、Huffman编码和二阶上下文建模来压缩数据,其压缩比可与目前可用的最佳通用压缩方法相媲美。它在速度上与 deflate 相似,但提供了更高的压缩率。

注意:该压缩编码仅可用于 https 通信。

ngx_brotli 包含两个模块:

  • ngx_brotli filter module - 用于动态压缩响应体。
  • ngx_brotli static module - 用于使用预压缩的 .br 文件进行服务(静态站点)。

安装方法

获取 ngx_brotli

  1. 确保你安装了 git,如果没有,使用命令安装:
1
bash复制代码$ yum -y install git
  1. 从仓库获取 ngx_brotli
1
bash复制代码$ git clone https://github.com/google/ngx_brotli.git

如果服务器位于国内,建议使用镜像:

1
bash复制代码$ git clone https://gitee.com/xlsw/ngx_brotli.git
  1. 初始化子模块
1
2
3
bash复制代码$ cd ngx_brotli
$ git submodule update --init
$ cd ~

获取 Nginx 源码

  1. 如果你已经安装了 Nginx,请查看版本:
1
bash复制代码$ nginx -v

本示例已安装 nginx 版本为:

nginx version: nginx/1.20.1

  1. 获取并解压 Nginx 源码(将字符x替换为你的 Nginx 版本号,如未安装可自定义)
1
bash复制代码$ wget https://nginx.org/download/nginx-1.x.x.tar.gz && tar zxvf nginx-1.x.x.tar.gz

本示例版本为 1.20.1,则命令为:

1
bash复制代码$ wget https://nginx.org/download/nginx-1.20.1.tar.gz && tar zxvf nginx-1.20.1.tar.gz
  1. 清除垃圾文件
1
bash复制代码$ rm nginx-1.20.1.tar.gz

安装

以下两种方式选择一种即可,已通过 yum 源安装 Nginx 建议选择第一种 动态模块加载

动态模块加载

  1. 安装编译所需环境
1
bash复制代码$ yum -y install pcre pcre-devel zlib zlib-devel openssl openssl-devel
  1. 进入 Nginx 源码目录并编译子模块

官方文档说明为:

1
2
3
bash复制代码$ cd nginx-1.x.x
$ ./configure --with-compat --add-dynamic-module=/路径/ngx_brotli
$ make modules

本示例则使用(ngx_brotli已克隆到本地~目录下):

1
2
3
bash复制代码$ cd ~/nginx-1.20.1
$ ./configure --with-compat --add-dynamic-module=../ngx_brotli
$ make modules
  1. 将生成的 .so 动态库移动到 Nginx 的 modules 目录(默认为/etc/nginx/modules)
1
bash复制代码$ cp objs/*.so /etc/nginx/modules
  1. 为 .so 动态库文件添加权限
1
bash复制代码$ chmod 644 /etc/nginx/modules/*.so
  1. 向 nginx.conf 中添加配置(添加至 http 块之上)
1
2
bash复制代码load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;

模块静态编译并安装

将第一行中的字符x替换为你上文中获取的 Nginx 源码版本号

1
2
3
bash复制代码$ cd nginx-1.x.x
$ ./configure --add-module=/path/to/ngx_brotli
$ make && make install

该操作将会直接将ngx_brotli编译并直接安装到 Nginx 中

配置项

brotli_static
  • 语法:brotli_static on|off|always
  • 默认值:off
  • 上下文:http, server, location

启用或禁用自动检查是否有.br预压缩文件是否存在,然后再使用;如果值为always则不会检查而直接使用预压缩文件。

brotli
  • 语法:brotli on|off
  • 默认值:off
  • 上下文:http, server, location, if

启用或禁用动态压缩响应体。

brotli_types
  • 语法:brotli_types <mime 类型> [..]
  • 默认值:text/html
  • 上下文:http, server, location

除了text/html外,还支持对特定的MIME类型进行动态压缩。特殊值*会匹配所有MIME类型,Content-Type为text/html的响应体将总是被压缩。

brotli_buffers
  • 语法:brotli_buffers <number> <size>
  • 默认值:32 4k|16 8k
  • 上下文:http, server, location

已被弃用,忽略该条配置项。

brotli_comp_level
  • 语法:brotli_comp_level <级别>
  • 默认值:6
  • 上下文:http, server, location

设置 Brotli 动态压缩质量级别,取值范围从0至11。取值越大但性能损耗越高,压缩比越高,建议默认。

brotli_window
  • 语法:brotli_window <大小>
  • 默认值:512k
  • 上下文:http, server, location

设置 Brotli 窗口大小,取值为1k, 2k, 4k, 8k, 16k, 32k, 64k, 128k, 256k, 512k, 1m, 2m, 4m, 8m和16m。Gzip 压缩使用固定大小的32k窗口,这意味着 Brotli 窗口最多可以比 deflate 窗口大 512 倍。这种差异在 Web 服务器上下文中几乎无关紧要,因为大于 32KB 的文本文件是少数。

brotli_min_length
  • 语法:brotli_min_length <长度>
  • 默认值:20
  • 上下文:http, server, location

设置启用 Brotli 压缩的响应体最小长度,该值只会根据响应头中的Content-Length字段进行判断。

变量

$brotli_ratio:实现压缩比,原始响应大小和压缩响应大小之间的比率计算结果。

示例配置

1
2
3
4
5
6
7
8
bash复制代码brotli on;
brotli_comp_level 6;
brotli_static on;
brotli_types application/atom+xml application/javascript application/json application/rss+xml
application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype
application/x-font-ttf application/x-javascript application/xhtml+xml application/xml
font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon
image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml;

本文转载自: 掘金

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

【Go夯实基础】syncMap的缺陷与解决方法

发表于 2021-11-17

1、存在的问题

  • 为了考虑通用性,所有的key和value都是interface{},换言之,我们失去了类型检查提供的安全性而且被迫更多的使用类型断言,后果:
    • 在每次调用API后都小心翼翼地使用类型断言,你的代码里面出现无数的if v,ok=value.(xxType);!ok{}

2、解决方案

2.1 方案一

  • 如果有明确类型,一个简单的思路是封装snyc.Map,外部函数限制传给sync.Map的API的参数类型,但是灵活性较差
1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码type StringMap struct{
m sync.Map
}
func (s *StringMap) Store(key,value string){
s.m.Store(key,value)
}
func (s *StringMap) Load(key string)(value string,ok bool){
v, ok := s.m.Load(key)
if v != nil {
value = v.(string)
}
return
}

2.2 方案二

  • 用反射来帮助我们做类型检查(下面的代码来自于极客时间《go核心36讲》附属源代码)
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
go复制代码type ConcurrentMap struct {
m sync.Map
keyType reflect.Type
valueType reflect.Type
}

//创建ConcurrentMap
func NewConcurrentMap(keyType, valueType reflect.Type) (*ConcurrentMap, error) {
if keyType == nil {
return nil, errors.New("nil key type")
}
if !keyType.Comparable() {
return nil, fmt.Errorf("incomparable key type: %s", keyType)
}
if valueType == nil {
return nil, errors.New("nil value type")
}
cMap := &ConcurrentMap{
keyType: keyType,
valueType: valueType,
}
return cMap, nil
}

func (cMap *ConcurrentMap) Delete(key interface{}) {
//传入的参数类型必须要和 初始化的ConcurrentMap的key类型一致
if reflect.TypeOf(key) != cMap.keyType {
return
}
cMap.m.Delete(key)
}

本文转载自: 掘金

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

使用 Reflections 扫描被注解标记的类

发表于 2021-11-17

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

这篇文章具体实现功能是:自定义一个注解,装配类会扫描所给包路径所有被注解类进行配置。

想直接看实现方式可以跳到目录里的 Google救我

起因

最近在捣鼓 Netty,通过 Netty 实现一个简单的 IM Demo,其中使用到了自定义的协议,格式如下:

其中指令类型指明了内容的解析类型。

例如

1
2
3
4
5
6
Java复制代码 public final class Command {
  //登录请求指令
     public static final byte LOGIN_REQUEST = 1;
     
     //...其余指令
 }

然后需要创建对应的协议内容对象以便进行解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java复制代码 @Data   //lombok
 public class LoginRequestPacket extends Packet {
 ​
     private String userId;
 ​
     private String username;
 ​
     private String password;
 ​
     @Override
     public Byte getCommand() {
         return LOGIN_REQUEST;
    }
 }

最后需要在解码器中将指令和对应 Pakcet 关联起来

1
2
3
4
5
6
7
8
9
10
11
12
13
Java复制代码 public PacketCodec() {
     packetMap = new HashMap<>();
     packetMap.put(Command.LOGIN_REQUEST, LoginRequestPacket.class);
     packetMap.put(Command.LOGIN_RESPONSE, LoginResponsePacket.class);
     packetMap.put(Command.MESSAGE_REQUEST, MessageRequestPacket.class);
     packetMap.put(Command.ENTER_GROUP, EnterGroupRequestPacket.class);
     packetMap.put(Command.RESPONSE, ResponsePacket.class);
     packetMap.put(Command.GROUP_MESSAGE, GroupMessageRequestPacket.class);
     packetMap.put(Command.LOGOUT_REQUEST, LogoutRequestPacket.class);
     packetMap.put(Command.LOGOUT_RESPONSE, LogoutResponsePacket.class);
     packetMap.put(Command.EXIT_GROUP, ExitGroupRequestPacket.class);
  // 就这一堆,我去好家伙,那是真的麻烦,前面弄着忘记配了,测试还会出问题导致通信一直没反应
 }

思路

为了能让自己不用每次添加命令都跑到解码器进行添加,我想能不能直接在创建对应 Packet 的时候他能自己添加到解析器呢?很容易就联想到 Mybatis 中的 @MapperScan自动扫描 Mapper 的功能,使用 Mybatis 创建接口时,需要在接口处添加@Mapper如下:

1
2
3
4
Java复制代码 @Mapper
 public interface TableMapper{
  //接口
 }

于是乎,就按照这个思路来了。其中关键点在于:

  1. @MapperScan配置了需要扫描的包路径,即其注解中的basePackage元素
  2. 需要通过@Mapper标记需要装配的类

因为自己的 IM 中主要是PacketCodec需要进行扫描装配,那么我就在其构造方法中加入扫描配置的逻辑就好

剩下的就是创建一个被标记类,并且传入 Command 的值与该 Packet 进行绑定即可

初遇难处

按照思路,其实最简单的是先去看 Mybatis 中包扫描的源码。先查看@MapperScan,其中有一行代码如下,MapperScannerRegistrar会提前加载。

1
2
Java复制代码 // @Import 是 Spring 框架中的注解,用于说明注解元素属性中的类需要提前与被注解类加载
 @Import({MapperScannerRegistrar.class})

顺着看 MapperScannerRegistrar,我去?!都是些操作与 Spring 相关的方法,涉及到 BeanDefinition 、BeanDefinitionRegistry…

卧槽,完了,我这 Netty 小 Demo 一开始哪想得到会用这些东西,别说 BeanDefinition 了,连个容器都没…到哪注册去….难不成给这个小 Demo 换个血型?

我再想想有没有其他思路好了…其实还有一个思路,便是用ClassLoader然后传入 Packet 所在的路径一个一个进行解析装配,不过想到麻烦的路径操作,以及后面的维护难度,还是不要堆屎山了。

Google救我

在寻找其他方式的时候,我发现了个好东西 —— Reflections。芜湖,直接起飞~简单方便,一用就会

Reflections 是一个反射框架,能够扫描 classpath,获取元数据信息

参考链接:

www.jianshu.com/p/49199793a…

blog.csdn.net/java_faep/a…

着手修改

创建 Packet 注解

1
2
3
4
5
6
Java复制代码 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.TYPE})
 public @interface Packet {
 ​
     byte value() default -1; // 接受传入的 Command 指令值,用于与被注解类绑定
 }

标记类

1
2
3
4
5
6
Java复制代码 @Data
 @codec.Packet(LOGIN_REQUEST) //自定义的注解,LOGIN_REQUEST 是其指令值
 public class LoginRequestPacket extends Packet {
 ​
     //...
 }

修改 PacketCodec 构造方法,也就是需要进行扫描装配的类

1
2
3
4
5
6
7
8
9
10
11
12
Java复制代码 public PacketCodec() {
     packetMap = new HashMap<>();
  // 配置需要扫描的包路径,我的是 PacketCodec 类所在包的 protocol 包下
     Reflections reflections = new Reflections(this.getClass().getPackage().getName() + ".protocol");
     // getTypesAnnotatedWith(注解类),即可获得被对应注解类标记的类
     Set<Class<?>> packets = reflections.getTypesAnnotatedWith(codec.Packet.class);
     // 对被注解类的处理
     for (Class<?> pClazz : packets) {
         // 绑定指令值和被注解类,取代了之前的 packetMap.put(Command.LOGIN_REQUEST, LoginRequestPacket.class);
         packetMap.put(pClazz.getAnnotation(codec.Packet.class).value(), (Class<? extends Packet>) pClazz);
    }
 }

修改后程序能够正常运行,扫描标记类进行配置大功告成。虽然还是需要在 Packet 上进行注解,但是不用回到解码器当中进行配置,也算是轻松了不少。

本文转载自: 掘金

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

C# 使用数据库对ListView控件数据绑定实现(查询搜索

发表于 2021-11-17

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

前言

之前的文章博主写了一篇关于ListView控件的简单处理,数据处理局限性很大,对于这个控件我们大都数都是连接数据库进行使用,我们的数据大多数都是动态的,所以我们如果只是简单的添加数据是有局限性的,所以今天这篇文章博主使用数据库对ListView这个控件进行数据绑定,算是对我们上一篇文章的扩展吧!!!嘿嘿,不会的童鞋可以评论或者私信问博主哦❤

每日一遍,防止颓废

image-20211116194511132

(我们要为自己坚持着,哪怕困难再大再多)

1.将我们上一篇文章的项目打开,把静态的数据删除。我们采用数据库处理不会的同学需要看上一篇文章

image-20211116204704882

2.使用数据库对ListView数据展示操作

2.1 双击窗体界面自动生成函数

image-20211116204827282

2.2. 创建我们需要的数据库并写入值

博主使用的是SQL server 没有这个数据库的同学可以用MySQL博主之前的文章有写

image-20211117161250009

2.3. 使用代码对ListView控件与数据库建立连接并绑定数据

image-20211117160711255

2.4. 使用数据库对LIstVIew控件进行数据绑定查询效果展示

image-20211117161647765

3.使用对话框对数据库创建搜索查询

image-20211117162155951

4.加一个窗体实现新建文件操作

4.1 使用conTextMenuStrip控件实现右击新建

image-20211117163004178

4.2 实现窗体和窗体之间的跳转

新建一个窗体文件,并拖好样式,在需要跳转的函数里面实例化一个窗体2,使用Show展示。

image-20211117163205800

4.3 对”新建文件“窗体代码处理

image-20211117163532209

5.整体代码展示,效果图

我的作品4 00_00_00-00_00_30

5.1窗体1核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
ini复制代码     private void Form1_Load(object sender, EventArgs e)
{
string connString = "server=.;database=mychat; User ID = sa; Pwd=123456";//写好数据库打开语句,博主使用的是SQL server数据库,如果你是MySQL 可以看博主之前的文章里面有讲解。
SqlConnection conn = new SqlConnection(connString);//实列化
conn.Open();//打开数据库
string sql = "select name,type,size,date,Imageid from listview";//查询语句
SqlCommand cmd = new SqlCommand(sql, conn);//建立连接
SqlDataReader reader = cmd.ExecuteReader();
listView1.Items.Clear();//清空listView
while (reader.Read())//查询结果遍历,并进行数据绑定
{
ListViewItem item = new ListViewItem(reader[0].ToString().Trim());//Trim()清空空格
item.ImageIndex = Convert.ToInt32(reader[4]);//绑定图标id
item.SubItems.Add(reader[1].ToString().Trim());//数据绑定
item.SubItems.Add(reader[2].ToString().Trim());
item.SubItems.Add(reader[3].ToString().Trim());
this.listView1.Items.Add(item);
}
reader.Close();
conn.Close();
}
private void button4_Click(object sender, EventArgs e)
{
string connString = "server=.;database=mychat; User ID = sa; Pwd=123456";
SqlConnection conn = new SqlConnection(connString);
conn.Open();
string sql =string.Format("select name,type,size,date,Imageid from listview where name like '%{0}%';", textBox1.Text);//实现模糊查询
SqlCommand cmd = new SqlCommand(sql, conn);
SqlDataReader reader = cmd.ExecuteReader();
listView1.Items.Clear();
while (reader.Read())//添加数据和上面代码一致
{
ListViewItem item = new ListViewItem(reader[0].ToString().Trim());
item.ImageIndex =Convert.ToInt32(reader[4]);
item.SubItems.Add(reader[1].ToString().Trim());
item.SubItems.Add(reader[2].ToString().Trim());
item.SubItems.Add(reader[3].ToString().Trim());
this.listView1.Items.Add(item);
}
reader.Close();
conn.Close();
}

private void 新建文件ToolStripMenuItem_Click(object sender, EventArgs e)
{
Form2 form2 = new Form2();
form2.Show();
}

private void listView1_SelectedIndexChanged(object sender, EventArgs e)
{

}

5.2 窗体二核心代码:

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
scss复制代码 private void button2_Click(object sender, EventArgs e)//重置按钮
{
textBox1.Clear();//对textbox清空
textBox2.Clear();
textBox3.Clear();
textBox4.Clear();
textBox5.Clear();
}
private void button1_Click(object sender, EventArgs e)//添加按钮
{
string connString = "server=.;database=mychat; User ID = sa; Pwd=123456";
SqlConnection conn = new SqlConnection(connString);
conn.Open();
string sql = string.Format("INSERT INTO listview(name, type, size, date, Imageid) VALUES('{0}','{1}','{2}','{3}',{4});",textBox1.Text, textBox2.Text, textBox3.Text+"MB", textBox4.Text,Convert.ToInt32(textBox5.Text));//添加数据语句
SqlCommand cmd = new SqlCommand(sql, conn);
int x =cmd.ExecuteNonQuery();//添加操作,成功了返回你添加数据的条数,否则为0
if (x > 0)
{
MessageBox.Show("添加成功");
this.Close();//添加成功,关闭这个窗体
}
else
MessageBox.Show("添加失败");
}
}

总结

博主大致讲解了这个控件的操作,删除和添加差不多,我们可以在后面添加一个删除按钮,通过name进行删除,增加一个函数就可以了,博主觉得很简单就没有做更多的处理了,对于一个初学者,这篇文章还是能看懂的连接数据库可能对于初学者不友好,但是只要你认真学,跟着博主的逻辑走,还是可以理解的,博主这篇文章是基于上一篇文章基础之上的扩展篇。好了,创作不易,点赞关注评论收藏!!!!

12354689

本文转载自: 掘金

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

闲不下来-nginx 链表结构(七) 闲不下来-nginx

发表于 2021-11-17

闲不下来-nginx 链表结构

ngx_list_t 是 Nginx 封装的链表容器,链表容器内存分配是基于内存池进行的,操作方便,效率高。Nginx 链表容器和普通链表类似,均有链表表头和链表节点,通过节点指针组成链表。其在文件core/ngx_list.h中结构定义如下:

链表结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码typedef struct ngx_list_part_s  ngx_list_part_t;

struct ngx_list_part_s {
void *elts; // 指向该节点数据区的首地址
ngx_uint_t nelts; // 该节点数据区实际存放的元素个数
ngx_list_part_t *next; // 指向链表的下一个节点
};


typedef struct {
ngx_list_part_t *last; // 指向链表中最后一个节点
ngx_list_part_t part; // 链表中表头包含的第一个节点
size_t size; // 元素的字节大小
ngx_uint_t nalloc; // 链表中每个节点所能容纳元素的个数
ngx_pool_t *pool; // 该链表节点空间的内存池对象
} ngx_list_t;

nginx-链表结构-ZYedu2

所以,了解 nginx 再次封装的基本结构,可以画个示意图,这样会方便理解。

链表方法

Nginx 链表的操作只有两个:创建链表 和 添加元素。由于链表的内存分配是基于内存池,所有内存的销毁由内存池进行,即链表没有销毁操作。

创建链表

关于源文件可去core/ngx_list.c中,本文源文件的版本是 0.5

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
c复制代码ngx_list_t *
ngx_list_create(ngx_pool_t *pool, ngx_uint_t n, size_t size)
{
ngx_list_t *list;

// 分配链表表头的内存
list = ngx_palloc(pool, sizeof(ngx_list_t));
if (list == NULL) {
return NULL;
}

// 分配节点数据区内存,并返回该节点数据区的首地址
list->part.elts = ngx_palloc(pool, n * size);
if (list->part.elts == NULL) {
return NULL;
}

// 初始化节点属性
list->part.nelts = 0;
list->part.next = NULL;
list->last = &list->part;
list->size = size;
list->nalloc = n;
list->pool = pool;

return list;
}

添加元素

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
c复制代码void *
ngx_list_push(ngx_list_t *l)
{
void *elt;
ngx_list_part_t *last;

// last节点指针指向链表最后一个节点
last = l->last;

// 若最后一个节点的数据区已满
if (last->nelts == l->nalloc) {

/* the last part is full, allocate a new list part */

// 则分配一个新的节点
last = ngx_palloc(l->pool, sizeof(ngx_list_part_t));
if (last == NULL) {
return NULL;
}

// 分配节点数据区内存,并返回该节点数据区的首地址
last->elts = ngx_palloc(l->pool, l->nalloc * l->size);
if (last->elts == NULL) {
return NULL;
}

// 初始化新节点结构
last->nelts = 0;
last->next = NULL;

// 把新节点连接到现有链表中
l->last->next = last;
l->last = last;
}

// 计算存储新元素的位置
elt = (char *) last->elts + l->size * last->nelts;

// 实际存放元素加1
last->nelts++;

// 返回新元素所在位置
return elt;
}

添加元素到链表时,都是从最后一个节点开始,其过程:

  1. 首先判断最后一个节点的数据区是否由内存存放新增加的元素;
  2. 若足以存储该新元素,则返回存储新元素内存的位置;
  3. 若没有足够的内存存储新增加的元素,则分配一个新的节点,再把该新的节点连接到现有链表中,并返回存储新元素内存的位置。

此处链表与数组的区别:

  1. 数组数据区满时要扩充数据区空间;而链表每次要分配节点及其数据区。
  2. 添加的元素可以是整数,也可以是一个结构。

测试程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
c复制代码#include "ngx_config.h"
#include <stdio.h>
#include "ngx_conf_file.h"
#include "nginx.h"
#include "ngx_core.h"
#include "ngx_string.h"
#include "ngx_palloc.h"
#include "ngx_list.h"

volatile ngx_cycle_t *ngx_cycle;

void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err,
const char *fmt, ...)
{
}
void dump_list_part(ngx_list_t* list, ngx_list_part_t* part)
{
int *ptr = (int*)(part->elts);
int loop = 0;

printf(" .part = 0x%x\n", &(list->part));
printf(" .elts = 0x%x ", part->elts);
printf("(");
for (; loop < list->nalloc - 1; loop++)
{
printf("%d, ", ptr[loop]);
}
printf("%d)\n", ptr[loop]);
printf(" .nelts = %d\n", part->nelts);
printf(" .next = 0x%x", part->next);
if (part->next)
printf(" -->\n");
printf(" \n");
}
void dump_list(ngx_list_t* list)
{
if (list)
{
printf("list = 0x%x\n", list);
printf(" .last = 0x%x\n", list->last);
printf(" .part = %d\n", &(list->part));
printf(" .size = %d\n", list->size);
printf(" .nalloc = %d\n", list->nalloc);
printf(" .pool = 0x%x\n", list->pool);

printf("elements: \n");
ngx_list_part_t *part = &(list->part);
while(part)
{
dump_list_part(list, part);
part = part->next;
}
printf("\n");
}
}

int main()
{
ngx_pool_t *pool;
int i;

printf("--------------------------------\n");
printf("create a new pool:\n");
printf("--------------------------------\n");
pool = ngx_create_pool(1024, NULL);

printf("--------------------------------\n");
printf("alloc an list from the pool:\n");
printf("--------------------------------\n");
ngx_list_t *list = ngx_list_create(pool, 5, sizeof(int));

if(NULL == list)
{
return -1;
}
for (i = 0; i < 5; i++)
{
int *ptr = ngx_list_push(list);
*ptr = 2*i;
}

dump_list(list);

ngx_destroy_pool(pool);
return 0;
}

编写本例子的 Makefile:

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
bash复制代码CXX = gcc
CXXFLAGS +=-g -Wall -Wextra

NGX_ROOT =/Users/mf/Documents/mysource/nginx-0.5

TARGETS =ngx_list_t_test
TARGETS_C_FILE= $(TARGETS).c

CLEANUP = rm-f $(TARGETS) *.o

all:$(TARGETS)

clean:
$(CLEANUP)

CORE_INCS =-I. \
-I$(NGX_ROOT)/src/core \
-I$(NGX_ROOT)/src/event \
-I$(NGX_ROOT)/src/event/modules \
-I$(NGX_ROOT)/src/os/unix \
-I$(NGX_ROOT)/objs \

NGX_PALLOC =$(NGX_ROOT)/objs/src/core/ngx_palloc.o
NGX_STRING =$(NGX_ROOT)/objs/src/core/ngx_string.o
NGX_ALLOC =$(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o
NGX_LIST =$(NGX_ROOT)/objs/src/core/ngx_list.o

$(TARGETS):$(TARGETS_C_FILE)
$(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING) $(NGX_ALLOC) $(NGX_LIST) $^ -o $@

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码↳ ./ngx_list_t_test                                                                                                                                 00:24:13 
--------------------------------
create a new pool:
--------------------------------
--------------------------------
alloc an list from the pool:
--------------------------------
list = 0xa2009840
.last = 0xa2009848
.part = -1577019320
.size = 4
.nalloc = 5
.pool = 0xa2009800
elements:
.part = 0xa2009848
.elts = 0xa2009878 (0, 2, 4, 6, 8)
.nelts = 5
.next = 0x0

参考

  • blog.csdn.net/livelylittl…
  • tc.dreamcat.ink/archives/39…

本文转载自: 掘金

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

什么是spirngAop,SpringAop的理解 什么是s

发表于 2021-11-17

什么是springAOP?

AOP(Aspect Oriented Programming),面向切面编程,还有我们熟知的OOP(面向对象编程)和POP(面向过程编程)。

​用官方的介绍面向切面编程通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

看着这一段官方解释感觉很晦涩难懂,其实简单来说,Aop就是用来对我们的程序的方法通过动态代理的方式对我们的核心代码进行功能增加和或者增强等,让我们的原始类或者方法的维护性更高,降低耦合。看到这儿感觉还是不懂请往后看,我会有一个生动形象的例子进行说明。

比如:

  • 日志记录
  • 性能统计
  • 安全控制
  • 事务处理
  • 异常处理等

使用传统对方法进行功能性增强

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class Demo01 {
public static void main(String[] args) {
Car car = new Car();
car.carRun();
}
}
class Car{
public void carRun(){
// System.out.println("=========log===========");
System.out.println("car is running");
}

public void carStop(){
// System.out.println("=========log===========");
System.out.println("car is stopping");
}
}

​ 这段代码中我们如果需要在每一个方法执行的前面加入一个日志记录的功能就需要在每个方法的前面加入一个打印日志的的代码,假如我们有很多的方法都需要这样的日志打印我们就需要给很多的方法加上打印日志的代码,这绝对是不太好的,这样写你可能觉得大不了复制粘贴就行,但是假如后期我要对打印的日志内容进行改变,你就很头疼,或者对其中的一些方法进行修改。这不符合我们程序员的思维,程序员应该用最少的代码做相同的事情,而且我们后期维护要更加方便。

但是在了解Aop之前建议先去了解一下Java的代理(Jdk动态代理,cglib代理)
我们这儿主要是理解Aop,Aop的原理其实就是利用动态代理技术实现的,所以就得自己去探究一下了,其实也不算很难。

但是在了解Aop之前我们要知道Aop的一些专业名词

  • Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
  • Joinpoint (连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
  • Pointcut(切点):被增强的连接点是切入点
  • Advice(增强):Advice 定义了在 pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。
  • weaving(织入):将切面应用到目标对象并导致代理对象创建的过程

看了这些专业名称,说实话我第一次了解这些的时候,看着这些专业名次也是很难受的,计算机行业总会创造一些专业性看着又晦涩难懂的名词,但是当我们理解了后,也不算这么的难

之前看过一个老师的将Aop举了一个例子,感觉对Aop的功能的理解有很好的帮助

想象一个常见,你有有个十几套房子的普通老百姓,你需要对房子进行售卖或者出租,但是你卖房子要经历一下几个步骤,比如:

1. 去贴小广告,或者在网上发布登录信息

2. 带别人去看房

3. 签合同(核心功能)

4. 卖或者租房子(核心功能)

但是你这样一个普通老百姓,哪儿有时间去做前两步事情,你就想着卖房子签合同,于是就去找了中介,于是中介就帮你把你的十几套房子去打广告,让中介带别人看房,然后你就只需要进行最后两步事情。这是个两全其美的事情。

我觉得这个例子举得很好,这就类似于Aop的功能,中介可以对很多房子进行代理帮我们售卖,就像Aop对我们的方法进行代理,增强我们的方法。我能自己写代码时我们的核心业务代码还是让我们自己来管理,但是我们需要进行日志记录或者事务管理时,就可以使用Aop来帮助我们完成

接着上面的例子,我们解释一下这些名次

  • 连接点:就是你要售卖的那10几套房子
  • 切点:你不可能把你的房子全给卖了吧,总得留个几套自己住吧,于是你要售卖的那些房子就是切点
  • 增强:就是你要找到中介帮你打广告,看房子,这就叫增强
  • 切面:由点构成面,由你要卖的那10套房子就构成了切面,切面就是你给中介进行代理的房子组合在了一起构成了切面
  • 织入:就是你将房子授权给中介的过程

这是画的一张草图,进一步理解一下这些名词
在这里插入图片描述

使用注解进行Advice

  • @Before: 在目标方法被调用之前做增强处理,只需要指定切入点表达式即可
  • @AfterReturning: 在目标方法正常完成后做增强,@AfterReturning除了指定切入点表达式后,还可以指定一个返回值形参名returning,代表目标方法的返回值
  • @AfterThrowing: 主要用来处理程序中未处理的异常,@AfterThrowing除了指定切入点表达式后,还可以指定一个throwing的返回值形参名,可以通过该形参名来访问目标方法中所抛出的异常对象
  • @After: 在目标方法完成之后做增强,无论目标方法时候成功完成。@After可以指定一个切入点表达式
  • @Around : 环绕通知,在目标方法完成前后做增强处理,环绕通知是最重要的通知类型,像事务,日志等都是环绕通知,注意编程中核心是一个ProceedingJoinPoint

这些注解的简单使用

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
java复制代码@Aspect
@Component
public class Log {

@Before("execution(public * *(..))")
public void before(){
System.out.println("Log before advice ");
}

@AfterReturning("")
public void AfterReturning(){
System.out.println("Log after returning advice ");
}

@AfterThrowing("")
public void AfterThrowing(){
System.out.println("Log after throwing advice ");
}

@After("")
public void After(){
System.out.println("Log after advice ");
}

@Around("")
public void around(ProceedingJoinPoint joinPoint,User user) throws Throwable{
System.out.println("log start");
System.out.println("log end");
}*/

}

这就是我对springAop的理解,本人只是一个大三的学生,如有理解错误的地方,请大家指正

本文转载自: 掘金

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

为什么MySQL字符串不加引号索引失效?《死磕MySQL系列

发表于 2021-11-17

群里一个小伙伴在问为什么MySQL字符串不加单引号会导致索引失效,这个问题估计很多人都知道答案。没错,是因为MySQL内部进行了隐式转换。

本期文章就聊聊什么是隐式转换,为什么会发生隐式转换。

系列文章

字符串可以这样加索引,你知吗?《死磕MySQL系列 七》

无法复现的“慢”SQL《死磕MySQL系列 八》

什么?还在用delete删除数据《死磕MySQL系列 九》

MySQL统计总数就用count(*),别花里胡哨的《死磕MySQL系列 十》

文章总目录

一、几大索引失效原因

你肯定在网上看到过非常多关于索引失效原因的文章,但是一定要自己亲手尝试一下,因为版本不同引发的结果不会一致。

1.带头大哥不能死

这局经典语句是说创建索引要符合最左侧原则。

例如表结构为u_id,u_name,u_age,u_sex,u_phone,u_time

创建索引为idx_user_name_age_sex。

查询条件必须带上u_name这一列。

2.不在索引列上做任何操作

不在索引列上做任何计算、函数、自动或者手动的类型转换,否则会进行全表扫描。简而言之不要在索引列上做任何操作。

3.俩边类型不等

例如建立了索引idx_user_name,name字段类型为varchar

在查询时使用where name = kaka,这样的查询方式会直接造成索引失效。

正确的用法为where name = “kaka”。

4.不适当的like查询会导致索引失效

创建索引为idx_user_name

执行语句为select * from user where name like “kaka%”;可以命中索引。

执行语句为select name from user where name like “%kaka”;可以使用到索引(仅在8.0以上版本)。

执行语句为select * from user where name like ‘’%kaka”;会直接导致索引失效

5.范围条件之后的索引会失效

创建索引为idx_user_name_age_sex

执行语句select * from user where name = ‘kaka’ and age > 11 and sex = 1;

上面这条sql语句只会命中name和age索引,sex索引会失效。

复合索引失效需要查看key_len的长度即可。

总结:%在后边会命令索引,当使用了覆盖索引时任何查询方式都可命中索引。

以上就是咔咔关于索引失效会出现的原因总结,在很多文章中没有标注MySQL版本,所以你有可能会看到is null 、or索引会失效的结论。

二、从规则方面说明索引失效的原因

问题的答案就是第3点,两边类型不一致导致索引失效。

下图是表结构,目前这个表存在两个索引,一个主键索引,一个普通索引phone。

分别执行以下两条SQL语句

1
2
3
csharp复制代码explain select * from evt_sms where phone = 13020733815;

explain select * from evt_sms where phone = '13020733815';

在这里插入图片描述

在这里插入图片描述

从上图可看出,执行第一条SQL没有使用到索引,第二条SQL却使用到了索引。

不错,你也发现了两条SQL的不同,第二条SQL跟第一条SQL逻辑一致,不同的是一个查询条件有引号,一个没有。

问题:为什么逻辑相同的SQL却是用不了索引

选择索引是优化器大哥的工作,大哥做事肯定轮不到咱们去教,因为大哥有自己的一套规则。

对于优化器来说,如果等号两边的数据类型不一致,则会发生隐式转换。

例如,explain select * from evt_sms where phone = 13020733815;这条SQL语句就会变为explain select * from evt_sms where cast(phone as signed int) = 13020733815;

由于对索引列进行了函数操作,从而导致索引失效。

问题:为什么会把左侧的列转为int类型呢?

优化器大哥就是根据这个规则进行判断,是把字符串转为数字,还是把数字转为字符串。

若返回1,则把字符串转为数字。

若返回0,则把数字转为字符串。

问题:select * from evt_sms where id = “193014410456945216”这条SQL语句能用上索引吗?

如果你忘记了表结构,可以翻到文章开头再看下表evt_sms的索引。

可以知道列id添加了主键索引,类型为int类型。

根据规则得到,MySQL8.0以上的版本是将字符串转为数字。

所以说,函数操作的是等号右边的数据,跟索引列没有关系,所以可以用上索引。

那么来到数据库验证一下结论,你答对了吗?

三、从索引结构说明索引失效原因

有这样一个需求,要统计每年双11注册用户数量。

可以看到在evt_sms表中是没有给create_time创建索引的,于是你会执行alter table evt_sms add index idx_ctime(create_time),给create_time添加上索引。

接着你就执行了下面的SQL语句。

1
sql复制代码explain select count(*) from evt_sms where month(create_time) = 11;

上线没一会数据库出现了大量的慢查询,导致非常多的SQL返回失败。

此时公司大牛肯定会直接指出问题,索引列进行函数操作。

问题:为什么索引列使用函数就用不上索引了呢?

你现在看到的create_time索引结构图。

若此时执行的是where create_time = ‘2021-11-16’,那么MySQL就会非常快的等位到对应位置,并返回结果。

但是,做了函数操作,例如month(2021-11-16)得到的值是11。

当MySQL拿到返回的这个11时,在索引结构中根据就不知道怎么办。MySQL之所以能使用快速定位,是因为B+树的有序性。

而使用了函数对索引列进行操作后就会破坏索引的有序性,因此优化器大哥会选择执行代价最低的索引来继续执行。

四、结论

本期文章给大家介绍了两个案例,一个隐式转换,一个对索引列进行函数操作。

两种情况的本质是一样的,都是在索引列上进行了函数操作,导致全表扫描。

类似于这两种情况的还是字符集问题,不过一般这个问题会会很少发生,如有新业务需要新创建表,都会设置为之前的字符集。

两张表的字符集不同在进行join时也会导致隐式字符集转换,导致索引失效。

“

坚持学习、坚持写作、坚持分享是咔咔从业以来所秉持的信念。愿文章在偌大的互联网上能给你带来一点帮助,我是咔咔,下期见。

”

本文转载自: 掘金

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

Spring 控制反转(IOC)

发表于 2021-11-17

Spring 控制反转(IOC)

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

IOC概念

  • 控制反转,把对象创建和对象间的调用过程,交给Spring进行管理。
  • 使用IOC可以降低对象间的耦合度。
  • IOC底层原理:XML解析,工厂模式,反射。

现在我们来讲讲 怎么使用 IOC。

一、Spring配置

详细在代码的注解里。

1、别名

1
2
xml复制代码<!--    别名(给id为user取别名),如果取了别名,我们也可以通过别名获取到这个对象-->
<alias name="user" alias="aaaa"/>

2、bean的配置

Spring Ioc容器的核心就是要玩坏bean!

基本属性:

1
2
3
4
5
6
xml复制代码<!--    id: bean的唯一标识符
class:对象所对应的全限定名:包名+类型
name:也是别名,而且name可以起多个别名,也可以用逗号,空格,分号分隔-->
<bean id="user1" class="mq.pojo.User" name="user2,user3 user4;">
<constructor-arg name="name" value="maomao"/>
</bean>

scope属性(重点是singleton和prototype):

一般情况下是使用singleton,但是要整合struts2时则要在ActionBean中使用prototype。而request和session属性基本上是用不到的。

singleton(默认值):单例模式,所谓单例对象,就是在spring容器中只会存在一个的实例

prototype属性:多例原型被标记为多例的对象,每次再获得才会创建并且每次创建都是新的对象

request: web环境下,对象与request生命周期一致,也就是每当请求处理完一次,对象将从spring容器中移除

session:web环境下,对象与session生命周期一致

在下一篇博客会详细讲这个属性。

3、import

一般用于团队开发使用,可以将多个配置文件,导入合并成一个

image.png

使用的时候使用总配置文件就行

二、Spring IOC容器

SpringIOC的核心为IOC容器,它主要有两种:
BeanFactory:IOC容器的基本实现,是Spring内部的使用接口,不提供开发人员使用。加载配置文件的时候不会去创建对象,在获取对象使用才去创建对象。

ApplictionContext:BeanFactory接口的子接口,提供更多更强大的功能,一般有开发人员使用,加载配置文件时候就会创建对象。

ApplicationContext的实现有四种方式:

FileSystemXmlApplicationContext:加载配置文件的时候采用的是项目的路径。

ClassPathXmlApplicationContext:加载配置文件的时候根据ClassPath位置。(重点)

XmlWebApplicationContext:在Web环境下初始化监听器的时候会加载该类。

AnnotationConfigApplicationContext:根据注解的方式启动Spring 容器。(介绍)

1、依赖注入(DI)

  • 依赖:bean对象的创建依赖于容器
  • 注入:bean对象中的所有属性,由容器来注入

环境搭建

  1. 导入Spring相关的jar包
1
2
3
4
5
6
xml复制代码<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
  1. 编写实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class Hello {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "Hello{" +
"name='" + name + '\'' +
'}';
}
}

构造器注入

1.无参构造创建对象(默认)
  1. 编写beans.xml文件(重点),spring主要就在这个文件里进行一些列的操作
    有点类似Mybatis中的Mapper.xml一样,详细的在代码中有注释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- 使用spring创建对象,在spring中这些都称为Bean
类型 变量名 = new 类型();
Hello hello =new Hello();
bean=对象 new Hello();
id=变量名 class= new 的对象
property 相当于给对象中的属性设置一个值
-->
<bean id="hello" class="mq.pojo.Hello">
<!-- value :具体的值,基本的数据类型
ref : 引用spring容器中创建好的对象-->
<property name="name" value="spring"/>
</bean>
</beans>

测试类
这里使用ClassPathXmlApplicationContext 加载配置文件,也是最常用的方式。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class MyTest {

public static void main(String[] args) {
//获取spring的上下文对象,拿到spring的容器
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
//我们的对象现在都在springspring中管理,我们要使用,直接取
//getBean:参数就是spring配置文件中bean的id
Hello hello = (Hello) context.getBean("hello");
System.out.println(hello.toString());
}
}

结果:
在这里插入图片描述
name=spring,就是我在bean对象的时候给name属性的value值为spring

2.有参构造创建对象

我们需要在实体类中User中添加一个有参构造的方法

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class UserT {
private String name;
public UserT(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
}
public void show(){
System.out.println("name="+ name );
}
}

编写xml配置文件
三种方式都行

  • 下标赋值
1
2
3
4
xml复制代码<!--    第一种,下标赋值-->
<bean id="user" class="mq.pojo.User">
<constructor-arg index="0" value="maomao"/>
</bean>
  • 通过类型来创建
1
2
3
4
xml复制代码<!--第二种,通过类型创建,不建议使用-->
<bean id="user" class="mq.pojo.User">
<constructor-arg type="java.lang.String" value="maomao"/>
</bean>
  • 通过参数名
1
2
3
4
xml复制代码<!--    第三种,通过参数名-->
<bean id="user" class="mq.pojo.User">
<constructor-arg name="name" value="maomao"/>
</bean>

测试类:

1
2
3
4
5
6
7
8
java复制代码    public static void main(String[] args) {
//获取spring的上下文对象,拿到spring的容器
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
//我们的对象现在都在springspring中管理,我们要使用,直接取
//getBean:参数就是spring配置文件中bean的id
User user = (User) context.getBean("user");
user.show();
}

结果
在这里插入图片描述

image.png
小结:
在配置文件加载的时候,容器中管理的对象就已经被初始化了,也就是前面讲Spring配置的时候的单例模式,即对象就被创建了一个。

  • User的对象是由Spring创建的
  • User对象的属性是由bean的value设置

这个过程就是控制反转:

控制:谁来控制对象的创建,传统应用程序的对象是由程序本身控制创建的,使用Spring后,对象是由Spring IOC容器来创建和管理的.
反转:程序本身不创建对象,而变成被动的接收对象.

依赖注入:就是利用set方法来进行注入的.

IOC是一种编程思想,由主动的编程变成被动的接收.

我们彻底不用再程序中去改动代码了,要实现不同的操作,只需要在xml配置文件中进行修改,所谓的loC就是:对象由Spring来创建,管理,装配!

Set方式注入(重点)

要求被注入的属性 , 必须有set方法 , set方法的方法名由set + 属性首字母大写 ,这个例子由几乎所有属性的注入方式,有String、map、list、set、Properties、String[]、实体类(Bean的注入)对象等,如果是刚开始学的,请耐心看完beans.xml里的配置

实体类:
Address 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class Address {
private String address;

public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "Address{" +
"address='" + address + '\'' +
'}';
}
}

Student 类

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
java复制代码public class Student {
private String name;
private Address address;
private String[] books;
private List<String> hobbys;
private Map<String,String> card;
private Set<String> games;
private String wife;
private Properties info;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", address=" + address.toString() +
", books=" + Arrays.toString(books) +'\n'+
", hobbys=" + hobbys +
", card=" + card +
", games=" + games +
", wife='" + wife + '\n' +
", info=" + info +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public String[] getBooks() {
return books;
}
public void setBooks(String[] books) {
this.books = books;
}
public List<String> getHobbys() {
return hobbys;
}
public void setHobbys(List<String> hobbys) {
this.hobbys = hobbys;
}
public Map<String, String> getCard() {
return card;
}
public void setCard(Map<String, String> card) {
this.card = card;
}
public Set<String> getGames() {
return games;
}
public void setGames(Set<String> games) {
this.games = games;
}
public String getWife() {
return wife;
}
public void setWife(String wife) {
this.wife = wife;
}
public Properties getInfo() {
return info;
}
public void setInfo(Properties info) {
this.info = info;
}
}

编写beans.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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

<bean id="address" class="com.mq.pojo.Address">
<property name="address" value="长沙"/>
</bean>

<bean id="student" class="com.mq.pojo.Student">
<!-- 第一种:普通值注入,value-->
<property name="name" value="小毛"/>
<!-- 第二种:Bean注入,ref-->
<property name="address" ref="address"/>
<!-- 数组注入-->
<property name="books">
<array>
<value>java教程</value>
<value>数据结构</value>
<value>C语言</value>
<value>go语言</value>
</array>
</property>
<!-- List注入-->
<property name="hobbys">
<list>
<value>听歌</value>
<value>上网</value>
<value>玩游戏</value>
<value>打篮球</value>
</list>
</property>
<!-- Map注入-->
<property name="card">
<map>
<entry key="银行卡" value="123456"/>
<entry key="银行卡2" value="123456"/>
</map>
</property>
<!-- Set注入-->
<property name="games">
<set>
<value>LOL</value>
<value>CF</value>
<value>王者荣耀</value>
</set>
</property>
<!-- null值注入-->
<property name="wife">
<null></null>
</property>
<property name="info">
<props>
<prop key="学号">20</prop>
<prop key="性别">男</prop>
<prop key="国籍">中国</prop>
</props>
</property>
</bean>
</beans>

测试类

1
2
3
4
5
6
java复制代码    public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");

Student student = (Student) context.getBean("student");
System.out.println(student.getName());
}

结果:
在这里插入图片描述

拓展方式的注入

主要有p 命名空间的注入 和 c命名空间注入

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码public class User {
private String name;
private int age;

public User() {
}

public User(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
1
2
3
4
5
xml复制代码<!--    p 命名空间的注入,可以直接注入属性的值:property-->
<bean id="user" class="com.mq.pojo.User" p:name="xiaoqian" p:age="20"/>

<!-- c命名空间注入,通过构造器注入:construct-args-->
<bean id="user2" class="com.mq.pojo.User" c:age="18" p:name="xiaoxioa"/>

注意点:p命名和c命名空间不能直接使用,需要xml导入约束

1
2
xml复制代码 xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"

测试结果:
p命名注入:

image.png
c命名注入:

image.png
注意:C命名空间注入 ,原理是使用构造器注入,所以必须要在实体类中加上有参构造器

海绵宝宝,今天的学习就到此为止吧,明天依旧学习噢!

本文转载自: 掘金

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

1…307308309…956

开发者博客

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