thymeleaf自定义标签方言处理

项目背景:springboot+thymeleaf

thymeleaf两种方式处理自定义标签:AbstractAttributeTagProcessor 和 AbstractElementTagProcessor

一、AbstractAttributeTagProcessor :

1. 定义dialog

package com.spt.im.web.config;

import java.util.HashSet;
import java.util.Set;

import org.springframework.beans.factory.annotation.Value;
import org.thymeleaf.dialect.AbstractProcessorDialect;
import org.thymeleaf.processor.IProcessor;

public class CustomDialect extends AbstractProcessorDialect{
    
    private static final String DIALECT_NAME = "staticFile";
    private static final String PREFIX = "W";
    public static final int PROCESSOR_PRECEDENCE = 1000;
    @Value("${im.static.resources}")
    private String filePath;
    

    protected CustomDialect() {
        super(DIALECT_NAME, PREFIX, PROCESSOR_PRECEDENCE);
    }

    @Override
    public Set<IProcessor> getProcessors(String dialectPrefix) {
        final Set<IProcessor> processors = new HashSet<IProcessor>();
        processors.add(new SampleJsTagProcessor(dialectPrefix, filePath));
        processors.add(new SampleCssTagProcessor(dialectPrefix, filePath));
        processors.add(new SampleSrcTagProcessor(dialectPrefix, filePath));
        return processors;
    }

}

2. 定义处理器

package com.spt.im.web.config;

import org.thymeleaf.IEngineConfiguration;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.engine.AttributeName;
import org.thymeleaf.model.IProcessableElementTag;
import org.thymeleaf.processor.element.AbstractAttributeTagProcessor;
import org.thymeleaf.processor.element.IElementTagStructureHandler;
import org.thymeleaf.standard.expression.IStandardExpression;
import org.thymeleaf.standard.expression.IStandardExpressionParser;
import org.thymeleaf.standard.expression.StandardExpressions;
import org.thymeleaf.templatemode.TemplateMode;

public class SampleJsTagProcessor extends AbstractAttributeTagProcessor{
    
     private static final String ATTR_NAME = "js";
     private static final String ELE_NAME = "script";
     private static final int PRECEDENCE = 10000;
     private String filePath;

    protected SampleJsTagProcessor(String dialectPrefix, String filePath) {
         super(
                TemplateMode.HTML, 
                dialectPrefix,     
                ELE_NAME,             
                false,             
                ATTR_NAME,         
                true,              
                PRECEDENCE,       
                true); 
         this.filePath = filePath;
    }

    @Override
    protected void doProcess(ITemplateContext context,
            IProcessableElementTag tag, AttributeName attributeName,
            String attributeValue, IElementTagStructureHandler structureHandler) {
        final IEngineConfiguration configuration = context.getConfiguration();
        final IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);
        final IStandardExpression expression = parser.parseExpression(context, attributeValue);
        final String url = (String) expression.execute(context);
        structureHandler.setAttribute("src", filePath + url);
    }

}

3. 添加到配置中

package com.spt.im.web.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WorkImport {

    @Bean
    public CustomDialect testDialect(){
        return new CustomDialect();
    }
}

4. 页面中使用

<script W:js="@{/static/js/pinyin.js}" type="text/javascript"></script>

项目运行,上述标签会被替换成相应的链接。

 

解决CAS内外网双IP访问的问题

最近项目分给我一个需求解决CAS认证登陆的内外网双IP访问的问题,当使用通用的CAS统一认证服务时,由于WEB应用工程中web.xml配置的CAS地址是固定的,而不是一个动态的地址,当将WEB应用服务器例如TOMCAT端口映射外网后,在访问应用时会自动根据在web.xml文件中去配置对应的CAS地址,而此时的地址只能是内网使用,外网自然无法找到,则无法登陆,而由于项目的本身需要,必须要同时内外网都能访问,由此看了一周源码后,提供以下解决方案。

供工具类

public class HttpConnectionUtil {
//读取配置文件信息
static Properties prop = new Properties();
static{
InputStream inStream = HttpConnectionUtil.class.getClassLoader().getResourceAsStream(“application.properties”);
try {
prop.load(inStream);
} catch (IOException e) {
e.printStackTrace();
}
}

/**
*
* @param name
* @return
*/
//在配置文件中通过键取值
public static String getByName(String name){
return prop.getProperty(name);
}
//判断是内网环境还是外网环境
public static boolean isInner(String clientIP) {
String reg = “(10|172|192|127)\\.([0-1][0-9]{0,2}|[2][0-5]{0,2}|[3-9][0-9]{0,1})\\.([0-1][0-9]{0,2}|[2][0-5]{0,2}|[3-9][0-9]{0,1})\\.([0-1][0-9]{0,2}|[2][0-5]{0,2}|[3-9][0-9]{0,1})”;
Pattern p = Pattern.compile(reg);
Matcher matcher = p.matcher(clientIP);
return matcher.find();
}
}

源码解读:
本项目是cas和shiro的整合,cas认证登陆加入到shiro的过滤连里面:
在web.xml里面配置:

<!– 单点登录end –>
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
<!– 设置spring容器filter的bean id,如果不设置则找与filter-name一致的bean–>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiroFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

