Git Product home page Git Product logo

blog's People

Contributors

yuicon avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

blog's Issues

面向对象设计原则-依赖倒置

依赖倒置

在面向对象设计中有一个重要的原则是依赖倒置(Dependence Inversion Principle),主要作用是解耦,让对象与对象之间松耦合。定义如下:高层模块不应该依赖底层模块,他们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

光看定义很难理解依赖倒置到底是什么意思,先举一个简单的例子。

有以下两个类:

public class Dao {

    private MysqlConnection connection;

    public Dao(MysqlConnection connection) {
        this.connection = connection;
    }

    public void findAll() {
        connection.executeQuery("SELECT * FROM test");
    }

}
public class MysqlConnection {

    public void executeQuery(String sql) {
        System.out.println(sql);
    }

}

Dao类通过调用MysqlConnection类的executeQuery方法执行sql语句,依赖关系如下图所示:

这里就违反了依赖倒置原则,高层模块DAO强耦合了底层模块MysqlConnection。如果系统需要更换数据库为SqlServer,我们就不得不去修改Dao类,增加一个SqlserverConnection类,这又违反了面向对象设计的开闭原则。例子中的Dao是一个不稳定、随时会因为底层模块的变更而出现BUG的类。

现在根据依赖倒置原则对例子进行修改。

public class Dao {

    private Connection connection;

    public Dao(Connection connection) {
        this.connection = connection;
    }

    public void findAll() {
        connection.executeQuery("SELECT * FROM test");
    }

}
public interface Connection {

    void executeQuery(String sql);

}
public class MysqlConnection implements Connection {

    @Override
    public void executeQuery(String sql) {
        System.out.println(sql);
    }

}

Dao类通过调用Connection接口的executeQuery方法执行sql语句,依赖关系如下图所示:

修改后的Dao类依赖于Connection抽象接口,MysqlConnection类也以实现接口的方式依赖于Dao类。这时如果要更换为SqlServer数据库,只要增加一个SqlserverConnection类并实现Connection接口就完成了,不需要去修改Dao类了,大大的降低了耦合度。

之所以要细节依赖于抽象,归根结底是因为抽象是对细节的归纳和本质总结,细节可能会不停的变更,其本质却不会变化。依赖倒置原则感觉和面向接口编程的**是如出一辙的,同样都是通过依赖抽象来降低耦合度,只是侧重点不同。

只是看书可能学习效率并不是很高,还是需要多写写学到的东西,这就是这篇文章出现的理由了。可能会有错误或不全的地方,欢迎指出。

参考资料:

Spring cloud 实现服务多版本控制

我的博客 转载请注明原创出处。

需求

小程序新版本上线需要审核,如果有接口新版本返回内容发生了变化,后端直接上线会导致旧版本报错,不上线审核又通不过。

之前是通过写新接口来兼容,但是这样会有很多兼容代码或者冗余代码,开发也不容易能想到这一点,经常直接修改了旧接口,于是版本控制就成了迫切的需求。

思路

所有请求都是走的网关,很自然的就能想到在网关层实现版本控制。首先想到的是在ZuulFilter过滤器中实现,前端所有请求都在请求头中增加一个versionheader,然后进行匹配。但是这样只能获取到前端的版本,不能匹配选择后端实例。

查询资料后发现应该在负载均衡的时候实现版本控制。同样是前端所有请求都在请求头中增加一个versionheader,后端实例都配置一个版本的tag

实现

首先需要说明的是我选择的控制中心是consul,网关是zuul

负载均衡策略被抽象为IRule接口,项目默认情况下使用的IRule的子类ZoneAvoidanceRule extends PredicateBasedRule,我们需要实现一个PredicateBasedRule的子类来替换ZoneAvoidanceRule

PredicateBasedRule需要实现一个过滤的方法我们就在这个方法里实现版本控制,过滤后就是默认的负载均衡策略了,默认是轮询。

    /**
     * Method that provides an instance of {@link AbstractServerPredicate} to be used by this class.
     * 
     */
    public abstract AbstractServerPredicate getPredicate();

VersionPredicate

我们可以看到PredicateBasedRulegetPredicate()方法需要返回一个AbstractServerPredicate实例,这个实例具体定义了版本控制的业务逻辑。代码如下:

private static class VersionPredicate extends AbstractServerPredicate {

        private static final String VERSION_KEY = "version";

        @Override
        public boolean apply(@NullableDecl PredicateKey predicateKey) {
            if (predicateKey == null) {
                return true;
            }
            RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();
            String version = request.getHeader(VERSION_KEY);
            if (version == null) {
                return true;
            }
            ConsulServer consulServer = (ConsulServer) predicateKey.getServer();
            if (!consulServer.getMetadata().containsKey(VERSION_KEY)) {
                return true;
            }
            return consulServer.getMetadata().get(VERSION_KEY).equals(version);
        }
    }

首先来了解下负载均衡的过程。一个请求到达网关后会解析出对应的服务名,然后会获取到该服务的所有可用实例,之后就会调用我们的过滤方法过滤出该请求可用的所有服务实例,最后进行轮询负载均衡。

PredicateKey类就是上层方法将可用实例ServerloadBalancerKey封装后的类。版本控制的业务逻辑如下:

  • 判断predicateKey是否为null,是的话直接返回truetrue代表该实例可用
  • 通过RequestContext获取当前请求实例HttpServletRequest,再通过请求实例获取请求头里的版本号
  • 判断前端请求是否带了版本号,没带的话就不进行版本控制直接返回true
  • 获取服务实例并转换成ConsulServer类,这里是因为我用的注册中心是consul,选择其他的可自行转换成对应的实现类
  • 判断服务实例是否设置了版本号(例:spring.cloud.consul.discovery.tags="version=1.0.0"),可以看到我们是用consultags实现的版本控制,可以设置不同的tag实现很多功能
  • 同样服务实例没有设置版本号的话也是直接返回true
  • 最后进行版本匹配,返回匹配成功的服务实例

注意的点

最终实现如下:

/**
 * @author Yuicon
 */
@Slf4j
public class VersionRule extends PredicateBasedRule {

    private final CompositePredicate predicate;

    public VersionRule() {
        super();
        this.predicate = createCompositePredicate(new VersionPredicate(),
                new AvailabilityPredicate(this, null));
    }

    @Override
    public AbstractServerPredicate getPredicate() {
        return this.predicate;
    }

    private CompositePredicate createCompositePredicate(VersionPredicate versionPredicate,
                                                        AvailabilityPredicate availabilityPredicate) {
        return CompositePredicate.withPredicates(versionPredicate, availabilityPredicate)
                .build();
    }

    private static class VersionPredicate extends AbstractServerPredicate {

        private static final String VERSION_KEY = "version";

        @Override
        public boolean apply(@NullableDecl PredicateKey predicateKey) {
            if (predicateKey == null) {
                return true;
            }
            RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();
            String version = request.getHeader(VERSION_KEY);
            if (version == null) {
                return true;
            }
            ConsulServer consulServer = (ConsulServer) predicateKey.getServer();
            if (!consulServer.getMetadata().containsKey(VERSION_KEY)) {
                return true;
            }
            log.info("id is {}, header is {}, metadata is {}, result is {}",
                    consulServer.getMetaInfo().getInstanceId(),
                    version, consulServer.getMetadata().get(VERSION_KEY),
                    consulServer.getMetadata().get(VERSION_KEY).equals(version));
            return consulServer.getMetadata().get(VERSION_KEY).equals(version);
        }
    }

}

原本我是加上@component注解后在本地直接测试通过了。可是在更新到生产服务器后却出现大部分请求都找不到的服务实例的错误,搞的我一头雾水,赶紧回滚到原来的版本。

查询了很多资料后才找到一篇文章,发现需要一个Config类来声明替换原有的负载均衡策略类。代码如下:

@RibbonClients(defaultConfiguration = RibbonGatewayConfig.class)
@Configuration
public class RibbonGatewayConfig {

    @Bean
    public IRule versionRule() {
        return new VersionRule();
    }

}

到此为止版本控制算是实现成功了。

结尾

在实际使用过程中发现还是有很多问题。比如前端版本号是全局唯一的,当其中一个服务升级了版本号,就需要将所有服务都升级到该版本号,即使代码没有任何更改。比较好的解决方案是前端根据不同服务传递不同的版本号,不过前端反馈实现困难。

还有个妥协的方案,就是利用配置中心来对具体服务是否开启版本控制进行配置,因为现在的需求只是一小段时间里需要版本控制,小程序审核过后就可以把旧服务实例关了。大家如果有更好的方案欢迎讨论。

Redis的ziplist数据结构笔记

OBJ_ENCODING_ZIPLIST

ziplist也就是压缩列表,是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,在Redis2.8的时候是作为小数据量的列表底层实现。ziplist本身并没有定义结构体,下面是《redis设计与实现》里的说明图。

ziplist entry

下面是ziplistentry逻辑定义,并不是实际的编码,但是可以用来理解entry是怎么组成的。

/* We use this function to receive information about a ziplist entry.
 * Note that this is not how the data is actually encoded, is just what we
 * get filled by a function in order to operate more easily. */
typedef struct zlentry {
    unsigned int prevrawlensize; // 存储上一个元素的长度数值所需要的字节数
    unsigned int prevrawlen;     // 前一个元素的长度
    unsigned int lensize;        // 存储元素的长度数值所需要的字节数,可以是1、2或者5字节,整数总是使用一个字节
    unsigned int len;            // 表示元素的长度
    unsigned int headersize;     /* prevrawlensize + lensize. */
    unsigned char encoding;      // 标记是字节数组还是整数
    unsigned char *p;            // 压缩链表以字符串的形式保存,该指针指向当前元素起始位置
} zlentry;

一个entry的数据组成如下图所示:

一个entry的第一部分是prevrawlen,编码了前一个元素的长度;entry的第二部分是encoding,编码了元素自身的长度和类型;entry的第三部分是value,存放了元素本身,有些entry并没有这一部分。

prevrawlen

每个zlentry都存储了前一个元素的长度,用字段prevrawlen来表示。

通过下面的源码可以知道在上一个元素小于254字节时,prevlensize等于1字节,否则prevlensize等于5字节。当prevlensize等于5字节时,prevrawlen的第一个字节会被设置为0xFE,后面的四个字节才是前一个元素的长度。

