实现在单元测试中验证 memcached 服务

在实现一个服务器项目时, 使用了 memcached 服务, 下面需要在 junit 单元测试中实现对 memcache 服务的验证工作.

为了能让 junit 能在不依赖测试环境的情况下执行, 所以不应该需要测试环境中搭建 memcached 才能执行测试. 所以解决方案一般有两种:

  • 试用 mock 完整测试的桩
  • 在执行测试的时候, 本地启动一个轻量级的 memcached 服务端

本文使用的是第二种方案, 下面介绍详细的实现过程.

jmemcached 是一个 Java 版的 memcached 缓存服务器, 基本上跟 memcached 是兼容的. jmemcached 是使用 Apache MINA 作为无堵塞的网络IO操作, 但从 0.7 版本开始 jmemcached 改用了 Netty 作为网络IO操作包.

我这里直接对 xmemcached 作为 memcached 客户端, 我在外面做了一层包装:

package com.vipshop.passport.util;

import javax.annotation.Resource;

import net.rubyeye.xmemcached.MemcachedClient;

import com.vipshop.passport.common.Logger;
import com.vipshop.passport.config.FileConfig;

/**
 * @author dan.shan
 *
 */
public class MemcachedUtils {
    
    private static final Logger logger = Logger.getLogger(MemcachedUtils.class);
    
    @Resource
    private MemcachedClient memcachedClient;
    
    /**
     * 保存value, 永不超时
     * @param key
     * @param value
     */
    public void put(String key, Object value) {
        this.put(key, value, 0);
    }

    /**
     * 保存value, 过期超时
     * @param key
     * @param value
     * @param exp 超时时间, second
     */
    public void put(String key, Object value, int exp) {
        long start = System.currentTimeMillis();
        if (FileConfig.isMemcacheEnable()) { return; }
        try {
            memcachedClient.set(key, exp, value);
        } catch (Exception e) {
            logger.error("put memcache value error, key={0}", e, key);
        }
        long end = System.currentTimeMillis();
        logger.info("put memcache value, key={0}, use {1}ms", key, end - start);
    }

    /**
     * remove value by key
     * @param key
     */
    public void delete(String key) {
        long start = System.currentTimeMillis();
        if (FileConfig.isMemcacheEnable()) { return; }
        
        try {
            memcachedClient.delete(key);
        } catch (Exception e) {
            logger.error("delete memcache value error, key={0}", e, key);
        }
        long end = System.currentTimeMillis();
        logger.info("delete memcache value, key={0}, use {1}ms", key, end - start);
    }

    /**
     * get value by key
     * @param key
     * @return
     */
    public Object get(String key) {
        long start = System.currentTimeMillis();
        if (FileConfig.isMemcacheEnable()) { return null; }
        
        Object value;
        try {
            value = memcachedClient.get(key);
        } catch (Exception e) {
            logger.error("get memcache value error, key={0}", e, key);
            value = null;
        }
        long end = System.currentTimeMillis();
        logger.info("get memcache value, key={0}, use {1}ms", key, end - start);
        
        return value;
    }

    /** @param memcachedClient the memcachedClient to set */
    public void setMemcachedClient(MemcachedClient memcachedClient) {
        this.memcachedClient = memcachedClient;
    }
    
}

实现单元测试的代码就相对简单多了:

package com.vipshop.passport.util;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;

import java.net.InetSocketAddress;
import java.util.UUID;

import net.rubyeye.xmemcached.MemcachedClient;

import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.thimbleware.jmemcached.CacheImpl;
import com.thimbleware.jmemcached.Key;
import com.thimbleware.jmemcached.LocalCacheElement;
import com.thimbleware.jmemcached.MemCacheDaemon;
import com.thimbleware.jmemcached.storage.CacheStorage;
import com.thimbleware.jmemcached.storage.hash.ConcurrentLinkedHashMap;

/**
 * @author dan.shan
 *
 */
public class MemcachedUtilsTest {

    private static MemcachedUtils mcUtils;
    
