Nacos 配置中心基本使用(十六)


基於dataid為yaml的文件擴展配置

spring-cloud-starter-alibaba-nacos-confifig默認支持的文件格式是properties, 如果我們想用其他格式的文件,可以只需要完成以下兩步:
1、在應用的 bootstrap.properties 配置文件中顯示的聲明 dataid 文件擴展名。如下所示bootstrap.properties
spring.cloud.nacos.config.file-extension=yaml
2、在Nacos控制台,修改配置文件的類型,改成yml

針對profifile粒度配置

spring-cloud-starter-alibaba-nacos-confifig 在加載配置的時候,不僅僅加載了以 dataid 為${spring.application.name}.${file-extension:properties} 為前綴的基礎配置,還加載了dataid為 ${spring.application.name}-${profile}.${file-extension:properties} 的基礎配置。在日常開發中如果遇到多套環境下的不同配置,可以通過Spring 提供的${spring.profiles.active} 這個配置項來配置。
  • 在bootstrap.properties中添加profifile
spring.profiles.active=develop
  • Nacos 上新增一個dataid為:nacos-confifig-develop.yaml的基礎配置,如下所示:
Data ID: nacos-dubbo-provider-develop.yaml 
Group : DEFAULT_GROUP 
配置格式: YAML 
配置內容: current.env: develop-env
如果需要切換到生產環境,只需要更改 ${spring.profiles.active} 參數配置即可。如下所示:
spring.profiles.active=product

Nacos 中的Namespace和Group

在nacos中提供了namespace和group命名空間和分組的機制。,它是Nacos提供的一種數據模型,也就是我們要去定位到一個配置,需要基於namespace- > group ->dataid來實現。namespace可以解決多環境以及多租戶數據的隔離問題。比如在多套環境下,可以根據指定環境創建不同的namespace,實現多環境隔離。或者在多租戶的場景中,每個用戶可以維護自己的namespace,實現每個用戶的配置數據和注冊數據的隔離。group是分組機制,它的緯度是實現服務注冊信息或者DataId的分組管理機制,對於group的用法,沒有固定的規則,它也可以實現不同環境下的分組,也可以實現同一個應用下不同配置類型或者不同業務類型的分組。
 
官方建議是,namespace用來區分不同環境,group可以專注在業務層面的數據分組。實際上在使用過程中,最重要的是提前定要統一的口徑和規定,避免不同的項目團隊混用導致后期維護混亂的問題。

自定義namespace

在沒有明確指定 ${spring.cloud.nacos.config.namespace} 配置的情況下, 默認使用的是 Nacos上 Public 這個namespae。如果需要使用自定義的命名空間,可以通過以下配置來實現:
 
spring.cloud.nacos.config.namespace=b3404bc0-d7dc-4855-b519-570ed34b62d7
該配置必須放在 bootstrap.properties 文件中。此外spring.cloud.nacos.config.namespace 的值是 namespace 對應的 id,id 值可以在 Nacos的控制台獲取。
並且在添加配置時注意不要選擇其他的 namespae,否則將會導致讀取不到正確的配置。

自定義group

在沒有明確指定 ${spring.cloud.nacos.config.group} 配置的情況下, 默認使用的是DEFAULT_GROUP 。如果需要自定義自己的 Group,可以通過以下配置來實現:
spring.cloud.nacos.config.group=DEVELOP_GROUP
該配置必須放在 bootstrap.properties 文件中。並且在添加配置時 Group 的值一定要和spring.cloud.nacos.config.group 的配置值一致

自定義擴展的DataId

Spring Cloud Alibaba Nacos Confifig 從 0.2.1 版本后,可支持自定義 Data Id 的配置。關於這部分詳細的設計可參考 這里。 一個完整的配置案例如下所示:
spring.application.name=opensource-service-provider 
spring.cloud.nacos.config.server-addr=127.0.0.1:8848

# config external configuration
# 1、Data Id 在默認的組 DEFAULT_GROUP,不支持配置的動態刷新
spring.cloud.nacos.config.extension-configs[0].data-id=ext-config-common01.properties
# 2、Data Id 不在默認的組,不支持動態刷新
spring.cloud.nacos.config.extension-configs[1].data-id=ext-config-common02.properties
spring.cloud.nacos.config.extension-configs[1].group=GLOBALE_GROUP
# 3、Data Id 既不在默認的組,也支持動態刷新
spring.cloud.nacos.config.extension-configs[2].data-id=ext-config-common03.properties
spring.cloud.nacos.config.extension-configs[2].group=REFRESH_GROUP
spring.cloud.nacos.config.extension-configs[2].refresh=true
可以看到:
  • 通過 spring.cloud.nacos.config.extension-configs[n].data-id 的配置方式來支持多個Data Id 的配置。
  • 通過 spring.cloud.nacos.config.extension-configs[n].group 的配置方式自定義 Data Id所在的組,不明確配置的話,默認是 DEFAULT_GROUP。
  • 通過 spring.cloud.nacos.config.extension-configs[n].refresh 的配置方式來控制該Data Id 在配置變更時,是否支持應用中可動態刷新, 感知到最新的配置值。默認是不支持的。