#define ZIP_BIG_PREVLEN 254 /* Max number of bytes of the previous entry, for
                               the "prevlen" field prefixing each entry, to be
                               represented with just a single byte. Otherwise
                               it is represented as FF AA BB CC DD, where
                               AA BB CC DD are a 4 bytes unsigned integer
                               representing the previous entry len. */

/* Return the number of bytes used to encode the length of the previous
 * entry. The length is returned by setting the var 'prevlensize'. */
#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do {                          
    if ((ptr)[0] < ZIP_BIG_PREVLEN) {                                          
        (prevlensize) = 1;                                                     
    } else {                                                                   
        (prevlensize) = 5;                                                     
    }                                                                          
} while(0);

encoding

在处理完prevrawlen字段,接下来就是处理encoding字段了。encoding字段的前两个bit用来标识类型,如果元素是一个整数的话encoding字段固定为一个字节,字节前两个bit固定为11。然后判断元素的大小进而将整数范围存储在encoding字段的后6个bit里。其中特殊的是当元素大于等于0且小于等于12的时候,元素会直接被写入encoding字段中,就没有后面的value字段了。

/* Check if string pointed to by 'entry' can be encoded as an integer.
 * Stores the integer value in 'v' and its encoding in 'encoding'. */
int zipTryEncoding(unsigned char *entry, unsigned int entrylen, long long *v, unsigned char *encoding) {
    long long value;

    if (entrylen >= 32 || entrylen == 0) return 0;
    if (string2ll((char*)entry,entrylen,&value)) {
        /* Great, the string can be encoded. Check what's the smallest
         * of our encoding types that can hold this value. */
        if (value >= 0 && value <= 12) {
            *encoding = ZIP_INT_IMM_MIN+value;
        } else if (value >= INT8_MIN && value <= INT8_MAX) {
            *encoding = ZIP_INT_8B;
        } else if (value >= INT16_MIN && value <= INT16_MAX) {
            *encoding = ZIP_INT_16B;
        } else if (value >= INT24_MIN && value <= INT24_MAX) {
            *encoding = ZIP_INT_24B;
        } else if (value >= INT32_MIN && value <= INT32_MAX) {
            *encoding = ZIP_INT_32B;
        } else {
            *encoding = ZIP_INT_64B;
        }
        *v = value;
        return 1;
    }
    return 0;
}

当元素是一个字符串时,同样会根据元素的长度设置encoding字段:

  • 当元素小于等于63字节时,encoding字段为1字节,前两位bit为00,后6位用于存储元素长度
  • 当元素小于等于16383字节时,encoding字段为2字节,前两位bit为01,其余位用于存储元素长度
  • 当元素小于等于4294967295字节时,encoding字段为5字节,第1个字节前两位bit为10,后4个字节用于存储元素长度
/* Write the encoidng header of the entry in 'p'. If p is NULL it just returns
 * the amount of bytes required to encode such a length. Arguments:
 *
 * 'encoding' is the encoding we are using for the entry. It could be
 * ZIP_INT_* or ZIP_STR_* or between ZIP_INT_IMM_MIN and ZIP_INT_IMM_MAX
 * for single-byte small immediate integers.
 *
 * 'rawlen' is only used for ZIP_STR_* encodings and is the length of the
 * srting that this entry represents.
 *
 * The function returns the number of bytes used by the encoding/length
 * header stored in 'p'. */
unsigned int zipStoreEntryEncoding(unsigned char *p, unsigned char encoding, unsigned int rawlen) {
    unsigned char len = 1, buf[5];

    if (ZIP_IS_STR(encoding)) {
        /* Although encoding is given it may not be set for strings,
         * so we determine it here using the raw length. */
        if (rawlen <= 0x3f) {
            if (!p) return len;
            buf[0] = ZIP_STR_06B | rawlen;
        } else if (rawlen <= 0x3fff) {
            len += 1;
            if (!p) return len;
            buf[0] = ZIP_STR_14B | ((rawlen >> 8) & 0x3f);
            buf[1] = rawlen & 0xff;
        } else {
            len += 4;
            if (!p) return len;
            buf[0] = ZIP_STR_32B;
            buf[1] = (rawlen >> 24) & 0xff;
            buf[2] = (rawlen >> 16) & 0xff;
            buf[3] = (rawlen >> 8) & 0xff;
            buf[4] = rawlen & 0xff;
        }
    } else {
        /* Implies integer encoding, so length is always 1. */
        if (!p) return len;
        buf[0] = encoding;
    }
    /* Store this length at p. */
    memcpy(p,buf,len);
    return len;
}

value

value字段就非常的简单了,直接把元素放在encoding字段后面就可以了,它可以是一个整数或者是字节数组,元素的长度和类型都已经存储在encoding字段里了。

总结

可以看到ziplist的实现是非常蛋疼的,Redis为了节省一些内存也是丧心病狂。不过这样内存是节省了,却增加了cpu的负担,redis也只是在数据量比较少的场景才会使用这种数据结构。作为一个Java程序员看到这样的数据结构其实是比较奇怪的,还是ArrayList好用:)

参考资料

《redis设计与实现》

Spring Boot中使用 Spring Security 构建权限系统

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

权限控制是非常常见的功能,在各种后台管理里权限控制更是重中之重.在Spring Boot中使用 Spring Security 构建权限系统是非常轻松和简单的.下面我们就来快速入门 Spring Security .在开始前我们需要多对多关系的用户角色类,一个restful的controller.

参考项目代码地址

- 添加 Spring Security 依赖

首先我默认大家都已经了解 Spring Boot 了,在 Spring Boot 项目中添加依赖是非常简单的.把对应的
spring-boot-starter-*** 加到pom.xml 文件中就行了

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

- 配置 Spring Security

简单的使用 Spring Security 只要配置三个类就完成了,分别是:

  • UserDetails

这个接口中规定了用户的几个必须要有的方法

public interface UserDetails extends Serializable {

    //返回分配给用户的角色列表
    Collection<? extends GrantedAuthority> getAuthorities();
    
    //返回密码
    String getPassword();

    //返回帐号
    String getUsername();

    // 账户是否未过期
    boolean isAccountNonExpired();

    // 账户是否未锁定
    boolean isAccountNonLocked();

    // 密码是否未过期
    boolean isCredentialsNonExpired();

    // 账户是否激活
    boolean isEnabled();
}
  • UserDetailsService

这个接口只有一个方法 loadUserByUsername,是提供一种用 用户名 查询用户并返回的方法。

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
  • WebSecurityConfigurerAdapter

这个内容很多,就不贴代码了,大家可以自己去看.

我们创建三个类分别继承上述三个接口

  • 此 User 类不是我们的数据库里的用户类,是用来安全服务的.
/**
 * Created by Yuicon on 2017/5/14.
 * https://segmentfault.com/u/yuicon
 */
public class User implements UserDetails {

    private final String id;
    //帐号,这里是我数据库里的字段
    private final String account;
    //密码
    private final String password;
    //角色集合
    private final Collection<? extends GrantedAuthority> authorities;

    User(String id, String account, String password, Collection<? extends GrantedAuthority> authorities) {
        this.id = id;
        this.account = account;
        this.password = password;
        this.authorities = authorities;
    }

    //返回分配给用户的角色列表
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @JsonIgnore
    public String getId() {
        return id;
    }

    @JsonIgnore
    @Override
    public String getPassword() {
        return password;
    }
    
    //虽然我数据库里的字段是 `account`  ,这里还是要写成 `getUsername()`,因为是继承的接口
    @Override
    public String getUsername() {
        return account;
    }
    // 账户是否未过期
    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    // 账户是否未锁定
    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    // 密码是否未过期
    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    // 账户是否激活
    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return true;
    }
}
  • 继承 UserDetailsService
/**
 * Created by Yuicon on 2017/5/14.
 * https://segmentfault.com/u/yuicon
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    // jpa
    @Autowired
    private UserRepository userRepository;

    /**
     * 提供一种从用户名可以查到用户并返回的方法
     * @param account 帐号
     * @return UserDetails
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
        // 这里是数据库里的用户类
        User user = userRepository.findByAccount(account);

        if (user == null) {
            throw new UsernameNotFoundException(String.format("没有该用户 '%s'.", account));
        } else {
            //这里返回上面继承了 UserDetails  接口的用户类,为了简单我们写个工厂类
            return UserFactory.create(user);
        }
    }
}
  • UserDetails 工厂类
/**
 * Created by Yuicon on 2017/5/14.
 * https://segmentfault.com/u/yuicon
 */
final class UserFactory {

    private UserFactory() {
    }

    static User create(User user) {
        return new User(
                user.getId(),
                user.getAccount(),
                user.getPassword(),
            mapToGrantedAuthorities(user.getRoles().stream().map(Role::getName).collect(Collectors.toList()))
        );
    }
    
    //将与用户类多对多的角色类的名称集合转换为 GrantedAuthority 集合
    private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) {
        return authorities.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}
  • 重点, 继承 WebSecurityConfigurerAdapter 类
/**
 * Created by Yuicon on 2017/5/14.
 * https://segmentfault.com/u/yuicon
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // Spring会自动寻找实现接口的类注入,会找到我们的 UserDetailsServiceImpl  类
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                // 设置UserDetailsService
                .userDetailsService(this.userDetailsService)
                // 使用BCrypt进行密码的hash
                .passwordEncoder(passwordEncoder());
    }

    // 装载BCrypt密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //允许跨域
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**").allowedOrigins("*")
                        .allowedMethods("GET", "HEAD", "POST","PUT", "DELETE", "OPTIONS")
                        .allowCredentials(false).maxAge(3600);
            }
        };
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 取消csrf
                .csrf().disable()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 允许对于网站静态资源的无授权访问
                .antMatchers(
                        HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/favicon.ico",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/webjars/**",
                        "/swagger-resources/**",
                        "/*/api-docs"
                ).permitAll()
                // 对于获取token的rest api要允许匿名访问
                .antMatchers("/auth/**").permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        // 禁用缓存
        httpSecurity.headers().cacheControl();
    }
}

- 控制权限到 controller

使用 @PreAuthorize("hasRole('ADMIN')") 注解就可以了

/**
 * 在 @PreAuthorize 中我们可以利用内建的 SPEL 表达式:比如 'hasRole()' 来决定哪些用户有权访问。
 * 需注意的一点是 hasRole 表达式认为每个角色名字前都有一个前缀 'ROLE_'。所以这里的 'ADMIN' 其实在
 * 数据库中存储的是 'ROLE_ADMIN' 。这个 @PreAuthorize 可以修饰Controller也可修饰Controller中的方法。
 **/
@RestController
@RequestMapping("/users")
@PreAuthorize("hasRole('USER')") //有ROLE_USER权限的用户可以访问
public class UserController {

   @Autowired
    private UserRepository repository;

    @PreAuthorize("hasRole('ADMIN')")//有ROLE_ADMIN权限的用户可以访问
    @RequestMapping(method = RequestMethod.GET)
    public List<User> getUsers() {
        return repository.findAll();
    }
}

- 结语

Spring Boot中 Spring Security 的入门非常简单,很快我们就能有一个满足大部分需求的权限系统了.而配合 Spring Security 的好搭档就是 JWT 了,两者的集成文章网络上也很多,大家可以自行集成.因为篇幅原因有不少代码省略了,需要的可以参考参考项目代码

Spring Boot中使用Swagger2构建API文档

程序员都很希望别人能写技术文档,自己却很不愿意写文档。因为接口数量繁多,并且充满业务细节,写文档需要花大量的时间去处理格式排版,代码修改后还需要同步修改文档,经常因为项目时间紧等原因导致文档滞后于代码,接口调用方的抱怨声不绝于耳。而程序员是最擅长"偷懒"的职业了,自然会有多种多样的自动生成文档的插件.今天要介绍的就是Swagger.

接下来我们在Spring Boot中使用Swagger2构建API文档

Swagger是一个简单但功能强大的API表达工具。它具有地球上最大的API工具生态系统,数以千计的开发人员,使用几乎所有的现代编程语言,都在支持和使用Swagger。使用Swagger生成API,我们可以得到交互式文档,自动生成代码的SDK以及API的发现特性等。

我们先来看看具体效果:
tim 20170806152444

可以看到Swagger-Ui是以controller分类,点击一个controller可以看到其中的具体接口,再点击接口就可以看到接口的信息了,如图:

tim 20170806152444

我们可以看到该接口的请求方式,返回数据信息和需要传递的参数.而且以上数据是自动生成的,即使代码有一些修改,Swagger文档也会自动同步修改.非常的方便.

  • 构建RESTful API

在使用Swagger2前我们需要有一个RESTful API的项目. Spring-Boot创建RESTful API项目非常的方便和快速,这里不再介绍如何创建,需要的可以参照项目代码

  • 添加Swagger2依赖

在pom.xml文件中加入以下依赖.

        <dependency>
            <groupId>io.springfox</groupId>
	    <artifactId>springfox-swagger2</artifactId>
	    <version>2.7.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.7.0</version>
        </dependency>
  • 创建Swagger2的Java配置类

    通过@Configuration注解,表明它是一个配置类,@EnableSwagger2 注解开启swagger2。apiInfo() 方法配置一些基本的信息。createRestApi() 方法指定扫描的包会生成文档,默认是显示所有接口,可以用@ApiIgnore注解标识该接口不显示。

/**
 * Created by Yuicon on 2017/5/20.
 * https://github.com/Yuicon
 */
@Configuration
@EnableSwagger2
public class Swagger2 {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                // 指定controller存放的目录路径
                .apis(RequestHandlerSelectors.basePackage("com.digag.web"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                 // 文档标题
                .title("DigAg")
                // 文档描述
                .description("https://github.com/Yuicon")
                .termsOfServiceUrl("https://github.com/Yuicon")
                .version("v1")
                .build();
    }

}
  • 编辑文档接口信息

先看一个例子:

    @ApiOperation(value="创建条目")
    @RequestMapping(method = RequestMethod.POST)
    public JsonResult<Entry> saveEntry(@RequestBody @ApiParam(value = "条目对象", required = true) Entry entry, HttpServletRequest request) {
        return entryService.create(entry, request);
    }

Swagger2提供了一些注解来丰富接口的信息,常用的有:

@ApiOperation:用在方法上,说明方法的作用

  • value: 表示接口名称
  • notes: 表示接口详细描述

@ApiImplicitParams:用在方法上包含一组参数说明

@ApiImplicitParam:用在@ApiImplicitParams注解中,指定一个请求参数的各个方面

  • paramType:参数位置
  • header 对应注解:@RequestHeader
  • query 对应注解:@RequestParam
  • path 对应注解: @PathVariable
  • body 对应注解: @requestbody
  • name:参数名
  • dataType:参数类型
  • required:参数是否必须传
  • value:参数的描述
  • defaultValue:参数的默认值

@ApiResponses:用于表示一组响应

@ApiResponse:用在@ApiResponses中,一般用于表达一个错误的响应信息

  • code:状态码

  • message:返回自定义信息

  • response:抛出异常的类

  • 访问文档

swagger2文档的默认地址是 /swagger-ui.html, 本地开发的访问 http://localhost:8080/swagger-ui.html就可以看到自动生成的文档了.

完整结果示例可查看项目代码

参考信息

Swagger注解文档
Swagger官方网站

Spring理论基础-控制反转和依赖注入

第一次了解到控制反转(Inversion of Control)这个概念,是在学习Spring框架的时候。IOCAOP作为Spring的两大特征,自然是要去好好学学的。而依赖注入(Dependency Injection,简称DI)却使得我困惑了挺久,一直想不明白他们之间的联系。

控制反转

控制反转顾名思义,就是要去反转控制权,那么到底是哪些控制被反转了?在2004年 Martin fowler 大神就提出了

“哪些方面的控制被反转了?”

这个问题,他总结出是依赖对象的获得被反转了。

在单一职责原则的设计下,很少有单独一个对象就能完成的任务。大多数任务都需要复数的对象来协作完成,这样对象与对象之间就有了依赖。一开始对象之间的依赖关系是自己解决的,需要什么对象了就New一个出来用,控制权是在对象本身。但是这样耦合度就非常高,可能某个对象的一点小修改就会引起连锁反应,需要把依赖的对象一路修改过去。

如果依赖对象的获得被反转,具体生成什么依赖对象和什么时候生成都由对象之外的IOC容器来决定。对象只要在用到依赖对象的时候能获取到就可以了,常用的方式有依赖注入和依赖查找(Dependency Lookup)。这样对象与对象之间的耦合就被移除到了对象之外,后续即使有依赖修改也不需要去修改原代码了。

总结一下,控制反转是指把对象的依赖管理从内部转移至外部。

依赖注入

控制反转是把对象之间的依赖关系提到外部去管理,可依赖是提到对象外面了,对象本身还是要用到依赖对象的,这时候就要用到依赖注入了。顾名思义,应用需要把对象所需要的依赖从外部注入进来。可以是通过对象的构造函数传参注入,这种叫做构造器注入(Constructor Injection)。如果是通过JavaBean的属性方法传参注入,就叫做设值方法注入(Setter Injection)

不管是通过什么方式注入的,如果是我们手动注入的话还是显得太麻烦了。这时候就需要一个容器来帮我们实现这个功能,自动的将对象所需的依赖注入进去,这个容器就是前面提到的IOC容器了。

控制反转和依赖注入的关系也已经清晰了,它们本质上可以说是一样的,只是具体的关注点不同。控制反转的关注点是控制权的转移,而依赖注入则内含了控制反转的意义,明确的描述了依赖对象在外部被管理然后注入到对象中。实现了依赖注入,控制也就反转了。

例子

  • 首先是传统的方式,耦合非常严重。
public class Main {

    public static void main(String[] args) {
        OrderService service = new OrderService();
        service.test();
    }

}
public class OrderService {

    private OrderDao dao = new OrderDao();

    public void test() {
        dao.doSomeThing();
    }

}
public class OrderDao {

    public void doSomeThing() {
        System.out.println("test");
    }

}
  • 接下来是没有使用容器的方式,松耦合了,但是手动注入非常的麻烦。
public class Main {

    public static void main(String[] args) {
        Dao dao = new OrderDao();
        OrderService service = new OrderService(dao);
        service.test();
    }

}
public interface Dao {

    void doSomeThing();

}
public class OrderDao implements Dao {

    @Override
    public void doSomeThing() {
        System.out.println("test");
    }

}
public class OrderService {

    private Dao dao;

    public OrderService(Dao dao) {
        this.dao = dao;
    }

    public void test() {
        dao.doSomeThing();
    }

}
  • 接下来使用容器造福人类。
// 引导类要放在项目根目录下,也就是在 src 下面
public class Main {

    public static void main(String[] args) {
        // 生成容器
        Container container = new Container(Main.class);
        // 获取Bean
        OrderService service = container.getBean(OrderService.class);
        // 调用
        service.test();
    }

}
@Component
public class OrderService {

    @Autowired
    private Dao dao;

    public void test() {
        dao.doSomeThing();
    }

    public Dao getDao() {
        return dao;
    }

    public void setDao(Dao dao) {
        this.dao = dao;
    }
}
@Component
public class OrderDao implements Dao {

    @Override
    public void doSomeThing() {
        System.out.println("test");
    }

}
public interface Dao {

    void doSomeThing();

}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Component {
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.METHOD})
public @interface Autowired {
}
public class Container {

    private List<String> classPaths = new ArrayList<>();

    private String separator;

    private Map<Class, Object> components = new HashMap<>();

    public Container(Class cls) {
        File file = new File(cls.getResource("").getFile());
        separator = file.getName();
        renderClassPaths(new File(this.getClass().getResource("").getFile()));
        make();
        di();
    }

    private void make() {
        classPaths.forEach(classPath -> {
            try {
                Class c = Class.forName(classPath);
                // 找到有 @ioc.Component 注解的类并实例化
                if (c.isAnnotationPresent(Component.class)) {
                    components.put(c, c.newInstance());
                }
            } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }
        });
    }

    /**
     * 注入依赖
     */
    private void di() {
        components.forEach((aClass, o) -> Arrays.stream(aClass.getDeclaredFields()).forEach(field -> {
            if (field.isAnnotationPresent(Autowired.class)) {
                try {
                    String methodName = "set" + field.getType().getName().substring(field.getType().getName().lastIndexOf(".") + 1);
                    Method method = aClass.getMethod(methodName, field.getType());
                    if (field.getType().isInterface()) {
                        components.keySet().forEach(aClass1 -> {
                            if (Arrays.stream(aClass1.getInterfaces()).anyMatch(aClass2 -> aClass2.equals(field.getType()))) {
                                try {
                                    method.invoke(o, components.get(aClass1));
                                } catch (IllegalAccessException | InvocationTargetException e) {
                                    e.printStackTrace();
                                }
                            }
                        });
                    } else {
                        method.invoke(o, components.get(field.getType()));
                    }
                } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }));
    }

    /**
     * 该方法会得到所有的类,将类的全类名写入到classPaths中
     *
     * @param file 包
     */
    private void renderClassPaths(File file) {
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            Arrays.stream(Objects.requireNonNull(files)).forEach(this::renderClassPaths);
        } else {
            if (file.getName().endsWith(".class")) {
                String classPath = file.getPath()
                        .substring(file.getPath().lastIndexOf(separator) + separator.length() + 1)
                        .replace('\\', '.')
                        .replace(".class", "");
                classPaths.add(classPath);
            }
        }
    }

    public <T> T getBean(Class c) {
        return (T) components.get(c);
    }

}

