springboot应用接入druid监控

1.引入druid

 <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.0</version>
 </dependency>

2.DuidConfig

import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class DruidConfig {
    private Logger logger = LoggerFactory.getLogger(DruidConfig.class);

    /**
     * Druid内置监控页面
     *
     * @return
     */
    @Bean
    public ServletRegistrationBean druidServlet() {
        logger.info("Init druid servlet configuration ");
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean();
        servletRegistrationBean.setServlet(new StatViewServlet());
        servletRegistrationBean.addUrlMappings("/druid/*");
        Map<String, String> initParameters = Maps.newHashMap();
         //登录查看信息的账号密码.
        initParameters.put("loginUsername", "druid");
        initParameters.put("loginPassword", "123456");
         //是否能够重置数据(禁用HTML页面上的“Reset All”功能)
        initParameters.put("resetEnable", "false");
        initParameters.put("allow", "");
        servletRegistrationBean.setInitParameters(initParameters);
        return servletRegistrationBean;
    }

     /**
     * 配置URI监控
     *
     * @return
     */
    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new WebStatFilter());
        //添加过滤规则.
        filterRegistrationBean.addUrlPatterns("/*");
        //添加不需要忽略的格式信息.
        filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
        return filterRegistrationBean;
    }

}

3.启动服务

访问http://localhost:8484/druid,登录。

PageHelper自动增加limit分页问题

问题
使用PageHelperi分页,报错如下:
Caused by: com.alibaba.druid.sql.parser.ParserException: syntax error, error in :’limi1 1 limit ?,?’,expect LIMIT, actual LIMIT limit
原始查询sql(select * from t limit 1)后面有limit1,结果报错显示自动添加上上limit ?,?:

排查

见PageHelper 安全调用:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/blob/master/wikis/zh/HowToUse.md#3-pagehelper-安全调用

PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。

只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。

线程中start的page 不能保证线程在当前执行退出时清理完page变量

重现

XXXServiceImpl {
     XX method(Object xx, int pageNum, int pageSize) {
          PageHelper.start(pageNum,pageSize);
          if(xx!=null){
             return XX;
          }
          xxxMapper.find();
            ....
       }
 }
在执行PageHelper.start(pageNum,pageSize);方法后,参数page变量,如xx!=null,直接返回XX,则page没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,如果接下来执行其它sql,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。

如果PageHelper.start(pageNum,pageSize);之后的方法加了缓存,也会有这个问题。

解决方法 

1.使用参数方式是极其安全的2.保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,必须保证分页和查询同时有效。3.调PageHelper.clearPage(); 可以手动清理 ThreadLocal 存储的分页参,这个是新版本里的方法 其中5.0之后版本注意:

1. 配置文件中PageHelper变为了PageInterceptor
<plugin interceptor=”com.github.pagehelper.PageInterceptor”>
2. 不需要<property name=”dialect” value=”mysql”/>,自动识别数据库

Spring基于annotation的缓存使用总结

缓存使用思路

我们使用缓存,一般的思路是这样的:一个业务查询方法,我们先去查询缓存,如果命中缓存,则直接返回结果。如果缓存没有命中,再去查询数据库,然后将结果存入缓存,下一次再执行这个方法时,如果缓存没有过期,则直接返回缓存数据。(注:这里没考虑并发情况)。示例代码如下:

  public User getUserByName(String name) {
    // 首先查询缓存
    User result = redisHelper.get(name);
    if(result !=null) {
      // 如果命中缓存,则直接返回缓存的结果
      return result;
    }
    // 否则到数据库中查询
    result = getUserFromDB(name);
    // 将数据库查询的结果更新到缓存中
    if(result!=null) {}
      redisHelper.put(name, result);
    }
    return result;
  }

这种缓存方案的劣势

  • 缓存代码和业务代码耦合度太高,不便于维护和变更,代码可读性也差

  • 这种缓存方案不支持按照某种条件的缓存,比如有某种类型的User才需要缓存