多個 Data Id 同時配置時,他的優先級關系是 spring.cloud.nacos.config.extension-configs[n].data-id 其中 n 的值越大,優先級越高。
Note spring.cloud.nacos.config.extension-configs[n].data-id 的值必須帶文件擴展名,文件擴展名既可支持 properties,又可以支持 yaml/yml。 此時spring.cloud.nacos.config.file-extension 的配置對自定義擴展配置的 Data Id 文件擴展名沒有影響;通過自定義擴展的 Data Id 配置,既可以解決多個應用間配置共享的問題,又可以支持一個應用有多個配置文件。為了更加清晰的在多個應用間配置共享的 Data Id ,你可以通過以下的方式來配置:通過自定義擴展的 Data Id 配置,既可以解決多個應用間配置共享的問題,又可以支持一個應用有多個配置文件。為了更加清晰的在多個應用間配置共享的 Data Id ,你可以通過以下的方式來配置:
 
# 配置支持共享的 Data Id 
spring.cloud.nacos.config.shared-configs[0].data-id=common.yaml
# 配置 Data Id 所在分組,缺省默認 DEFAULT_GROUP
spring.cloud.nacos.config.shared-configs[0].group=GROUP_APP1
# 配置Data Id 在配置變更時,是否動態刷新,缺省默認 false
spring.cloud.nacos.config.shared-configs[0].refresh=true
可以看到:
  • 通過 spring.cloud.nacos.config.shared-configs[n].data-id 來支持多個共享 Data Id 的配置。
  • 通過 spring.cloud.nacos.config.shared-configs[n].group 來配置自定義 Data Id 所在的組,不明確配置的話,默認是 DEFAULT_GROUP。
  • 通過 spring.cloud.nacos.config.shared-configs[n].refresh 來控制該Data Id在配置變更時,是否支持應用中動態刷新,默認false。

配置的優先級

Spring Cloud Alibaba Nacos Confifig 目前提供了三種配置能力從 Nacos 拉取相關的配置。
A: 通過 spring.cloud.nacos.config.shared-configs[n].data-id 支持多個共享 Data Id 的配置
B: 通過 spring.cloud.nacos.config.extension-configs[n].data-id 的方式支持多個擴展Data Id 的配置
C: 通過內部相關規則(應用名、應用名+ Profifile )自動生成相關的 Data Id 配置當三種方式共同使用時,他們的一個優先級關系是:A < B < C

配置動態刷新

配置的動態刷新,僅需要使用@RefreshScope注解即可。

使用配置類來創建bean時,若要實現注入bean的刷新,需要在配置類和Bean創建方法上均加上@RefreshScope注解。在對應配置被修改后,所有開啟了刷新的注入bean在下一次調用時會重新進行初始化並替換掉之前注入的Bean。因bean替換時,棄用的bean會執行銷毀方法來釋放資源,故自定義Bean建議實現Closeable接口。

需要注意的是,由於@RefreshScope注解底層是使用cglib動態代理來實現,而cglib是創建動態子類繼承來完成功能的增強,在使用@RefreshScope注解刷新包含final屬性/final方法的bean時,會導致返回的代理對象為null的情況。典型的例子比如Elasticsearch的RestHighLevelClient。此時需要將需要刷新的Bean封裝一層,避免final屬性/final方法的問題。

下面是常用的數據源類型bean的動態刷新配置代碼:

Nacos配置
mybatis:
  datasource:
    enable: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
    minIdle: 5
    validationQuery: select version()
    keepAlive: true
    initialSize: 5
    maxWait: 60000
    poolPreparedStatements: true
    filters: stat,config
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: org.postgresql.Driver
    url: jdbc:postgresql://127.0.0.1:5432/postgres
    username: xxx
    password: xxx
    maxPoolPreparedStatementPerConnectionSize: 20
    testOnBorrow: true
    testWhileIdle: true
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    testOnReturn: false
    maxActive: 30
    validationQueryTimeout: 10
    
