---
- hosts: 192.168.26.21
  gather_facts: True

  tasks:
    # common tasks
    - name: Install epel-release
      yum: 
        name: epel-release
        state: present 

    - name: Temporary Disable SELinux
      command: setenforce 0

    - name: Disable SELinux
      shell: sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config

    # Install apache server
    - name: Install httpd
      yum:
        name: httpd
        state: present
      
    - name: Start httpd
      service:
        name: httpd
        state: started
        enabled: True

    - name: Permit 80/tcp port
      firewalld:
        service: http
        permanent: True
        immediate: True
        state: enabled

    #Install php82
    - name: Install epel-release-latest-7
      yum:
        name: https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
        state: present
    
    - name: Install remirepo
      yum:
        name: https://rpms.remirepo.net/enterprise/remi-release-7.rpm
        state: present
      
    - name: Enable remi-php82 repo
      shell: yum-config-manager --enable remi-php82

    - name: Install php82
      yum:
        name:
          - php82-php
          - php82-php-mbstring
          - php82-php-mysqlnd
          - php82-php-gd
          - php82-php-xml
        state: present

    - name: Install mariadb-server
      yum:
        name: mariadb-server
        state: present
      
    - name: Start mariadb-server
      service:
        name: mariadb
        state: started
        enabled: True
    
    - name: Install pip
      yum:
        name: python2-pip
        state: present

    - name: Install Python Library
      pip:
        name: PyMySQL==0.10.0
        state: present

    - name: Create Database
      mysql_db:
        name: wordpressdb
        state: present

    - name: Create database user
      mysql_user:
        name: wordpress
        password: wordpress
        priv: 'wordpressdb.*:ALL'
        state: present

    # Install wordpress
    - name: Install wordpress
      get_url:
        url: https://wordpress.org/wordpress-6.2.tar.gz
        dest: /tmp/wordpress-6.2.tar.gz
        checksum: md5:34f279efe623025641bc11a69e3c02fa

    - name: Extract wordpress-6.X.tar.gz into /var/www/wordpress
      unarchive:
        src: /tmp/wordpress-6.2.tar.gz
        dest: /var/www/
        remote_src: True

    - name: Recursive Modify file permission
      file:
        path: /var/www/wordpress
        owner: apache
        group: apache
        mode: '0766'
        recurse: True
        state: directory
      
    - name: Add apache virtual host config
      copy:
        src: wordpress.conf
        dest: /etc/httpd/conf.d/wordpress.conf

    - name: Restart httpd
      service:
        name: httpd
        state: restarted

    - name: Add wordpress config
      copy:
        src: wp-config.php
        dest: /var/www/wordpress/wp-config.php
        owner: apache
        group: apache
        mode: '0744'

一、课前准备

二、环境简述

  • node20, 192.168.26.20/24, gw: 192.168.26.2, ansible管理端
  • node21, 192.168.26.21/24, gw: 192.168.26.2,被管理端
  • node22, 192.168.26.22/24, gw: 192.168.26.2,被管理端
  • node23, 192.168.26.23/24, gw: 192.168.26.2, 被管理端

三、搭建流程

  1. 最小化安装1台CentOS,安装后关机,给虚拟机创建一个快照;
  2. 从快照克隆新虚拟机,使用链接克隆方式,克隆出4台虚拟机;
  3. 打开这4台虚拟机,配置静态ip地址、主机名,完成后开机;
  4. 在node20上安装Ansible,使用ssh-keygen生成服务端公钥对;
  5. 在node20上使用copy-ssh-id命令,与node21/22/23配置无密码访问;
  6. 完成后关机,对node21/22/23创建虚拟机快照;

四、本地配置

  • 下载安装git客户端msi安装包, https://www.git-scm.com/
  • 下载安装VSCode,https://code.visualstudio.com/
  • 在VSCode插件库里安装Remote-SSH插件
  • git客户端安装后桌面右键Git bash here,使用ssh-keygen生成密钥对,使用ssh-copy-id连接node20配置无密码登录
  • 打开VScode,在remote-ssh插件配置文件中填写如下内容,配置文件位置:C:\Users\lixm.ssh\config

    Host 192.168.26.20
    HostName 192.168.26.20
    User root
    Port 22
    IdentityFile "C:\Users\lixm\.ssh\id_rsa" 
  • 完成后即可在本地Windows远程编辑Linux主机中的文件

五、 LAMP环境搭建步骤

1. Install Base Environment

  • epel-release
  • turn off selinux

2. Install Apache Server

  • Install httpd
  • Set httpd service enable and start service
  • Open http 80/tcp port

