Apache配置详解

1. 虚拟主机概念

我们要想实现一个web站点,而且能够在互联网上被访问,首先它再能运行在操作系统,而且这个操作系统还要运行在物理主机上(第一它是一个主机)。在互联网上能够被访问,那我们需要一个主机,需要一个IP地址,需要一个时时在线的服务器,这需要多少资源?对众多小型站点来讲或者说对某种需求来讲,有可能都用不到服务器,也就是每天就10个人左右访问,只是需要我们在线而已,如果我们就为这一点点的需求就投入重大的资源的话是非常浪费的。我们就期望能够像我们使用虚拟机一样,虚拟的OS一样或虚拟的PC一样,能够在一台物理主机上虚拟出来多个可以同时运行的站点或者我们把它称为主机因此就把它称为虚拟主机。

2. Apache 主机的类型

1. 中心主机

2. 虚拟主机

  1. 基于IP: 端口相同,IP地址不同。

  2. 基于端口:IP相同,端口不同。

  3. 基于域名:IP地址相同,端口相同主机名不同

注意:所有的虚拟主机的配置我们都需要取消中心主机,也就是注释掉 DocumentRoot 这是配置虚拟主机的前提

3. 基于域名的虚拟主机

例如我采用的 xampp 所以配置虚拟主机就在 C:\xampp\apache\conf\extra\httpd-vhosts.conf 中配置,由于这个文件默认被 include 到主配置文件中了,所以在这里的修改都可以生效。

首先需要保证主配置文件中的中心主机被取消了也就是:

1
#DocumentRoot "C:/xampp/htdocs"

然后打开 httpd-vhosts.conf 配置文件,按照下面的格式配置虚拟主机

1
2
3
4
5
6
7
8
9
<VirtualHost *:80>
DocumentRoot "C:/xampp/htdocs"
ServerName localhost
<Directory "C:/xampp/htdocs">
Options Indexes FollowSymLinks Includes ExecCGI
AllowOverride All
Require all granted
</Directory>
</VirtualHost>

1. <VirtualHost *:80>

apache监听本机的所有 IP 和 80 端口做多域名虚拟主机

2. DocumentRoot

表示服务器的根目录

3. ServerName

就是表示域名,我们采用域名方式配置虚拟主机,所以每个虚拟主机的域名应该是不一样的才行

4. Directory

对根目录的规则应用,其中涉及到对于目录的访问权限和其他配置问题

5. 对于 Directory 指令解析

1. Options

配置在特定目录使用哪些特性,常用的值和基本含义如下

  1. ExecCGI: 在该目录下允许执行CGI脚本。
  2. FollowSymLinks: 在该目录下允许文件系统使用符号连接。
  3. Indexes: 当用户访问该目录时,如果用户找不到DirectoryIndex指定的主页文件(例如index.html),则返回该目录下的文件列表给用户。
  4. SymLinksIfOwnerMatch: 当使用符号连接时,只有当符号连接的文件拥有者与实际文件的拥有者相同时才可以访问。

所以我们一般在配置 PHP 的时候所配置的内容是

1
Options Indexes FollowSymLinks Includes ExecCGI

2. AllowOverride

允许存在于.htaccess文件中的指令类型(.htaccess文件名是可以改变的,其文件名由AccessFileName指令决定):

  • None: 当AllowOverride被设置为None时。不搜索该目录下的.htaccess文件(可以减小服务器开销)。
  • All: 在.htaccess文件中可以使用所有的指令。

建议关闭这个选项,因为apache在文档中已经明确支持不建议使用它了,主要是会降低服务器性能,.htaccess 文件可以做到的我们都可以在 Directory 指令中做的更好。

3. Order

控制在访问时Allow和Deny两个访问规则哪个优先,也就是黑白名单的匹配顺序

  • Allow:允许访问的主机列表(可用域名或子网,例如:Allow from 192.168.0.0/16)。
  • Deny:拒绝访问的主机列表。

更详细的用法可参看:order用法

匹配原则:

1
Allow,Deny

First, all Allow directives are evaluated; at least one must match, or the request is rejected. Next, all Deny directives are evaluated. If any matches, the request is rejected. Last, any requests which do not match an Allow or a Deny directive are denied by default.

1
Deny,Allow

First, all Deny directives are evaluated; if any match, the request is denied unless it also matches an Allow directive. Any requests which do not match any Allow or Deny directives are permitted.

4. DirectoryIndex

主页文件设置 一般都是 index.html index.php

5. Deny /Allow

黑白名单书写规则如下:

1
2
3
4
5
Allow from example.org
Allow from .net example.edu
Allow from 10.1.2.3
Allow from 192.168.1.104 192.168.1.205
Allow all

4. 基于端口的虚拟主机

对于同一个 ip 来做虚拟主机,就是需要监听不同的端口,首先需要在主配置文件中添加新的监听端口:

1
2
Listen 80
Listen 81

然后再虚拟主机里面配置

1
2
3
4
5
6
<Virtualhost *:80>
DocumentRoot "/var1"
</Virtualhost>
<Virtualhost *:81>
DocumentRoot "/var2"
</Virtualhost>

5. 基于ip的虚拟主机

这个就不过多赘述了,完全和基于端口的配置方式一样。也就是修改的是ip 而非端口、

6.服务器的优化 (MPM: Multi-Processing Modules)

apache2主要的优势就是对多处理器的支持更好,在编译时同过使用–with-mpm选项来决定apache2的工作模式。如果知道当前的apache2使用什么工作机制,可以通过httpd -l命令列出apache的所有模块,就可以知道其工作方式:

  • prefork:如果httpd -l列出prefork.c,则需要对下面的段进行配置。

    1
    2
    3
    4
    5
    6
    7
    <IfModule prefork.c>
    StartServers 5 #启动apache时启动的httpd进程个数。
    MinSpareServers 5 #服务器保持的最小空闲进程数。
    MaxSpareServers 10 #服务器保持的最大空闲进程数。
    MaxClients 150 #最大并发连接数。
    MaxRequestsPerChild 1000 #每个子进程被请求服务多少次后被kill掉。0表示不限制,推荐设置为1000。
    </IfModule>

在该工作模式下,服务器启动后起动5个httpd进程(加父进程共6个,通过ps -ax|grep httpd命令可以看到)。当有用户连接时,apache会使用一个空闲进程为该连接服务,同时父进程会fork一个子进程。直到内存中的空闲进程达到MaxSpareServers。该模式是为了兼容一些旧版本的程序。我缺省编译时的选项。

  • worker:如果httpd -l列出worker.c,则需要对下面的段进行配置:
1
2
3
4
5
6
7
8
<IfModule worker.c> 
StartServers 2 #启动apache时启动的httpd进程个数。
MaxClients 150 #最大并发连接数。
MinSpareThreads 25 #服务器保持的最小空闲线程数。
MaxSpareThreads 75 #服务器保持的最大空闲线程数。
ThreadsPerChild 25 #每个子进程的产生的线程数。
MaxRequestsPerChild 0 #每个子进程被请求服务多少次后被kill掉。0表示不限制,推荐设置为1000。
</IfModule>

该模式是由线程来监听客户的连接。当有新客户连接时,由其中的一个空闲线程接受连接。服务器在启动时启动两个进程,每个进程产生的线程数是固定的(ThreadsPerChild决定),因此启动时有50个线程。当50个线程不够用时,服务器自动fork一个进程,再产生25个线程。

  • perchild:如果httpd -l列出perchild.c,则需要对下面的段进行配置:
1
2
3
4
5
6
7
8
<IfModule perchild.c> 
NumServers 5 #服务器启动时启动的子进程数
StartThreads 5 #每个子进程启动时启动的线程数
MinSpareThreads 5 #内存中的最小空闲线程数
MaxSpareThreads 10 #最大空闲线程数
MaxThreadsPerChild 2000 #每个线程最多被请求多少次后退出。0不受限制。
MaxRequestsPerChild 10000 #每个子进程服务多少次后被重新fork。0表示不受限制。
</IfModule>