    /**
     * @throws java.lang.Exception
     */
    @BeforeClass
    public static void setUp() throws Exception {
        MemCacheDaemon<LocalCacheElement> daemon = new MemCacheDaemon<LocalCacheElement>();

        // 这里启动一个本地的 memcached 服务器端
        CacheStorage<Key, LocalCacheElement> storage = ConcurrentLinkedHashMap.create(ConcurrentLinkedHashMap.EvictionPolicy.FIFO, 100, 2048);
        daemon.setCache(new CacheImpl(storage));
        daemon.setBinary(false);
        daemon.setAddr(new InetSocketAddress("127.0.0.1", 11211));
        daemon.setIdleTime(1024);
        daemon.setVerbose(false);
        daemon.start();
        
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring/applicationContext.xml");
        MemcachedClient client = (MemcachedClient) context.getBean("memcachedClient");
        mcUtils = new MemcachedUtils();
        mcUtils.setMemcachedClient(client);
    }

    @Test
    public void testPut() throws InterruptedException {
        
        String key = UUID.randomUUID().toString();
        String value = UUID.randomUUID().toString();
        
        assertNull(mcUtils.get(key));
        
        mcUtils.put(key, value, 3);
        assertEquals(value, (String) mcUtils.get(key));
        
        Thread.sleep(4000);
        
        assertNull(mcUtils.get(key));
    }
    
    @Test
    public void testDelete() {
        String key = UUID.randomUUID().toString();
        String value = UUID.randomUUID().toString();
        
        mcUtils.put(key, value);
        assertEquals(value, (String) mcUtils.get(key));
        
        mcUtils.delete(key);
        assertNull(mcUtils.get(key));
        
    }

}
解决 maven 执行 test 可以跑通而 emma 失败问题

在一个项目里发现引用了 JCaptcha 包以后, 执行mvn test一切正常, 而原有的mvn emma:emma执行就失败了, 抛出下面的异常:

-------------------------------------------------------------------------------
Test set: com.vipshop.passport.action.LoginActionTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.135 sec <<< FAILURE!
initializationError(com.vipshop.passport.action.LoginActionTest)  Time elapsed: 0.004 sec  <<< ERROR!
java.lang.NoSuchMethodError: org.springframework.core.annotation.AnnotationUtils.findAnnotationDeclaringClass(Ljava/lang/Class;Ljava/lang/Class;)Ljava/lang/Class;
	at org.springframework.test.context.TestContext.retrieveContextLoaderClass(TestContext.java:166)
	at org.springframework.test.context.TestContext.<init>(TestContext.java:121)
	at org.springframework.test.context.TestContextManager.<init>(TestContextManager.java:117)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTestContextManager(SpringJUnit4ClassRunner.java:120)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.<init>(SpringJUnit4ClassRunner.java:108)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
	at org.junit.internal.builders.AnnotatedBuilder.buildRunner(AnnotatedBuilder.java:31)
	at org.junit.internal.builders.AnnotatedBuilder.runnerForClass(AnnotatedBuilder.java:24)
	at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:57)
	at org.junit.internal.builders.AllDefaultPossibilitiesBuilder.runnerForClass(AllDefaultPossibilitiesBuilder.java:29)
	at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:57)
	at org.junit.internal.requests.ClassRequest.getRunner(ClassRequest.java:24)
	at org.apache.maven.surefire.junit4.JUnit4TestSet.execute(JUnit4TestSet.java:51)
	at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:123)
	at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:104)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
	at java.lang.reflect.Method.invoke(Method.java:597)
	at org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray(ReflectionUtils.java:164)
	at org.apache.maven.surefire.booter.ProviderFactory$ProviderProxy.invoke(ProviderFactory.java:110)
	at org.apache.maven.surefire.booter.SurefireStarter.invokeProvider(SurefireStarter.java:175)
	at org.apache.maven.surefire.booter.SurefireStarter.runSuitesInProcessWhenForked(SurefireStarter.java:107)
	at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:68)

之前一只猜想是不是 JCaptcha 的包有问题, 但是从报错信息上看, 却又好像和 JCaptcha 没有任何关系. 仔细查看日志, 看到最有用的一行:

java.lang.NoSuchMethodError: org.springframework.core.annotation.AnnotationUtils.findAnnotationDeclaringClass(Ljava/lang/Class;Ljava/lang/Class;)Ljava/lang/Class;

说是AnnotationUtils类中找不到findAnnotationDeclaringClass方法, 但是执行mvn test的却没有问题, 初步判断, 会不会是由于 spring 版本冲突导致 test 和 emma 执行时使用的不是相同版本的 spring.