jedispool:
  config:
    enable: true
    host: 127.0.0.1
    port: 6379
    password:
    timeOut: 30000
    maxIdle: 500
    maxWaitMillis: 50000
    maxTotal: 10000
 
es:
  config:
    enable: true
    ip: 127.0.0.1
    port: 9200

基於Jedis的redis數據源

配置類

package cn.com.geostar.datasource;
 
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
 
/**
 * @author xiawei
 * @date 2020/8/10 11:33
 */
 
@Component
@RefreshScope
@ConfigurationProperties(prefix = "jedispool.config")
public class JedisPoolConfigure {
    private boolean enable;
    private String host;
    private Integer port;
    private String password;
    private Integer timeOut;
    private Integer maxIdle;
    private Integer maxWaitMillis;
    private Integer maxTotal;
 
 // getter setter
}

Bean創建

 /**
     * jedis連接池,開啟刷新
     */
    @Bean
    @RefreshScope
    public JedisPool jedisPool() {
        if (!poolConfig.isEnable()) {
            logger.warn("redis datasource is disable !");
            return null;
        }
 
        String host = poolConfig.getHost();
        Integer port = poolConfig.getPort();
 
        if (StringUtils.isBlank(host) || port == null) {
            logger.warn("redis datasource config: host/port must be specified!");
            return null;
        }
 
        int timeOut = Optional.ofNullable(poolConfig.getTimeOut()).orElseGet(() -> {
            logger.info("redis datasource config: timeOut not be specified, used default value 30000 .");
            return 30000;
        });
 
        Integer maxTotal = Optional.ofNullable(poolConfig.getMaxTotal()).orElseGet(() -> {
            logger.info("redis datasource config: maxTotal not be specified, used default value 10000 .");
            return 10000;
        });
        Integer maxIdle = Optional.ofNullable(poolConfig.getMaxIdle()).orElseGet(() -> {
            logger.info("redis datasource config: maxIdle not be specified, used default value 500 .");
            return 500;
        });
        Integer maxWaitMillis = Optional.ofNullable(poolConfig.getTimeOut()).orElseGet(() -> {
            logger.info("redis datasource config: maxWaitMillis not be specified, used default value 50000 .");
            return 50000;
        });
        String password = poolConfig.getPassword();
 
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(maxTotal);
        config.setMaxIdle(maxIdle);
        config.setMaxWaitMillis(maxWaitMillis);
        if (StringUtils.isBlank(password)) {
            logger.info("redis datasource init successful, host = " + host + ", port = " + port);
            return new JedisPool(config, host, port, timeOut);
        } else {
            logger.info("redis datasource init successful, host = " + host + ", port = " + port);
            return new JedisPool(config, host, port, timeOut, password);
        }
    }

基於官方RestHighLevelClient的ES數據源

Client封裝類

package cn.com.geostar.datasource;
 
import org.elasticsearch.client.RestHighLevelClient;
 
import java.io.Closeable;
import java.io.IOException;
 
/**
 * @author xiawei
 * @date 2020/8/11 9:29
 */
public class ElasticSearchRestClient implements Closeable {
    private RestHighLevelClient restHighLevelClient;
 
    public RestHighLevelClient getRestHighLevelClient() {
        return restHighLevelClient;
    }
 
    public void setRestHighLevelClient(RestHighLevelClient restHighLevelClient) {
        this.restHighLevelClient = restHighLevelClient;
    }
 
    @Override
    public void close() throws IOException {
        restHighLevelClient.close();
    }
}

配置類

package cn.com.geostar.datasource;
 
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
 
/**
 * @author xiawei
 * @date 2020/8/10 14:26
 */
@Component
@RefreshScope
@ConfigurationProperties(prefix = "es.config")
public class ElasticSearchConfigure {
    private String ip;
    private Integer port;
    private boolean enable;
 
    // getter setter
}

Bean創建

 /**
     * es初始化,開啟刷新
     */
    @Bean
    @RefreshScope
    public ElasticSearchRestClient restHighLevelClient() {
        String ip = elasticSearchConfig.getIp();
        Integer port = elasticSearchConfig.getPort();
        if (!elasticSearchConfig.isEnable()) {
            logger.warn("elasticsearch datasource is disable!");
            return null;
        }
        if (StringUtils.isBlank(ip) || port == null) {
            logger.warn("elasticsearch datasource config: ip/port must be specified!");
            return null;
        }
        RestClientBuilder builder = RestClient.builder(new HttpHost(ip, port, "http"));
        RestHighLevelClient client = new RestHighLevelClient(builder);
        ElasticSearchRestClient esrc = new ElasticSearchRestClient();
        esrc.setRestHighLevelClient(client);
        logger.info("elasticsearch datasource init successful, ip = " + ip + ", port = " + port);
        return esrc;
    }