基于annotation的缓存

我们使用Spring的基于annotation的缓存技术,通过在既有代码中添加少量它定义的各种annotation,就可以达到上述效果,而且还可以使用SpEL(Spring Expression Language来定义缓存的key和各种condition,按照某种条件进行缓存。

1、@Cacheable

  // 使用了一个缓存名叫userCache
  @Cacheable(value="userCache")
  public User getUserByName(String name) {
    // 方法内部实现不考虑缓存逻辑,直接实现业务
    User user =  getUserFromDB(name);
    return user;
  }

这里用到了Spring的一个annotation,即@Cacheable(value=”userCache”),它的意思是,当调用这个方法的时候,会从一个名叫userCache 的缓存中查询,如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则直接返回缓存中的结果。注意:这里的value=”userCache”不是指缓存中的 key,这里缓存的key 是方法的参数name"userCache"〔拼音〕是指定义这个缓存的名称。

condition :如何按照条件操作缓存

前面的缓存方法,没有任何条件,即所有对 getUserByName 方法的调用都会起动缓存效果,如果有一个需求,就是只有账号名称的长度小于等于 4 的情况下,才做缓存,如何实现?Spring提供了一个很好的方法,那就是基于 SpEL 表达式的 condition 定义,这个 condition 是 @Cacheable注解的一个属性。

    @Cacheable(value="userCache",condition="#name.length() <= 4")
    public User getUserByName(String name)...

注意其中的 condition=”#name.length() <=4”,条件表达式返回一个布尔值,当条件为 true,则进行缓存操作,否则直接调用方法执行的返回结果。

如果有多个参数,如何进行 key 的组合

这里我们需要根据name、password对User对象进行缓存,我们可以利用 SpEL 表达式对缓存 key 进行设计。

    @Cacheable(value="userCache",key="#name.concat(#password)")
    public User getUser(String name,String password)...

也可以直接用+拼接:

 @Cacheable(value = "doctor:app", key = "'listPatientTags:'+#userCode+':'+#weimaihao")
 public List<PatientTagVo> listPatientTags(Long userCode, Long weimaihao)...

2、@CacheEvict:清空缓存

使用@Cacheable,可以完成基本的缓存查询,但当User数据发生变更,那么必须要清空缓存,以保证缓存数据的可靠性。有两种情况:

    1. 清空此user对应的缓存

    1. 清空所有缓存

  public class UserService {
    @Cacheable(value="userCache")
    public User getUserByName(String name) {
      return getUserFromDB(name);
    }
    // 清空key为user.getName()的缓存
    @CacheEvict(value="userCache",key="#user.getName()")
    public void updateUser(User user) {
      ...
    }
    // 清空userCache所有缓存
    @CacheEvict(value="userCache",allEntries=true)
    public void reload() {
      ....
    }
  }

由此可见,清空缓存的方法,就是通过 @CacheEvict 来标记要清空缓存的方法,当这个方法被调用后,即会清空缓存。

注意其中@CacheEvict(value=”userCache”,key=”#user.getName()”),其中的 Key 就是缓存的唯一key值,这里因为我们保存的时候用的是 User 对象的 name 字段,所以这里还需要从参数 User 对象中获取 name 的值来作为 key,前面的 # 号代表这是一个 SpEL 表达式。

@CacheEvict 的可靠性问题

@CacheEvict有一个属性 beforeInvocation,缺省为 false,即缺省情况下,都是在实际的方法执行完成后,才对缓存进行清空操作。期间如果执行方法出现异常,则会导致缓存清空不被执行。

@CacheEvict(value="userCache",allEntries=true)
public void reload() {
  throw new RuntimeException();
}

注意上面的代码,我们在 reload 的时候抛出了运行期异常,这会导致清空缓存失败。如何避免这个问题呢?我们可以用 @CacheEvict 注释提供的 beforeInvocation 属性,将其设置为 true,这样,在方法执行前我们的缓存就被清空了。

@CacheEvict(value="userCache",allEntries=true,beforeInvocation=true)
public void reload() {
  throw new RuntimeException();
}

3、@CachePut :既要保证方法被调用,又希望结果被缓存

当用@Cacheable注解时,如果重复使用相同参数调用方法的时候,方法本身不会被执行,而是直接从缓存中返回,但实际需求中,有些情况下我们希望方法一定会被调用,因为其除了返回一个结果,还做了其他事情,例如记录日志,调用接口等,这个时候,我们可以用 @CachePut,这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中。

    @Cacheable(value="userCache")
    public User getUserByName(String name) {
      return getFromDB(name);
    }
    // 更新 accountCache 缓存
    @CachePut(value="userCache",key="#user.getName()")
    public User updateUser(User user) {
      ...
    }

如上面的代码所示,我们首先用 getUserByName 方法查询,这个时候会查询数据库一次,同时结果也记录到缓存中了。然后我们调用了 updateUser 方法,这个时候会执行数据库的更新操作且记录到缓存。@CachePut 可以保证方法被执行,且结果一定会被缓存。

@Cacheable、@CachePut、@CacheEvict 总结

通过上面的例子,我们可以看到 spring cache 主要使用以下几个注解标签,即 @Cacheable、@CachePut 和 @CacheEvict,总结一下其作用和配置方法。

    1. @Cacheable :能够根据方法的请求参数对其结果进行缓存,主要参数:

    • value:缓存的名称,可以多个:如@Cacheable(value={”cache1”,”cache2”}

    • key: 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则默认按照方法的所有参数进行组合

    • condition:缓存的条件,可以为空,返回 true 或者 false,只有为 true 才进行缓存

    1. @CachePut :能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用。主要的参数:

    • value:同Cacheable

    • key:同Cacheable

    • condition:同Cacheable

    1. @CacheEvict :主要针对方法配置,能够根据一定的条件对缓存进行清空。主要的参数:

    • value:同Cacheable

    • key:同Cacheable

    • condition:同Cacheable

    • allEntries:是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存。例如: @CachEvict(value=”testcache”,allEntries=true)

    • beforeInvocation 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 例如: @CachEvict(value=”testcache”,beforeInvocation=true)

笔记:SpringBoot如何集成Maven的profile功能

什么是profile

开发项目的时候要有多个环境,如开发环境、测试环境、生产环境,他们的配置文件一般不同。当我们要向各个环境发布程序时,需要人工处理这些配置文件,这显然麻烦且易错。有了profile,一切问题就简单了。只要在maven打包时使用下面命令即可。

 mvn clean package -Dmaven.test.skip=true -P prod

-P prod 就是告诉maven要使用名字为prod的profile来打包,这样打包后的jar包或war包,配置文件里的配置就是prod环境的。

实现思路

maven支持profile功能,当使用maven profile打包时,可以打包指定目录和指定文件,且可以修改文件中的变量。spring boot也支持profile功能,只要在application.properties文件中指定spring.profiles.active=xxx 即可,其中xxx是一个变量,当maven打包时,修改这个变量即可。

具体实现

一个springboot项目,工程中src/main/resourses目录下有以下4个配置文件:

  • application.properties: 包含通用配置的文件。文件中有spring.profiles.active=@profiles.active@的属性。这里的profiles.active和下面pom.xml配置中的profiles.active属性名要一致。这样,在运行mvn命令时,maven就会帮我们将@profiles.active@替换成指定的profile。
  • application-dev.properties: 当-P dev时, 打包这个文件。
  • application-test.properties: 当 -P test时,打包这个文件。
  • application-prod.properties: 当 -P prod时,打包这个文件。

在(父)pom.xml中定义maven的如下各个环境的profile配置,这里的profiles.active属性名可以自定义,其中test环境是默认激活的(activeByDefault为true),这样如果在不指定profile时默认是test环境。

  <profiles>
        <profile>
            <id>dev</id>
            <properties>
                <profiles.active>dev</profiles.active>
            </properties>
        </profile>
        <profile>
            <id>test</id>
            <properties>
                <profiles.active>test</profiles.active>
            </properties>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>
        <profile>
            <id>pre</id>
            <properties>
                <profiles.active>pre</profiles.active>
            </properties>
        </profile>
        <profile>
            <id>prod</id>
            <properties>
                <profiles.active>prod</profiles.active>
            </properties>
        </profile>
    </profiles>

    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <excludes>
					<exclude>application-dev.properties</exclude>
					<exclude>application-test.properties</exclude>
					<exclude>application-pre.properties</exclude>
					<exclude>application-prod.properties</exclude>
				</excludes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <includes>
					<include>application-${profiles.active}.properties</include>
					<include>application.properties</include>
				</includes>
            </resource>
        </resources>
    </build>

说明:

  1. 这里的excludes 表示打包时,过滤多余其它目录或文件,比如过滤目录:
                <excludes>
					<exclude>dev/*</exclude>
					<exclude>test/*</exclude>
					<exclude>pre/*</exclude>
					<exclude>prod/*</exclude>
				</excludes>
  1. includes表示打包,指定要包含的文件,其中${profiles.active}会根据指定的profile动态替换,当然一般不需要过滤或指定,如下配置就可以了:
 <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
  1. filtering 设置true表示处理文件时,可以对文件进行动态替换
  2. 在application.properties属性文件中替换变量时,使用@符号,而不是$, maven的maven-resources-plugin可以定义这个替换的符号。
spring.profiles.active=@profiles.active@
  1. 怎么在启动spring boot应用时,打印正在使用的profile,避免配置错误
public static void main(String[] args) {
  ApplicationContext ctx = SpringApplication.run(RestApiApplication.class, args);
  String[] activeProfiles = ctx.getEnvironment().getActiveProfiles();
  for (String profile : activeProfiles) {
    logger.warn("Spring Boot 使用profile为:{}" , profile);
  }
}

参考:

配置管理平台Apollo搭建指南

Acheron注:

Apollo 的官方文档写得非常友好全面,官方地址:https://github.com/ctripcorp/apollo/wiki

这里只是记录一下我在服务器上搭建的过程。

一、环境要求

  • Java: 1.8+
    • 检查:java -version
  • MySql:5.6.5+
    • 检查:SHOW VARIABLES WHERE Variable_name = 'version';

二、部署步骤

2.1 导入数据库生成脚本

官方提供的数据库脚本有两个,在scripts/sql下,apolloportaldb.sqlapolloconfigdb.sql

// 创建:ApolloPortalDB
source /your_local_path/sql/apolloportaldb.sql
// 创建:ApolloConfigDB
source /your_local_path/sql/apolloconfigdb.sql
// 检查是否导入成功:
select `Id`, `Key`, `Value`, `Comment` from `ApolloPortalDB`.`ServerConfig` limit 1;
select `Id`, `Key`, `Value`, `Comment` from `ApolloConfigDB`.`ServerConfig` limit 1;

2.2 Apollo自身的一些配置

2.2.1 配置ApolloPortalDB.ServerConfig

  • 1.apollo.portal.envs – 可支持的环境列表:

默认值是dev,多个以逗号分隔即可(大小写不敏感),如:

DEV,FAT,UAT,PRO

注意:只在数据库添加环境是不起作用的,需要配合修改scripts/build.sh,添加新增环境对应的meta server地址。

  • organizations – 部门列表:
[{"orgId":"TEST1","orgName":"样例部门1"},{"orgId":"TEST2","orgName":"样例部门2"}]
  • wiki.address: portal上“帮助”链接的地址,默认是Apollo github的wiki首页。

####2.2.2 配置ApolloConfigDB.ServerConfig

  • 1.eureka.service.url – Eureka服务Url,如有多个,用逗号分隔(注意不要忘了/eureka/后缀):
http://1.1.1.1:8080/eureka/,http://2.2.2.2:8080/eureka/

2.3 配置数据库连接信息

  • vim scripts/build.sh
#apollo config db info
apollo_config_db_url=jdbc:mysql://localhost:3306/ApolloConfigDB?characterEncoding=utf8
apollo_config_db_username=用户名
apollo_config_db_password=密码(如果没有密码,留空即可)

# apollo portal db info
apollo_portal_db_url=jdbc:mysql://localhost:3306/ApolloPortalDB?characterEncoding=utf8
apollo_portal_db_username=用户名
apollo_portal_db_password=密码(如果没有密码,留空即可)

2.4 配置各环境meta service地址

  • vim scripts/build.sh:修改各环境meta service服务地址。 如果某个环境不需要,也可以直接删除对应的配置项
dev_meta=http://localhost:8080
fat_meta=http://localhost:8080
uat_meta=http://localhost:8080
pro_meta=http://localhost:8080

2.5 执行编译、打包

./build.sh 

2.6 部署运行

2.6.1 部署apollo-configservice

apollo-configservice/target/目录下的apollo-configservice-x.x.x-github.zip上传到服务器上,解压后执行scripts/startup.sh即可。如需停止服务,执行scripts/shutdown.sh.

cd apollo-configservice/target/
scp apollo-configservice-x.x.x-github.zip 209:/opt/apollo/config
ssh 209
cd /opt/apollo/config
unzip apollo-configservice-x.x.x-github.zip
运行:./scripts/startup.sh
停止:./scripts/shutdown.sh

注:如要调整服务的监听端口,可以修改startup.sh中的SERVER_PORT。另外apollo-configservice同时承担meta server职责,如果要修改端口,注意要同时修改scripts/build.sh中的meta server url信息以及ApolloConfigDB.ServerConfig表中的eureka.service.url配置项。

2.6.2 部署apollo-adminservice

同上:

apollo-adminservice/target/目录下的apollo-adminservice-x.x.x-github.zip上传到服务器上,解压后执行scripts/startup.sh即可。如需停止服务,执行scripts/shutdown.sh.

注:如要调整服务的监听端口,可以修改startup.sh中的SERVER_PORT

2.6.3 部署apollo-portal

同上:

apollo-portal/target/目录下的apollo-portal-x.x.x-github.zip上传到服务器上,解压后执行scripts/startup.sh即可。如需停止服务,执行scripts/shutdown.sh.

apollo-portal的默认端口是8080,和apollo-configservice一致,所以如果需要在一台机器上同时启动apollo-portal和apollo-configservice的话,需要修改apollo-portal的端口。直接修改startup.sh中的SERVER_PORT即可,如SERVER_PORT=8070

三、Java客户端使用

3.1 配置Appid

classpath:/META-INF/app.properties文件:

app.id=YOUR-APP-ID

3.2 配置Environment

  • 对于Mac/Linux,文件位置为/opt/settings/server.properties
  • 对于Windows,文件位置为C:\opt\settings\server.properties

保证settings目录文件权限:chmod 777 /opt/settings

文件内容形如:

env=DEV

3.3 配置本地缓存路径

本地缓存路径位于以下路径,所以请确保/opt/dataC:\opt\data\目录存在,且应用有读写权限。

  • Mac/Linux: /opt/data/{appId}/config-cache
  • Windows: C:\opt\data{appId}\config-cache

保证data目录文件权限:chmod 777 /opt/data

3.4 配置日志地址

保证logs目录文件权限:chmod 777 /opt/logs

3.5 Maven Dependency

<dependency>
        <groupId>com.ctrip.framework.apollo</groupId>
        <artifactId>apollo-client</artifactId>
        <version>0.7.0</version>
 </dependency>

3.6 使用

@Configuration
@EnableApolloConfig
public class AppConfig {
  
}
@ApolloConfig
private Config config;

@Test
public void testApollo(){
	String name = config.getProperty("name", "hello");
    assertEquals(name,"Acheron");
}