3. Install PHP

  • Install remi-repo
  • Install php82 packages
  • Restart httpd

4. Install Mariadb

  • Install mariadb-server
  • Set mariadb service enable and start service
  • Install Python Library PyMySQL
  • Create database wordpressdb
  • Create user wordpress@localhost and grant permission to access wordpressdb

5. Install Wordpress

  • Download wordpress package
  • Unarchive package to target directory
  • Recursive modify files permission
  • Upload httpd virtualhost config file
  • Restart httpd service
  • Upload wordpress config file

六、other

pip install PyMySQL==0.10.0 -i https://mirrors.aliyun.com/pypi/simple/

开发应用程序时,我们会使用开发环境,例如,使用内存数据库以便快速启动。而运行在生产环境时,我们会使用生产环境,例如,使用MySQL数据库。如果应用程序可以根据自身的环境做一些适配,无疑会更加灵活。

Spring为应用程序准备了Profile这一概念,用来表示不同的环境。例如,我们分别定义开发、测试和生产这3个环境:

  • native
  • test
  • production

创建某个Bean时,Spring容器可以根据注解@Profile来决定是否创建。例如,以下配置:

@Configuration
@ComponentScan
public class AppConfig {
    @Bean
    @Profile("!test")
    ZoneId createZoneId() {
        return ZoneId.systemDefault();
    }

    @Bean
    @Profile("test")
    ZoneId createZoneIdForTest() {
        return ZoneId.of("America/New_York");
    }
}

如果当前的Profile设置为test,则Spring容器会调用createZoneIdForTest()创建ZoneId,否则,调用createZoneId()创建ZoneId。注意到@Profile("!test")表示非test环境。

在运行程序时,加上JVM参数-Dspring.profiles.active=test就可以指定以test环境启动。

实际上,Spring允许指定多个Profile,例如:

-Dspring.profiles.active=test,master

可以表示test环境,并使用master分支代码。

要满足多个Profile条件,可以这样写:

@Bean
@Profile({ "test", "master" }) // 同时满足test和master
ZoneId createZoneId() {
    ...
}

使用Conditional

除了根据@Profile条件来决定是否创建某个Bean外,Spring还可以根据@Conditional决定是否创建某个Bean。

例如,我们对SmtpMailService添加如下注解:

@Component
@Conditional(OnSmtpEnvCondition.class)
public class SmtpMailService implements MailService {
    ...
}

它的意思是,如果满足OnSmtpEnvCondition的条件,才会创建SmtpMailService这个Bean。OnSmtpEnvCondition的条件是什么呢?我们看一下代码:

public class OnSmtpEnvCondition implements Condition {
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return "true".equalsIgnoreCase(System.getenv("smtp"));
    }
}

因此,OnSmtpEnvCondition的条件是存在环境变量smtp,值为true。这样,我们就可以通过环境变量来控制是否创建SmtpMailService

Spring只提供了@Conditional注解,具体判断逻辑还需要我们自己实现。Spring Boot提供了更多使用起来更简单的条件注解,例如,如果配置文件中存在app.smtp=true,则创建MailService

@Component
@ConditionalOnProperty(name="app.smtp", havingValue="true")
public class MailService {
    ...
}

如果当前classpath中存在类javax.mail.Transport,则创建MailService

@Component
@ConditionalOnClass(name = "javax.mail.Transport")
public class MailService {
    ...
}

后续我们会介绍Spring Boot的条件装配。我们以文件存储为例,假设我们需要保存用户上传的头像,并返回存储路径,在本地开发运行时,我们总是存储到文件:

@Component
@ConditionalOnProperty(name = "app.storage", havingValue = "file", matchIfMissing = true)
public class FileUploader implements Uploader {
    ...
}

在生产环境运行时,我们会把文件存储到类似AWS S3上:

@Component
@ConditionalOnProperty(name = "app.storage", havingValue = "s3")
public class S3Uploader implements Uploader {
    ...
}

其他需要存储的服务则注入Uploader

@Component
public class UserImageService {
    @Autowired
    Uploader uploader;
}

当应用程序检测到配置文件存在app.storage=s3时,自动使用S3Uploader,如果存在配置app.storage=file,或者配置app.storage不存在,则使用FileUploader

可见,使用条件注解,能更灵活地装配Bean。

小结

Spring允许通过@Profile配置不同的Bean;

Spring还提供了@Conditional来进行条件装配,Spring Boot在此基础上进一步提供了基于配置、Class、Bean等条件进行装配。

在开发应用程序时,经常需要读取配置文件。最常用的配置方法是以key=value的形式写在.properties文件中。