该模式下,子进程的数量是固定的,线程数不受限制。当客户端连接到服务器时,又空闲的线程提供服务。 如果空闲线程数不够,子进程自动产生线程来为新的连接服务。该模式用于多站点服务器。

7.别名设置

对于不在DocumentRoot指定的目录内的页面,既可以使用符号连接,也可以使用别名。别名的设置如下:

1
2
3
4
5
6
7
8
Alias /download/ "/var/www/download/" #访问时可以输入:http://www.custing.com/download/ 

<Directory "/var/www/download"> #对该目录进行访问控制设置
Options Indexes MultiViews
AllowOverride AuthConfig
Order allow,deny
Allow from all
</Directory>

6、CGI设置

1
2
3
4
5
6
7
8
9
# 访问时可以:http://www.clusting.com/cgi-bin/,但是该目录下的CGI脚本文件要加可执行权限
ScriptAlias /cgi-bin/ "/mnt/software/apache2/cgi-bin/"

<Directory "/usr/local/apache2/cgi-bin"> #设置目录属性
AllowOverride None
Options None
Order allow,deny
Allow from all
</Directory>

8、日志的设置

(1) 错误日志的设置
  • ErrorLog logs/error_log :日志的保存位置

  • LogLevel warn #日志的级别

显示的格式日下:

1
[Mon Oct 10 15:54:29 2005] [error] [client 192.168.10.22] access to /download/ failed, reason: user admin not allowed access 
(2) 访问日志设置

日志的缺省格式有如下几种:

  • LogFormat “%h %l %u %t “%r” %>s %b “%{Referer}i” “%{User-Agent}i”” combined
  • LogFormat “%h %l %u %t “%r” %>s %b” common #common为日志格式名称
  • LogFormat “%{Referer}i -> %U” referer
  • LogFormat “%{User-agent}i” agent
  • CustomLog logs/access_log common

格式中的各个参数如下:

1
2
3
4
5
6
7
8
9
%h --客户端的ip地址或主机名 
%l --The 这是由客户端 identd 判断的RFC 1413身份,输出中的符号 "-" 表示此处信息无效。
%u --由HTTP认证系统得到的访问该网页的客户名。有认证时才有效,输出中的符号 "-" 表示此处信息无效。
%t --服务器完成对请求的处理时的时间。
"%r" --引号中是客户发出的包含了许多有用信息的请求内容。
%>s --这个是服务器返回给客户端的状态码。
%b --最后这项是返回给客户端的不包括响应头的字节数。
"%{Referer}i" --此项指明了该请求是从被哪个网页提交过来的。
"%{User-Agent}i" --此项是客户浏览器提供的浏览器识别信息。

下面是一段访问日志的实例:

1
2
3
192.168.10.22 - bearzhang [10/Oct/2005:16:53:06 +0800] "GET /download/ HTTP/1.1" 200 1228 
192.168.10.22 - - [10/Oct/2005:16:53:06 +0800] "GET /icons/blank.gif HTTP/1.1" 304 -
192.168.10.22 - - [10/Oct/2005:16:53:06 +0800] "GET /icons/back.gif HTTP/1.1" 304 –

9、用户认证的配置

(1) httpd.conf用户认证配置:
1
2
3
4
5
6
7
AccessFileName .htaccess 
.........
Alias /download/ "/var/www/download/"
<Directory "/var/www/download">
Options Indexes
AllowOverride AuthConfig
</Directory>
(2) create a password file:
1
/usr/local/apache2/bin/htpasswd -c /var/httpuser/passwords bearzhang
(3) configure the server to request a password and tell the server which users are allowed access.
1
2
3
4
5
6
7
vi /var/www/download/.htaccess: 

AuthType Basic
AuthName "Restricted Files"
AuthUserFile /var/httpuser/passwords
Require user bearzhang
#Require valid-user #all valid user

10、虚拟主机的配置总结

(1)基于IP地址的虚拟主机配置
1
2
3
4
5
6
7
8
9
10
11
Listen 80 

<VirtualHost 172.20.30.40>
DocumentRoot /www/example1
ServerName www.example1.com
</VirtualHost>

<VirtualHost 172.20.30.50>
DocumentRoot /www/example2
ServerName www.example2.org
</VirtualHost>
(2) 基于IP和多端口的虚拟主机配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Listen 172.20.30.40:80 
Listen 172.20.30.40:8080
Listen 172.20.30.50:80
Listen 172.20.30.50:8080

<VirtualHost 172.20.30.40:80>
DocumentRoot /www/example1-80
ServerName www.example1.com
</VirtualHost>

<VirtualHost 172.20.30.40:8080>
DocumentRoot /www/example1-8080
ServerName www.example1.com
</VirtualHost>

<VirtualHost 172.20.30.50:80>
DocumentRoot /www/example2-80
ServerName www.example1.org
</VirtualHost>

<VirtualHost 172.20.30.50:8080>
DocumentRoot /www/example2-8080
ServerName www.example2.org
</VirtualHost>
(3) 单个IP地址的服务器上基于域名的虚拟主机配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Ensure that Apache listens on port 80 
Listen 80

# Listen for virtual host requests on all IP addresses
NameVirtualHost *:80

<VirtualHost *:80>
DocumentRoot /www/example1
ServerName www.example1.com
ServerAlias example1.com. *.example1.com
# Other directives here
</VirtualHost>

<VirtualHost *:80>
DocumentRoot /www/example2
ServerName www.example2.org
# Other directives here
</VirtualHost>
(4) 在多个IP地址的服务器上配置基于域名的虚拟主机:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Listen 80 

# This is the "main" server running on 172.20.30.40
ServerName server.domain.com
DocumentRoot /www/mainserver

# This is the other address
NameVirtualHost 172.20.30.50

<VirtualHost 172.20.30.50>
DocumentRoot /www/example1
ServerName www.example1.com
# Other directives here ...
</VirtualHost>

<VirtualHost 172.20.30.50>
DocumentRoot /www/example2
ServerName www.example2.org
# Other directives here ...
</VirtualHost>
(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
Listen 80 
Listen 8080

NameVirtualHost 172.20.30.40:80
NameVirtualHost 172.20.30.40:8080

<VirtualHost 172.20.30.40:80>
ServerName www.example1.com
DocumentRoot /www/domain-80
</VirtualHost>

<VirtualHost 172.20.30.40:8080>
ServerName www.example1.com
DocumentRoot /www/domain-8080
</VirtualHost>

<VirtualHost 172.20.30.40:80>
ServerName www.example2.org
DocumentRoot /www/otherdomain-80
</VirtualHost>

<VirtualHost 172.20.30.40:8080>
ServerName www.example2.org
DocumentRoot /www/otherdomain-8080
</VirtualHost>
(6) 基于域名和基于IP的混合虚拟主机的配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Listen 80 

NameVirtualHost 172.20.30.40

<VirtualHost 172.20.30.40>
DocumentRoot /www/example1
ServerName www.example1.com
</VirtualHost>

<VirtualHost 172.20.30.40>
DocumentRoot /www/example2
ServerName www.example2.org
</VirtualHost>

<VirtualHost 172.20.30.40>
DocumentRoot /www/example3
ServerName www.example3.net
</VirtualHost>

11.SSL加密的配置

首先在配置之前先来了解一些基本概念:

a. 证书的概念:首先要有一个根证书,然后用根证书来签发服务器证书和客户证书,一般理解:服务器证书和客户证书是平级关系。SSL必须安装 服务器证书来认证。 因此:在此环境中,至少必须有三个证书:根证书,服务器证书,客户端证书。 在生成证书之前,一般会有一个私钥,同时用私钥生成证书请求,再利用证书服务器的根证来签发证书。 SSL所使用的证书可以自己生成,也可以通过一个商业性CA(如Verisign 或 Thawte)签署证书。

b. 签发证书的问题:如果使用的是商业证书,具体的签署方法请查看相关销售商的说明;如果是知己签发的证书,可以使用openssl自带的CA.sh 脚本工具。

如果不为单独的客户端签发证书,客户端证书可以不用生成,客户端与服务器端使用相同的证书。

(1) conf/ssl.conf 配置文件中的主要参数配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Listen 443 

SSLPassPhraseDialog buildin
#SSLPassPhraseDialog exec:/path/to/program
SSLSessionCache dbm:/usr/local/apache2/logs/ssl_scache
SSLSessionCacheTimeout 300
SSLMutex file:/usr/local/apache2/logs/ssl_mutex

<VirtualHost _default_:443>
# General setup for the virtual host
DocumentRoot "/usr/local/apache2/htdocs"
ServerName www.example.com:443
ServerAdmin you@example.com
ErrorLog /usr/local/apache2/logs/error_log
TransferLog /usr/local/apache2/logs/access_log

SSLEngine on
SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL

SSLCertificateFile /usr/local/apache2/conf/ssl.crt/server.crt
SSLCertificateKeyFile /usr/local/apache2/conf/ssl.key/server.key
CustomLog /usr/local/apache2/logs/ssl_request_log "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x "%r" %b"
</VirtualHost>

(2) 创建和使用自签署的证书:

a. Create a RSA private key for your Apache server

1
/usr/local/openssl/bin/openssl genrsa -des3 -out /usr/local/apache2/conf/ssl.key/server.key 1024 

b. Create a Certificate Signing Request (CSR)

1
/usr/local/openssl/bin/openssl req -new -key /usr/local/apache2/conf/ssl.key/server.key -out /usr/local/apache2/conf/ssl.key/server.csr 

c. Create a self-signed CA Certificate (X509 structure) with the RSA key of the CA

1
2
3
4
/usr/local/openssl/bin/openssl req -x509 -days 365 -key /usr/local/apache2/conf/ssl.key/server.key -in /usr/local/apache2/conf/ssl.key/server.csr -out /usr/local/apache2/conf/ssl.crt/server.crt 
/usr/local/openssl/bin/openssl genrsa 1024 -out server.key
/usr/local/openssl/bin/openssl req -new -key server.key -out server.csr
/usr/local/openssl/bin/openssl req -x509 -days 365 -key server.key -in server.csr -out server.crt

(3) 创建自己的CA(认证证书),并使用该CA来签署服务器的证书。

1
2
3
4
5
6
7
8
9
10
mkdir /CA 
cd /CA
cp openssl-0.9.7g/apps/CA.sh /CA
./CA.sh -newca
openssl genrsa -des3 -out server.key 1024
openssl req -new -key server.key -out server.csr
cp server.csr newreq.pem
./CA.sh -sign
cp newcert.pem /usr/local/apache2/conf/ssl.crt/server.crt
cp server.key /usr/local/apache2/conf/ssl.key/

参考文章

深入理解Apache虚拟主机

Apache配置文件httpd.conf详解

Apache文档

GOF23 常用设计模式

GOF23 常用设计模式

一、单例设计模式

1.饿汉式

1
2
3
4
5
6
7
8
public class GOF23 {
// 饿汉式
private static final List list = new ArrayList();

public static List getInstance() {
return list;
}
}

2.懒汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    // 懒汉式
// 使用双检锁,并且由于 jvm 指令重排序我们需要使用 volatile 关键字抑制指令重排序
public class GOF{
private static volatile List list1;
public static List getInstanceLazy() {
synchronized (Object.class) {
if (list1 == null) {
synchronized (Object.class){
list1 = new ArrayList();
return list;
}
}else {
return list;
}
}
}
}

3.静态内部类

1
2
3
4
5
6
7
8
9
10
public class GOF{
// 静态内部类
public static class SingletonClass{
private static final List list2 = new ArrayList();
}
// 首先要去加载这个静态内部类,然后这个时候会调用 cinit 方法这个是天然的线程安全的,然后返回这个对象即可 可以延时加载
public List getStaticInstance(){
return SingletonClass.list2;
}
}

4.枚举

1
2
3
4
5
6
public class GOF23 {
// 采用枚举的方式 不能延时加载 由 jvm 底层完成的
public enum SingleInstanceEnum{
INSTANCE_ENUM
}
}

二、工厂设计模式

工厂设计模式的好处就是把对象的创建和使用分开。在软公众需要遵循的三个原则:开闭原则(对修改关闭,对拓展开放)、依赖翻转(面向接口依赖而不是类的强依赖)、迪米特法则(和朋友通讯而不是陌生人)。

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
public interface Car {
public void run();
}

public class Audi implements Car {
@Override
public void run() {
System.out.println("audi run");
}
}

public class Daben implements Car {
@Override
public void run() {
System.out.println("daben run");
}
}

public class CarFactory {
public static Car createCar(String type) {
if (type.equals("daben")) {
return new Daben();
} else if (type.equals("audi")) {
return new Audi();
}else {
return null;
}
}
}

public class CarClient {
public static void main(String[] args) {
Daben daben = (Daben) CarFactory.createCar("daben");
daben.run();
}
}

2.工厂方法

这个比上面好一点的就是我们不用直接修改原来的代码而是可以面向拓展而不是修改就是开闭原则是满足的。其实我们在项目中主要使用的是简单工厂而不是这个,因为这个的类文件剧增。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface CarFactory {
public Car createFactory();
}

public class BenChiFactory implements CarFactory {
@Override
public Car createFactory() {
return new Daben();
}
}

public class AudiFactory implements CarFactory {
@Override
public Car createFactory() {
return new Audi();
}
}

3.抽象工厂

抽象工厂其实和上面的两个方式区别很大,主要是因为上面两个的维度是创建产品而这个地方的维度是创建一个产品组。也就是比如我们有一个车的工厂,其实这个工厂是创建很多车的零部件而不是一个车,那么我们有多个车的工厂的目的就是为了创建不同层次的车零件。比如下面这个例子,就是他的维度是横向的一个维度。

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
// 座椅类型
public interface Seat {
public void desc();
}

class LuxurySeat implements Seat {

@Override
public void desc() {
System.out.println("high quality seat");
}
}

class LowSeat implements Seat {

@Override
public void desc() {
System.out.println("low seat");
}
}

// 引擎类型
public interface Engine {
public void run();
}

class LuxuryEngine implements Engine {

@Override
public void run() {
System.out.println("high speed engine");
}
}

class LowEngine implements Engine {

@Override
public void run() {
System.out.println("low engine");
}
}

// 车的抽象工厂
public interface AbstractCarFactory {
public Seat createSeat();
public Engine creaateEngine();
}

// 两个工厂的实现,分别是高级车工厂,低级车工厂
public class LuxuryCarFactory implements AbstractCarFactory {
@Override
public Seat createSeat() {
return new LuxurySeat();
}

@Override
public Engine creaateEngine() {
return new LuxuryEngine();
}
}

public class LowCarFactory implements AbstractCarFactory{
@Override
public Seat createSeat() {
return new LowSeat();
}

@Override
public Engine creaateEngine() {
return new LowEngine();
}
}

三、建造者模式

建造者模式分为两部分,第一个部分其实就是工厂模式创建对象,然后另外一部分就是组装创建整个大对象。

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
// 创建工厂
public interface BuilderFactory {
Engine createEngine();
Seat createSeat();
}

public class AirShipBuilderFactory implements BuilderFactory {

@Override
public Engine createEngine() {
return new Engine() {
@Override
public void run() {

}
};
}

@Override
public Seat createSeat() {
return new Seat() {
@Override
public void desc() {


}
};
}
}
// 组装工厂
public class AirDirectFactory {

private BuilderFactory builderFactory;

public AirShip buildAirShip() {
Engine engine = builderFactory.createEngine();
Seat seat = builderFactory.createSeat();
return new AirShip(engine, seat);
}

public AirDirectFactory(BuilderFactory builderFactory) {
this.builderFactory = builderFactory;
}
}

四、原型模式

可以称之为克隆或者拷贝,然后在拷贝的对象上修改就可以产生新对象,其实js 的原型就是这么干的。

这里需要注意一下这里的一个深拷贝的问题,如果没有作深拷贝而是直接的做了一个 super 的调用可能导致状态关联问题。

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
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
@Data
@Accessors(chain = true,fluent = true)
public class Sheep implements Cloneable{
private String name;
private Date birthday;

/**
* 注意在克隆的时候需要进行深拷贝负责就会出现错误结果
* @return
* @throws CloneNotSupportedException
*/
@Override
protected Object clone() throws CloneNotSupportedException {

Sheep clone = (Sheep) super.clone();
clone.birthday = (Date) this.birthday.clone();
return clone;
}
}

public class PrototypeClient {
public static void main(String[] args) throws CloneNotSupportedException {
Sheep sheep1 = new Sheep("A", new Date());
Sheep sheep2 = (Sheep) sheep1.clone();
sheep2.birthday(new Date()).name("sss");
System.out.println(sheep2);
}
}

另外我们除了使用上面的克隆方法我们还可以使用序列化的方式直接生成一个一模一样的对象出来。具体来说就是我们可以用输入输出流来生成对象

五、适配器模式

将一个接口转化成另外一个接口,从而使两个原来不能一起工作的组件一起工作。

我们可以想像一下一个 usb 转 hdmi ,那么就有一个目标方和一个被适配方。也就是两个接口再有一个适配器就完成了。

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
// 目标方的接口
public interface Target {
void use();
}
// 被适配方 其实最好是一个接口这里直接写成了一个类
public class BeAdapter {
public void hello(){
System.out.println("say hello");
}
}

// 适配器 实现了目标方,组合了被适配方
@Data
@AllArgsConstructor
public class AdapterDemo implements Target {
// 其实这里可以进一步抽象 也就是所有的被适配对象另起一个接口即可
BeAdapter beAdapter;

public void use() {
beAdapter.hello();
}
}

public class AdapterClient {
public static void test(Target target){
target.use();
}

public static void main(String[] args) {
AdapterDemo adapterDemo = new AdapterDemo(new BeAdapter());
test(adapterDemo);
}
}

六、代理模式

1.静态代理

这种代理模式就是在代理对象上保留一个被代理对象的指针,然后有些事情直接调用被代理对象的方法完成,并且可以做一些前置或者后置的操作。

2.动态代理

1.JDK 代理

其实关键部分就两个:第一个就是一个处理器,所有的对代理的请求的方法都走到了处理器。另外一个就是生成代理对象过程。

处理器

1
2
3
4
5
6
7
8
@AllArgsConstructor
public class SimpleHandler implements InvocationHandler {
RealObject real;
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("heihei");
return (Object) method.invoke(real, args);
}
}

被代理对象

1
2
3
4
5
6
7
8
9
public interface ObjectInterface {
public void hello();
}

public class RealObject implements ObjectInterface {
public void hello(){
System.out.println("hello");
}
}

生成代理对象的过程

1
2
3
4
5
6
7
8
public class JdkClient {
public static void main(String[] args) {
RealObject realObject = new RealObject();
SimpleHandler simpleHandler = new SimpleHandler(realObject);
ObjectInterface proxyInstance = (ObjectInterface) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{ObjectInterface.class}, simpleHandler);
proxyInstance.hello();
}
}

七、桥接模式

桥接模式就是为了处理两个维度的事情,比如说我们定义一个电脑类,那么我们如果需要售卖笔记本、平板、台式机而这些东西又有不同的品牌那么我们就需要建立很多的类在电脑这个类下面。但是我们采用桥接的话我们抽出一个接口这个接口代表了品牌,然后有一个抽象类(电脑类型)包含了品牌的引用,我们只需要在不同的电脑类型下面建立各个类型,在创建一个品牌类型的电脑的时候直接 new 出来就行了。

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
// 品牌
public interface Brand {

}
class Dell implements Brand {

}
class Levon implements Brand {

}

// 电脑的类型
@AllArgsConstructor
public abstract class AbstractComputer {
private Brand brand;
public void sell(){
System.out.println("sell...");
}
}

class DesktopComp extends AbstractComputer {
public DesktopComp(Brand brand) {
super(brand);
}
}

class PadComp extends AbstractComputer {
public PadComp(Brand brand) {
super(brand);
}
}
//使用
public class BridgeClient {
public static void main(String[] args) {
DesktopComp levonDesktopComp = new DesktopComp(new Levon());
}
}

八、策略模式

策略模式就是将一个方法传入到一个地方,简单点来说就是传入一个回调方法,真实是使用的时候看我们传递进去的那个方法作用。

九、模板设计模式

在上层的抽象类中放一些抽象方法先调用上,子类在继承的时候实现这些类就行了。

10、观察者模式

在对一些变量修改或者方法调用的时候胡触发观察者的方法调用或者状态的转变,其实就相当于埋点。

Lombok 常用功能

Lombok 常用功能

1.导入

1
2
3
4
5
6
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
<scope>provided</scope>
</dependency>

2.@Getter/@Setter

这两个注解可以在类上也可以在字段上,看需要的粒度而定。

3.@ToString

生成一个 json 类型的字符串

4.@EqualsAndHashCode

生成 equals 和 hashcode

5.@NoArgsConstructor

生成午无参构造器

6.@RequiredArgsConstructor

只对标有 @NonNull 的字段生成构造器,并且初始化的时候会检查NonNull 的字段如果为空则抛出异常。

7.@AllArgsConstructor

所有字段的构造器

8.@Data

1
@Data`是一个集合体。包含`Getter`,`Setter`,`RequiredArgsConstructor`,`ToString`,`EqualsAndHashCode

9.@Value

可以帮忙生成一个不可变对象。对于所有的字段都将生成final的。同@Data@Value是一个集合体。包含Getter,AllArgsConstructor,ToString,EqualsAndHashCode

10.@Builder

1
Room room = builder().name("name").id("id").createTime(new Date()).occupation("1").occupation("2").build();

11.@Log

一个日志属性,可以直接使用的。

12.@UtilityClass

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
@UtilityClass
public class Utility {

public String getName() {
return "name";
}
}

public static void main(String[] args) {
// Utility utility = new Utility(); 构造函数为私有的,
System.out.println(Utility.getName());

}

13.@Cleanup

用于流等可以不需要关闭使用流对象.

1
2
3
4
5
6
7
8
9
10
@Cleanup
OutputStream outStream = new FileOutputStream(new File("text.txt"));
@Cleanup
InputStream inStream = new FileInputStream(new File("text2.txt"));
byte[] b = new byte[65536];
while (true) {
int r = inStream.read(b);
if (r == -1) break;
outStream.write(b, 0, r);
}

Java Class对象与反射机制

Java Class对象与反射机制

一、关于Class对象

1.基本介绍

其实我们在任何一个 Java 应用程序中都会存在 class 对象,这个 class 对象其实就是每一个类、类型、数组、接口 等等被加载的时候在 jvm 中创建的。也就是这些类型的一个映射,这些类型在虚拟机中的实体。正是由于存在这个对象在 Jvm 中我们可以通过一定的方式获取这个对象然后去用这个对象去对我们的各种类型做一些加载获取属性等等操作,当然主要是对类进行操作并能够执行他的方法,探知未知的类的各种信息。

2.官方文档解释

这是我摘录的 Class 类的 javadoc 内容:

  1. Instances of the class Class represent classes and interfaces in a running Java application.
  2. An enum is a kind of class and an annotation is a kind of interface.
  3. Every array also belongs to a class that is reflected as a Class object that is shared by all arrays with the same element type and number of dimensions.
  4. The primitive Java types (boolean, byte, char, short, int, long, float, and double), and the keyword void are also represented as Class objects.
  5. Class has no public constructor.Instead Class objects are constructed automatically by the Java Virtual Machine as classes are loaded and by calls to the defineClass method in the class loader.