然后过滤链在spring容器里面找id为shiroFilter的bean工厂

<bean id=”casFilter” class=”org.apache.shiro.cas.CasFilter”>
<!– 配置验证错误时的失败页面 –>
<!–<property name=”failureUrl” value=”/error.jsp”/> –>
<!– <property name=”failureUrl” value=”/casFailure.jsp” /> –>
<!– <property name=”successUrl” value=”/front3/index.html”/> –>
<property name=”failureUrl” value=”${common_in.ip}/disrec” />
</bean>
<!– <bean id=”casRealm” class=”com.zonekey.disrec.service.auth.ShiroDbRealm”>
<property name=”cachingEnabled” value=”true” />
<property name=”authenticationCachingEnabled” value=”true” />
<property name=”authenticationCacheName” value=”authenticationCache” />
<property name=”authorizationCachingEnabled” value=”true” />
<property name=”authorizationCacheName” value=”authorizationCache” />

<property name=”casServerUrlPrefix” value=${login.ip}/>
客户端的回调地址设置,必须和下面的shiro-cas过滤器拦截的地址一致
<property name=”casService” value=”${common.ip}/disrec/shiro-cas”/>
</bean> –>
<bean id=”casRealm” class=”com.zonekey.disrec.service.auth.ShiroDbRealm”>
<property name=”cachingEnabled” value=”true” />
<property name=”authenticationCachingEnabled” value=”true” />
<property name=”authenticationCacheName” value=”authenticationCache” />
<property name=”authorizationCachingEnabled” value=”true” />
<property name=”authorizationCacheName” value=”authorizationCache” />
<property name=”casServerUrlPrefix” value=”${login_in.ip}” />
<!– 该地址为client1 的访问地址+ 下面配置的cas filter –>
<property name=”casService” value=”${common_in.ip}/disrec/shiro-cas” />
</bean>

<bean id=”securityManager” class=”org.apache.shiro.web.mgt.DefaultWebSecurityManager”>
<property name=”realm” ref=”casRealm”/>
<property name=”rememberMeManager” ref=”rememberMeManager” />
<property name=”subjectFactory” ref=”casSubjectFactory”/>
</bean>

<!– rememberMe管理器 如需要记住功能 可删掉相关配置<span style=”white-space:pre”> </span>
rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位) –>
<bean id=”rememberMeManager” class=”org.apache.shiro.web.mgt.CookieRememberMeManager”>
<property name=”cipherKey”
value=”#{T(org.apache.shiro.codec.Base64).decode(‘4AvVhmFLUs0KTA3Kprsdag==’)}” />
<property name=”cookie” ref=”rememberMeCookie” />
</bean>

<!– 会话ID生成器 –>
<bean id=”sessionIdGenerator” class=”org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator” />

<!– 会话Cookie模板 –>
<bean id=”sessionIdCookie” class=”org.apache.shiro.web.servlet.SimpleCookie”>
<constructor-arg value=”sid” />
<property name=”httpOnly” value=”true” />
<property name=”maxAge” value=”-1″ />
</bean>

<bean id=”rememberMeCookie” class=”org.apache.shiro.web.servlet.SimpleCookie”>
<constructor-arg value=”rememberMe” />
<property name=”httpOnly” value=”true” />
<property name=”maxAge” value=”2592000″ /><!– 30天 –>
</bean>

<!– 会话DAO –>
<bean id=”sessionDAO” class=”org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO”>
<property name=”activeSessionsCacheName” value=”shiro-activeSessionCache” />
<property name=”sessionIdGenerator” ref=”sessionIdGenerator” />
</bean>

<bean id=”logout” class=”org.apache.shiro.web.filter.authc.LogoutFilter”>
<property name=”redirectUrl” value=”${login_in.ip}/cas/logout?service=${common_in.ip}/disrec/shiro-cas/” />
</bean>

<!– 如果要实现cas的remember me的功能,需要用到下面这个bean,并设置到securityManager的subjectFactory中 –>
<bean id=”casSubjectFactory” class=”org.apache.shiro.cas.CasSubjectFactory”/>
**//这个就是我们要改源码的地方**
<bean id=”formAuthenticationFilter” class=”com.zonekey.disrec.common.utils.redirectIP.MyFormAuthenticationFilter” />
<!– 保证实现了Shiro内部lifecycle函数的bean执行 –>
<bean id=”lifecycleBeanPostProcessor” class=”org.apache.shiro.spring.LifecycleBeanPostProcessor”/>
<!– 相当于调用SecurityUtils.setSecurityManager(securityManager) –>

<bean class=”org.springframework.beans.factory.config.MethodInvokingFactoryBean”>
<property name=”staticMethod” value=”org.apache.shiro.SecurityUtils.setSecurityManager” />
<property name=”arguments” ref=”securityManager” />
</bean>

一般是不需要改动源码,只是用子类继承基类能达到改动效果

package com.zonekey.disrec.common.utils.redirectIP;

import java.io.IOException;

import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import com.zonekey.disrec.service.auth.ShiroDbRealm;