例如,MailService根据配置的app.zone=Asia/Shanghai来决定使用哪个时区。要读取配置文件,我们可以使用上一节讲到的Resource来读取位于classpath下的一个app.properties文件。但是,这样仍然比较繁琐。

Spring容器还提供了一个更简单的@PropertySource来自动读取配置文件。我们只需要在@Configuration配置类上再添加一个注解:

@Configuration
@ComponentScan
@PropertySource("app.properties") // 表示读取classpath的app.properties
public class AppConfig {
    @Value("${app.zone:Z}")
    String zoneId;

    @Bean
    ZoneId createZoneId() {
        return ZoneId.of(zoneId);
    }
}

Spring容器看到@PropertySource("app.properties")注解后,自动读取这个配置文件,然后,我们使用@Value正常注入:

@Value("${app.zone:Z}")
String zoneId;

注意注入的字符串语法,它的格式如下:

  • "${app.zone}"表示读取key为app.zone的value,如果key不存在,启动将报错;
  • "${app.zone:Z}"表示读取key为app.zone的value,但如果key不存在,就使用默认值Z

这样一来,我们就可以根据app.zone的配置来创建ZoneId

还可以把注入的注解写到方法参数中:

@Bean
ZoneId createZoneId(@Value("${app.zone:Z}") String zoneId) {
    return ZoneId.of(zoneId);
}

可见,先使用@PropertySource读取配置文件,然后通过@Value${key:defaultValue}的形式注入,可以极大地简化读取配置的麻烦。

另一种注入配置的方式是先通过一个简单的JavaBean持有所有的配置,例如,一个SmtpConfig

@Component
public class SmtpConfig {
    @Value("${smtp.host}")
    private String host;

    @Value("${smtp.port:25}")
    private int port;

    public String getHost() {
        return host;
    }

    public int getPort() {
        return port;
    }
}

然后,在需要读取的地方,使用#{smtpConfig.host}注入:

@Component
public class MailService {
    @Value("#{smtpConfig.host}")
    private String smtpHost;

    @Value("#{smtpConfig.port}")
    private int smtpPort;
}

注意观察#{}这种注入语法,它和${key}不同的是,#{}表示从JavaBean读取属性。"#{smtpConfig.host}"的意思是,从名称为smtpConfig的Bean读取host属性,即调用getHost()方法。一个Class名为SmtpConfig的Bean,它在Spring容器中的默认名称就是smtpConfig,除非用@Qualifier指定了名称。

使用一个独立的JavaBean持有所有属性,然后在其他Bean中以#{bean.property}注入的好处是,多个Bean都可以引用同一个Bean的某个属性。例如,如果SmtpConfig决定从数据库中读取相关配置项,那么MailService注入的@Value("#{smtpConfig.host}")仍然可以不修改正常运行。

小结

Spring容器可以通过@PropertySource自动读取配置,并以@Value("${key}")的形式注入;

可以通过${key:defaultValue}指定默认值;

#{bean.property}形式注入时,Spring容器自动把指定Bean的指定属性值注入。

在Java程序中,我们经常会读取配置文件、资源文件等。使用Spring容器时,我们也可以把“文件”注入进来,方便程序读取。

例如,AppService需要读取logo.txt这个文件,通常情况下,我们需要写很多繁琐的代码,主要是为了定位文件,打开InputStream。

Spring提供了一个org.springframework.core.io.Resource(注意不是javax.annotation.Resource),它可以像Stringint一样使用@Value注入:

@Component
public class AppService {
    @Value("classpath:/logo.txt")
    private Resource resource;

    private String logo;

    @PostConstruct
    public void init() throws IOException {
        try (var reader = new BufferedReader(
                new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
            this.logo = reader.lines().collect(Collectors.joining("\n"));
        }
    }
}

注入Resource最常用的方式是通过classpath,即类似classpath:/logo.txt表示在classpath中搜索logo.txt文件,然后,我们直接调用Resource.getInputStream()就可以获取到输入流,避免了自己搜索文件的代码。

也可以直接指定文件的路径,例如:

@Value("file:/path/to/logo.txt")
private Resource resource;

但使用classpath是最简单的方式。上述工程结构如下:

spring-ioc-resource
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── itranswarp
        │           └── learnjava
        │               ├── AppConfig.java
        │               └── AppService.java
        └── resources
            └── logo.txt

使用Maven的标准目录结构,所有资源文件放入src/main/resources即可。

练习

使用Spring的Resource注入app.properties文件,然后读取该配置文件。

小结

Spring提供了Resource类便于注入资源文件。

最常用的注入是通过classpath以classpath:/path/to/file的形式注入。