其实里面主要说了这么几件事:

1.class 对象代表类和接口

这个比较好理解其实我们可以看一个例子就能明白:

1
Class clazz = Long.class;

前面是 Class 类型后面就是一个具体的变量,那么为什么我们没有 new 这个对象就能获取到呢? 其实在这里我们使用了 Long 这个类,那么这个类就会被 jvm 加载之后在 jvm 中形成了他所对应的 class 对象,我们在赋值的时候直接从 jvm 取到即可,这里我们也比较好理解这个类的对象其实在内存中只有一份,也就是单例的,这是因为没必要存在多份,他只是一个类的一个内存映射。

2.枚举是类注解是接口

注解是接口是头一回听说,但是他的定义方式比较能说明问题,应该是在编译期做了转换吧。

3.同维同类型数组class 对象相同

这个也就是说他们的类型和维数一致才可以,具体看一个例子:

1
2
3
int[] arr1=new int[1];
int[] arr2=new int[2];
Assert.check(arr1.getClass()==arr2.getClass());

4.基本类型也有 class 对象

5.class 对象不用创建

class 对象是没有公共的构造方法,也没有静态的 getInsteance() 所以说没办法从外部构造方法,是直接的由 jvm 在加载类的时候调用了类加载的 defineClass() 方法完成 class 对象的创建的。

3.class 对象获取

对于 class 对象的获取方式有三种:

  1. 使用类型属性

    1
    Class clazz = Long.class
  2. 调用对象的 getClass() 方法

    1
    2
    List list = new ArrayList();
    Class clazz = list.getClass();
  3. 使用forName()静态方法

    1
    2
    3
    4
    5
    try {
    Class.forName("com.apple.concurrent.Dispatch");
    } catch (ClassNotFoundException e) {
    e.printStackTrace();
    }

二、关于反射

先写一个简单的 javaBean 然后对这个 bean 就进行一些操作:

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 grammer;

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 void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
}

然后我们主要讨论关于对于属性、方法、构造器的获取以及使用。

1.通过 class 对象创建对象

1
2
Class<User> userClass = User.class;
User userInstance = userClass.newInstance();

2. 获取所有的构造方法

注意以下两个都是获取构造器列表,但是前面的是可以获取到私有的构造器,而后者并不可以,其他的都是同理的对于方法和字段来说

1
2
Constructor<?>[] constructors = userClass.getDeclaredConstructors();
Constructor<?>[] constructors1 = userClass.getConstructors();

3. 获取所有的属性

1
Field[] fields = userClass.getDeclaredFields();

4.获取所有的方法

1
Method[] methods = userClass.getDeclaredMethods();

5.获取某个私有属性

这个要注意对于私有属性,只有用 fieldName.setAccessible(true); 设置访问权限后 jvm 再运行的时候不会做权限验证 也就不会报错了

1
2
3
4
5
Field fieldName = userClass.getDeclaredField("name");
fieldName.setAccessible(true);
System.out.println(fieldName.getName());
// 设置值需要调用那个对应的对象 需要指明对象才可以
fieldName.set(userInstance,"lwen");

6.获取某个方法并调用

这个地方需要指明我们调用的参数的具体类型,否则会由于我们的 java 重载机制而出现模糊不清的情况。当然如果是私有方法我们在调用之前必须要指定他们的访问权限,否则也会调用失败,对于构造方法也是这样的。

1
2
3
Method setNameMethod = userClass.getDeclaredMethod("setName", String.class);
//调用方法也是需要指明对象
setNameMethod.invoke(userInstance,"lwen");

7.反射性能问题

反射的性能一般比普通调用的性能要低很多,一般一个方法的执行时间应该是 30 倍的关系,我们希望能够加快反射的速度可以使用跳过安全检查 setAccessible(true) 来提升效率,一般相对使用反射来说的话是提升 4 倍效率。另外我们为了操作 class 我们还可以使用一些第三方的操作字节码的类库比如操作底层字节码的 ASM ,以及基于代码的 javassist 和 GCLIB 等等。

这里说一下关于 javassist 的使用,如何使用它来生成一个 class 文件。

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 grammer;
import javassist.*;
import java.io.IOException;
public class JavassistDemo {
public static void main(String[] args) throws CannotCompileException, NotFoundException, IOException {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("out.Empl");

// 创建属性
CtField int_age = CtField.make("private int age;", ctClass);
ctClass.addField(int_age);

// 创建方法
CtMethod method = (CtMethod) CtMethod.make("public int getAge(){return this.age;}", ctClass);
ctClass.addMethod(method);

// 创建构造器
CtConstructor constructor = new CtConstructor(new CtClass[]{CtClass.intType}, ctClass);
constructor.setBody("{this.age=age;}");
ctClass.addConstructor(constructor);

// 生成
ctClass.writeFile();

}
}

MyBatis笔记五:缓存

MyBatis笔记五:缓存

Mybatis 的缓存是分为两级缓存的,一个是本地缓存,也就是默认的缓存,这一个缓存是默认开启的。这个缓存是 sqlSession 级别的缓存也就是一个数据库会话的缓存,这个缓存其实说白了就是 sqlSession 级别的一个 Map 。

1.一级缓存

一级缓存底层是一个 map 。虽然缓存默认开启的但是我们也会遇到缓存失效的情况:

  • sqlSession 不同
  • 查询条件不同,导致返回的数据都不同
  • 在两次查询之间进行了插删改操作
  • 手动使用了 sqlSession.clearCache 方法清除了缓存。

2.二级缓存

二级缓存是在一级缓存的基础之上的,因为我们的一级缓存是在会话关闭之后这个缓存数据就失效了,那么我们的二级缓存的做的事情就是在会话关闭的时候我们的一级缓存的数据会被转移到一级缓存中,然后新的会话就可以参照二级缓存中的数据。

二级缓存是基于 namespace 的缓存,也就是每一个mapper就是一个二级缓存的空间,他们之间是互相不干扰的。不同的 namespace 查出的数据放到自己的缓存中,也就是 map 中。

1.开启二级缓存

默认的情况是开启的,我们也应该显示的配置,就是在settings标签中配置。

2.使用缓存

在 mapper 的 xml 中,使用cache标签,配置当前的 namespace 的缓存策略。

1
<cache eviction="FIFO" flushInterval="60000" readOnly="true" size="12" type=""/>
  • eviction:缓存的回收策略:

    • LRU - 最近最少使用的:移除最长时间不被使用的对象。
    • FIFO -先进先出:按对象进入缓存的顺序来移除它们。
    • SOFT - 软引用:移除基于垃圾回收器状态和软引用规则的对象。
    • WEAK - 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
    • 默认的是LRU.
  • flushInterval:缓存刷新间隔。缓存多长时间清空一次,默认不清空,设置一个毫秒值

  • readonly:是否只读:

    • true:只读; mybatis认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。

      mybatis为了加快获取速度,直接就会将数据在缓存中的引用交给用户。不安全,速度快

    • false:非只读: mybatis觉得获取的数据可能会被修改。mybatis会利用序列化&反序列的技术克隆一份新的数据给你。 安全,速度慢

  • size:缓存存放多少元素;

  • type=”” 指定自定义缓存的全类名,但是这个类要实现Cache接口即可;

当然如果我们什么东西都不写的话,也就是只写了一个cache标签,他么上述的属性会自动的被应用。

然后就是我们的Cache的自定义,其实Cache的自定义比较简单也就是我们直接实现它的Cache 接口然后实现一些东西就好,但是更好的就是我们其实不用自己写这些东西,在Mybatis的Github上有很多已经写好的cache适配包不用我们自己写、然后再缓存标签中表明type为我们的适配包的类就好了。

3.POJO实现序列化接口

我们的每一个POJO都需要实现序列化接口,否则就会报错,因为我们如果没有开启 readonly 的话我们就必须采用序列化的方式来获取缓存下来的POJO对象。

4.注意

只有当我们的 sqlSession 关闭以后我们的一级缓存内容才会放到二级缓存中去,否则一直是一级缓存在起作用的。

  1. cacheEnabled=true: false:关闭缓存(二级缓存关闭) (一级缓存一直可用)
  2. 每个select标签都有useCache=”true”. 如果为false:不使用缓存(一级缓存依然使用,二级缓存不使用)
  3. 每个增删改标签的: flushCache=”true”: 增册改执行完成后就会清除缓存。他是清除一级二级缓存。
  4. sqlSession. clearCache( );只是清除当前session的一级缓存,因为我们都是用的 sqlSession 的方法,自然清除的一级缓存。
  5. localCacheScope:本地缓存作用域:(默认的一级缓存是SESSION) ;如果当前会话缓存为 STATEMENT:可以禁用一级缓存。】、

3.缓存原理

image

MyBatis笔记四:动态SQL

MyBatis笔记四:动态SQL

什么是动态SQL? 简单来说就是类似于OGNL 表达式的这种 SQL 标签的嵌套然后帮助我们生成SQL语句而避免另外我们的拼字符串的操作。

1.if标签

if 标签中的 test 属性就是用来测试条件的,然后里面的条件之间可以采用 and or来连接,当然我们也可以使用 && 这种,但是注意我们只能使用它们的实体符号而不能直接使用 && 这种。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void DynamicSelectIf() throws IOException {
SqlSessionFactory sessionFactory = GettingStart.getSessionFactory("mybatis.xml");
SqlSession sqlSession = null;
try {
sqlSession = sessionFactory.openSession(true);
EmployeeDynamicMapper mapper = sqlSession.getMapper(EmployeeDynamicMapper.class);
Employee emp = mapper.getEmp(new Employee(13, "lwen", 12));
System.out.println(emp);

}finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}
1
2
3
4
5
6
7
8
9
10
<select id="getEmp" resultType="lwen.entries.Employee">
select *
from employee where
<if test="id!=null">
id=#{id}
</if>
<if test="name!=null">
and name=#{name}
</if>
</select>

但是注意的一点就是假如我们没有传入我们的 id 字段的话,我们的sql就会报错,因为拼接会出问题啊。那么出来的 sql 就会是:

1
select * from employee where and name=#{name}

显然会报错!

解决方案1:

1
2
3
4
5
6
7
8
9
10
11
12
13
<select id="getEmp" resultType="lwen.entries.Employee">
select *
from employee where
<if test="1=1">
1=1
</if>
<if test="id!=null">
and id=#{id}
</if>
<if test="name!=null">
and name=#{name}
</if>
</select>

解决方案2:

2.where标签

1
2
3
4
5
6
7
8
9
10
11
12
<select id="getEmpWhere" resultType="lwen.entries.Employee">
select *
from employee
<where>
<if test="id!=null">
id=#{id}
</if>
<if test="name!=null">
and name=#{name}
</if>
</where>
</select>

可以看到我们删除了 where 关键字而是加上了 where标签这样的话虽然我们不传 id 我们的 sql 也是拼装正常的,不会报错。注意的一点就是我们的 where 标签只能解决当我们的 条件前面多出来的 and 或者 or 而不能解决后面的 and 比如说我们的 sql写成了:

1
2
3
4
5
6
7
8
9
10
11
12
<select id="getEmpWhere" resultType="lwen.entries.Employee">
select *
from employee
<where>
<if test="id!=null">
id=#{id} and
</if>
<if test="name!=null">
name=#{name}
</if>
</where>
</select>

如果不传 name 的话就会报错。

可以看到我们的sql 语句是这样的:

1
EmployeeDynamicMapper.getEmpWhere1 - ==>  Preparing: select * from employee where id=? and ; 

3.trim标签

trim标签就是可以给一个语句加上一个前缀一个后缀,删除某个前缀删除某个后缀。

  • prefix:加前缀
  • prefixOverrides:删前缀
  • suffix:加后缀
  • suffixOverrides:删后缀
1
2
3
4
5
6
7
8
9
10
11
12
<select id="getEmpWhere1" resultType="lwen.entries.Employee">
select *
from employee
<trim prefix="where" suffixOverrides="and">
<if test="id!=null">
id=#{id} and
</if>
<if test="name!=null">
name=#{name}
</if>
</trim>;
</select>

解决上述问题呢。

4.choose标签

这个标签的功能就类似于带有 break 的 switch-case 语句,就是只会进入一个分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<select id="getEmpWhere2" resultType="lwen.entries.Employee">
select *
from employee
<where>
<choose>
<when test="id!=null">
id=#{id}
</when>
<when test="name!=null">
name like #{name}
</when>
<otherwise>
age=1
</otherwise>
</choose>
</where>
</select>

可以看到这里的功能说白了就是根据我们传入了哪个属性就用哪个属性查询,如果啥都没有的话就使用我们默认的就好。

5.set标签

我们上面都是在讨论关于选择的 条件判断问题,但是如果我们希望是更新也是条件进行的呢?我们这里就有与 where 对应的标签来保证我们的逗号不会多出来。

1
2
3
4
5
6
7
8
9
10
11
<update id="updateEmp">
update employee
<set>
<if test="name!=null">
name=#{name},
</if>
<if test="age!=null">
age=#{age}
</if>
</set>
</update>

同样的既然我们上面可以使用 trim 做一个通用的查询,那我们肯定可以使用 trim 做一个更通用的更新

6.foreach标签

1
2
3
4
5
6
7
<select id="getEmpIn" resultType="lwen.entries.Employee">
select *
from employee where id in
<foreach collection="ids" item="id" separator="," open="(" close=")" index="index">
#{id}
</foreach>
</select>

主要用于 in 这种枚举类型的。自然的我们也是可以批量保存的 就是采用这种方式,也就是 values 的位置数据很多。

7.内置参数

  • _databaseId 这个表示的就是 databaseProvider 也就是我们配置的数据源厂商的名字了
  • _parameter 这个表示的就是我们的方法的入参,如果单个参数就是它本身,如果是一个对象什么的就是一个 map

8.变量声明,绑定

我们的某一个参数过来的值我们没办法修改,或者说给他加一些修饰,修改。比如我们的某个like查询我们希望给传过来的东西加上 % 那么我们没办法直接在 sql 中添加,这时候我们就可以使用一个新变量,然后再新变量里修饰原来的变量。

1
<bind name="_new" value="'%'+name"/>

9.sql抽取

1
2
3
4
5
6
7
8
9
10
11
<sql id="select">
select name,id,${other}
from employee;
</sql>

<select id="ha">
<include refid="select">
<property name="other" value="age"/>
</include>
where id=#{id}
</select>

我们可以使用 sql 标签抽取 sql模板,然后使用 include 标签应用模板,之所以称为模板说的是他们是可以被重用的并且里面的内容是可变的。我们可以在 include 中使用 property 标签向模板中注入我们的定义的变量,只不过要注意的是变量的名字必须是采用 ${} 不能是 #{}

MyBatis笔记三:SQL映射文件

MyBatis笔记三:SQL映射文件

1.简单的CRUD

1.绑定

首先呢我们还是需要把我们的 Mapper 接口和我们的 mapper xml 进行绑定,绑定的方式就是采用 namespace 了。不具体多说了。

2.接口

接着就是写我们的 Mapper 接口了,写Mapper接口的时候有一些注意事项,就是我们的 插删改 操作是可以有返回值的,默认情况下这些都是返回我们影响的行数,但是这里我们可以返回 Integer , Long , Boolean 类型,前两个好理解,就是我们常见的那种行数。然后后面具体就是布尔值是我们的行数大于0 就返回真。当然我们也可以不返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
package lwen.dao;

import lwen.entries.Employee;

public interface EmployeeMapper {
Employee getEmployeeById(Integer id);

boolean addEmpl(Employee employee);

boolean updateEmpl(Employee employee);

boolean deleteEmpl(Integer id);
}

3.mapper xml

然后就是在 mapper 的 xml文件中写 sql 语句了,具体就是几个 动作标签 ,然后里面配置上我们的 sql ,至于我们的 sql 的参数我们采用了 #{..} 的方式,然后就是我们需要注意的一点就是我们的 sql 标签的 select 标签是含有一个返回值类型的,但是其他的标签是没有的,需要注意一下。

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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC " -//mybatis.org//DTD Mapper 3.e//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="lwen.dao.EmployeeMapper">
<select id="selectOneEmployee" resultType="lwen.entries.Employee">
select * from employee where id=#{id}
</select>

<!--添加-->
<insert id="addEmpl">
insert into employee (name, age) values (#{name}, #{age})
</insert>

<!--修改-->
<update id="updateEmpl">
update employee
set name = #{name}, age = #{age}
</update>

<!--删除-->
<delete id="deleteEmpl">
delete from employee where id=#{id}
</delete>
</mapper>

4.测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void CURDTest() throws IOException {
SqlSessionFactory sessionFactory = GettingStart.getSessionFactory("mybatis.xml");
SqlSession sqlSession = null;
try {
sqlSession = sessionFactory.openSession(true);
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
System.out.println(mapper.addEmpl(new Employee(1,"lwen",18)));
}finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}

测试类的地方我们也看到了一个比较奇怪的地方就是 sqlSession = sessionFactory.openSession(true); 我们在获取 session 的时候加了一个参数,其实这个参数就是说我们的每一条sql是否为一个事物,或者说是不是一个自动提交的 sql 。如果我们不开启这个自动提交的话我们在执行完若干条sql以后我们需要手动的调用 sql 的 sqlSession.commit(); 方法了。

2.获取自增主键

我们在 Mybatis 中需要将我们插入的值获取到,然后返回给我们传入的 JavaBean 的话我们就可以才用这个方式把自动增长的主键的值封装获取我们在 Service 层就可以获取到我们插入的数据的 id 。

1
2
3
<insert id="addEmpl" useGeneratedKeys="true" keyProperty="id">
insert into employee (name, age) values (#{name}, #{age})
</insert>

useGeneratedKeys 这个属性说我们需要开启自动增长的策略并获取增长的值,然后我们需要把这个值封装进我们的 Java bean 的哪个属性就是我们的 keyProperty 来确定了。

1
2
3
Employee employee = new Employee(1, "lwen", 18);
System.out.println(mapper.addEmpl(employee));
System.out.println(employee);

Employee(id=13, name=lwen, age=18) 虽然我们传入了 id=1 但是 bean 被修改成了 13。

3.参数处理

1.单个参数

这个时候我们直接采用 #{} 取出值,不做特殊处理,我们的 #{} 里面写什么都可以,不用和我们的方法参数对应。

2.多个参数

多个参数就设计到绑定的问题了,也就是要么我们进行参数的绑定,要么我们就进行参数的顺序处理。

1.param顺序处理

1
2
3
<select id="getEmplByIdAndName" resultType="lwen.entries.Employee">
select * from employee where id=#{param1} and name=#{param2}
</select>

这里我们的 Java 代码是有两个参数的,所以注意我们的这个地方参数是从 1 开始的不是 0.

2.args顺序处理

1
2
3
<select id="getEmplByIdAndName2" resultType="lwen.entries.Employee">
select * from employee where id=#{arg0} and name=#{arg1}
</select>

这里的参数有是从 0 开始的。

注意: 以上两种代码我们的 Mapper 接口不需要做任何的额外的配置,就可以直接可以工作了如下:

1
Employee getEmplByIdAndName(Integer id, String name);

3.命名参数

上面的代码虽然可以工作但是我们需要注意的就是这个地方我们的代码是没有任何的具体的意义的,因为就是我们看不出来这个变量的具体意义。我们就可以和我们的接口的参数绑定,那么就是用命名参数。

首先我们需要在Mapper接口中写相关的注解,确定参数:

1
Employee getEmplByIdAndName1(@Param("id") Integer id,@Param("name") String name);

然后我们在 xml 中就可以应用这些参数了:

1
2
3
<select id="getEmplByIdAndName1" resultType="lwen.entries.Employee">
select * from employee where id=#{id} and name=#{name}
</select>

当然采用命名参数肯定是最好的方式了。

3.传递pojo

如果我们的参数太多了我们使用命名参数就很麻烦,我们就可以自己构建 pojo 。但是如果这些属性之间没什么关系,然后我们不用自己创建一个没什么用的封装类,我们直接使用 map 就好了。然后我们在 sql 就可以直接使用 #{key} 就可以获取对应的 value 。但是如果这个参数经常被使用的话我们就可以自己封装一个类数据传输类 TO。

其实真正在 Mybatis 中他自动把我们的参数封装到了 map中所以我们这么写也是很自然的。

1
Employee getEmplByIdAndName3(Map<String, Object> map);

我们的xml 可以直接取出我们的map中的key

1
2
3
<select id="getEmplByIdAndName3" resultType="lwen.entries.Employee">
select * from employee where id=#{id} and name=#{name}
</select>
1
2
3
4
Map<String, Object> map = new HashMap();
map.put("id", 12);
map.put("name", "lwen");
Employee employee4 = mapper.getEmplByIdAndName3(map);

4.List/Array 特殊情况

  • public Employee getEmp(@Param(“id”)Integer id,String lastName);

    取值:id : #{id/param1} lastName :#{param2}

  • public Employee getEmp(Integer id,@Param(“e”)Employee emp);

    取值:id: #{param1} lastName:#{param2.lastName/e.lastName}

特别注意

如果是Collection(List、Set)类型或者是数组,也会特殊处理。也是把传入的list或者数组封装在map中。

比如:public Employee getEmpById(Listids);

取值:取出第一个id的值:#{list[e]}

4.参数处理原理

我们从源码的角度来看看我们的 Mybatis 框架是如何处理我们的传入的参数,以及绑定的参数的。

1.代理对象

首先我们在获取到的 mapper 上打断点,然后 step into:

image

接着我们就能来到代理类了:org.apache.ibatis.binding.MapperProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//放行Object对象的方法,不做代理
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
//真正执行的 sqlSession
return mapperMethod.execute(sqlSession, args);
}

可以看到我们在这里生成了对应方法的代理对象,也就是给我们的 Mapper 接口生成了代理对象,接着就用代理对象 mapperMethod 来调用我们的接口方法。

2.execute核心逻辑

我们单步进入 execute 方法可以看到主要的逻辑如下:

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
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}

也就是我们所有的采用Mapper 接口的方法开发的最后采用的代理生成代理对象以后我们的插删改操作还是调用了我们 sqlSession 底层的 Select、Delete 方法等等。

里面的套路就是先对我们的方法的参数进行转换,然后执行对应的动作方法,传入我们的参数。

3.参数转换

convertArgsToSqlCommandParam() 主要完成了这个功能,这里我们就着重看看这个方法。

最后追溯到下面这个方法,代码补偿我就全部贴上来了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}

我们看看这个方法的执行逻辑:

1.names获取