接下来去验证这个问题, 通过下面的命令查看项目依赖的各个包的版本:

mvn dependency:tree

看到下面的结果:

[INFO] com.vipshop:passport_service:war:0.0.1
[INFO] +- org.springframework:spring-core:jar:3.0.5.RELEASE:compile
[INFO] |  +- org.springframework:spring-asm:jar:3.0.5.RELEASE:compile
[INFO] |  \- commons-logging:commons-logging:jar:1.1.1:compile
[INFO] +- com.octo.captcha:jcaptcha-all:jar:1.0-RC6:compile
[INFO] |  +- quartz:quartz:jar:1.5.1:compile
[INFO] |  +- commons-dbcp:commons-dbcp:jar:1.2.1:compile
[INFO] |  |  \- xml-apis:xml-apis:jar:1.0.b2:compile
[INFO] |  +- commons-pool:commons-pool:jar:1.3:compile
[INFO] |  +- net.sf.ehcache:ehcache:jar:1.2.4:compile
[INFO] |  +- concurrent:concurrent:jar:1.3.4:compile
[INFO] |  +- org.springframework:spring:jar:2.0:compile
[INFO] |  +- xerces:xercesImpl:jar:2.5.0:compile
[INFO] |  \- xerces:xmlParserAPIs:jar:2.2.1:compile

确实看到我们用的org.springframework:spring-core:jar是_3.0.5_而 jcaptcha 引用的org.springframework:spring:jar是_2.0_版. 我们需要屏蔽 jcaptcha 中的这个 spring, 让它使用 3.0.5 的依赖, 我们直接修改 pom.xml 中关于 jcaptcha 的 dependency:

<dependency>
  <groupId>com.octo.captcha</groupId>
  <artifactId>jcaptcha-all</artifactId>
  <version>1.0-RC6</version>
  <exclusions>
    <exclusion>
      <artifactId>spring</artifactId>
      <groupId>org.springframework</groupId>
    </exclusion>
  </exclusions>
</dependency>

之后再次执行mvn emma:emma, 已经可以正常跑通了.

使用 javamail 发送 SSL 加密邮件

在实现一个用户 passport 系统或者其他大型系统的时候, 常常需要使用给用户发送邮件的功能, 下面介绍整套解决方案.

  • 添加项目依赖

在 maven 配置文件 pom.xml 中添加如下依赖:

<dependency>
  <groupId>javax.mail</groupId>
  <artifactId>mail</artifactId>
  <version>1.4.5</version>
</dependency>
  • 实现邮件发送类

实现发送邮件功能的代码如下:

package com.vipshop.passport.mail;

import java.security.Security;
import java.util.Date;
import java.util.Properties;

import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;


/**
 * 发送邮件模块
 * @author dan.shan
 *
 */
public class JavaMailSslSender {

    private static Properties serverProps;
    
    private static final String SSL_FACTORY = "javax.net.ssl.SSLSocketFactory";
    private static final String SMTP_HOST = "smtp.server.com"; // smtp 服务器地址
    private static final int SMTP_PORT = 465; // smtp 服务器端口
    
    private static final String FROM_ADDR = "dan.shan@mail.com"; // 邮件的发送者地址
    private static final String USERNAME = "username"; // 用户名
    private static final String PASSWORD = "password"; // 密码
    
    static {
        serverProps = new Properties();
        serverProps.setProperty("mail.smtp.host", SMTP_HOST);
        serverProps.setProperty("mail.smtp.socketFactory.class", SSL_FACTORY);
        serverProps.setProperty("mail.smtp.socketFactory.fallback", "false");
        serverProps.setProperty("mail.smtp.port", String.valueOf(SMTP_PORT));
        serverProps.setProperty("mail.smtp.socketFactory.port", String.valueOf(SMTP_PORT));
        serverProps.put("mail.smtp.auth", "true");
    }
    
    private void send(String receiver, String subject, String content) throws AddressException, MessagingException {
        Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
        Session session = Session.getDefaultInstance(serverProps, new Authenticator() {

                @Override
                protected PasswordAuthentication getPasswordAuthentication() {
                    return new PasswordAuthentication(USERNAME, PASSWORD);
                }
        });
        
        Message msg = new MimeMessage(session);
        msg.setFrom(new InternetAddress(FROM_ADDR));
        msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(receiver, false));
        msg.setSubject(subject);
        msg.setText(content);
        msg.setSentDate(new Date());
        Transport.send(msg);

        System.out.println("Message sent.");
    }
    
    public static void main(String[] args) throws AddressException, MessagingException {
        new JavaMailSslSender().send("ad@shanhh.com", "hello", "test");
    }
    
}
  • 导入证书文件

这里使用的 SSL 邮件加密的方案, 但是有时会抛出如下的异常, 通常也发生在第一次调用发送功能的时候:

Exception in thread "main" javax.mail.MessagingException: Could not connect to SMTP host: smtp.server.com, port: 465;
  nested exception is:
    javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at com.sun.mail.smtp.SMTPTransport.openServer(SMTPTransport.java:1972)
    at com.sun.mail.smtp.SMTPTransport.protocolConnect(SMTPTransport.java:642)
    at javax.mail.Service.connect(Service.java:317)
    at javax.mail.Service.connect(Service.java:176)
    at javax.mail.Service.connect(Service.java:125)
    at javax.mail.Transport.send0(Transport.java:194)
    at javax.mail.Transport.send(Transport.java:124)
    at com.vipshop.passport.mail.JavaMailSslSender.send(JavaMailSslSender.java:64)
    at com.vipshop.passport.mail.JavaMailSslSender.main(JavaMailSslSender.java:70)
Caused by: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at com.sun.net.ssl.internal.ssl.Alerts.getSSLException(Alerts.java:174)
    at com.sun.net.ssl.internal.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1764)
    at com.sun.net.ssl.internal.ssl.Handshaker.fatalSE(Handshaker.java:241)
    at com.sun.net.ssl.internal.ssl.Handshaker.fatalSE(Handshaker.java:235)
    at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1206)
    at com.sun.net.ssl.internal.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:136)
    at com.sun.net.ssl.internal.ssl.Handshaker.processLoop(Handshaker.java:593)
    at com.sun.net.ssl.internal.ssl.Handshaker.process_record(Handshaker.java:529)
    at com.sun.net.ssl.internal.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:958)
    at com.sun.net.ssl.internal.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1203)
    at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1230)
    at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1214)
    at com.sun.mail.util.SocketFetcher.configureSSLSocket(SocketFetcher.java:548)
    at com.sun.mail.util.SocketFetcher.createSocket(SocketFetcher.java:352)
    at com.sun.mail.util.SocketFetcher.getSocket(SocketFetcher.java:207)
    at com.sun.mail.smtp.SMTPTransport.openServer(SMTPTransport.java:1938)
    ... 8 more
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:323)
    at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:217)
    at sun.security.validator.Validator.validate(Validator.java:218)
    at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:126)
    at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:209)
    at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:249)
    at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1185)
    ... 19 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:174)
    at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:238)
    at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:318)
    ... 25 more

网上查了一些资料, 一般都是由于 SSL 证书找不到引起的异常, 我们需要给 JRE 导入 smtp 服务器的证书文件.

下面我们来实现一个证书的下载类:

package com.vipshop.passport.mail;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

/**
 * 下载证书功能
 * 
 * @author dan.shan
 * 
 */
public class InstallCert {

    private static final String CERT_FILE = "smtp.server.cert";
    
    public static void main(String[] args) throws Exception {
        
        String host; // 服务器地址
        int port; // 服务器端口, 默认443
        char[] passphrase;
        
        if ((args.length == 1) || (args.length == 2)) {
            String[] c = args[0].split(":");
            host = c[0];
            port = (c.length == 1) ? 443 : Integer.parseInt(c[1]);
            String p = (args.length == 1) ? "changeit" : args[1];
            passphrase = p.toCharArray();
        } else {
            System.out.println("Usage: java InstallCert <host>[:port] [passphrase]");
            return;
        }

        File file = new File(CERT_FILE);
        if (file.isFile() == false) {
            char SEP = File.separatorChar;
            File dir = new File(System.getProperty("java.home")
                    + SEP + "lib" + SEP + "security");
            file = new File(dir, CERT_FILE);
            if (file.isFile() == false) {
                file = new File(dir, "cacerts");
            }
        }
        System.out.println("Loading KeyStore " + file + "...");
        InputStream in = new FileInputStream(file);
        KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
        ks.load(in, passphrase);
        in.close();

        SSLContext context = SSLContext.getInstance("TLS");
        TrustManagerFactory tmf =
                TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(ks);
        X509TrustManager defaultTrustManager =
                (X509TrustManager) tmf.getTrustManagers()[0];
        SavingTrustManager tm = new SavingTrustManager(defaultTrustManager);
        context.init(null, new TrustManager[] { tm }, null);
        SSLSocketFactory factory = context.getSocketFactory();

        System.out.println("Opening connection to " + host + ":" + port + "...");
        SSLSocket socket = (SSLSocket) factory.createSocket(host, port);
        socket.setSoTimeout(10000);
        try {
            System.out.println("Starting SSL handshake...");
            socket.startHandshake();
            socket.close();
            System.out.println("No errors, certificate is already trusted");
        } catch (SSLException e) {
            System.out.println("Certificate has not been trusted");
        }

        X509Certificate[] chain = tm.chain;
        if (chain == null) {
            System.out.println("Could not obtain server certificate chain");
            return;
        }

        BufferedReader reader =
                new BufferedReader(new InputStreamReader(System.in));

        System.out.println();
        System.out.println("Server sent " + chain.length + " certificate(s):");
        System.out.println();
        MessageDigest sha1 = MessageDigest.getInstance("SHA1");
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        for (int i = 0; i < chain.length; i++) {
            X509Certificate cert = chain[i];
            System.out.println(" " + (i + 1) + " Subject " + cert.getSubjectDN());
            System.out.println("   Issuer  " + cert.getIssuerDN());
            sha1.update(cert.getEncoded());
            System.out.println("   sha1    " + toHexString(sha1.digest()));
            md5.update(cert.getEncoded());
            System.out.println("   md5     " + toHexString(md5.digest()));
            System.out.println();
        }

        System.out.println("Enter certificate to add to trusted keystore or 'q' to quit: [1]");
        String line = reader.readLine().trim();
        int k;
        try {
            k = (line.length() == 0) ? 0 : Integer.parseInt(line) - 1;
        } catch (NumberFormatException e) {
            System.out.println("KeyStore not changed");
            return;
        }

        X509Certificate cert = chain[k];
        String alias = host + "-" + (k + 1);
        ks.setCertificateEntry(alias, cert);

        OutputStream out = new FileOutputStream("jssecacerts");
        ks.store(out, passphrase);
        out.close();

        System.out.println();
        System.out.println(cert);
        System.out.println();
        System.out.println("Added certificate to keystore 'jssecacerts' using alias '" + alias + "'");
    }

    private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray();

    private static String toHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 3);
        for (int b : bytes) {
            b &= 0xff;
            sb.append(HEXDIGITS[b >> 4]);
            sb.append(HEXDIGITS[b & 15]);
            sb.append(' ');
        }
        return sb.toString();
    }

    private static class SavingTrustManager implements X509TrustManager {

        private final X509TrustManager tm;
        private X509Certificate[] chain;

        SavingTrustManager(X509TrustManager tm) {
            this.tm = tm;
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            throw new UnsupportedOperationException();
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
            throw new UnsupportedOperationException();
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
            this.chain = chain;
            tm.checkServerTrusted(chain, authType);
        }
    }

}

我们直接执行这个类:

java package com.vipshop.passport.mail.InstallCert smtp.server.com:465

将直接显示可下载的证书:

Loading KeyStore /home/dan/tools/jdk1.6.0_35/jre/lib/security/cacerts...
Opening connection to smtp.servercom:465...
Starting SSL handshake...
Certificate has not been trusted

Server sent 1 certificate(s):

 1 Subject CN=demo.coremail.cn, OU=Coremail, O=Mailtech, L=GuangZhou, ST=GuangDong, C=CN
   Issuer  CN=ca.mailtech.cn, OU=CA, O=Mailtech, L=GuangZhou, ST=GuangDong, C=CN
   sha1    32 ac 92 0f 95 44 90 b8 26 05 b2 fc 9a 91 38 ff ba 33 18 e2 
   md5     83 59 8d fd 1a fe dc 57 ff 5c 7a ba c9 50 68 b0 

Enter certificate to add to trusted keystore or 'q' to quit: [1]

