你真的了解java.lang.String吗?

[b]你真的了解java.lang.String吗?[/b]

一直觉得自己的Java基础技术还算可以,自从看了一些大牛的博客后,发现自己对Java的理解还只是皮毛。之前看过JVM的一些知识以及java.lang.String的源码,以为自己对它很了解,但真的是这样吗?在这之前我对字符串的认识一直停留在很基础的认识上:
1、String对象是不可变的,一旦创建内容将不可能被修改。
2、JVM中存在一个字符串常量池,String.intern()方法可以把运行时产生的字符串放入常量池中。
3、对于以下代码有:s1和s2指向同一个对象,s3和s1指向不同的对象但值相等。
        String s1 = "Hello World";
String s2 = "Hello World";
String s3 = new String("Hello World");


[b]Java字符串真的是不可变的吗?[/b]
今天在stackoverflow上看到这样的一个问题,Java字符串真的是不可变的吗?并贴出了一段修改字符串的代码。

String s1 = "Hello World";
String s2 = "Hello World";
String s3 = s1.substring(6);
System.out.println(s1); // Hello World
System.out.println(s2); // Hello World
System.out.println(s3); // World

Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[])field.get(s1);
value[6] = 'J';
value[7] = 'a';
value[8] = 'v';
value[9] = 'a';
value[10] = '!';

System.out.println(s1); // Hello Java!
System.out.println(s2); // Hello Java!
System.out.println(s3); // World

看到代码后,我恍然大悟。虽然看过String的源码,知道String的内部是一个final的char数组,也知道final数组的值可以修改,可以通过反射机制去修改实例的一些变量值,但从来没有想到过字符串的值可以通过这种方式修改。

接着看了下面的一些回答,了解到最后一行代码System.out.println(s3); // World在java 7u6版本之前会输出Java!对比了一下java 6u45和java 7u45的String的源码,发现在java 6u45版本中substring产生的新字符串和原字符串共用的一个char数组,只是构造字符串时修改了offset和count的值。而在java 7u45的版本的源码中并不存在offset和count这两个常量,在substring的时候是从原char数组中拷贝出来了this.value = Arrays.copyOfRange(value, offset, offset+count)。这样的话在java 7u6之前,调用字符串的substring的时候,产生的字符串和原字符串实际是共用了同一个char数组,可以节约内存空间。

下图是java 7u6之前版本中s1、s2、s3字符串在JVM的存储结构。所以通过反射直接修改char数组的值是可以修改字符串的。
[img]http://dl2.iteye.com/upload/attachment/0094/0738/9af1105a-5cdc-30df-ae18-9afef3acc199.png[/img]


对问题的回答:String对象是不可变的,但这仅意味着你无法通过调用它的公有方法来改变它的值。这个问题也揭示了为什么反射技术在某些场景下非常实用,但在大多数情况下,你应该避免使用它。

[b]那么在java 7u6版本为什么要做这样的修改呢?[/b]
虽然共用一个char数组可以节约内存空间,但这样会带来内存泄漏的问题。假如你有一个很长的母字符串,使用substring想从其中得到一个有用的子字符串,然后你只使用这个子字符串。这是你的母字符串就会成为垃圾被JVM回收掉,但是由于母字符串的char数组一直被字符串使用无法回收,整个母字符串的内容一致存在内存中,导致内存泄漏。

[b]关于字符串常量池[/b]
在这之前,我一直在想Java的字符串常量池是存在什么地方,Heap?PermGen?并且写了一些测试代码证实了字符串常量池是存储在PermGen。但看到大牛的文章后,发现我的认识还是那么的狭小,下面是大牛对字符串常量池和String.intern()的总结:
[list]
[*]不要在Java 6及以前的版本使用String.intern()将字符串放入常量池,因为这是的字符串常量池是存储在固定大小的内存区(PermGen)
[*]Java 7和8中将字符串常量池存储在堆内存中。
[*]Java 7和8中可以通过JVM参数-XX:StringTableSize来控制字符串常量池的大小。
[*]-XX:StringTableSize的默认值在Java 7中是1009,在Java 8中大概是25~50K
[*]在多线程情况下可以随意使用Stirng.intern()方法。8个写线程只比1个写线程的负载多了17%,1个写7个读线程只比一个线程的负载多了9%
[*]字符串常量池不是线程隔离的
[*]虽然Java 7+对String.intern()做了很多优化,但是它还是花费CPU资源。在简单的例子中没有调用String.intern()方法的程序比调用了的程序要快3.5倍。所以不需要对所有的字符串调用String.intern()方法,只需要对经常被使用的字符串(例如:省、市等)调用String.intern()方法加入常量池。
[/list]