后记

一些概念在脑海里总以为是清晰的,等实际用到或者是写成文字的时候就发现有很多不理解的地方。本文的目的就是梳理下概念,做些记录。这次自己尝试实现了下IOC容器,一开始写就知道自己之前的理解有问题了。好歹是写出了个能用的版本,用来应付文章中的例子。后面可以去参考下Spring的实现,估计能学到不少东西。

参考资料

在spring boot中使用jmh进行性能测试

长期处于CRUD工作中的我突然有一天关心起自己项目的qps了.便用jmeter测试了访问量最大的接口,发现只有可怜的17r/s左右......看到网络上比比皆是的几百万qps,我无地自容啊.

于是就准备进行性能优化了,不过在优化前我们需要进行性能测试,这样才能知道优化的效果如何.比如我第一步就是用redis加了缓存,结果测试发现居然比不加之前还要慢???所以性能测试是非常重要的一环,而jmh就是非常适合的性能测试工具了.

准备工作

准备工作非常的简单,引入jmhmaven包就可以了.

        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>1.22</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>1.22</version>
            <scope>provided</scope>
        </dependency>

第一个例子

package jmh;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

/**
 * Benchmark
 *
 * @author wangpenglei
 * @since 2019/11/27 13:54
 */
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class Benchmark {

    public static void main(String[] args) throws Exception {
        // 使用一个单独进程执行测试,执行5遍warmup,然后执行5遍测试
        Options opt = new OptionsBuilder().include(Benchmark.class.getSimpleName()).forks(1).warmupIterations(5)
                .measurementIterations(5).build();
        new Runner(opt).run();
    }

    @Setup
    public void init() {
   
    }

    @TearDown
    public void down() {

    }

    @org.openjdk.jmh.annotations.Benchmark
    public void test() {

    }

}

@BenchmarkMode

这个注解决定了测试模式,具体的内容网络上非常多,我这里采用的是计算平均运行时间

@OutputTimeUnit(TimeUnit.MILLISECONDS)

这个注解是最后输出结果时的单位.因为测试的是接口,所以我采用的是毫秒.如果是测试本地redis或者本地方法这种可以换更小的单位.

@State(Scope.Benchmark)

这个注解定义了给定类实例的可用范围,因为spring里的bean默认是单例,所以我这里采用的是运行相同测试的所有线程将共享实例。可以用来测试状态对象的多线程性能(或者仅标记该范围的基准).

@setup @teardown @benchmark

非常简单的注解,平常测试都有的测试前初始化*测试后清理资源**测试方法*.

如何与spring共同使用

因为我们需要spring的环境才能测试容器里的bean,所以需要在初始化方法中手动创建一个.我查了一下资料没发现什么更好的方法,就先自己手动创建吧.

    private ConfigurableApplicationContext context;
    private AppGoodsController controller;

    @Setup
    public void init() {
        // 这里的WebApplication.class是项目里的spring boot启动类
        context = SpringApplication.run(WebApplication.class);
        // 获取需要测试的bean
        this.controller = context.getBean(AppGoodsController.class);
    }

    @TearDown
    public void down() {
        context.close();
    }

开始测试

写好测试方法后启动main方法就开始测试了,现在会报一些奇奇怪怪的错误,不过不影响结果我就没管了.运行完成后会输出结果,这时候可以对比下优化的效果.

Result "jmh.Benchmark.testGetGoodsList":
  65.969 ±(99.9%) 10.683 ms/op [Average]
  (min, avg, max) = (63.087, 65.969, 69.996), stdev = 2.774
  CI (99.9%): [55.286, 76.652] (assumes normal distribution)


# Run complete. Total time: 00:02:48

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                   Mode  Cnt   Score    Error  Units
Benchmark.testGetGoodsList  avgt    5  65.969 ± 10.683  ms/op

Process finished with exit code 0

开头的负优化

在文章开头我讲了一个负优化的例子,我用redis加了缓存后居然比直接数据库查询还要慢!其实原因很简单,我在本地电脑上测试,连接的redis却部署在服务器上.这样来回公网的网络延迟就已经很大了.不过数据库也是通过公网的,也不会比redis快才对.最后的原因是发现部署redis的服务器带宽只有1m也就是100kb/s,很容易就被占满了.最后优化是redis加缓存与使用内网连接redis.

优化结果

优化前速度:

Result "jmh.Benchmark.testGetGoodsList":
  102.419 ±(99.9%) 153.083 ms/op [Average]
  (min, avg, max) = (65.047, 102.419, 162.409), stdev = 39.755
  CI (99.9%): [≈ 0, 255.502] (assumes normal distribution)


# Run complete. Total time: 00:03:03

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                   Mode  Cnt    Score     Error  Units
Benchmark.testGetGoodsList  avgt    5  102.419 ± 153.083  ms/op

Process finished with exit code 0

优化后速度(为了模拟内网redis速度,连的是本地redis):

Result "jmh.Benchmark.testGetGoodsList":
  29.210 ±(99.9%) 2.947 ms/op [Average]
  (min, avg, max) = (28.479, 29.210, 30.380), stdev = 0.765
  CI (99.9%): [26.263, 32.157] (assumes normal distribution)


# Run complete. Total time: 00:02:49

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                   Mode  Cnt   Score   Error  Units
Benchmark.testGetGoodsList  avgt    5  29.210 ± 2.947  ms/op

Process finished with exit code 0

可以看到大约快了3.5倍,其实还有优化空间,全部数据库操作都通过redis缓存的话,大概1ms就处理完了.

Java 基础整理

- 面向对象的三个基本特征

  1. 封装

面向对象的程序设计中,某个类把所需要的数据(也可以说是类的属性)和对数据的操作(也可以说是类的行为)全部都封装在类中,分别称为类的成员变量和方法(或成员函数)。这种把成员变量和成员函数封装在一起的编程特性称为封装。

  1. 继承

继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。

  1. 多态

多态性(polymorphisn)是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。
实现多态,有二种方式,覆盖,重载。
覆盖,是指子类重新定义父类的虚函数的做法。
重载,是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。

- 抽象类和接口有什么区别

接口和抽象类都是继承树的上层,他们的共同点如下:

  1. 都是上层的抽象层。
  2. 都不能被实例化
  3. 都能包含抽象的方法,这些抽象的方法用于描述类具备的功能,但是不比提供具体的实现。

他们的区别如下:

  1. 在抽象类中可以写非抽象的方法,从而避免在子类中重复书写他们,这样可以提高代码的复用性,这是抽象类的优势;接口中只能有抽象的方法。
  2. 一个类只能继承一个直接父类,这个父类可以是具体的类也可是抽象类;但是一个类可以实现多个接口。

- session 与 cookie 区别

Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;
Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。

- session一致性的架构设计常见方法

session同步法:多台web-server相互同步数据
客户端存储法:一个用户只存储自己的数据
反向代理hash一致性:四层hash和七层hash都可以做,保证一个用户的请求落在一台web-server上
后端统一存储:web-server重启和扩容,session也不会丢失

- ArrayList和LinkedList的大致区别

  1. ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。 (LinkedList是双向链表,有next也有previous)
  2. 对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针。
  3. 对于小数据量新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。

- java中创建线程的三种方法

  1. 继承Thread类创建线程
  2. 实现Runnable接口创建线程
  3. 使用Callable和Future创建线程

-CountDownLatch

等待多线程完成的CountDownLatch

-CyclicBarrier

同步屏障CyclicBarrier

-Semaphore

控制并发线程数的Semaphore

-Exchanger

两个线程进行数据交换的Exchanger

-ThreadLocal

ThreadLocal实现方式&使用介绍—无锁化线程封闭

-Java垃圾回收机制

Java垃圾回收机制(GC)相关问题

- 类加载机制

JVM 类加载机制详解
深入探讨 Java 类加载器

Kafka 安装实践笔记

最近终于有需要消息队列的业务需求出现了,就决定自己搭建一个Kafka集群作为业务测试用。如果没问题的话就不去购买云服务了,还能省下不少钱呢。对于Kafka之前自己也写过demo稍微了解了一下,这次就把实践的要点记录下来。

安装Kafka

Kafka的安装可以说是非常简单了。

  • 首先去官网找到下载链接,注意要下载二进制版本。
  • 然后用wget下载到服务器上,再解压(tar xzvf)就ok了
  • Kafka是依赖于zookeeper的,不过官方的安装包里已经自带了zookeeper,单机版直接用已经写好的脚本启动就可以了,我是按文档搭了个zookeeper集群。
bin/zookeeper-server-start.sh config/zookeeper.properties
需要后台运行的加 -daemon 参数
bin/zookeeper-server-start.sh -daemon config/zookeeper.properties
用netstat -ntlp命令可以看到
tcp        0      0 0.0.0.0:2181            0.0.0.0:*               LISTEN      {pid}/java
就是启动成功了。

服务器参数配置,Broker Configs

参数配置非常的重要,这里只是重点讲下几个基本都要配置的点。首先打开配置文件,vi config/server.properties

broker.id

broker.id指的是当前节点的id,必须是唯一不重复的整数。

例子: broker.id=0

advertised.listeners

advertised.listeners,设置这个是为了让客户端拿到正确的主机地址,默认是拿的java.net.InetAddress.getCanonicalHostName()的值,会拿到类似于izbp1i1jfn47dnqehu39pez:9092这样的地址,所以就报连接错误了。

例子: advertised.listeners=PLAINTEXT://111.222.333.444:9092

log.dirs

log.dirs指的是消息存放的目录,可以指定多个用,分隔,默认是/tmp/kafka-logs。这里有个坑/tmp目录是会不定期清理数据的,所以放这里可能消息就会神秘消失。建议指定一个其他的目录。