首先就是一堆names的获取,这个names又是什么?其实这是当前类的一个属性,记录的东西是Mapper 接口的参数的名字他是在这个类的构造方法中初始化的,也就是在我们调用方法之前 names 就已经确定好了。

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
// get names from @Param annotations
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
if (isSpecialParameter(paramTypes[paramIndex])) {
// skip special parameters
continue;
}
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
hasParamAnnotation = true;
name = ((Param) annotation).value();
break;
}
}
if (name == null) {
// @Param was not specified.
if (config.isUseActualParamName()) {
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// use the parameter index as the name ("0", "1", ...)
// gcode issue #71
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}

这就是 ParamNameResolver 的构造函数,里面最重要的逻辑就是确定我们的 names :

1.获取 @param 注解标注的参数名,然后把这个注解的value值作为 names 的value,然后把参数的位置作为 key 。比如我们的方法是 findAll(@Parma("name") String name,Integer age) 最后生成的names这个map里面就是 {0->name,1->1} 至于为什么是 1->1 我们接下来再说

2.如果我们的 @param 不存在的话并且我们配置了 isUseActualParamName 我们会尝试采用 JDK1.8 的新特性也就是使用 -paramters 编译参数,通过反射直接获取到我们方法的参数名。

3.如果还是不行的话我们就使用参数的索引值,也就是 0,1 上面的注释也是特别清楚。

2.方法没有参数

没有参数的时候就会直接返回null

3.单参数

只有一个参数并且没有 @param 注解的时候,我们就直接获取names的第一个key也就是0

4.多参数

这个地方有两部分,当有 @Param 注解的时候就是把 names 的 value 作为 key ,然后我们的真正的参数作为 value 。然后当然为了保险起见 Mybatis 还未每一个参数生成了一个 以 paramIndex 作为key 以值作为 value 的 也就是我们用的 param0 .. paramN .可是我们会说为啥我们还可以用 args0 和 args1 来取值呢?

这个就看上面的 names 获取值的时候我们可以采用编译器的反射机制获取,因为如果我们编译器不支持这个特性的话我们的参数就会被抹掉,用args0 args1 来代替。

5.取值规则

1.#{}与${}区别

#{}:是以预编译的形式,将参数设置到sq1语句中;PreparedStatement;防止sq1注入

${}:取出的值直接拼装在sq1语句中;会有安全问题;
大多情况下,我们去参数的值都应该去使用#{};
原生jdbc不支持占位符的地方我们就可以使用${}进行取值,比如分表;按照年份分表拆分
select * from ${year}_salary where xxx;

2.#{}注意事项

在#{} 中我们可以放入 javaType、jdbcType、mode(存储过程)、numericScale、
resultMap、typeHandler、jdbcTypeName、expression(未来准备支持的功能);

比较重要的就是:jdbcType通常需要在某种特定的条件下被设置:
在我们数据为null的时候,有些数据库可能不能识别mybatis对nu11的默认处理。比如Oracle(报错),因为他们对null的映射是到了 Other 类型,然后就会导致JdbcType OTHER:无效的类型;因为mybatis对所有的nu11都映射的是原生Jdbc的OTHER类型,
由于全局配置中:jdbcTypeForNull=OTHER;oracle不支持;两种办法
1、#{email,jdbcType=OTHER};
2、<setting name="jdbcTypeForNull" value="NULL"/>

6.查询

1.返回集合结果

返回集合结果的时候我们的 resultType 不能写成 list 、set 类型而是要写做我们的集合里面的元素的类型。

1
2
3
4
5
<select id="getLikeByName" resultType="lwen.entries.Employee">
select *
from employee
where name like #{name};
</select>

2.返回map

也就是将我们的 bean 的属性作为 key 然后 值作为 value 来存储到一个 map 中,其实这个只存一条数据,那么我们的语句的返回类型就是我们的 map 他会自动的做封装。

1
2
3
<select id="getEmplReturenMap" resultType="map">
select * from employee where id=#{id};
</select>
1
Map<String, Object> getEmplReturenMap(Integer id);

3.返回多条记录的map

如果我们想让map的key是我们的某一条属性,然后value是我们的实体对象,那么我们的封装结果就必须是元素的类型,也就是我们的实体类的类型,但是我们的key则需要我们另行指定我们采用的方式就是使用一个注解在方法上。

1
2
@MapKey("name")
Map<String, Employee> getEmplReturnMaps(String name);
1
2
3
4
<select id="getEmplReturnMaps" resultType="lwen.entries.Employee">
select *
from employee where name like #{name};
</select>

4.返回自定义结果集

有时候对于Mybatis自带的一些默认封装的规则不能满足我们的需求的时候,我们可以采用 resultMap 自定义结果集。这里就演示一些自定义结果集,但是注意 resultMap 与 resultType 只能存在一个。

1
2
3
4
5
6
7
8
9
10
11
<resultMap id="MyEmp" type="lwen.entries.Employee">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
</resultMap>

<select id="getEmplByResultMap" resultMap="MyEmp">
select *
from employee
where id = #{id};
</select>

可以看到重点就是写我们的 resultMap 标签,然后再标签中我们自己封装规则。注意的一点就是如果说我们在 result 中没有封装 bean 中的其他属性他会自动帮我们封装,也就是我们可以把一些特殊的字段和我们的 bean 结合起来。其他的正常的自动封装。然后就是 id 字段我们在 resultMap 中指明以后Mybatis就会帮我们自动封装,并且做一些查询优化。

5.关联查询

1.union

1
2
3
4
<select id="getById" resultMap="EmpNew">
select employee.id id,employee.name name,age,d_id did,department.name dname
from employee,department where employee.id=#{id} and employee.d_id=department.id;
</select>

我们采用的联合查询,得到了关联查询的结果,这也是常用的套路,但是注意的一点就是我们的 result 的封装并不是使用的自带的封装规则而是采用了 我们自定义的 resultMap 因为我们的 employee 中有一个 department 的对象,我们无法直接封装,至少列名都没办法对应。

1
2
3
4
5
6
7
<resultMap id="EmpNew" type="lwen.entries.EmployeeNew">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
<result column="did" property="department.id"/>
<result column="dname" property="department.name"/>
</resultMap>

2.association关联

使用 association 标签可以给一个 javaBean 的内部引用创建关联关系,与上面的效果类似,但是方法不同。

1
2
3
4
5
6
7
8
9
<resultMap id="EmpNew1" type="lwen.entries.EmployeeNew">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
<association property="department" javaType="lwen.entries.Department">
<id column="did" property="id"/>
<result column="dname" property="name"/>
</association>
</resultMap>
1
2
3
4
<select id="getByIdAssociation" resultMap="EmpNew1">
select employee.id id,employee.name name,age,d_id did,department.name dname
from employee,department where employee.id=#{id} and employee.d_id=department.id;
</select>

3.association多步查询

当我们需要 department 的 id 然后作为我们封装 Employee 的条件的时候我们就需要用 id 查询这个 Department

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<resultMap id="EmpNew2" type="EmployeeNew">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
<association property="department" select="lwen.dao.DepartmentMapper.getDepById" column="d_id">
</association>
</resultMap>

<select id="getEmplStepByStep" resultMap="EmpNew2">
select * from employee where id=#{id}
</select>

<select id="getDepById" resultType="lwen.entries.Department">
select * from department where id=#{id}
</select>

可以看到我们在 association 中调用了 lwen.dao.DepartmentMapper.getDepById 这个方法其实就是只进行了一个简单的 按照 id 查询,然后我们传入的 column 就是作为我们查询 department 的参数。最后做关联。

4.懒加载

需要我们在以前的分布查询的基础之上添加上一个配置项,这个配置项是在我们的全局配置文件中的。

1
2
<setting name="lazyloadingEnabled" value="true"/>
<setting name="aggressivelazyLoading" value="false"/>

配置这两项的时候我们在查询获取一个对象的时候我们只有在引用他们的值得时候才真的去加载这些东西,否则不会加载。

5.一对多映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<resultMap id="EmpNew3" type="Department">
<id column="id" property="id"/>
<result column="name" property="name"/>
<association property="employees" select="lwen.dao.EmployeeNewMapper.getEmplsByDeptId" column="id"/>
</resultMap>

<select id="getEmplStepByDepIdStep" resultMap="EmpNew3">
select *
from department where id=#{id};
</select>

<select id="getEmplsByDeptId" resultType="lwen.entries.Employee">
select * from employee where d_id=#{did}
</select>