[b]参考文献:[/b]
http://java-performance.info/changes-to-string-java-1-7-0_06/
http://stackoverflow.com/questions/20945049/is-a-java-string-really-immutable
http://java-performance.info/string-intern-in-java-6-7-8/
http://java-performance.info/string-intern-java-6-7-8-multithreaded-access/
http://java-performance.info/string-intern-java-7-8-part-3/
<think>这个错误表明在创建`DingTalkOAuth2User`对象时,传入的构造器参数与实际定义的构造器参数不匹配。错误信息指出:-需要(定义):`Collection<?extendsGrantedAuthority>`,`Map<String,Object>`,`String`,`String`,`String`,`String`(6个参数)-找到(实际传入):`String`,`Map<String,Object>`,`String`,`String`,`String`(5个参数)因此,问题在于构造器调用时参数个数不正确。我们需要调整构造器的定义或调用方式。在之前提供的代码中,`DingTalkOAuth2User`类的构造器定义如下:```javapublicDingTalkOAuth2User(Collection<?extendsGrantedAuthority>authorities,Map<String,Object>attributes,StringnameAttributeKey,StringunionId,Stringnick,StringavatarUrl){super(authorities,attributes,nameAttributeKey);this.unionId=unionId;this.nick=nick;this.avatarUrl=avatarUrl;}```而在`DingTalkOAuth2UserService`中创建对象时:```javareturnnewDingTalkOAuth2User(oauth2User.getName(),attributes,unionId,nick,avatarUrl);```这里只传入了5个参数,而构造器需要6个。因此,我们需要修正构造器的调用。###修正方案在`DingTalkOAuth2UserService`中,我们需要构造一个包含6个参数的对象。但是,我们注意到在`DingTalkOAuth2User`的构造器中,第一个参数是`authorities`(权限集合),第三个参数是`nameAttributeKey`(名称属性键)。而在调用时,第一个参数我们传入了`oauth2User.getName()`(一个字符串),这显然不匹配。实际上,在`DingTalkOAuth2UserService`中,我们通过`delegate.loadUser(userRequest)`已经获取了标准OAuth2用户对象`oauth2User`,这个对象中已经包含了权限和属性键信息。我们可以从该对象中获取权限集合和名称属性键。###修改后的`DingTalkOAuth2UserService`:```javapublicclassDingTalkOAuth2UserServiceimplementsOAuth2UserService<OAuth2UserRequest,OAuth2User>{@OverridepublicOAuth2UserloadUser(OAuth2UserRequestuserRequest)throwsOAuth2AuthenticationException{//获取标准OAuth2用户信息DefaultOAuth2UserServicedelegate=newDefaultOAuth2UserService();OAuth2Useroauth2User=delegate.loadUser(userRequest);Map<String,Object>attributes=oauth2User.getAttributes();StringunionId=(String)attributes.get("unionId");Stringnick=(String)attributes.get("nick");StringavatarUrl=(String)attributes.get("avatarUrl");//获取权限集合和名称属性键Collection<?extendsGrantedAuthority>authorities=oauth2User.getAuthorities();//注意:在SpringSecurityOAuth2中,名称属性键通常是在OAuth2UserRequest中获取的//但也可以从属性中推断,或者使用默认值。这里我们使用OAuth2User的标准方法获取名称属性键//实际上,在DefaultOAuth2User中,名称属性键是通过userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()获得的//为了简化,我们可以这样处理:StringnameAttributeKey=userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();if(nameAttributeKey==null){nameAttributeKey="sub";//默认值,但钉钉可能是"unionId"或其他,需要根据实际情况调整}//构建自定义用户对象,传入6个参数returnnewDingTalkOAuth2User(authorities,//权限集合attributes,//属性nameAttributeKey,//名称属性键unionId,nick,avatarUrl);}}```###修正后的`DingTalkOAuth2User`类:保持原来的6参数构造器不变。###注意事项:1.**名称属性键(nameAttributeKey)**:在OAuth2协议中,用于标识用户唯一性的属性名称。对于钉钉,可能是`"unionId"`或者`"openId"`。这个值应该与在`application.yml`中配置的`user-name-attribute`一致,或者在钉钉开放平台中配置的一致。我们在构造器中传入这个值,以便在`DingTalkOAuth2User`对象中正确设置。2.**权限集合(authorities)**:我们从`oauth2User`对象中直接获取,这样保持了原有的权限设置。3.**如果钉钉返回的用户信息中没有`unionId`、`nick`、`avatarUrl`等字段**,需要根据钉钉实际的返回数据结构调整。钉钉返回的用户信息结构可能如下:```json{"unionId":"xxx","nick":"张三","avatarUrl":"https://xxx"}```但请以实际返回为准。如果字段名不一致,请修改为实际字段名。###完整代码修正:####`DingTalkOAuth2User`类```javaimportorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.oauth2.core.user.DefaultOAuth2User;importjava.util.Collection;importjava.util.Map;publicclassDingTalkOAuth2UserextendsDefaultOAuth2User{privatefinalStringunionId;privatefinalStringnick;privatefinalStringavatarUrl;publicDingTalkOAuth2User(Collection<?extendsGrantedAuthority>authorities,Map<String,Object>attributes,StringnameAttributeKey,StringunionId,Stringnick,StringavatarUrl){super(authorities,attributes,nameAttributeKey);this.unionId=unionId;this.nick=nick;this.avatarUrl=avatarUrl;}publicStringgetUnionId(){returnunionId;}publicStringgetNick(){returnnick;}publicStringgetAvatarUrl(){returnavatarUrl;}}```####`DingTalkOAuth2UserService`类```javaimportorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;importorg.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;importorg.springframework.security.oauth2.client.userinfo.OAuth2UserService;importorg.springframework.security.oauth2.core.OAuth2AuthenticationException;importorg.springframework.security.oauth2.core.user.OAuth2User;importjava.util.Collection;importjava.util.Map;publicclassDingTalkOAuth2UserServiceimplementsOAuth2UserService<OAuth2UserRequest,OAuth2User>{@OverridepublicOAuth2UserloadUser(OAuth2UserRequestuserRequest)throwsOAuth2AuthenticationException{//使用默认服务载用户OAuth2UserService<OAuth2UserRequest,OAuth2User>delegate=newDefaultOAuth2UserService();OAuth2Useroauth2User=delegate.loadUser(userRequest);Map<String,Object>attributes=oauth2User.getAttributes();//从属性中提取钉钉用户信息StringunionId=(String)attributes.get("unionId");Stringnick=(String)attributes.get("nick");StringavatarUrl=(String)attributes.get("avatarUrl");//获取权限和名称属性键Collection<?extendsGrantedAuthority>authorities=oauth2User.getAuthorities();StringnameAttributeKey=userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();returnnewDingTalkOAuth2User(authorities,attributes,nameAttributeKey,unionId,nick,avatarUrl);}}```这样,我们就修正了构造器参数不一致的问题。---§§相关问题§§:1.在SpringSecurityOAuth2中,如何自定义用户权限(GrantedAuthority)?2.钉钉登录返回的用户信息中,如何获取手机号等敏感信息?3.如何处理OAuth2登录过程中的错误(如用户拒绝授权)?4.在SpringBoot应用中,如何同时支持多种OAuth2登录方式(如钉钉、微信、GitHub)?</think>这个错误表明在创建 `DingTalkOAuth2User` 对象时,传入的参数数量和类型与类中定义的构造器不匹配。让我们修复这个问题并提供完整的解决方案: ### 问题分析 错误信息显示: - 类定义的构造器需要 6 个参数:`(权限集合, 属性Map, 名称属性键, unionId, nick, avatarUrl)` - 实际调用时只提供了 5 个参数:`(String, Map, String, String, String)` ### 修复后的完整代码 #### 1. 修正 `DingTalkOAuth2User` 类 ```java import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import java.util.Collection; import java.util.Map; public class DingTalkOAuth2User extends DefaultOAuth2User { private final String unionId; private final String nick; private final String avatarUrl; private final String mobile; // 添手机号字段 // 修正构造器参数 public DingTalkOAuth2User(Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes, String nameAttributeKey, String unionId, String nick, String avatarUrl, String mobile) { // 添手机号参数 super(authorities, attributes, nameAttributeKey); this.unionId = unionId; this.nick = nick; this.avatarUrl = avatarUrl; this.mobile = mobile; } // 添新的构造器重载(可选) public DingTalkOAuth2User(Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes, String nameAttributeKey, String unionId, String nick, String avatarUrl) { this(authorities, attributes, nameAttributeKey, unionId, nick, avatarUrl, null); } // Getters public String getUnionId() { return unionId; } public String getNick() { return nick; } public String getAvatarUrl() { return avatarUrl; } public String getMobile() { return mobile; } // 手机号getter } ``` #### 2. 修正 `DingTalkOAuth2UserService` ```java import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.OAuth2User; import java.util.Collections; import java.util.Map; public class DingTalkOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> { @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { // 获取标准OAuth2用户信息 DefaultOAuth2UserService delegate = new DefaultOAuth2UserService(); OAuth2User oauth2User = delegate.loadUser(userRequest); // 钉钉返回的用户数据结构 Map<String, Object> attributes = oauth2User.getAttributes(); // 提取钉钉用户信息 String unionId = (String) attributes.get("unionId"); String nick = (String) attributes.get("nick"); String avatarUrl = (String) attributes.get("avatarUrl"); String mobile = (String) attributes.getOrDefault("mobile", null); // 获取手机号 // 获取名称属性键(重要) String nameAttributeKey = userRequest.getClientRegistration() .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); // 构建自定义用户对象(使用修正后的构造器) return new DingTalkOAuth2User( Collections.emptyList(), // 初始权限为空 attributes, nameAttributeKey, // 名称属性键 unionId, nick, avatarUrl, mobile ); } } ``` #### 3. 安全配置类(添权限设置) ```java @Configuration @EnableWebSecurity public class SecurityConfig { @Autowired private DingTalkOAuth2UserService dingTalkOAuth2UserService; @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeRequests(authorize -> authorize .antMatchers("/", "/login**").permitAll() .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo .userService(dingTalkOAuth2UserService) ) .successHandler(this::onAuthenticationSuccess) ) .logout(logout -> logout .logoutSuccessUrl("/").permitAll() ); return http.build(); } private void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { DingTalkOAuth2User user = (DingTalkOAuth2User) authentication.getPrincipal(); // 创建带权限的用户对象(解决权限问题) Collection<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("ROLE_USER")); // 创建带权限的用户 DingTalkOAuth2User userWithRoles = new DingTalkOAuth2User( authorities, user.getAttributes(), user.getName(), // 名称属性键 user.getUnionId(), user.getNick(), user.getAvatarUrl(), user.getMobile() ); // 生成JWT String token = JwtUtils.generateToken(userWithRoles.getNick()); response.addHeader("Authorization", "Bearer " + token); try { response.getWriter().write("{\"token\":\"" + token + "\"}"); } catch (IOException e) { throw new RuntimeException(e); } } } ``` #### 4. 获取手机号的额外步骤(钉钉特殊处理) ```java @Service public class DingTalkService { @Value("${dingtalk.app-key}") private String appKey; @Value("${dingtalk.app-secret}") private String appSecret; // 获取用户手机号(需要额外请求) public String getMobile(String accessToken, String unionId) { RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("x-acs-dingtalk-access-token", accessToken); Map<String, Object> request = new HashMap<>(); request.put("userId", unionId); HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers); ResponseEntity<Map> response = restTemplate.postForEntity( "https://api.dingtalk.com/v1.0/contact/users/me", entity, Map.class ); Map<String, Object> body = response.getBody(); return (String) body.get("mobile"); } } ``` ### 关键修正点解释 1. **构造器参数修正**: - 添了缺失的 `nameAttributeKey` 参数 - 添了可选的 `mobile` 参数用于存储手机号 - 提供了构造器重载以保持向后兼容 2. **权限处理**: - 在 `DingTalkOAuth2UserService` 中初始权限设置为空集合 - 在登录成功后添实际权限(如 `ROLE_USER`) 3. **名称属性键获取**: ```java String nameAttributeKey = userRequest.getClientRegistration() .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); ``` 这是必须的关键参数,从客户端注册信息中获取 4. **手机号获取**: - 钉钉需要单独请求获取手机号 - 需要添 `x-acs-dingtalk-access-token` 请求头 ### 钉钉用户信息数据结构 典型的钉钉用户信息响应: ```json { "unionId": "xxx", "nick": "张三", "avatarUrl": "https://xxx", "mobile": "13800138000", "stateCode": "86" } ``` ### 应用配置补充 `application.yml` 添: ```yaml dingtalk: app-key: YOUR_APP_KEY app-secret: YOUR_APP_SECRET ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值