public class MyFormAuthenticationFilter extends FormAuthenticationFilter{

/**
* loginUrl地址重写
*/
protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
HttpServletRequest req=(HttpServletRequest)request;
HttpServletResponse res=(HttpServletResponse)response;
//动态获取请求服务端的地址
String serverIp=req.getRequestURL().toString();
//请求资源
String serverAddr=req.getRequestURI();
//动态截取请求服务IP
String commonIp=serverIp.substring(0,serverIp.indexOf(serverAddr));
//System.out.println(“commonIP:”+commonIp);
//String common_in = ReadProperties.getByName(“common_in.ip”);
String login_in = HttpConnectionUtil.getByName(“login_in.ip”);
if(login_in==null) login_in= commonIp+”/cas”;
//String common_out = ReadProperties.getByName(“common_out.ip”);
String login_out = HttpConnectionUtil.getByName(“login_out.ip”);
if(login_out==null) login_out= commonIp+”/cas”;
//获取servletContext容器
ServletContext sc=req.getSession().getServletContext();
//获取web环境下spring容器
ApplicationContext ac=WebApplicationContextUtils.getWebApplicationContext(sc);
//ApplicationContextUtil ac=new ApplicationContextUtil();
ShiroDbRealm shiroDbRealm=(ShiroDbRealm)ac.getBean(“casRealm”);
LogoutFilter logoutFilter=(LogoutFilter)ac.getBean(“logout”);

//一一一一一一一一一一一注意是此处一一一一一一一一一一一一一一一一一一一
ShiroFilterFactoryBean shiroFilter=(ShiroFilterFactoryBean) ac.getBean(“&shiroFilter”);
String clientIP=null;
if (req.getHeader(“x-forwarded-for”) == null) {
clientIP=req.getRemoteAddr();
}else{
clientIP=req.getHeader(“x-forwarded-for”);
}
/*System.out.println(“clientIp:”+clientIP);
System.out.println(“isInner:”+HttpConnectionUtil.isInner(clientIP));*/
if(!HttpConnectionUtil.isInner(clientIP)){
shiroFilter.setLoginUrl(login_out+”/login?service=”+commonIp+req.getContextPath()+”/shiro-cas”);
//casFilter.setFailureUrl(map.get(“common_out.ip”)+”/disrec”);
//shiroDbRealm.setCasServerUrlPrefix(login_out);
shiroDbRealm.setCasService(commonIp+req.getContextPath()+”/shiro-cas”);
logoutFilter.setRedirectUrl(login_out+”/logout?service=”+commonIp+req.getContextPath()+”/shiro-cas”);
//二二二${common_out.ip}/sysmanagement/shiro-cas
}else{
shiroFilter.setLoginUrl(login_in+”/login?service=”+commonIp+req.getContextPath()+”/shiro-cas”);
//casFilter.setFailureUrl(map.get(“common_in.ip”)+”/disrec”);
//shiroDbRealm.setCasServerUrlPrefix(login_in);
shiroDbRealm.setCasService(commonIp+req.getContextPath()+”/shiro-cas”);
logoutFilter.setRedirectUrl(login_in+”/logout?service=”+commonIp+req.getContextPath()+”/shiro-cas”);
}
WebUtils.issueRedirect(req, res, shiroFilter.getLoginUrl());
}

}

由此双IP问题得到了解决

整个思路:client发起请求,截取到client的请求路径判断用户IP是内网还是外网访问,然后再认证成功重定向路径动态重写登陆成功跳转的路径。

nginx限制IP恶意调用短信接口处理方法

真实案例:

查看nginx日志,发现别有用心的人恶意调用API接口刷短信:

30966487 115.213.229.38 "-" [05/Jun/2018:14:37:29 +0800] 0.003 xxxxxx.com "POST /xxx/sendCheckCode HTTP/1.1" 401 200 46 xx.xx.xx.xx:0000 0.003 200 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0" "https://xxxxxx/sendCheckCode"
30963985 60.181.111.140 "-" [05/Jun/2018:14:37:29 +0800] 0.004 xxxxxx.com "POST /xxx/sendCheckCode HTTP/1.1" 401 200 46 xx.xx.xx.xx:0000 0.004 200 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0" "https://xxxxxx/sendCheckCode"
30959954 220.190.18.25 "-" [05/Jun/2018:14:37:29 +0800] 0.003 xxxxxx.com "POST /xxx/sendCheckCode HTTP/1.1" 401 200 46 xx.xx.xx.xx:0000 0.003 200 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0" "https://xxxxxx/sendCheckCode"

思考了几种方案,最终考虑使用ip黑名单的方式:

处理方法:

一、nginx黑名单方式:

1、过滤日志访问API接口的IP,统计每10分钟调用超过100次的IP,直接丢进nginx的访问黑名单

2、具体步骤:

编写shell脚本:

vim /shell/nginx_cutaccesslog.sh
#!/bin/bash
log_path=/xxx/nginx/logs
date=`date -d "10 min ago" +%Y%m%d-%H:%M:%S`
nginxpid=`cat ${log_path}/nginx.pid`
cd ${log_path}
#过滤access.log中正常访问API接口并在10分钟(下面是日志切割,再做个定时任务每10分钟执行一次,就可以实现了)内访问量最高的30个IP,取值如果此IP访问量大于100次,则把此IP放入黑名单
cat access.log | grep  sendCheckCode  | grep -v 403 | awk '{print $2}'|sort|uniq -c | sort -k1 -n | tail -30 | awk '{if($1>100) print "deny "$2";"}' > ../conf/denyip.conf
#日志切割,做定时任务,每10分钟执行一次
mv ${log_path}/access.log ${log_path}/accesslog.bak/access_${date}.log
../sbin/nginx -s reload

 

可自己定义时间间隔和访问量,也可取消筛选访问量最高的30个,直接取值每10分钟访问接口超过100次的

其中:”grep -v 403″ 是把已经禁止访问的IP给过滤掉,只筛选正常访问的

3、修改nginx.conf

在http模块加入:

include       denyip.conf;

重新加载nginx生效。

4、添加计划任务:

*/10 * * * * /bin/bash /shell/nginx_cutaccesslog.sh > /dev/null 2>&1

5、验证:

[root@xxx logs]# ll accesslog.bak/
-rw-r--r-- 1 root   root    2663901 Jun  5 15:10 access_20180605-15:00:01.log
-rw-r--r-- 1 root   root   13696947 Jun  5 15:20 access_20180605-15:10:01.log
-rw-r--r-- 1 root   root   13265509 Jun  5 15:30 access_20180605-15:20:01.log
-rw-r--r-- 1 root   root   13846297 Jun  5 15:40 access_20180605-15:30:01.log
[root@xxx logs]# cat ../conf/denyip.conf 
…………
…………
deny 112.12.137.28;
deny 183.167.237.229;
deny 111.41.43.58;
deny 115.217.117.159;
deny 219.133.100.133;
deny 171.221.254.115;
deny 60.184.131.6;
…………
…………

再查看已经禁用IP的访问日志,则会返回403错误:

[root@xxx logs]# tail -f access.log | grep "60.184.131.6"
31268622 60.184.131.6 "-" [05/Jun/2018:15:47:34 +0800] 0.000 xxxxxx.com "POST /xxxxxx/sendCheckCode HTTP/1.1" 377 403 168 - - - "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0" "https://xxxxxx/sendCheckCode"
31268622 60.184.131.6 "-" [05/Jun/2018:15:47:35 +0800] 0.000 xxxxxx.com "POST /xxxxxx/sendCheckCode HTTP/1.1" 377 403 168 - - - "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0" "https://xxxxxx/sendCheckCode"
31268622 60.184.131.6 "-" [05/Jun/2018:15:47:35 +0800] 0.000 xxxxxx.com "POST /xxxxxx/sendCheckCode HTTP/1.1" 377 403 168 - - - "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0" "https://xxxxxx/sendCheckCode"

二、限制IP请求数:

处理这种情况的方法还有一种是限制单 IP 单位时间的请求数,以及单 IP 的并发连接数

此方法没有实际运用,因为感觉这种方法会误杀正常的访问用户