基於Druid連接池和Mybatis持久層框架的關系數據庫

DataSource配置類

package cn.com.geostar.datasource;
 
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
 
/**
 * @author xiawei
 * @date 2020/8/11 11:25
 */
@Component
@RefreshScope
@ConfigurationProperties(prefix = "mybatis.datasource")
public class MybatisDataSourceConfig {
    private boolean enable;
    private String connectionProperties;
    private Integer minIdle;
    private String validationQuery;
    private Boolean keepAlive;
    private Integer initialSize;
    private Integer maxWait;
    private Boolean poolPreparedStatements;
    private String filters;
    private String type;
    private String driverClassName;
    private String url;
    private String username;
    private String password;
    private Integer maxPoolPreparedStatementPerConnectionSize;
    private Boolean testOnBorrow;
    private Boolean testWhileIdle;
    private Integer timeBetweenEvictionRunsMillis;
    private Integer minEvictableIdleTimeMillis;
    private Boolean testOnReturn;
    private Integer maxActive;
    private Integer validationQueryTimeout;
 
   // getter setter
}

Bean創建

 @Bean
    @RefreshScope
    public DataSource dataSource() throws SQLException {
        if (!mybatisDataSourceConfig.isEnable()) {
            logger.warn("mybatis datasource is disable !");
            return null;
        }
        String validationQuery = mybatisDataSourceConfig.getValidationQuery();
        String filters = mybatisDataSourceConfig.getFilters();
        String driverClassName = mybatisDataSourceConfig.getDriverClassName();
        String url = mybatisDataSourceConfig.getUrl();
        String username = mybatisDataSourceConfig.getUsername();
        String password = mybatisDataSourceConfig.getPassword();
 
        if (StringUtils.isBlank(validationQuery) || StringUtils.isBlank(filters) || StringUtils.isBlank(driverClassName)
                || StringUtils.isBlank(url) || StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
            logger.warn("mybatis datasource config: validationQuery、filters、driverClassName、url、username、password must be specified!");
            return null;
        }
 
        DruidDataSource druidDataSource = checkDruidDataSourceParams(mybatisDataSourceConfig);
        logger.info("druidDataSource init successful, url = " + url);
        return druidDataSource;
    }
 
    private DruidDataSource checkDruidDataSourceParams(MybatisDataSourceConfig mybatisDataSourceConfig) throws SQLException {
        String connectionProperties = Optional.ofNullable(mybatisDataSourceConfig.getConnectionProperties())
                .orElseGet(() -> {
                    logger.info("mybatis datasource config: connectionProperties not be specified," +
                            " used default value {druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000} .");
                    return "druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000";
                });
        Integer minIdle = Optional.ofNullable(mybatisDataSourceConfig.getMinIdle())
                .orElseGet(() -> {
                    logger.info("mybatis datasource config: minIdle not be specified, used default value {5} .");
                    return 5;
                });
        Boolean keepAlive = Optional.ofNullable(mybatisDataSourceConfig.getKeepAlive())
                .orElseGet(() -> {
                    logger.info("mybatis datasource config: keepAlive not be specified, used default value {true} .");
                    return true;
                });
        Integer initialSize = Optional.ofNullable(mybatisDataSourceConfig.getInitialSize())
                .orElseGet(() -> {
                    logger.info("mybatis datasource config: initialSize not be specified, used default value {5} .");
                    return 5;
                });
        Integer maxWait = Optional.ofNullable(mybatisDataSourceConfig.getMaxWait())
                .orElseGet(() -> {
                    logger.info("mybatis datasource config: maxWait not be specified, used default value {60000} .");
                    return 60000;
                });
        Boolean poolPreparedStatements = Optional.ofNullable(mybatisDataSourceConfig.getPoolPreparedStatements())
                .orElseGet(() -> {
                    logger.info("mybatis datasource config: poolPreparedStatements not be specified, used default value {true} .");
                    return true;
                });
        Integer maxPoolPreparedStatementPerConnectionSize = Optional.ofNullable(mybatisDataSourceConfig.getMaxPoolPreparedStatementPerConnectionSize())
                .orElseGet(() -> {
                    logger.info("mybatis datasource config: maxPoolPreparedStatementPerConnectionSize not be specified, used default value {20} .");
                    return 20;
                });
        Boolean testOnBorrow = Optional.ofNullable(mybatisDataSourceConfig.getTestOnBorrow())
                .orElseGet(() -> {
                    logger.info("mybatis datasource config: testOnBorrow not be specified, used default value {true} .");
                    return true;
                });
        Boolean testWhileIdle = Optional.ofNullable(mybatisDataSourceConfig.getTestWhileIdle())
                .orElseGet(() -> {
                    logger.info("mybatis datasource config: testWhileIdle not be specified, used default value {true} .");
                    return true;
                });
        Integer timeBetweenEvictionRunsMillis = Optional.ofNullable(mybatisDataSourceConfig.getTimeBetweenEvictionRunsMillis())
                .orElseGet(() -> {
                    logger.info("mybatis datasource config: timeBetweenEvictionRunsMillis not be specified, used default value {60000} .");
                    return 60000;
                });
        Integer minEvictableIdleTimeMillis = Optional.ofNullable(mybatisDataSourceConfig.getMinEvictableIdleTimeMillis())
                .orElseGet(() -> {
                    logger.info("mybatis datasource config: minEvictableIdleTimeMillis not be specified, used default value {300000} .");
                    return 300000;
                });
        Boolean testOnReturn = Optional.ofNullable(mybatisDataSourceConfig.getTestOnReturn())
                .orElseGet(() -> {
                    logger.info("mybatis datasource config: testOnReturn not be specified, used default value {false} .");
                    return false;
                });
        Integer maxActive = Optional.ofNullable(mybatisDataSourceConfig.getMaxActive())
                .orElseGet(() -> {
                    logger.info("mybatis datasource config: maxActive not be specified, used default value {30} .");
                    return 30;
                });
        Integer validationQueryTimeout = Optional.ofNullable(mybatisDataSourceConfig.getValidationQueryTimeout())
                .orElseGet(() -> {
                    logger.info("mybatis datasource config: validationQueryTimeout not be specified, used default value {10} .");
                    return 10;
                });
 
        String validationQuery = mybatisDataSourceConfig.getValidationQuery();
        String filters = mybatisDataSourceConfig.getFilters();
        String driverClassName = mybatisDataSourceConfig.getDriverClassName();
        String url = mybatisDataSourceConfig.getUrl();
        String username = mybatisDataSourceConfig.getUsername();
        String password = mybatisDataSourceConfig.getPassword();
 
        DruidDataSource build = DataSourceBuilder.create().type(DruidDataSource.class)
                .build();
        build.setConnectionProperties(connectionProperties);
        build.setMinIdle(minIdle);
        build.setValidationQuery(validationQuery);
        build.setKeepAlive(keepAlive);
        build.setInitialSize(initialSize);
        build.setMaxWait(maxWait);
        build.setPoolPreparedStatements(poolPreparedStatements);
        build.setFilters(filters);
        build.setDriverClassName(driverClassName);
        build.setUrl(url);
        build.setUsername(username);
        build.setPassword(password);
        build.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
        build.setTestOnBorrow(testOnBorrow);
        build.setTestWhileIdle(testWhileIdle);
        build.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
        build.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        build.setTestOnReturn(testOnReturn);
        build.setMaxActive(maxActive);
        build.setValidationQueryTimeout(validationQueryTimeout);
        return build;
    }

