从Redis反序列化UserDetails对象异常后发现FastJson序列化的一些问题
最近在使用SpringSecurity+JWT实现认证授权的时候,出现Redis在反序列化userDetails的异常。通过实践发现,使用不同的序列化方法和不同的fastJson版本,异常信息各不相同。所以特地记录了下来。
最近在使用SpringSecurity+JWT实现认证授权的时候,出现Redis在反序列化userDetails的异常。通过实践发现,使用不同的序列化方法和不同的fastJson版本,异常信息各不相同。所以特地记录了下来。
一、项目代码
先来看看我项目中redis相关配置信息。
1.自定义的redis序列化器
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import com.alibaba.fastjson.parser.ParserConfig;
import org.springframework.util.Assert;
import java.nio.charset.Charset;
/**
* Redis使用FastJson序列化
*
* @author mosul
*/
public class FastJsonRedisSerializer<T> implements RedisSerializer<T>
{
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
static
{
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
public FastJsonRedisSerializer(Class<T> clazz)
{
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException
{
if (t == null)
{
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException
{
if (bytes == null || bytes.length <= 0)
{
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
protected JavaType getJavaType(Class<?> clazz)
{
return TypeFactory.defaultInstance().constructType(clazz);
}
}
2.redis配置类
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
/**
* 指定特定的连接工厂
* @return
*/
/*@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory();
}*/
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
3.redis工具类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* Redis帮助类
*
* @author mosul
*/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisHelper
{
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout)
{
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public long deleteObject(final Collection collection)
{
return redisTemplate.delete(collection);
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
{
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
{
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey)
{
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 删除Hash中的数据
*
* @param key
* @param hkey
*/
public void delCacheMapValue(final String key, final String hkey)
{
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.delete(key, hkey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
}
4.自己系统中的UserDetails
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1L;
// 系统用户
private SysUser user;
// 用户权限列表
private List<SysPermission> permissionList;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return permissionList.stream()
.filter(permission -> permission.getPermission() != null)
.map(permission -> new SimpleGrantedAuthority(permission.getPermission()))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
5.登录设置
@Override
public String login(SysUser sysUser) {
String token = null;
//密码需要客户端加密后传递
try {
UserDetails userDetails = sysUserService.loadUserByUsername(sysUser.getUsername());
if(!passwordEncoder.matches(sysUser.getPassword(),userDetails.getPassword())){
throw new BadCredentialsException("密码不正确");
}
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
token = jwtTokenUtil.generateToken(userDetails);
String key = "login:" + sysUser.getUsername();
//设置redis
redisHelper.setCacheObject(key,userDetails);
//insertLoginLog(username);
} catch (AuthenticationException e) {
LOGGER.warn("登录异常:{}", e.getMessage());
}
return token;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserMapper.selectOne(new QueryWrapper<SysUser>().eq("username", username));
List<SysPermission> permissionsByUser = sysUserRoleMapper.findPermissionsByUser(sysUser.getUserId());
sysUser.setPassword(new BCryptPasswordEncoder().encode(sysUser.getPassword()));
// 将系统的用户信息和权限信息封装成UserDetails
UserDetails userDetail = new LoginUser(sysUser, permissionsByUser);
return userDetail;
}
6.JWT校验
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
String username = jwtTokenUtil.getUserNameFromToken(authToken.trim());
LOGGER.info("checking username:{}", username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//从redis中获取userDetails
String redisKey = "login:" + username;
UserDetails userDetails = redisHelper.getCacheObject(redisKey);
if(Objects.isNull(userDetails)){
throw new RuntimeException("用户未登录");
}
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
//存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
LOGGER.info("authenticated user:{}", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
//放行
filterChain.doFilter(request, response);
}
7.fastjson版本
<!--fastjson依赖-->
<!--第一个版本-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<!--第二个版本-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.22</version>
</dependency>
上面的代码中,先根据用户名获取用户对应的用户信息和权限信息,然后构建SpringSecurity的UserDetails对象,用户登录的时候将这个UserDetails对象放入redis中,后续校验请求携带的token与redis中的信息是否一致。
二、异常信息
1.版本一报错信息
需要说明的是,在redis系列化时,是正常的,对应的值也成功设置近缓存了,但是在JWT校验阶段,执行UserDetails userDetails = redisHelper.getCacheObject(redisKey);时出现异常,反序列化失败。
针对这个问题,首先上面的代码逻辑是没有问题的,但是与fastjson反序列化不兼容导致的问题。
根据异常信息提示,设置属性authorities错误,猜想下是因为LoginUser中没有authorities属性,但也说不过去,同样没有属性username和password怎么不会报错?
带着这个疑问,我们先给将LoginUser代码改为下面这种形式。
@Data
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1L;
private SysUser user;
private List<SysPermission> permissionList;
private List<GrantedAuthority> authorities;
public LoginUser() {
}
public LoginUser(SysUser user, List<SysPermission> permissionList) {
this(user,permissionList,null);
}
/**
* 针对fastJson中redis反序列化报错的改进
* org.springframework.data.redis.serializer.SerializationException:
* Could not deserialize: set authorities error; nested exception is com.alibaba.fastjson.JSONException: set authorities error
*
* @param user
* @param permissionList
* @param authorities
*/
public LoginUser(SysUser user, List<SysPermission> permissionList, List<GrantedAuthority> authorities) {
//返回当前用户的权限
List<GrantedAuthority> authoritieList = permissionList.stream()
.filter(permission -> permission.getPermission() != null)
.map(permission -> new SimpleGrantedAuthority(permission.getPermission()))
.collect(Collectors.toList());
this.user = user;
this.permissionList = permissionList;
this.authorities = authoritieList;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
发现这里改完之后,是可以正常运行的。 而且发现,当我们给getAuthorities()赋简单的值的时候,不会出现这个问题。
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authoritieList = new ArrayList<>();
authoritieList.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
return authoritieList;
}
而且我们在构造函数中提前将权限列表加载出来,赋值给一个属性,属性不一定非得名为authorities,如属性名为authoritiesList。然后返回这个属性也不会报错。
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authoritieList;
}
通过上面两个测试可以猜测下,如果构造函数中提前将比较复杂的实现暴露了,反系列化也不会报错。可能出现异常的原因fastjson是对不确定结果无法反系列化,如果是简单的结果或在构造器中就已经知道了确定结果,就不会出现反序列化的异常。
2.版本二报错信息
在使用fastJson 2.x版本的时候,同时需要对redis配置类做如下修改。
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
/*java.lang.ClassCastException:
* com.alibaba.fastjson.JSONObject cannot be cast to org.springframework.security.core.userdetails.UserDetails
* */
String[] acceptNames = {"org.springframework.security.core.authority.SimpleGrantedAuthority"};
GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer(acceptNames);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
可能有些朋友的序列化器配置如下:
@Configuration
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<Object,Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
/*
* 序列化后会产生java类型说明,如果不需要用“Jackson2JsonRedisSerializer”
* 和“ObjectMapper ”配合效果更好
*/
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
这种配置同样也会出现异常,异常信息如下:org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Cannot construct instance of `org.springframework.security.core.authority.SimpleGrantedAuthority` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
同样的,我们可以将fastjson改为如下版本:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.22</version>
</dependency
同时使用其自带的序列化器GenericFastJsonRedisSerializer, 进行如下配置:
/*解决如下两个异常需要添加的配置:
* 1.java.lang.ClassCastException:
* com.alibaba.fastjson.JSONObject cannot be cast to org.springframework.security.core.userdetails.UserDetails
*
* 2.org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Cannot construct instance of `org.springframework.security.core.authority.SimpleGrantedAuthority`
* (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
* */
String[] acceptNames = {"org.springframework.security.core.authority.SimpleGrantedAuthority"};
GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer(acceptNames);
序列化配置修改完成之后,还是会出现设置属性authorities错误,同样需要对LoginUser做上述修改。
3.发现FastJson反系列的一般问题
正如上面所说的,同样没有属性username和password怎么不会报错?于是做了一系列测试。发现了在低版本的fastJson中,对应集合类型接口方法中包含较复杂的实现(不是直接显示赋值),反序化可能要求必须有对应的属性。
定义了一个有不同返回值类型的几种方法来测试。
public interface CrazyDetails {
List<String> getApps();
User getUser();
String getName();
String[] getNodes();
Collection<String> getTests();
List<User> getUsers();
}
定义一个实现类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MosulApp implements CrazyDetails{
@Override
public User getUser() {
return new User(this.appInfo.name);
}
private AppInfo appInfo;
@Override
public List<User> getUsers() {
List<User> userList = new ArrayList<>();
for(int i = 0; i < this.appInfo.name.length(); i ++) {
User user1 = new User("" + i);
userList.add(user1);
}
return userList;
}
@Override
public String[] getNodes() {
String[] strings = new String[2];
strings = new String[]{this.appInfo.name,this.appInfo.details};
return strings;
}
/*private List<String> apps;*/
//报错,添加需要private List<String> tests
@Override
public Collection<String> getTests() {
List<String> list = Arrays.asList(appInfo.name);
return list;
}
@Override
public List<String> getApps() {
List<String> objects = new ArrayList<>();
for(int i = 0; i < this.appInfo.name.length(); i ++) {
objects.add("tt" + i);
}
return objects;
}
@Override
public String getName() {
return this.appInfo.name;
}
}
在测试发现fastJson 1.x版本对于Arrays.asList(appInfo.name);反序列化失败,fastJson 2.x版本则可以反序列化成功,但对于UserDetails中Collection<? extends GrantedAuthority> getAuthorities()中如果有比较复杂的实现,fastJson 2.x版本反序列化还是会失败。所以为了保险起见,最后在自定义的UserDetails中添加authorities属性,除了这种方法能外,应该也跟自定义的序列化器相关设置有关,需要进行探索。
三、Redis和SpringSecutiry相关配置
基于上述测试,最终fastJson选用2.0.22版本,最后将redis配置类和SpringSecutiry中UserDetails实现类修改为如下所示。
1.redis配置类
@Configuration
public class RedisConfig {
/**
* 指定特定的连接工厂
* @return
*/
/*@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory();
}*/
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
/* FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);*/
/*解决java.lang.ClassCastException:
* com.alibaba.fastjson.JSONObject cannot be cast to org.springframework.security.core.userdetails.UserDetails
* */
String[] acceptNames = {"org.springframework.security.core.authority.SimpleGrantedAuthority"};
GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer(acceptNames);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
2.LoginUser类
@Data
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1L;
// 用户信息
private SysUser user;
// 用户权限列表
private List<SysPermission> permissionList;
// SpringSecurity对应的权限信息
private List<GrantedAuthority> authorities;
public LoginUser() {
}
public LoginUser(SysUser user, List<SysPermission> permissionList) {
this(user,permissionList,null);
}
/**
* 针对fastJson中redis反序列化报错的改进
* org.springframework.data.redis.serializer.SerializationException:
* Could not deserialize: set authorities error; nested exception is com.alibaba.fastjson.JSONException: set authorities error
*
* @param user
* @param permissionList
* @param authorities
*/
public LoginUser(SysUser user, List<SysPermission> permissionList, List<GrantedAuthority> authorities) {
//返回当前用户的权限
List<GrantedAuthority> authoritieList = permissionList.stream()
.filter(permission -> permission.getPermission() != null)
.map(permission -> new SimpleGrantedAuthority(permission.getPermission()))
.collect(Collectors.toList());
this.user = user;
this.permissionList = permissionList;
this.authorities = authoritieList;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
更多推荐
所有评论(0)