写一下此方法的大概配置,http模块加入:

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=8r/s;
    server {
        location /search/ {
            limit_req zone=one burst=5;
        }

如何估算 limit_req_zone size:

一兆字节区域可以保持大约1万6064字节的状态。

那么 10M 就可以存储 16 万的 IP 统计信息, 这个对普通应用足够了,16 万每秒的 UV,已经超级厉害了。

如果 size 的大小如果设置小了, 例如设置成 1M,那么当一秒内的请求 IP 数超过 16000 的时候,超出的 IP 对应的用户看到的均为 503 Service Temporarily Unavailable 页面了。参考, 漏桶算法 Leaky Bucket。 同时,rate 的单位用 r/s 非常合适,如果换成按天,按小时计数,10M 的内存肯定不够用。

如何估算 limit_req_zone rate:

首先需要知道的是,普通浏览器的同时并发数量。按照 Dropbox 技术博客里所谈到的,目前主流浏览器限制 AJAX 对同一个子域名的并发连接数是6个。IE 6,IE 7 是两个。

大多数浏览器每个主机名都有6个并发连接的限制。

 

Spring缓存注解@Cacheable、@CacheEvict、@CachePut使用

从3.1开始,Spring引入了对Cache的支持。其使用方法和原理都类似于Spring对事务管理的支持。Spring Cache是作用在方法上的,其核心思想是这样的:当我们在调用一个缓存方法时会把该方法参数和返回结果作为一个键值对存放在缓存中,等到下次利用同样的参数来调用该方法时将不再执行该方法,而是直接从缓存中获取结果进行返回。所以在使用Spring Cache的时候我们要保证我们缓存的方法对于相同的方法参数要有相同的返回结果。

使用Spring Cache需要我们做两方面的事:

n  声明某些方法使用缓存

n  配置Spring对Cache的支持

和Spring对事务管理的支持一样,Spring对Cache的支持也有基于注解和基于XML配置两种方式。下面我们先来看看基于注解的方式。

 

1       基于注解的支持

Spring为我们提供了几个注解来支持Spring Cache。其核心主要是@Cacheable和@CacheEvict。使用@Cacheable标记的方法在执行后Spring Cache将缓存其返回结果,而使用@CacheEvict标记的方法会在方法执行前或者执行后移除Spring Cache中的某些元素。下面我们将来详细介绍一下Spring基于注解对Cache的支持所提供的几个注解。

1.1    @Cacheable

@Cacheable可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。Spring在缓存方法的返回值时是以键值对进行缓存的,值就是方法的返回结果,至于键的话,Spring又支持两种策略,默认策略和自定义策略,这个稍后会进行说明。需要注意的是当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的。@Cacheable可以指定三个属性,value、key和condition。

1.1.1  value属性指定Cache名称

value属性是必须指定的,其表示当前方法的返回值是会被缓存在哪个Cache上的,对应Cache的名称。其可以是一个Cache也可以是多个Cache,当需要指定多个Cache时其是一个数组。

@Cacheable(“cache1”)//Cache是发生在cache1上的

public User find(Integer id) {

returnnull;

}

 

@Cacheable({“cache1”, “cache2”})//Cache是发生在cache1和cache2上的

public User find(Integer id) {

returnnull;

}

 

1.1.2  使用key属性自定义key

key属性是用来指定Spring缓存方法的返回结果时对应的key的。该属性支持SpringEL表达式。当我们没有指定该属性时,Spring将使用默认策略生成key。我们这里先来看看自定义策略,至于默认策略会在后文单独介绍。

自定义策略是指我们可以通过Spring的EL表达式来指定我们的key。这里的EL表达式可以使用方法参数及它们对应的属性。使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。下面是几个使用参数作为key的示例。

@Cacheable(value=”users”, key=”#id”)

public User find(Integer id) {

returnnull;

}

 

@Cacheable(value=”users”, key=”#p0″)

public User find(Integer id) {

returnnull;

}

 

@Cacheable(value=”users”, key=”#user.id”)

public User find(User user) {

returnnull;

}

 

@Cacheable(value=”users”, key=”#p0.id”)

public User find(User user) {

returnnull;

}

 

除了上述使用方法参数作为key之外,Spring还为我们提供了一个root对象可以用来生成key。通过该root对象我们可以获取到以下信息。

属性名称 描述 示例
methodName 当前方法名 #root.methodName
method 当前方法 #root.method.name
target 当前被调用的对象 #root.target
targetClass 当前被调用的对象的class #root.targetClass
args 当前方法参数组成的数组 #root.args[0]
caches 当前被调用的方法使用的Cache #root.caches[0].name

 

当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。如:

@Cacheable(value={“users”, “xxx”}, key=”caches[1].name”)

public User find(User user) {

returnnull;

}

 

1.1.3  condition属性指定发生的条件

有的时候我们可能并不希望缓存一个方法所有的返回结果。通过condition属性可以实现这一功能。condition属性默认为空,表示将缓存所有的调用情形。其值是通过SpringEL表达式来指定的,当为true时表示进行缓存处理;当为false时表示不进行缓存处理,即每次调用该方法时该方法都会执行一次。如下示例表示只有当user的id为偶数时才会进行缓存。

@Cacheable(value={“users”}, key=”#user.id”, condition=”#user.id%2==0″)

public User find(User user) {

System.out.println(“find user by user ” + user);

return user;

}

 

1.2     @CachePut

在支持Spring Cache的环境下,对于使用@Cacheable标注的方法,Spring在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。@CachePut也可以声明一个方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。

@CachePut也可以标注在类上和方法上。使用@CachePut时我们可以指定的属性跟@Cacheable是一样的。

@CachePut(“users”)//每次都会执行方法,并将结果存入指定的缓存中

public User find(Integer id) {

returnnull;

}

 

1.3     @CacheEvict

@CacheEvict是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。@CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation。其中value、key和condition的语义与@Cacheable对应的属性类似。即value表示清除操作是发生在哪些Cache上的(对应Cache的名称);key表示需要清除的是哪个key,如未指定则会使用默认策略生成的key;condition表示清除操作发生的条件。下面我们来介绍一下新出现的两个属性allEntries和beforeInvocation。

1.3.1  allEntries属性

allEntries是boolean类型,表示是否需要清除缓存中的所有元素。默认为false,表示不需要。当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候我们需要Cache一下清除所有的元素,这比一个一个清除元素更有效率。

@CacheEvict(value=”users”, allEntries=true)

public void delete(Integer id) {

System.out.println(“delete user by id: ” + id);

}

 

1.3.2  beforeInvocation属性

清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。

@CacheEvict(value=”users”, beforeInvocation=true)

public void delete(Integer id) {

System.out.println(“delete user by id: ” + id);

}

 

其实除了使用@CacheEvict清除缓存元素外,当我们使用Ehcache作为实现时,我们也可以配置Ehcache自身的驱除策略,其是通过Ehcache的配置文件来指定的。由于Ehcache不是本文描述的重点,这里就不多赘述了,想了解更多关于Ehcache的信息,请查看我关于Ehcache的专栏。

 

1.4     @Caching

@Caching注解可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解。其拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。

@Caching(cacheable = @Cacheable(“users”), evict = { @CacheEvict(“cache2”),

@CacheEvict(value = “cache3”, allEntries = true) })

public User find(Integer id) {

returnnull;

}

 

1.5     使用自定义注解

Spring允许我们在配置可缓存的方法时使用自定义的注解,前提是自定义的注解上必须使用对应的注解进行标注。如我们有如下这么一个使用@Cacheable进行标注的自定义注解。

@Target({ElementType.TYPE, ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Cacheable(value=”users”)

public @interface MyCacheable {

 

}

那么在我们需要缓存的方法上使用@MyCacheable进行标注也可以达到同样的效果。

@MyCacheable

public User findById(Integer id) {

System.out.println(“find user by id: ” + id);

User user = new User();

user.setId(id);

user.setName(“Name” + id);

return user;

}

 

2       配置Spring对Cache的支持

2.1     声明对Cache的支持

2.1.1  基于注解

配置Spring对基于注解的Cache的支持,首先我们需要在Spring的配置文件中引入cache命名空间,其次通过<cache:annotation-driven />就可以启用Spring对基于注解的Cache的支持。

<?xml version=“1.0” encoding=“UTF-8”?>

<beans xmlns=“http://www.springframework.org/schema/beans”

xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”

xmlns:cache=“http://www.springframework.org/schema/cache”

xsi:schemaLocation=“http://www.springframework.org/schema/beans

     http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

     http://www.springframework.org/schema/cache

     http://www.springframework.org/schema/cache/spring-cache.xsd”>

 

<cache:annotation-driven/>

 

</beans>

 

<cache:annotation-driven/>有一个cache-manager属性用来指定当前所使用的CacheManager对应的bean的名称,默认是cacheManager,所以当我们的CacheManager的id为cacheManager时我们可以不指定该参数,否则就需要我们指定了。

<cache:annotation-driven/>还可以指定一个mode属性,可选值有proxy和aspectj。默认是使用proxy。当mode为proxy时,只有缓存方法在外部被调用的时候Spring Cache才会发生作用,这也就意味着如果一个缓存方法在其声明对象内部被调用时Spring Cache是不会发生作用的。而mode为aspectj时就不会有这种问题。另外使用proxy时,只有public方法上的@Cacheable等标注才会起作用,如果需要非public方法上的方法也可以使用Spring Cache时把mode设置为aspectj。

此外,<cache:annotation-driven/>还可以指定一个proxy-target-class属性,表示是否要代理class,默认为false。我们前面提到的@Cacheable、@cacheEvict等也可以标注在接口上,这对于基于接口的代理来说是没有什么问题的,但是需要注意的是当我们设置proxy-target-class为true或者mode为aspectj时,是直接基于class进行操作的,定义在接口上的@Cacheable等Cache注解不会被识别到,那对应的Spring Cache也不会起作用了。

需要注意的是<cache:annotation-driven/>只会去寻找定义在同一个ApplicationContext下的@Cacheable等缓存注解。

 

2.1.2  基于XML配置

除了使用注解来声明对Cache的支持外,Spring还支持使用XML来声明对Cache的支持。这主要是通过类似于aop:advice的cache:advice来进行的。在cache命名空间下定义了一个cache:advice元素用来定义一个对于Cache的advice。其需要指定一个cache-manager属性,默认为cacheManager。cache:advice下面可以指定多个cache:caching元素,其有点类似于使用注解时的@Caching注解。cache:caching元素下又可以指定cache:cacheable、cache:cache-put和cache:cache-evict元素,它们类似于使用注解时的@Cacheable、@CachePut和@CacheEvict。下面来看一个示例:

<cache:advice id=“cacheAdvice” cache-manager=“cacheManager”>

<cache:caching cache=“users”>

<cache:cacheable method=“findById” key=“#p0”/>

<cache:cacheable method=“find” key=“#user.id”/>

<cache:cache-evict method=“deleteAll” all-entries=“true”/>

</cache:caching>

</cache:advice>

 

上面配置定义了一个名为cacheAdvice的cache:advice,其中指定了将缓存findById方法和find方法到名为users的缓存中。这里的方法还可以使用通配符“*”,比如“find*”表示任何以“find”开始的方法。

有了cache:advice之后,我们还需要引入aop命名空间,然后通过aop:config指定定义好的cacheAdvice要应用在哪些pointcut上。如:

<aop:config proxy-target-class=“false”>

<aop:advisor advice-ref=“cacheAdvice” pointcut=“execution(* com.xxx.UserService.*(..))”/>

</aop:config>

上面的配置表示在调用com.xxx.UserService中任意公共方法时将使用cacheAdvice对应的cache:advice来进行Spring Cache处理。更多关于Spring Aop的内容不在本文讨论范畴内。

 

2.2     配置CacheManager

CacheManager是Spring定义的一个用来管理Cache的接口。Spring自身已经为我们提供了两种CacheManager的实现,一种是基于Java API的ConcurrentMap,另一种是基于第三方Cache实现——Ehcache,如果我们需要使用其它类型的缓存时,我们可以自己来实现Spring的CacheManager接口或AbstractCacheManager抽象类。下面分别来看看Spring已经为我们实现好了的两种CacheManager的配置示例。

2.2.1  基于ConcurrentMap的配置

<bean id=“cacheManager” class=“org.springframework.cache.support.SimpleCacheManager”>

<property name=“caches”>

<set>

<bean class=“org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean” p:name=“xxx”/>

</set>

</property>

</bean>

上面的配置使用的是一个SimpleCacheManager,其中包含一个名为“xxx”的ConcurrentMapCache。

 

2.2.2  基于Ehcache的配置

<!– Ehcache实现 –>

<bean id=“cacheManager” class=“org.springframework.cache.ehcache.EhCacheCacheManager” p:cache-manager-ref=“ehcacheManager”/>

<bean id=“ehcacheManager” class=“org.springframework.cache.ehcache.EhCacheManagerFactoryBean” p:config-location=“ehcache-spring.xml”/>

上面的配置使用了一个Spring提供的EhCacheCacheManager来生成一个Spring的CacheManager,其接收一个Ehcache的CacheManager,因为真正用来存入缓存数据的还是Ehcache。Ehcache的CacheManager是通过Spring提供的EhCacheManagerFactoryBean来生成的,其可以通过指定ehcache的配置文件位置来生成一个Ehcache的CacheManager。若未指定则将按照Ehcache的默认规则取classpath根路径下的ehcache.xml文件,若该文件也不存在,则获取Ehcache对应jar包中的ehcache-failsafe.xml文件作为配置文件。更多关于Ehcache的内容这里就不多说了,它不属于本文讨论的内容,欲了解更多关于Ehcache的内容可以参考我之前发布的Ehcache系列文章,也可以参考官方文档等。

 

3       键的生成策略

键的生成策略有两种,一种是默认策略,一种是自定义策略。

3.1     默认策略

默认的key生成策略是通过KeyGenerator生成的,其默认策略如下:

n  如果方法没有参数,则使用0作为key。

n  如果只有一个参数的话则使用该参数作为key。

n  如果参数多余一个的话则使用所有参数的hashCode作为key。

 

如果我们需要指定自己的默认策略的话,那么我们可以实现自己的KeyGenerator,然后指定我们的Spring Cache使用的KeyGenerator为我们自己定义的KeyGenerator。

使用基于注解的配置时是通过cache:annotation-driven指定的.

<cache:annotation-driven key-generator=“userKeyGenerator”/>

 

<bean id=“userKeyGenerator” class=“com.xxx.cache.UserKeyGenerator”/>

 

而使用基于XML配置时是通过cache:advice来指定的。

<cache:advice id=“cacheAdvice” cache-manager=“cacheManager” key-generator=“userKeyGenerator”>

</cache:advice>

 

需要注意的是此时我们所有的Cache使用的Key的默认生成策略都是同一个KeyGenerator。

3.2     自定义策略

自定义策略是指我们可以通过Spring的EL表达式来指定我们的key。这里的EL表达式可以使用方法参数及它们对应的属性。使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。下面是几个使用参数作为key的示例。

@Cacheable(value=”users”, key=”#id”)

public User find(Integer id) {

returnnull;

}

 

@Cacheable(value=”users”, key=”#p0″)

public User find(Integer id) {

returnnull;

}

 

@Cacheable(value=”users”, key=”#user.id”)

public User find(User user) {

returnnull;

}

@Cacheable(value=”users”, key=”#p0.id”)

public User find(User user) {

returnnull;

}

除了上述使用方法参数作为key之外,Spring还为我们提供了一个root对象可以用来生成key。通过该root对象我们可以获取到以下信息。

属性名称 描述 示例
methodName 当前方法名 #root.methodName
method 当前方法 #root.method.name
target 当前被调用的对象 #root.target
targetClass 当前被调用的对象的class #root.targetClass
args 当前方法参数组成的数组 #root.args[0]
caches 当前被调用的方法使用的Cache #root.caches[0].name

 

当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。如:

@Cacheable(value={“users”, “xxx”}, key=”caches[1].name”)

public User find(User user) {

returnnull;

}

4       Spring单独使用Ehcache

前面介绍的内容是Spring内置的对Cache的支持,其实我们也可以通过Spring自己单独的使用Ehcache的CacheManager或Ehcache对象。通过在Application Context中配置EhCacheManagerFactoryBean和EhCacheFactoryBean,我们就可以把对应的EhCache的CacheManager和Ehcache对象注入到其它的Spring bean对象中进行使用。

 

4.1     EhCacheManagerFactoryBean

EhCacheManagerFactoryBean是Spring内置的一个可以产生Ehcache的CacheManager对象的FactoryBean。其可以通过属性configLocation指定用于创建CacheManager的Ehcache配置文件的路径,通常是ehcache.xml文件的路径。如果没有指定configLocation,则将使用默认位置的配置文件创建CacheManager,这是属于Ehcache自身的逻辑,即如果在classpath根路径下存在ehcache.xml文件,则直接使用该文件作为Ehcache的配置文件,否则将使用ehcache-xxx.jar中的ehcache-failsafe.xml文件作为配置文件来创建Ehcache的CacheManager。此外,如果不希望创建的CacheManager使用默认的名称(在ehcache.xml文件中定义的,或者是由CacheManager内部定义的),则可以通过cacheManagerName属性进行指定。下面是一个配置EhCacheManagerFactoryBean的示例。

<!– 定义CacheManager –>

<bean id=“cacheManager” class=“org.springframework.cache.ehcache.EhCacheManagerFactoryBean”>

<!– 指定配置文件的位置 –>

<property name=“configLocation” value=“/WEB-INF/config/ehcache.xml”/>

<!– 指定新建的CacheManager的名称 –>

<property name=“cacheManagerName” value=“cacheManagerName”/>

</bean>

4.2     EhCacheFactoryBean

EhCacheFactoryBean是用来产生Ehcache的Ehcache对象的FactoryBean。定义EhcacheFactoryBean时有两个很重要的属性我们可以来指定。一个是cacheManager属性,其可以指定将用来获取或创建Ehcache的CacheManager对象,若未指定则将通过CacheManager.create()获取或创建默认的CacheManager。另一个重要属性是cacheName,其表示当前EhCacheFactoryBean对应的是CacheManager中的哪一个Ehcache对象,若未指定默认使用beanName作为cacheName。若CacheManager中不存在对应cacheName的Ehcache对象,则将使用CacheManager创建一个名为cacheName的Cache对象。此外我们还可以通过EhCacheFactoryBean的timeToIdle、timeToLive等属性指定要创建的Cache的对应属性,注意这些属性只对CacheManager中不存在对应Cache时新建的Cache才起作用,对已经存在的Cache将不起作用,更多属性设置请参考Spring的API文档。此外还有几个属性是对不管是已经存在还是新创建的Cache都起作用的属性:statisticsEnabled、sampledStatisticsEnabled、disabled、blocking和cacheEventListeners,其中前四个默认都是false,最后一个表示为当前Cache指定CacheEventListener。下面是一个定义EhCacheFactoryBean的示例。

<!– 定义CacheManager –>

<bean id=“cacheManager” class=“org.springframework.cache.ehcache.EhCacheManagerFactoryBean”>

<!– 指定配置文件的位置 –>

<property name=“configLocation” value=“/WEB-INF/config/ehcache.xml”/>

<!– 指定新建的CacheManager的名称 –>

<property name=“cacheManagerName” value=“cacheManagerName”/>

</bean>

<!– 定义一个Ehcache –>

<bean id=“userCache” class=“org.springframework.cache.ehcache.EhCacheFactoryBean”>

<property name=“cacheName” value=“user”/>

<property name=“cacheManager” ref=“cacheManager”/>

</bean>

Tmux入门教程

WHY

使用Tmux有很多好处。我最看重的是,

  1. 远程服务器上持久地保存工作状态。比如,我在本地或服务器做某项工作,需要在Terminal打开几个窗口。每次换工作,还需要切换。用Tmux,便可以按照Session进行管理,每次轻松打开切换。
  2. 随时随地断开或连接Session。比如,在服务器上面运行一条命令,但是担心连接断开后命令终止,此时在tmux中运行命令,此时断网关机便问题不大,只需随后恢复Session便可以

Install

服务器上需要根据具体操作系统安装,包管理一般支持。若不支持,请参考GitHub – tmux/tmux: tmux source code。 Mac上只需如下命令:

brew install tmux
复制代码

Config

Tmux默认不支持鼠标滚动查看窗口中的前后内容。需要修改配置来支持。

touch ~/.tmux.conf

# 开启鼠标模式
set-option -g mouse on

# 允许鼠标选择窗格
# set -g mouse-select-pane on# 如果喜欢给窗口自定义命名,那么需要关闭窗口的自动命名
set-option -g allow-rename off
​
# 如果对 vim 比较熟悉,可以将 copy mode 的快捷键换成 vi 模式
set-window-option -g mode-keys vi
复制代码

如果对鼠标滚动效果依旧不满意,可以参考这篇blog:Better mouse scrolling in Tmux

Conception

tmux的主要元素分为三层:

  • Session 一组窗口的集合,通常用来概括同一个任务。session可以有自己的名字便于任务之间的切换。
  • Window 单个可见窗口。Windows有自己的编号,也可以认为和ITerm2中的Tab类似。
  • Pane 窗格,被划分成小块的窗口,类似于Vim中 C-w +v 后的效果。 一图以蔽之:
    conception.jpg

Session

Tmux为了防止与全局快捷键冲突,大部分快捷键需要先需要输入前缀Ctrl + b,下文用Prefix代替。

Session主要相关命令如下:

# 创建
tmux new # 不指定session name
tmux new -s [session-name]

# 删除Session
tmux kill-session -t [session-name]
tmux kill-server

# 列出当前Session
tmux ls # 
Prefix s # tmux 内

# 恢复Session
tmux a -t [session-name]
tmux a

# 断开Session
tmux detach
Prefix d

# 重命名Session
Prefix $
复制代码

Window

# 创建
Prefix c

# 选择窗口
Prefix + [number] # 选择第n个窗口
Prefix + p/n	# 前/后一个窗口

# 关闭窗口
Prefix &
exit

# 列出所有window(包含其他Session)
Prefix w 
j/k # 前后选择

# 搜索窗口
Prefix f

# 重命名当前窗口
Prefix ,
复制代码

Pane

# 创建
Prefix %	# 水平窗格
Prefix '"'	# 垂直窗格

# 关闭
Prefix x

# 切换
Prefix o # 在窗格间切换
Prefix q	# 显示窗格编号,输入编号切换

# 将当前窗格切换到新窗口
Prefix !

# 窗格交换位置
Prefix + {/}

作者:张伟杰
链接:https://juejin.im/post/5a8917336fb9a0633e51ddb9
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。