例子: log.dirs=/mnt/kafka-logs

zookeeper.connect

zookeeper.connect指的是zookeeper的连接地址,集群要写多个,注意多台服务器要能联通。

例子: zookeeper.connect=111.16.212.244:2181,222.16.212.243:2181,333.16.212.236:2181

log.cleanup.policy

log.cleanup.policy指的是消息的清理策略,默认是168小时前的消息会被删除。可以改为压缩消息,同一个key的消息只保留最新的。不过建议在topic里设置,可以把重要的消息设置为压缩,不重要的设置为删除。

例子: log.cleanup.policy=compact

num.partitions

num.partitions指的是主题的默认分区数量。默认值是1,建议改成50,因为一个分区只允许一个消费者消费,分区数量多的话可以并发消费。

例子:num.partitions=50

default.replication.factor

default.replication.factor指的是自动创建主题的副本数量.默认是1,即只有一个主副本,相当于没有备份。建议改为大于等于2小于等于服务器数量的值。

例子: default.replication.factor=2

主题参数配置,Topic-Level Configs

主题的参数配置是写在生产者或者消费者的代码里的,同样是只提几个重要的。

cleanup.policy

cleanup.policy和服务器的log.cleanup.policy配置效果是一样的,只不过优先级高一些。

min.insync.replicas

min.insync.replicas指的是最小同步副本数量,默认是1个建议设为大于等于2,这样消息至少有一个副本。这里和生产者的acks配置有关,当acks设置为all的时候,消息需要复制到所有副本里才算成功,这里的所有副本数量不一定是固定的。比如一个主题有3个副本,其中一个副本因为同步进度太慢被踢出了isr(In-Sync Replicas),这时的所有副本其实只有2个(包括主副本),而不是3个。

例子: min.insync.replicas=2

分区数量和副本数量

Kafka是默认支持主题的自动创建的,默认的分区数量和副本数量配置已经在服务器配置里讲过了。当然也可以在spring等框架里配置,事实上主题的参数配置都可以在spring里配置,需要的可以去文档查询。

结尾

到这里Kafka的大部分业务无关的配置都已经搞定了,接下来就是写生产者和消费者了。之后还会写在spring boot里使用Kafka的笔记,到这里也只是简单的配置了Kafka,建议去阅读下《Kafka技术内幕:图文详解Kafka源码设计与实现》,这样才能更好的理解一些配置参数的意义和对Kafka的影响。

HashMap的实现原理笔记

HashMap是Java中常用的Map接口的实现类,因为在日常工作中非常频繁的出现,所以在大部分的Java面试中都会问几个关于HashMap的问题。掌握HashMap的实现原理,已经是Java程序员的基础操作了。

Map接口

映射(Map)是一种用于存放键/值对的数据结构。如果提供了键,就能直接找到相对应的值。HashMap(哈希映射)是Map接口的一个实现类,主要使用哈希来实现键与值的映射。

定义。映射是一种用于存放键/值对的数据结构,主要支持两种操作:插入(put),即将一组新的键值对存入映射中;查找(get),即根据给定的键得到相应的值。

HashMap的底层数据结构

HashMap的底层是用散列表实现的,散列表是一种用数组来存储键值对的数据结构,它使用一个散列函数将键转换成数组的一个索引然后存储值。不过会有不同的键被散列成同个索引的情况出现,这叫做碰撞冲突。HashMap用拉链法来解决这个问题,即散列表数组中的每个元素都指向一条链表,链表中的每个节点都存储了被散列到这个索引的键值对。

image

HashMap的散列函数

根据散列表的定义我们知道,想要弄清楚HashMap的实现,我们首先需要知道HashMap的散列函数是怎么实现的,即HashMap是如何将一个键映射到数组的索引的。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

上面就是HashMap的散列函数源码了,可以看到如果键为null的话它的索引固定为0,即HashMap是支持使用null作为键的。如果键不为null就用Java对象都有的hashCode方法获得一个哈希值,并将这个哈希值的低16位与高16位做异或处理,高16位不变。

这里有个问题就是为什么不直接用键的hashcode,而要将hashcode的低16位与高16位做异或处理?这里是因为有三个前提:

  • int有32位
  • HashMap的数组长度都是2的幂
  • 获取数组索引需要将键的哈希值和数组长度-1做一个与操作(&)得到,即tab[(n - 1) & hash]

首先因为HashMap的数组长度都是2的幂,(n - 1)的高16位都是0,所以只有键的低16位参与索引运算。如果直接用键的hashcode的话,就会有很多碰撞冲突,所以用这种方法使得hashcoede的高16位也参与到索引的运算中来。下面是字符串“1234”在数组长度为16的索引运算过程:

public static void main(String[] args) {
    int hashcode = "1234".hashCode();
    System.out.println(Integer.toBinaryString(hashcode));
    // 输出为 10111 0000100001000010
    System.out.println(Integer.toBinaryString(hashcode >>> 16));
    // 输出为 10111
    System.out.println(Integer.toBinaryString(hashcode ^ (hashcode >>> 16)));
    // 输出为 10111 0000100001010101
    System.out.println(Integer.toBinaryString(16 - 1));
    // 输出为 1111
    System.out.println(Integer.toBinaryString((16 - 1) & (hashcode ^ (hashcode >>> 16))));
    // 输出为 101
}

image

碰撞冲突

虽然HashMap对散列函数做了很多优化,但是碰撞冲突还是不可避免的会出现。为了解决这个问题HashMap使用了拉链法,使用链表来存储碰撞冲突的键值对。并在JDK 8中进行了优化,当链表长度到达某个指定值时HashMap会自动将链表优化为红黑树。频繁碰撞冲突还可能是因为数组长度不够的原因,HashMap还会根据键值对的数量进行自动扩容。

自动扩容

在讲HashMap的自动扩容前,先来看看HashMap有哪些相关的属性:

  • Node<K,V>[] table; 存放键值对的数组
  • int size; 已存放键值对的数量
  • int threshold; 当键值对的数量等于这个值的时候HashMap将进行扩容,值等于数组长度 * loadFactor
  • final float loadFactor; 负载因子,用于计算threshold的值,默认值为0.75
  • static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 数组长度的默认值16

根据这些属性可以知道,HashMap的自动扩容会根据数组长度和负载因子的积得到一个threshold的值,当键值对的数量等于threshold时就会开始扩容,下面是扩容的源码。大概过程是新建一个长度为旧数组两倍的新数组,并将原有的键值对都重新映射到新数组上。

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

后记

这次主要是理解了一下HashMap的实现原理,特别重点写了很多关于散列函数的理解,并没有按照源码一行行的去理解。之所以这样是因为写这篇的动力主要来源于面试……而面试则只要讲下原理就可以了,并不需要把源码背下来。之前看HashMap的时候对散列函数都是跳过去的,只知道是用来计算键的hash,不知道里面的原理。其实还有链表转红黑树的地方没有弄清楚,主要是红黑树不怎么理解,基础的重要性体现了出来。

Java基础-接口、lambda表达式

Java 8新增的lambda表达式毫无疑问是令人非常激动的,从此我们可以非常简洁的定义和使用代码块而不是用繁琐的匿名内部类来实现。而接口是lambda表达式的基础,要理解lambda表达式就要先理解接口的概念。

接口

Java中接口是对类行为的抽象。似乎继承也能做到这件事,它们的区别在于Java中类只能有一个父类,而接口是可以实现多个的。所以接口更倾向于类的一部分抽象,也就是行为的抽象,而不是类本身的抽象。

语法

要定义一个接口很简单,使用关键字interface后面再跟上接口名称就可以了。类可以用implements关键字来实现接口。

public interface A {
    void test();
}
  • 接口不允许有实例域,但可以有常量
  • 接口中的域都会自动声明为public static final
  • 接口中的方法都会自动声明为public
  • 接口中可以声明抽象方法,Java 8以后还可以声明静态方法和默认方法
// Java 8版本
public interface A {
    //常量
    String AUTHOR = "Yuicon";
    //抽象方法
    void test();
    //默认方法
    default void testDefault(){}
    //静态方法
    static void testStatic(){}
}

默认方法的冲突

如果一个类实现的接口中有签名相同的默认方法,那么就会有冲突的问题。在Java中解决这个问题有一些明确的规则:

  • 在超类中已有同签名的方法,就会忽略接口中的默认方法,也就是超类优先
  • 接口中默认方法和另一个默认方法或者抽象方法冲突的,必须要覆盖这个方法

lambda表达式

lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。之所以会有这么一个特性,是因为原先在Java中传递一个代码块是非常繁琐的一件事情,必须要构建一个对象。比如常用的Runnable接口:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("我好麻烦");
    }
};

lambda版本就非常简洁:

Runnable runnable = () -> System.out.println("我很简洁");

是的,lambda版本只要一行就完成了任务。

语法

在我看来lambda表达式是一种语法糖,它提供了一种简洁、易懂的方式来实现只有一个抽象方法的接口。关键词是只有一个抽象方法的接口,比如这样一个接口:

@FunctionalInterface
public interface A {
    void test();
}

A a = () -> System.out.println("test");
a.test();

其中@FunctionalInterface注解是用来标记接口为函数式接口,去掉也不会影响功能,添加了这个注解后编译器会检查接口内是否只有一个抽象方法。

lambda表达式主要有以下要素:

  • 参数
  • 箭头 ->
  • 方法体
  • 自由变量

参数

lambda表达式的参数和普通方法的参数并无太大区别,主要的区别点有:

  • 如果参数的类型可以被编译器推导出来,那么可以省略参数类型
  • 如果参数的类型可以被编译器推导出来,而且只有一个参数,那么就可以省略括号
Consumer<String> consumer = s -> System.out.println(s); 

方法体

lambda表达式的方法体内只有一条语句的时候,可以不加大括号且无需指定返回值,编译器会自动推导。方法体内有多条语句的时候就需要加大括号并手动指定返回值,不过lambda表达式是没有自己的作用域的,这点需要注意。

Supplier<String> supplier = () -> {
    String s = "test";
    return s;
};

自由变量

自由变量是指非参数而且不在方法体内定义的变量,我们来看一个例子:

    public static void main(String[] args) {
        String test = "test";
        A a = () -> System.out.println(test);
        a.test();
    }

例子中的变量test就是一个自由变量,代码块a引用了外部方法的变量,这就是一个闭包了。lambda表达式会复制一份自由变量的值,对象的话就是复制一个引用,因此lambda表达式离开了原作用域也能正常使用自由变量。不过lambda表达式对自由变量是有要求的,自由变量必须是不可变的,原因是并发执行时不安全。以下代码是错误的:

for (int i = 0; i < 9; i++) {
    // error
    A a = () -> System.out.println(i);
}

方法引用

方法引用是语法糖的语法糖,顾名思义方法引用是引用已有方法的一个特性。它的形式如下:

@FunctionalInterface
public interface A {

    void test(String s);

}

A a = System.out::println;
a.test("test");

之所以说方法引用是语法糖的语法糖是因为A a = System.out::println;完全等价于A a = s -> System.out.println(s);,方法引用有5种情况:

  • object::instanceMethod
  • this::instanceMethod
  • super::instanceMethod // 超类方法
  • Class::staticMethod
  • Class::instanceMethod

前4种情况和lambda表达式是完全等价的,第5种情况比较特殊,第一个参数会成为方法的目标。比如String::compareToIgnoreCase等同于 (x, y)-> x.compareToIgnoreCase(y)

构造器引用

构造器引用是引用对象的构造器,用的是特殊的方法名new,使用形式为Object::new,使用方法和方法引用差不多。

常用函数式接口

1655a409e1822948

JDK已经提供了常用的函数式接口基本上是不需要自己写函数式接口的。

后记

一周一篇是不可能一周一篇的,人懒起来就和咸鱼一样根本不会动弹。还好人是会变通的,上周少了这周补上不就行了!Java被人诟病繁琐不是一天两天了,在各种新生编程语言的追赶下Java也要加快自己的演进了,更改发布周期就是一个很好的信号。

参考资料:
《Java核心技术 卷1》

Java多线程基础-ThreadLocal

我的博客 转载请注明原创出处。

在多线程环境下,访问非线程安全的变量时必须进行线程同步,例如使用synchronized方式访问HashMap实例。但是同步访问会降低并发性,影响系统性能。这时候就可以用空间换时间,如果我们给每个线程都分配一个独立的变量,就可以用非同步的方式使用非线程安全的变量,我们称这种变量为线程局部变量。

顾名思义,线程局部变量是指每个线程都有一份属于自己独立的变量副本,不会像普通局部变量一样可以被其他线程访问到。Java并没有提供语言级的线程局部变量,而是在类库里提供了线程局部变量的功能,也就是这次的主角ThreadLocal类。

ThreadLocal的使用

68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f392f31342f313635643762616262343733326233343f773d34393026683d32323726663d706e6726733d3132393131

Java8版本的ThreadLocal有上图所示的4个public方法和一个protected的方法,第一个方法用于返回初始值,默认是null。第二个静态方法withInitial(Supplier<? extends S> supplier)Java8版本新添加的,后面三个实例方法则非常的简单。

Java8之前,使用ThreadLocal时想要设置初始值时需要继承ThreadLocal类覆盖protected T initialValue()方法才行,例如:

ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
        return 0;
    }
};

在Java8版本可以使用新添加的静态方法withInitial(Supplier<? extends S> supplier),非常方便的设置初始值,例如:

ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

System.out.println(threadLocal.get());
threadLocal.set(16);
System.out.println(threadLocal.get());
threadLocal.remove();
System.out.println(threadLocal.get());

// 同一个线程的输出
0
16
0

Process finished with exit code 0

ThreadLocal的原理

那么ThreadLocal是怎么实现线程局部变量的功能的呢?其实ThreadLocal的基本原理并没有十分复杂。ThreadLocal在内部定义了一个静态类ThreadLocalMapThreadLocalMap的键为ThreadLocal对象,ThreadLocalMap的值就是ThreadLocal存储的值,不过这个ThreadLocalMap是在Thread类里维护的。我们来看一下ThreadLocal的部分源码:

    // ThreadLocal的set方法
    public void set(T value) {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取Map
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 设置值
            map.set(this, value);
        else
            // 初始化Map
            createMap(t, value);
    }
    
    // ThreadLocal的createMap方法
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    // Thread类定义的实例域
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

可以看出ThreadLocal的核心实现就是ThreadLocalMap的实现了,ThreadLocalMap内部声明了一个Entry类来存储数据:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

ThreadLocalMap的实现与HashMap的实现有相似的地方,比如同样是使用数组存储数据和自动扩容,不同的是hash算法与hash碰撞后的处理不一样。

        // ThreadLocalMap的set方法
        private void set(ThreadLocal<?> key, Object value) {

            Entry[] tab = table;
            int len = tab.length;
            // 计算在Entry[]中的索引,每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小0x61c88647
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                // 如果键已存在就更新值
                if (k == key) {
                    e.value = value;
                    return;
                }
                // 代替无效的键
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
        
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

可以看到ThreadLocalMap把Entry[]数组当成一个圆环。从计算出来的索引位置开始,如果该索引已经有数据了就判断Key是否相同,相同就更新值。否则就直到找到一个空的位置把值放进去。获取值的时候也类似,从计算出来的索引位置开始一个一个检查Key是否相同,这样hash碰撞比较多的话可能性能就不是很好。

ThreadLocal的应用

ThreadLocal的应用是非常广的,比如Java工程师非常熟悉的Spring框架中就使用了ThreadLocal来把非线程安全的状态性对象封装起来,所以我们可以把绝大部分的Bean声明为singleton作用域。我们在编写多线程代码时也可以想想是用同步的方式访问非线程安全的状态性对象比较好,还是使用ThreadLocal把非线程安全的状态性对象封装起来更好。

后记

本来下定决心准备一周一篇的,结果偷懒了一次后赶上了公司旅游。这一下子摸了两篇,只能后面慢慢补了……ThreadLocal我很早就看到过了,一直没什么实感,直到在《精通Spring 4.X 企业应用开发实战》看到在Spring中的应用后才发现,我从来没想过为什么Spring里的Dao类可以声明为单例作用域……没有举一反三的能力就只能多看书了,活到老学到老。

参考资料:

  • 《Java核心技术 卷一》
  • 《精通Spring 4.X 企业应用开发实战》

Java基础-泛型笔记

我的博客 转载请注明原创出处。

之所以会想来写泛型相关的内容,是因为看到这样的一段代码:

当时我的内心是这样的:

所以就赶紧去复习了下,记录下来。基础不扎实,源码看不懂啊。

泛型介绍

Java 泛型(generics)是 JDK 5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数,在Java集合框架里使用的非常广泛。

定义的重点是提供了编译时类型安全检测机制。比如有这样的一个泛型类:

public class Generics <T> {

    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

然后写这样一个类:

public class Generics  {

    private Object value;

    public Object getValue() {
        return value;
    }

    public void setValue(Object value) {
        this.value = value;
    }
}

它们同样都能存储所有值,但是泛型类有编译时类型安全检测机制:

泛型类

一个类定义了一个或多个类型变量,那么就是泛型类。语法是在类名后用尖括号括起来,类型变量写在里面用逗号分开。然后就可以在方法的返回类型、参数和域、局部变量中使用类型变量了,但是不能在有static修饰符修饰的方法或域中使用。例子:

类定义参考上文例子
使用形式
Generics<String> generics = new Generics<String>();
后面尖括号内的内容在Jdk7以后可以省略
Generics<String> generics = new Generics<>();

泛型方法

一个方法定义了一个或多个类型变量,那么就是泛型方法。语法是在方法修饰符后面、返回类型前面用尖括号括起来,类型变量写在里面用逗号分开。泛型方法可以定义在普通类和泛型类中,泛型方法可以被static修饰符修饰。
例子:

    private <U> void out(U u) {
        System.out.println(u);
    }
    
    调用形式,
    Test.<String>out("test");
    大部分情况下<String>都可以省略,编译器可以推断出来类型
    Test.out("test");
    

类型变量的限定

有时候我们会有希望限定类型变量的情况,比如限定指定的类型变量需要实现List接口,这样我们就可以在代码对类型变量调用List接口里的方法,而不用担心会没有这个方法。

public class Generics <T extends List> {

    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }

    public void add(Object u) {
        value.add(u);
    }

    public static void main(String[] args) {
        Generics<List> generics = new Generics<>();
        generics.setValue(new ArrayList<>());
        generics.add("ss");
        System.out.println(generics.getValue());
    }
    
}

限定的语法是在类型变量的后面加extends关键字,然后加限定的类型,多个限定的类型要用&分隔。类型变量和限定的类型可以是类也可以是接口,因为Java中类只能继承一个类,所以限定的类型是类的话一定要在限定列表的第一个。

类型擦除

类型擦除是为了兼容而搞出来的,大意就是在虚拟机里是没有泛型类型,泛型只存在于编译期间。泛型类型变量会在编译后被擦除,用第一个限定类型替换(没有限定类型的用Object替换)。上文中的Generics <T>泛型类被擦除后会产生对应的一个原始类型:

public class Generics {

    private Object value;

    public Object getValue() {
        return value;
    }

    public void setValue(Object value) {
        this.value = value;
    }

}

之所以我们能设置和返回正确的类型是因为编译器自动插入了类型转换的指令。

    public static void main(String[] args) {
        Generics<String> generics = new Generics<>();
        generics.setValue("ss");
        System.out.println(generics.getValue());
    }
    
    javac Generics.java
    javap -c Generics
    编译后的代码
     public static void main(java.lang.String[]);
        Code:
       0: new           #3                  // class generics/Generics
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String ss
      11: invokevirtual #6                  // Method setValue:(Ljava/lang/Object;)V
      14: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      17: aload_1
      18: invokevirtual #8                  // Method getValue:()Ljava/lang/Object;
      21: checkcast     #9                  // class java/lang/String
      24: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      27: return

我们可以看到在21行插入了一条类型转换的指令。

类型擦除还带来了另一个问题,如果我们有一个类继承了泛型类并重写了父类的方法:

public class SubGenerics extends Generics<String> {

    @Override
    public void setValue(String value) {
        System.out.println(value);
    }

    public static void main(String[] args) {
        Generics<String> generics = new SubGenerics();
        generics.setValue("ss");
    }

}

因为类型擦除所以SubGenerics实际上有两个setValue方法,SubGenerics自己的setValue(String value)方法和从Generics继承来的setValue(Object value)方法。例子中的generics引用的是SubGenerics对象,所以我们希望调用的是SubGenerics.setValue。为了保证正确的多态性,编译器在SubGenerics类中生成了一个桥方法:

public void setValue(Object value) {
    setValue((String) value);
}

我们可以编译验证下:

Compiled from "SubGenerics.java"
public class generics.SubGenerics extends generics.Generics<java.lang.String> {
  public generics.SubGenerics();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method generics/Generics."<init>":()V
       4: return

  public void setValue(java.lang.String);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: aload_1
       4: invokevirtual #3                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       7: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #4                  // class generics/SubGenerics
       3: dup
       4: invokespecial #5                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #6                  // String ss
      11: invokevirtual #7                  // Method generics/Generics.setValue:(Ljava/lang/Object;)V
      14: return

  public void setValue(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #8                  // class java/lang/String
       5: invokevirtual #9                  // Method setValue:(Ljava/lang/String;)V
       8: return
}

引用《Java核心技术 卷一》

总之,需要记住有关 Java 泛型转换的事实:
1.虚拟机中没有泛型,只有普通的类和方法。
2.所有的类型参数都用它们的限定类型替换。
3.桥方法被合成来保持多态。
4.为保持类型安全性,必要时插人强制类型转换。

约束与局限性

  • 类型变量不能是基本变量,比如int,double等。应该使用它们的包装类Integer,Double
  • 虚拟机中没有泛型,所以不能使用运行时的类型查询,比如 if (generics instanceof Generics<String>) // Error
  • 因为类型擦除的原因,不能创建泛型类数组,比如Generics<String>[] generics = new Generics<String>[10]; // Error
  • 不能实例化类型变量,比如new T(...) new T[...] 或 T.class

通配符类型

通配符类型和上文中的类型变量的限定有些类似,区别是通配符类型是运用在声明的时候而类型变量的限定是在定义的时候。比如通配符类型Generics<? extends List>代表任何泛型Generics类型的类型变量是ListList的子类。

Generics<? extends List> generics = new Generics<ArrayList>();

不过这样声明之后Generics的方法也发生了变化,变成了

这样就导致了不能调用setValue方法

getValue方法是正常的

超类型限定

通配符限定还可以限定超类,比如通配符类型Generics<? super ArrayList>代表任何泛型Generics类型的类型变量是ArrayListArrayList的超类。

Generics<? super ArrayList> generics = new Generics<List>();

同样的,Generics的方法也发生了变化,变成了

调用getValue方法只能赋值给Object变量

调用setValue方法只能传入ArrayListArrayList的子类,超类List,Object等都不行

反射和泛型

虽然因为类型擦除,在虚拟机里是没有泛型的。不过被擦除的类还是保留了一些关于泛型的信息,可以使用反射相关的Api来获取。

类似地,看一下泛型方法

public static <T extends Comparable<? super T>> T min(T[] a)

这是擦除后

public static Comparable min(Coniparable[] a)

可以使用反射 API 来确定:

  • 这个泛型方法有一个叫做T的类型参数。
  • 这个类型参数有一个子类型限定, 其自身又是一个泛型类型。
  • 这个限定类型有一个通配符参数。
  • 这个通配符参数有一个超类型限定。
  • 这个泛型方法有一个泛型数组参数。

后记

周一就建好的草稿,到了星期天才写好,还是删掉了一些小节情况下,怕是拖延症晚期了......不过也是因为泛型的内容够多,虽然日常业务里很少自己去写泛型相关的代码,但是在阅读类库源码时要是不懂泛型就寸步难行了,特别是集合相关的。这次的大部分内容都是《Java核心技术 卷一》里的,这可是本关于Java基础的好书。不过还是老规矩,光读可不行,还是要用自己的语言记录下来。众所周知,人类的本质是复读机,把好书里的内容重复一遍,就等于我也有责任了!

Java基础-模块系统笔记(1)

Java 9开始,在Java的世界里多了一个叫模块(JSR376)的特性。模块系统的前身是Jigsaw项目。最初,该项目仅仅是为JDK设计、实现一个模块系统。后来项目组也希望它能为开发者所用——虽然,一开始它并不是Java SE平台规范的组成部分。随着项目的不断深入,Java平台对标准模块系统的呼求也日益增长,JCP批准该项目升级为JavaSE平台的一部分,也能服务于Java MEJava EE平台的需求。

官方对Java平台模块系统是这样描述的:

一种新的Java编程组件,即模块。它是自描述的代码与数据的集合,有以下特性:

  • 引入了一个新的可选阶段——链接时,它介于编译时和运行时之间,在此期间可以将一组模块组装并优化为定制的运行时镜像。
  • 为工具javacjlink增加了一些选项,以及在Java中,你可以指定模块路径,这些路径定位模块的定义。
  • 引入模块化JAR文件,该文件是一个JAR文件,其根目录中包含module-info.class文件。
  • 引入JMOD格式,它是一种类似于JAR的打包格式,但它可以包含原生代码和配置文件。

JDK本身已经模块化。有以下改变:

  • 使你能够将JDK的模块组合成各种配置,包括:

    • JREJDK一样的配置。
    • Java SE 8中定义的每个压缩配置文件的内容大致相同。
    • 自定义的配置,仅包含一组指定的模块及其所需的模块。
  • 重构JDKJRE运行时镜像以适应模块并提高性能、安全性和可维护性。

  • 定义了一个新的URI方案,用于命名存储在运行时映像中的模块,类和资源,而不会泄露映像的内部结构或格式。

  • 删除认可的标准覆盖机制和扩展机制。

  • Java运行时镜像中删除rt.jartools.jar

  • 默认情况下,大多数JDK的内部API都不可访问,但在所有或大部分功能都支持替换之前,可以访问一些关键的、广泛使用的内部API

运行jdeps -jdkinternals命令以确定你的的代码是否使用了内部JDK API

兼容性

先来说说模块系统的兼容性。Java一直是比较保守的,体现在更新上就是良好的兼容性。虽然看了官方对Java平台模块系统的描述好像改动非常大,但是你的旧项目即使不模块化也是能在新JDK上运行的。后面会讲到没有模块化的类包是如何与模块交互的。

为什么要模块化

既然不模块化也能好好的运行,那么为什么要这么大费周章的折腾代码呢?

第一点,Java 9前的Java程序,即使是一个简单的输出Hello World的程序,也必须将整个JDKJRE运行时镜像打包进去才能运行,这时Java引以为傲的数量繁多的类库反而成了累赘。比如开源的优秀编程库——Guava里有很多很实用的工具类,有时我们可能只用到了其中一个类而已,却不得不将整个Guava类库打包进我们的项目。

第二点,没法定义类是否能被其他包里的类引用到。比如我们编写了一个工具类,如果希望这个类只能被某些包里的其他类引用到,不暴露给其他包,Java 9前的Java程序是做不到这一点的。

如何模块化

模块化一个项目只要在项目的根目录创建一个module-info.class文件就可以了。

如图所示,我们创建了一个名为module的模块并用关键字exports导出了test包。如果有其他模块导入module模块就可以引用到Main类了,值得注意的是和test同级的包和test内部的包因为没有被导出,都是不能被引用到的。

而引用一个模块则是用关键字requires

然后就可以使用test包内的类了:

import test.Main;

/**
 * @author Yuicon
 */
public class Test {

    public static void main(String[] args) {
        Main main = new Main();
        System.out.println(main);
    }

}

不过在导入前还需要在模块依赖里添加要导入的模块:

java.base模块是默认导入的,里面有我们常用的类库:

模块的强封装性

我们知道模块化后就能控制那些包可以被引用到了。不过不止如此,一个模块的类是不能访问到其他模块里的类的私有属性的。听起来好像是理所当然的,这是因为原先我们是可以用反射来访问到私有属性的。模块化后就算反射也不能访问到了,算是加强了安全性。

不过这样的话,那些依赖反射来获取私有属性的框架和库就倒霉了。为了兼容这些框架和库,我们可以在模块定义里加一个关键字open

module-info.class

open module module {
    exports test;
}

这样我们就声明了一个开放的模块,在模块的所有软件包上授予深入的反射访问权限(访问公共和私有API)。

模块语句

在模块声明文件里一共有五种模块语句,分别是:

  • 导出语句(exports statement)
  • 开放语句(opens statement)
  • 需要语句(requires statement)
  • 使用语句(uses statement)
  • 提供语句(provides statement)

package test.driver;

/**
 * @author Yuicon
 */
public interface Driver {

    int getCode();

}
package test;


import test.driver.Driver;


/**
 * @author Yuicon
 */
public class DriverImpl implements Driver {

    @Override
    public int getCode() {
        return 10086;
    }
}
module module {
    exports test.driver; // 导出包
    provides Driver
            with DriverImpl; // 为接口Driver提供实现
}

module queue {
    requires module; // 导入包
    opens test; // 开放包的反射权限
    uses Driver; // 声明使用接口
}
package main;

import test.driver.Driver;

import java.util.ServiceLoader;


/**
 * @author Yuicon
 */
public class Main {

    public static void main(String[] args) {
        // 获取实现
        ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class);
        serviceLoader.findFirst().ifPresent(driver -> System.out.println(driver.getCode()));
    }

}

输出:

10086

Process finished with exit code 0

聚合模块

你可以创建一个不包含任何代码的模块,它收集并重新导出其他模块的内容,这样的模块称为聚合模块。假设有几个模块依赖于五个模块,你可以为这五个模块创建一个聚合模块。现在,你的模块只能依赖于一个模块——聚合模块。

为了方便,Java 9包含几个聚合模块,如java.sejava.se.eejava.se模块收集Java SE的不与Java EE重叠的部分。java.se.ee模块收集组成Java SE的所有模块,包括与Java EE重叠的模块。

待续

Redis的字符串类型

字符串对象

字符串数据类型是Redis里最常用的类型了,它的键和值都是字符串,使用起来非常的方便。虽然字符串数据类型的值都统称为字符串了,但是在实际存储时会根据值的不同自动选择合适的编码。字符串对象的编码一共有三种:intrawembstr

Redis对象

Redis用统一的数据结构来表示一个对象,具体定义如下:

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    // 当内存超限时采用LRU算法清除内存中的对象
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    // 该对象被引用数
    int refcount;
    // 对象的值指针
    void *ptr;
} robj;

其中type字段代表对象的类型,取值一共有7种:

/* A redis object, that is a type able to hold a string / list / set */

/* The actual Redis Object */
#define OBJ_STRING 0    /* 字符串对象. */
#define OBJ_LIST 1      /* 列表对象. */
#define OBJ_SET 2       /* 集合对象. */
#define OBJ_ZSET 3      /* 有序集合对象. */
#define OBJ_HASH 4      /* 哈希对象. */

/* The "module" object type is a special one that signals that the object
 * is one directly managed by a Redis module. In this case the value points
 * to a moduleValue struct, which contains the object value (which is only
 * handled by the module itself) and the RedisModuleType struct which lists
 * function pointers in order to serialize, deserialize, AOF-rewrite and
 * free the object.
 *
 * Inside the RDB file, module types are encoded as OBJ_MODULE followed
 * by a 64 bit module type ID, which has a 54 bits module-specific signature
 * in order to dispatch the loading to the right module, plus a 10 bits
 * encoding version. */
#define OBJ_MODULE 5    /* 模块对象. */
#define OBJ_STREAM 6    /* 流对象. */

然后是encoding字段,代表着对象值的实际编码类型,取值一共有11种:

/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this object. */
#define OBJ_ENCODING_RAW 0     /* 简单动态字符串 */
#define OBJ_ENCODING_INT 1     /* long类型的整数 */
#define OBJ_ENCODING_HT 2      /* 字典 */
#define OBJ_ENCODING_ZIPMAP 3  /* 压缩字典 */
#define OBJ_ENCODING_LINKEDLIST 4 /* 不再使用的旧列表,使用双端链表. */
#define OBJ_ENCODING_ZIPLIST 5 /* 压缩列表 */
#define OBJ_ENCODING_INTSET 6  /* 整数集合 */
#define OBJ_ENCODING_SKIPLIST 7  /* 跳跃表和字典 */
#define OBJ_ENCODING_EMBSTR 8  /* embstr编码的简单动态字符串 */
#define OBJ_ENCODING_QUICKLIST 9 /* 编码为ziplist的列表 */
#define OBJ_ENCODING_STREAM 10 /* 编码为listpacks的基数树 */

前面已经提到字符串对象只用到了long类型的整数简单动态字符串embstr编码的简单动态字符串这三种编码。

OBJ_ENCODING_INT

当字符串对象的值是一个整数且可以用long来表示时,字符串对象的编码就会是OBJ_ENCODING_INT编码。

可以看到,当值非常大的时候还是用OBJ_ENCODING_RAW来存储的。

OBJ_ENCODING_RAW

当字符串对象的值是一个字符串且长度大于32字节时,字符串对象的编码就会是OBJ_ENCODING_RAW编码。具体结构在下文。

OBJ_ENCODING_EMBSTR

当字符串对象的值是一个字符串且长度小于等于32字节时,字符串对象的编码就会是OBJ_ENCODING_EMBSTR编码。OBJ_ENCODING_EMBSTR编码和OBJ_ENCODING_RAW编码的区别主要有以下几点:

  • OBJ_ENCODING_RAW编码的对象在分配内存时会分配两次,分别创建redisObject对象和SDS对象。而OBJ_ENCODING_EMBSTR编码则是一次就分配好。
  • 同样的,OBJ_ENCODING_RAW编码的对象释放内存也需要两次,OBJ_ENCODING_EMBSTR编码则是一次。
  • OBJ_ENCODING_EMBSTR编码的数据都存储在连续的内存上,OBJ_ENCODING_RAW编码则不是。

SDS

字符串是Redis里非常常见的类型,而用C实现的RedisJava不一样。在C里字符串是用长度为N+1的字符数组实现的,且使用空字符串'\0'作为结束符号。获取字符串的长度需要遍历一遍,找到空字符串'\0'才知道字符串的长度,复杂度是O(N)

如果有一个长度非常大的字符串,单线程的Redis获取它的长度就可能会阻塞很久,这是不能接受的,所以Redis需要一种更高效的字符串类型。

Redis实现了一个叫SDS(simple dynamic string)的字符串类型,其中有两个变量来分别代表字符串的长度和字符数组未使用的字符数量,这样就可以用O(1)的复杂度来获取字符串的长度了,而且同样也是使用空字符串'\0'作为结束符号。

struct sdshdr {
    // 字符串长度
    int len;
    // 字符数组未使用的字符数量
    int free;
    // 保存字符串的字符数组
    char buf[];
}

扩容机制

SDS在字符数组空间不足于容纳新字符串的时候会自动扩容。

如果把一个C字符串拼接到一个SDS后面,当字符数组空间不足时,SDS会先扩容到刚好可以容纳新字符串的长度,然后再扩充新字符串的空字符长度,最终SDS的字符数组长度等于 2 * 新字符串 + 1(结束符号'\0')。不过当新字符串的大小超过1MB后,扩充的空字符长度大小会固定为1MB

之所以会有这个机制,是因为Redis作为一个NoSQL数据库,会频繁的修改字符串,扩容机制相当于给SDS做了一个缓冲池。把SDS连续增长N次字符串需要内存重分配N次优化成了SDS连续增长N次字符串最多需要内存重分配N次,这其实和Java里的StringBuilder实现**是一样的。

后记

我看过两本关于Redis的书,里面都是讲Redis如何实战的,并没有讲Redis的设计和实现。这也就导致了面试很尴尬,因为面试官最喜欢问原理相关的东西了,所以以后学习技术的时候不要从实战类的书籍开始了,还是先看懂原理比较好。

参考资料

这是《Redis设计与实现》里字符串一节的总结。

Redis的列表对象笔记

列表对象

列表数据类型是Redis里非常常用的类型,当我们想用一个键关联一组对象时就可以用列表数据类型来存储。列表的功能并不复杂,现在就来看看Redis是怎么实现列表功能的。

列表的编码

列表的编码一共有三种:

  • OBJ_ENCODING_ZIPLIST 压缩列表
  • OBJ_ENCODING_QUICKLIST 编码为ziplist的快速列表
  • OBJ_ENCODING_LINKEDLIST 不再使用的旧列表,使用双端链表

其中OBJ_ENCODING_LINKEDLIST编码如下面代码所示,已经被标记为不使用了。

#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */

OBJ_ENCODING_ZIPLIST编码虽然有定义创建函数,但是我下载了源码然后全文搜索也没找到哪里有调用这个函数,也已经不使用了。

robj *createZiplistObject(void) {
    unsigned char *zl = ziplistNew();
    robj *o = createObject(OBJ_LIST,zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}

OBJ_ENCODING_ZIPLIST

关于ziplist的实现可以查看笔记

OBJ_ENCODING_LINKEDLIST

Redis在版本3.2之前列表的底层编码有OBJ_ENCODING_LINKEDLISTOBJ_ENCODING_ZIPLIST两种,OBJ_ENCODING_LINKEDLIST是在元素比较大且数量比较多的情况下使用的。

/* Node, List, and Iterator are the only data structures used currently. */

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

typedef struct list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr, void *key);
    unsigned long len;
} list;

可以看到OBJ_ENCODING_LINKEDLIST定义了一个listlistNode结构。list结构引用了首尾两个节点,这样就可以快速的在首尾编辑节点。listNode结构引用了前后两个节点,实现了双端链表。

OBJ_ENCODING_QUICKLIST

总共三种编码,有两种编码没有用到,剩下的是就是列表的主角编码OBJ_ENCODING_LINKEDLIST了。quicklist的定义如下:

/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.
 * 'count' is the number of total entries.
 * 'len' is the number of quicklist nodes.
 * 'compress' is: -1 if compression disabled, otherwise it's the number
 *                of quicklistNodes to leave uncompressed at ends of quicklist.
 * 'fill' is the user-requested (or default) fill factor. */
typedef struct quicklist {
    quicklistNode *head; //指向列表的头节点
    quicklistNode *tail; //指向列表的尾节点
    unsigned long count; // 存储的元素总和      
    unsigned long len;   // 节点的数量     
    int fill : 16;       // 节点的大小设置   
    unsigned int compress : 16; // 节点压缩深度设置;0=off;1表示quicklist两端各有一个节点不压缩,中间的节点压缩
} quicklist;

看起来和OBJ_ENCODING_LINKEDLISTlist没什么区别,重点在于quicklistNode了。

/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
 * We use bit fields keep the quicklistNode at 32 bytes.
 * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
 * encoding: 2 bits, RAW=1, LZF=2.
 * container: 2 bits, NONE=1, ZIPLIST=2.
 * recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
 * attempted_compress: 1 bit, boolean, used for verifying during testing.
 * extra: 10 bits, free for future use; pads out the remainder of 32 bits */
typedef struct quicklistNode {
    struct quicklistNode *prev; // 前一个节点
    struct quicklistNode *next; // 后一个节点
    unsigned char *zl;          // 节点存储的值指针
    unsigned int sz;            // ziplist的大小
    unsigned int count : 16;    // ziplist中的数据项个数
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* was this node previous compressed? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

可以看到quicklistNode最神秘的就是存储值的结构了,quicklistNode默认使用ziplist实现。下面是quicklist添加元素的解析:

/* Add new entry to head node of quicklist.
 *
 * Returns 0 if used existing head.
 * Returns 1 if new head created. */
int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
    # 首先取出旧头节点
    quicklistNode *orig_head = quicklist->head;
    # 判断旧头节点的ziplist加上新元素后的大小是否超过限制,这个限制取决于quicklist结构的fill字段
    if (likely(
            _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
        # 没有超过限制就直接将元素加入到旧头节点的ziplist中
        quicklist->head->zl =
            ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
        quicklistNodeUpdateSz(quicklist->head);
    } else {
        # 超过限制就新建一个node
        quicklistNode *node = quicklistCreateNode();
        # 元素加入到新节点的ziplist中
        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);

        quicklistNodeUpdateSz(node);
        # 并成为新的头节点
        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
    }
    # 更新元素数量
    quicklist->count++;
    quicklist->head->count++;
    # 返回是否有新节点创建
    return (orig_head != quicklist->head);
}

结论

Redis在版本3.2之前分别在两种情况下使用OBJ_ENCODING_LINKEDLISTOBJ_ENCODING_ZIPLIST两种编码。OBJ_ENCODING_LINKEDLIST操作简单复杂度低但是内存占用高,OBJ_ENCODING_ZIPLIST则正好相反。后面Redis就实现了OBJ_ENCODING_QUICKLIST编码,融合了OBJ_ENCODING_LINKEDLISTOBJ_ENCODING_ZIPLIST两种编码的优势,成为了列表的唯一指定编码。

参考资料

【Redis源码剖析】 - 浅谈Redis内置数据结构之压缩列表ziplist

Redis源码剖析--快速列表quicklist

Redis源码剖析--quicklist

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.