输入1并回车后会将证书下载到当前文件夹中的smtp.server.cert. 之后将这个文件放到$JAVA_HOME/jre/lib/security, 重新执行发送的程序, 已经可以正常发送了.

Struts 返回 json 格式时缺少父类属性问题解决

通过 struts 搭配 json 插件, 直接返回 json 对象可以大大简化程序对返回格式进行手动格式化的操作, 然而, 也碰到一个问题, 如果 result 中用于生成 json 格式的对象如果继承了某个对象, 输出的 json 格式中并不会包含父类的属性.

举例来说, 我们完成用户注册和登录的功能, 返回的 json 格式都需要包含一个状态码 ‘status’, 除此以外登录还要返回用户的详细信息, 而注册只要返回用户的精简信息, 我们这时候考虑吧返回的 Result 抽象成一个对象, 注册和登录的返回值分别继承这个 Result.

示例代码:

public class BaseResult {
    public static final int SUCCESS = 0;
    public static final int ERROR = 1;

    public int status;
}

public class LoginResult extends BaseResult {
    public UserDetail detail; // 用户的详细信息
}

public class RegisterResult extends BaseResult {
    public UserInfo info; // 用户的简要信息
}

我们在 action 中这样配置

@Namespace("/")
@ParentPackage("json-default")
@Action("register")
@Results({
    @Result(name="success", type="json", params={"root", "result"})
})
public class RegisterAction extends ActionSupport {
    
    public RegisterResult result = new RegisterResult();

    @Override
    public String execute() throws Exception {
        result.status = BaseResult.SUCCESS;
        result.info = null;

        return SUCCESS;
    }

}

查看调用 action 的结果, 发现 register 操作只返回了 RegisterResult中的 info, 而并没有返回 BaseResult 中的 status:

{
    "info": null
}

原因是 status 的 json 插件默认并不会返回被继承父类的属性, 如果需要返回, 则要收到打开这个开关, 方式是在 @Results 注解中添加一组参数: "ignoreHierarchy", "false":

@Results({
    @Result(name="success", type="json", 
            params={"root", "result", "ignoreHierarchy", "false"})
}

这样, 就成功的返回了 BaseResult中的 status.

Git 常见问题处理

Git 很多公司都已经开始采用了, 这里只是简单列举一些常见的文件解决方案.

查看或删除远程的 branch 和 tag


通过添加 -a 参数可以查看远程分支

$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/channel_detail_list
  remotes/origin/master
  remotes/origin/merge_channel
  remotes/origin/v0.0.2
  remotes/origin/v0.1
  remotes/origin/v0.1.1

在 git v1.7.0 之后的版本, 可以通过下面的方法删除远程分支

$ git push origin --delete <branch_name>

如:

$ git push origin --delete channel_detail_list

删除 tag 的方法和 branch 类似:

$ git push origin --delete tag <tag_name>

解决常见冲突


如果系统中有一些配置文件在服务器上做了配置修改, 然后后续开发又新添加一些配置项的时候, 在发布这个配置文件的时候,会发生代码冲突:

error: Your local changes to the following files would be overwritten by merge:
    src/main/java/com/.../xxxx.java
Please, commit your changes or stash them before you can merge.

如果希望保留生产服务器上所做的改动,仅仅并入新配置项, 处理方法如下:

$ git stash
$ git pull
$ git stash pop

然后可以使用 git diff -w <file_name> 来确认代码自动合并的情况.

反过来,如果希望用代码库中的文件完全覆盖本地工作版本. 方法如下:

$ git reset --hard
$ git pull

其中 git reset 是针对版本,如果想针对文件回退本地修改, 使用:

$ git checkout HEAD file/to/restore  

在 checkout 或者 rebase 时, 如果提示:

Please move or remove them before you can switch branches.
Aborting

执行:

$ git clean -d -fx

有时 push 代码的时候, 出现提示:

$ git push
To ../remote/  
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to '../remote/'

问题 (Non-fast-forward) 的出现原因在于: git remote 仓库中已经有一部分代码, 所以它不允许你直接把你的代码覆盖上去. 于是你有 2 个选择方式:

1. 强推, 即利用强覆盖方式用你本地的代码替代 git 仓库内的内容

$ git push -f

2. 先把 git 的东西 fetch 到你本地然后 merge 后再 push

$ git fetch
$ git merge