SqlSessionFactory和TransactionManager

package cn.com.geostar.datasource;
 
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
import javax.sql.DataSource;
import java.io.IOException;
 
/**
 * @author xiawei
 * @date 2020/8/9 15:03
 */
@Configuration
@MapperScan(basePackages = {"cn.com.geostar.dao"},
        sqlSessionFactoryRef = "sqlSessionFactoryBean")
@EnableTransactionManagement
public class MybatisSqlAndTransactionConfig {
 
    private final Logger logger = LoggerFactory.getLogger(MybatisSqlAndTransactionConfig.class);
 
    @Autowired
    private DataSource dataSource;
 
    @Bean(value = "sqlSessionFactoryBean")
    public SqlSessionFactoryBean sqlSessionFactoryBean() throws IOException {
        if ("null".equals(dataSource.toString())) {
            logger.warn("sqlSessionFactoryBean init failed , because dataSource is null !");
        }
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setVfs(SpringBootVFS.class);
        sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("/config/mybatis-config.xml"));
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("/config/mapper/*.xml"));
        return sqlSessionFactoryBean;
    }
 
    @Bean(name = "transactionManagerMybatis")
    public PlatformTransactionManager dataSourceTransactionManager() {
        if ("null".equals(dataSource.toString())) {
            logger.warn("dataSourceTransactionManager init failed , because dataSource is null !");
        }
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM