Discuss / Java / 没有出现NPE异常

没有出现NPE异常

Topic source

净净一隅

#1 Created at ... [Delete] [Delete and Lock User]

MailService 装配了AOP,有一个public final变量test;UserSerivce注入了MailService,在UserSerivce里可以正常使用test变量

是版本问题?

1. MailService 

Component
public class MailService {
@Autowired(required = false)
private ZoneId zoneId = ZoneId.systemDefault();
@Value("#{smtpConfig.smtpHost}")
private String smtpHost;
@Value("#{smtpConfig.smtpPort}")
private int smtpPort;

public final String test="aaa";

public void setZoneId(ZoneId zoneId) {
this.zoneId = zoneId;
}
public String getTime() {
return ZonedDateTime.now(this.zoneId).format(DateTimeFormatter.ISO_ZONED_DATE_TIME);
}

public void sendLoginMail(User user) {
System.out.println(String.format("Hi, %s! You are logged in at %s", user.getName(), getTime()));
}

public void sendRegistrationMail(User user) {
System.out.println(String.format("Welcome, %s!", user.getName()));

}

@PostConstruct
public void init() {
System.out.println("Init mail service " + this.zoneId);
System.out.println("host: "+this.smtpHost);
System.out.println("port: "+this.smtpPort);
}
@PreDestroy
public void shutdown() {
System.out.println("Shutdown mail service");
}
}

2.  UserService 

package com.service;

import com.annotation.MetricTime;
import com.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

@Component
public class UserService {
@Autowired
private MailService mailService;
@Autowired
private DataSource dataSource;

public UserService(){
System.out.println("创建UserService bean");
}
public void setMailService(MailService mailService) {
this.mailService = mailService;
}

public User getUserFromDB(String email,String password) {
try(Connection conn=this.dataSource.getConnection()){
String sql="select id,name from user where email=? and password=? limit 1";
System.out.println(sql);
PreparedStatement ps = conn.prepareStatement(sql);
ps.setObject(1, email);
ps.setObject(2, password);
try(ResultSet rs=ps.executeQuery()){
while (rs.next()) {
long id = rs.getLong("id");
String name = rs.getString("name");
return new User(id,email,password,name);
}
}
}catch (SQLException e){
e.printStackTrace();
return null;
}catch (Exception e){
e.printStackTrace();
return null;
}
return null;
}

public User getUserFromDB(String email) throws SQLException {
try(Connection conn=this.dataSource.getConnection()){
String sql="select id,email,name from user where email=?";
System.out.println(sql);
PreparedStatement ps = conn.prepareStatement(sql);
ps.setObject(1, email);
try(ResultSet rs=ps.executeQuery()){
while (rs.next()) {
long id = rs.getLong("id");
String name = rs.getString("name");
User user=new User();
user.setName(name);
user.setId(id);
user.setEmail(email);
return user;
}
}
}
return null;
}

@MetricTime("login")
public User login(String email, String password) {
User user=getUserFromDB(email,password);
if(user!=null){
mailService.sendLoginMail(user);
}
return user;
}

public User registerUser2DB(User user) throws SQLException{
System.out.println(mailService.test);
try(Connection conn=this.dataSource.getConnection()) {
String sql="insert into user(name,email,password) values(?,?,?)";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setObject(1, user.getName());
ps.setObject(2,user.getEmail());
ps.setObject(3,user.getPassword());
int rs=ps.executeUpdate();
if(rs>0){
return user;
}
}
return null;
}

public User register(String email, String password, String name) throws SQLException{
if(getUserFromDB(email)!=null){
throw new RuntimeException("email exist.");
}
User user=new User();
user.setEmail(email);
user.setName(name);
user.setPassword(password);
this.registerUser2DB(user);
mailService.sendRegistrationMail(user);

System.out.println("mailService.test: "+this.mailService.test);

return user;
}

public DataSource getDataSource() {
return dataSource;
}

public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
}

3. LoggingAspect 

@Aspect
@Component
public class LoggingAspect {

@Before("execution(public * com.service.MailService.*(..))")
public void doAcessCheck(){
System.err.println("[Before] do access check...");
}
}

4. AppConfig 测试

@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class AppConfig {

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, SQLException {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
//MySqlConfig config= AppConfig.context.getBean(MySqlConfig.class);
UserService userService= context.getBean(UserService.class);
userService.register("Jimmy@123.com","123","Jimmy");
}

5. 运行日志 

select id,email,name from user where email=?

aaa

[Before] do access check...

Welcome, Jimmy!

mailService.test: aaa

净净一隅

#2 Created at ... [Delete] [Delete and Lock User]

<spring.version>5.2.17.RELEASE</spring.version>

廖雪峰

#3 Created at ... [Delete] [Delete and Lock User]

String初始化比较特殊,它直接从常量池获取。

你换成其他任何类都是null

Best of Me

#4 Created at ... [Delete] [Delete and Lock User]

我测试了下,常量(final修饰的基本类型,包括特殊的String)应该是比较特殊的成员变量,看字节码文件应该是在声明的时候初始化。除此以外成员变量,应该是像老师所说在构造器中初始化。我在想是不是隐含了static,类似静态变量会跟类一起初始化。

所以,在AOP代理模式下,用bean类名.常量,可以正常访问,相当于全局变量。

---

p.s. 常量要求在声明或构造器中初始化,不然编译器报错,不像普通成员变量可以有初始值。

dzy

#5 Created at ... [Delete] [Delete and Lock User]

看了下cglib的源码,生成的代理类的构造方法里面是会去调用super()方法的了,现在的版本好像不存在这个问题。即便不是String类型的,也可以访问得到。

private void emitConstructors(ClassEmitter ce, List constructors) {    boolean seenNull = false;    // 遍历持有的构造方法对应的MethodInfo的集合    for (Iterator it = constructors.iterator(); it.hasNext();) {        MethodInfo constructor = (MethodInfo)it.next();        // 如果currentData不为null 并且 当前构造方法的描述符是()V,直接跳过,进行下一次循环        if (currentData != null && !"()V".equals(constructor.getSignature().getDescriptor())) {            continue;        }        // 否则根据MethodInfo向类中声明一个public的构造方法        CodeEmitter e = EmitUtils.begin_method(ce, constructor, Constants.ACC_PUBLIC);        // 向构造方法中添加aload_0字节码,加载this引用到操作数栈顶        e.load_this();        // 插入dup字节码,复制栈顶元素        e.dup();        // 加载方法参数到操作数栈顶        e.load_args();        // 获取构造方法的方法签名        Signature sig = constructor.getSignature();        // 判断方法签名的描述符是否是()V,如果是,给seenNull赋值为true,表示访问过参数为null的构造方法        seenNull = seenNull || sig.getDescriptor().equals("()V");        // 然后向code中插入invokespecial字节码,调用父类的构造器        e.super_invoke_constructor(sig);        // 如果currentData为null        if (currentData == null) {            // 插入invokestatic字节码,调用CGLIB$BIND_CALLBACKS(Object)方法,            // 即调用静态方法将this作为参数传入,进行callbacks的绑定            e.invoke_static_this(BIND_CALLBACKS);            // 如果interceptDuringConstruction为false的话            if (!interceptDuringConstruction) {                // 继续向code中插入aload_0字节码                e.load_this();                // 然后给自身的CGLIB$CONSTRUCTED属性赋值为true                e.push(1);                e.putfield(CONSTRUCTED_FIELD);            }        }        // 向code中插入return字节码,表示方法返回        e.return_value();        e.end_method();    }    // 如果classOnly为false 并且 没有发现有无参构造器 并且 参数为null,报错    if (!classOnly && !seenNull && arguments == null)        throw new IllegalArgumentException("Superclass has no null constructors but no arguments were given");}

安生

#6 Created at ... [Delete] [Delete and Lock User]

我从堆里面去查找UserService类型对象,结果发现两个,一个是UserService,一个是UserService$$EnhanceBySpringCglib$$xxx

**UserService{"zoneId":"Asia/Shanghai"}
**

UserService$$EnhanceBySpringCglib$$xxx{"zoneId":null, "target":{"zoneId":"Asia/Shanghai"}}

其中UserService对象的zoneId是有值的,而UserService$$EnhanceBySpringCglib$$xxx对象的zoneId是null

debug发现注入对象是UserService$$EnhanceBySpringCglib$$xxx,因此userService.zonId为null是正常的,而userService.getZoneId()能够正常返回,是因为他实际调用的是target.getZoneId()。

源码也可以证明:spring会先创建目标对象,再把目标对象注入到代理对象中,虽然代理对象继承了目标对象,但他们之间的关系本质还是组合。

综上,为什么Spring不会初始化代理对象父类?就是因为代理对象可以访问到两个zoneId变量,但只需要为其中一个进行赋值,而Spring选择给target赋值,也就不需要对代理类的父类进行初始化了。


  • 1

Reply