go on with Learning Note 14 - Send Email With Spring
Reference Library
need add the following reference library files for velocity
%SPRING_DEP%\org.apache.commons\com.springsource.org.apache.commons.collections\3.2.1\com.springsource.org.apache.commons.collections-3.2.1.jar
%SPRING_DEP%\org.apache.commons\com.springsource.org.apache.commons.lang\2.1.0\com.springsource.org.apache.commons.lang-2.1.0.jar
%SPRING_DEP%\org.apache.velocity\com.springsource.org.apache.velocity\1.5.0\com.springsource.org.apache.velocity-1.5.0.jar
%SPRING_DEP%\org.apache.velocity\com.springsource.org.apache.velocity.tools.view\1.4.0\com.springsource.org.apache.velocity.tools.view-1.4.0.jar
Email Template File
//account.vm
Dear Customer,
Account No. :<strong>${accountId}</strong>
Password : ${password}
Now you are allowed to access:
<li><a href=${url1}>yahoo</a></li>
<li><a href=${url2}>google</a></li>
XXX Administrator
Spring Configuration
<!-- Configure Velocity for sending e-mail -->
<bean id="velocityEngine"
class="org.springframework.ui.velocity.VelocityEngineFactoryBean">
<property name="velocityProperties">
<props>
<prop key="resource.loader">class</prop>
<prop key="class.resource.loader.class">
org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
</prop>
</props>
</property>
</bean>
Implement Class
package test.spring.email;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import javax.mail.internet.MimeMessage;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.exception.VelocityException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.stereotype.Component;
import org.springframework.ui.velocity.VelocityEngineUtils;
@Component
public class AccountNotifierVelocityImpl implements AccountNotifier {
@Resource
private JavaMailSender mailSender;
@Resource
private SimpleMailMessage mailMessage;
public void setMailSender(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
public void setMailMessage(SimpleMailMessage mailMessage) {
this.mailMessage = mailMessage;
}
@Resource
private VelocityEngine velocityEngine;
public void setVelocityEngine(VelocityEngine velocityEngine) {
this.velocityEngine = velocityEngine;
}
@Override
public void notifyAccount(final String accountId, final String password) {
MimeMessagePreparator preparator = new MimeMessagePreparator() {
public void prepare(MimeMessage mimeMessage) throws Exception {
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,
true);
helper.setFrom(mailMessage.getFrom());
helper.setTo(mailMessage.getTo());
helper.setSubject(mailMessage.getSubject());
// pass value for all parameters
Map<String, String> model = new HashMap<String, String>();
model.put("accountId", accountId);
model.put("password", password);
model.put("url1", "http://www.google.com");
model.put("url2", "http://www.baidu.com");
String result = null;
try {
// account.vm must be in your classpath
result = VelocityEngineUtils.mergeTemplateIntoString(
velocityEngine, "account.vm", model).replaceAll(
"\n", "<br/>");
helper.setText(result, true);
} catch (VelocityException e) {
e.printStackTrace();
}
}
};
mailSender.send(preparator);
}
}
Junit Test
package test.spring.email;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class EmailTester {
@BeforeClass
public static void setUpBeforeClass() throws Exception {
}
@AfterClass
public static void tearDownAfterClass() throws Exception {
}
@Test
public void testEmailSender() {
ApplicationContext context = new ClassPathXmlApplicationContext(
"spring.xml");
AccountNotifier accountNotifier = (AccountNotifier) context
.getBean("accountNotifierVelocityImpl");
accountNotifier.notifyAccount("Account123", "pwd12345");
}
}
Friday, October 22, 2010
Thursday, October 21, 2010
Spring v3.0.2 Learning Note 14 - Send Email with Spring
Reference Library
%SPRING%\dist\org.springframework.core-3.0.2.RELEASE.jar
%SPRING%\dist\org.springframework.asm-3.0.2.RELEASE.jar
%SPRING%\dist\org.springframework.beans-3.0.2.RELEASE.jar
%SPRING%\dist\org.springframework.context-3.0.2.RELEASE.jar
%SPRING%\dist\org.springframework.context.support-3.0.2.RELEASE.jar
%SPRING%\dist\org.springframework.expression-3.0.2.RELEASE.jar
%SPRING%\dist\org.springframework.aspects-3.0.2.RELEASE.jar
%SPRING%\dist\org.springframework.aop-3.0.2.RELEASE.jar
%SPRING_DEP%\org.apache.commons\org.apache.common\com.springsource.org.apache.commons.logging\1.1.1\com.springsource.org.apache.commons.logging-1.1.1.jar
%SPRING_DEP%\javax.mail\com.springsource.javax.mail\1.4.0\com.springsource.javax.mail-1.4.0.jar
%SPRING_DEP%\org.aopalliance\com.springsource.org.aopalliance\1.0.0\com.springsource.org.aopalliance-1.0.0.jar
create a java project and add above library files to project classpath.
This is the interface class:
// interface class
package test.spring.email;
public interface AccountNotifier {
public void notifyAccount(String accountId, String password);
}
Send Email Using JavaMail API
package test.spring.email;
import java.util.Properties;
import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import org.springframework.stereotype.Component;
@Component
public class AccountNotifierImpl implements AccountNotifier {
@Override
public void notifyAccount(String accountId, String password) {
Properties props = new Properties();
props.put("mail.smtp.host", "smtp.gmail.com");
props.put("mail.smtp.port", "587");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
Session session = Session.getDefaultInstance(props,
new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("<gmail account>", "<gmail password>");
}
});
session.setDebug(true);
try {
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress("ABC@gmail.com"));
message.setRecipients(Message.RecipientType.TO, InternetAddress
.parse("DEF@yahoo.com"));
message.setSubject("Account Notification Test");
message.setText("Dear Customer,\n\n" + "Account No. : " + accountId
+ "\n" + "Password : " + password);
Transport.send(message);
System.out.println("success!");
} catch (MessagingException e) {
throw new RuntimeException(e);
}
}
}
Firstly, open a mail session connecting to an SMTP server by defining the properties. Then create a message from this session for constructing your e-mail. After that, send the e-mail by making a call to Transport.send().
This is the spring configuration file.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<context:component-scan base-package="test.spring.email" />
</beans>
Run this junit test, you can see the email has been sent out.
// junit test
package test.spring.email;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class EmailTester {
@BeforeClass
public static void setUpBeforeClass() throws Exception {
}
@AfterClass
public static void tearDownAfterClass() throws Exception {
}
@Test
public void testEmailSender() {
ApplicationContext context = new ClassPathXmlApplicationContext(
"spring.xml");
AccountNotifier accountNotifier = (AccountNotifier) context
.getBean("accountNotifierImpl");
accountNotifier.notifyAccount("Account12", "pwd123");
}
}
Send Email with Spring's MailSender
The core interface of Spring’s e-mail support is MailSender. If only sending plain text in the email, use this interface.
// implement class
package test.spring.email;
import javax.annotation.Resource;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.stereotype.Component;
@Component
public class AccountNotifierMailSenderImpl implements AccountNotifier {
@Resource
private MailSender mailSender;
public void setMailSender(MailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public void notifyAccount(String accountId, String password) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("ABC@yahoo.com");
message.setTo("DEF@yahoo.com");
message.setSubject("Account Notifier with Spring Mail Sender");
message.setText("Dear Customer,\n\n" + "Account No. : " + accountId
+ "\n" + "Password : " + password);
mailSender.send(message);
}
}
then, configure mailSender in spring as follows.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<context:component-scan base-package="test.spring.email" />
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="smtp.gmail.com" />
<property name="port" value="587" />
<property name="username" value="<your account>" />
<property name="password" value="<your password>" />
<property name="javaMailProperties">
<props>
<!-- Use SMTP-AUTH to authenticate to SMTP server -->
<prop key="mail.smtp.auth">true</prop>
<!-- Use TLS to encrypt communication with SMTP server -->
<prop key="mail.smtp.starttls.enable">true</prop>
<!-- print session debug info -->
<prop key="mail.debug">true</prop>
</props>
</property>
</bean>
</beans>
If you have a JavaMail session configured in your application server, you can first look it up with the help of JndiObjectFactoryBean.
<bean id="mailSession" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="mail/Session" />
</bean>
Or you can look up a JavaMail session through the <jee:jndi-lookup> element if you are using Spring 2.0 or later.
<jee:jndi-lookup id="mailSession" jndi-name="mail/Session" />
You can inject the JavaMail session into JavaMailSenderImpl for its use. In this case, you no longer need to set the host, port, username, or password.
<bean id="mailSender"
class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="session" ref="mailSession" />
</bean>
If you are using tomcat 6.x, you can define JavaMail session as follows.
Email Template
Sending MIME Message
// change bean definition
<bean id="mailMessage" class="org.springframework.mail.SimpleMailMessage">
<!-- <property name="from" value="ABC@gmail.com" /> -->
<property name="from">
<value><![CDATA[Application Notifier <noreply@gmail.com>]]></value>
</property>
<!-- <property name="to" value="DEF@yahoo.com" /> -->
<property name="to">
<value><![CDATA[System Customer <xiang.wang@ufinity.com>]]></value>
</property>
<property name="subject"
value="Account Notification Test with Email Template" />
<property name="text">
<value>
<![CDATA[ //---<<<<< include HTML tag within email content
<html><body>
Dear Customer,<p/>
Account No. :<b><style color="red"> %s</style></b><br/>
Password : %s<br/>
<br/>
Now you are allowed to access:<br/>
<ul>
<li><a href="www.yahoo.com">yahoo</a></li>
<li><a href="www.google.com">google</a></li>
</ul>
<br/>
XXX Administrator
</body></html>
]]>
</value>
</property>
</bean>
// implement class
package test.spring.email;
import java.io.File;
import javax.annotation.Resource;
import javax.mail.internet.MimeMessage;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.stereotype.Component;
@Component
public class AccountNotifierMailSenderImpl implements AccountNotifier {
@Resource
private JavaMailSender mailSender;
@Resource
private SimpleMailMessage mailMessage;
// should be JavaMailSende, not MailSender instead
public void setMailSender(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
public void setMailMessage(SimpleMailMessage mailMessage) {
this.mailMessage = mailMessage;
}
@Override
public void notifyAccount(final String accountId, final String password) {
MimeMessagePreparator preparator = new MimeMessagePreparator() {
public void prepare(MimeMessage mimeMessage) throws Exception {
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,
true);
helper.setFrom(mailMessage.getFrom());
helper.setTo(mailMessage.getTo());
helper.setSubject(mailMessage.getSubject());
helper.setText(String.format(mailMessage.getText(), accountId,
password), true); // must set true,otherwise, will show plain text in email
// add attachment
FileSystemResource file = new FileSystemResource(new File(
"d:/logo.jpg"));
helper.addAttachment("mylog.jpg", file);
}
};
mailSender.send(preparator);
}
}
In the prepare() method, you can prepare the MimeMessage object, which is precreated for JavaMailSender. If there’s any exception thrown, it will be converted into Spring’s mail runtime exception automatically.
%SPRING%\dist\org.springframework.core-3.0.2.RELEASE.jar
%SPRING%\dist\org.springframework.asm-3.0.2.RELEASE.jar
%SPRING%\dist\org.springframework.beans-3.0.2.RELEASE.jar
%SPRING%\dist\org.springframework.context-3.0.2.RELEASE.jar
%SPRING%\dist\org.springframework.context.support-3.0.2.RELEASE.jar
%SPRING%\dist\org.springframework.expression-3.0.2.RELEASE.jar
%SPRING%\dist\org.springframework.aspects-3.0.2.RELEASE.jar
%SPRING%\dist\org.springframework.aop-3.0.2.RELEASE.jar
%SPRING_DEP%\org.apache.commons\org.apache.common\com.springsource.org.apache.commons.logging\1.1.1\com.springsource.org.apache.commons.logging-1.1.1.jar
%SPRING_DEP%\javax.mail\com.springsource.javax.mail\1.4.0\com.springsource.javax.mail-1.4.0.jar
%SPRING_DEP%\org.aopalliance\com.springsource.org.aopalliance\1.0.0\com.springsource.org.aopalliance-1.0.0.jar
create a java project and add above library files to project classpath.
This is the interface class:
// interface class
package test.spring.email;
public interface AccountNotifier {
public void notifyAccount(String accountId, String password);
}
Send Email Using JavaMail API
package test.spring.email;
import java.util.Properties;
import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import org.springframework.stereotype.Component;
@Component
public class AccountNotifierImpl implements AccountNotifier {
@Override
public void notifyAccount(String accountId, String password) {
Properties props = new Properties();
props.put("mail.smtp.host", "smtp.gmail.com");
props.put("mail.smtp.port", "587");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
Session session = Session.getDefaultInstance(props,
new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("<gmail account>", "<gmail password>");
}
});
session.setDebug(true);
try {
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress("ABC@gmail.com"));
message.setRecipients(Message.RecipientType.TO, InternetAddress
.parse("DEF@yahoo.com"));
message.setSubject("Account Notification Test");
message.setText("Dear Customer,\n\n" + "Account No. : " + accountId
+ "\n" + "Password : " + password);
Transport.send(message);
System.out.println("success!");
} catch (MessagingException e) {
throw new RuntimeException(e);
}
}
}
Firstly, open a mail session connecting to an SMTP server by defining the properties. Then create a message from this session for constructing your e-mail. After that, send the e-mail by making a call to Transport.send().
This is the spring configuration file.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<context:component-scan base-package="test.spring.email" />
</beans>
Run this junit test, you can see the email has been sent out.
// junit test
package test.spring.email;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class EmailTester {
@BeforeClass
public static void setUpBeforeClass() throws Exception {
}
@AfterClass
public static void tearDownAfterClass() throws Exception {
}
@Test
public void testEmailSender() {
ApplicationContext context = new ClassPathXmlApplicationContext(
"spring.xml");
AccountNotifier accountNotifier = (AccountNotifier) context
.getBean("accountNotifierImpl");
accountNotifier.notifyAccount("Account12", "pwd123");
}
}
Send Email with Spring's MailSender
The core interface of Spring’s e-mail support is MailSender. If only sending plain text in the email, use this interface.
// implement class
package test.spring.email;
import javax.annotation.Resource;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.stereotype.Component;
@Component
public class AccountNotifierMailSenderImpl implements AccountNotifier {
@Resource
private MailSender mailSender;
public void setMailSender(MailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public void notifyAccount(String accountId, String password) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("ABC@yahoo.com");
message.setTo("DEF@yahoo.com");
message.setSubject("Account Notifier with Spring Mail Sender");
message.setText("Dear Customer,\n\n" + "Account No. : " + accountId
+ "\n" + "Password : " + password);
mailSender.send(message);
}
}
then, configure mailSender in spring as follows.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<context:component-scan base-package="test.spring.email" />
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="smtp.gmail.com" />
<property name="port" value="587" />
<property name="username" value="<your account>" />
<property name="password" value="<your password>" />
<property name="javaMailProperties">
<props>
<!-- Use SMTP-AUTH to authenticate to SMTP server -->
<prop key="mail.smtp.auth">true</prop>
<!-- Use TLS to encrypt communication with SMTP server -->
<prop key="mail.smtp.starttls.enable">true</prop>
<!-- print session debug info -->
<prop key="mail.debug">true</prop>
</props>
</property>
</bean>
</beans>
If you have a JavaMail session configured in your application server, you can first look it up with the help of JndiObjectFactoryBean.
<bean id="mailSession" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="mail/Session" />
</bean>
Or you can look up a JavaMail session through the <jee:jndi-lookup> element if you are using Spring 2.0 or later.
<jee:jndi-lookup id="mailSession" jndi-name="mail/Session" />
You can inject the JavaMail session into JavaMailSenderImpl for its use. In this case, you no longer need to set the host, port, username, or password.
<bean id="mailSender"
class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="session" ref="mailSession" />
</bean>
If you are using tomcat 6.x, you can define JavaMail session as follows.
<Context path="/myapp" docBase="myapp">
<!-- JavaMail session factory -->
<Resource name="mail/Session"
auth="Container"
type="javax.mail.Session"
username="yourusername"
password="yourpassword"
mail.debug="true"
mail.user="yourusername"
mail.password="yourpassword"
mail.transport.protocol="smtp"
mail.smtp.host="your.smtphost.com"
mail.smtp.auth="true"
mail.smtp.port="25"
mail.smtp.starttls.enable="true"/>
</Context>Email Template
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<context:component-scan base-package="test.spring.email" />
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="smtp.gmail.com" />
<property name="port" value="587" />
<property name="username" value="<yourname>" />
<property name="password" value="<password>" />
<property name="javaMailProperties">
<props>
<!-- Use SMTP-AUTH to authenticate to SMTP server -->
<prop key="mail.smtp.auth">true</prop>
<!-- Use TLS to encrypt communication with SMTP server -->
<prop key="mail.smtp.starttls.enable">true</prop>
<prop key="mail.debug">true</prop>
</props>
</property>
</bean>
<bean id="mailMessage" class="org.springframework.mail.SimpleMailMessage">
<!-- <property name="from" value="ABC@gmail.com" /> -->
<property name="from">
<value><![CDATA[Application Notifier <noreply@gmail.com>]]></value>
</property>
<!-- <property name="to" value="DEF@yahoo.com" /> -->
<property name="to">
<value><![CDATA[System Customer <DEF@yahoo.com>]]></value>
</property>
<property name="subject"
value="Account Notification Test with Email Template" />
<property name="text">
<value>
<![CDATA[
Dear Customer,
Account No. : %s
Password : %s
XXX Administrator
]]>
</value>
</property>
</bean>
</beans>package test.spring.email;
import javax.annotation.Resource;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.stereotype.Component;
@Component
public class AccountNotifierMailSenderImpl implements AccountNotifier {
@Resource
private MailSender mailSender;
@Resource
private SimpleMailMessage mailMessage;
public void setMailSender(MailSender mailSender) {
this.mailSender = mailSender;
}
public void setMailMessage(SimpleMailMessage mailMessage) {
this.mailMessage = mailMessage;
}
@Override
public void notifyAccount(String accountId, String password) {
SimpleMailMessage message = new SimpleMailMessage(mailMessage);
message.setText(String.format(mailMessage.getText(), accountId,
password));
mailSender.send(message);
}
}
Note the placeholders %s, which will be replaced by message parameters through String.format().Of course, you can also use a powerful templating language such as Velocity or FreeMarker to generate the message text according to a template. It’s also a good practice to separate mail message templates from bean configuration files.Each time you send e-mail, you can construct a new SimpleMailMessage instance from this injected template. Then you can generate the message text using String.format() to replace the %s placeholders with your message parameters.Sending MIME Message
// change bean definition
<bean id="mailMessage" class="org.springframework.mail.SimpleMailMessage">
<!-- <property name="from" value="ABC@gmail.com" /> -->
<property name="from">
<value><![CDATA[Application Notifier <noreply@gmail.com>]]></value>
</property>
<!-- <property name="to" value="DEF@yahoo.com" /> -->
<property name="to">
<value><![CDATA[System Customer <xiang.wang@ufinity.com>]]></value>
</property>
<property name="subject"
value="Account Notification Test with Email Template" />
<property name="text">
<value>
<![CDATA[ //---<<<<< include HTML tag within email content
<html><body>
Dear Customer,<p/>
Account No. :<b><style color="red"> %s</style></b><br/>
Password : %s<br/>
<br/>
Now you are allowed to access:<br/>
<ul>
<li><a href="www.yahoo.com">yahoo</a></li>
<li><a href="www.google.com">google</a></li>
</ul>
<br/>
XXX Administrator
</body></html>
]]>
</value>
</property>
</bean>
// implement class
package test.spring.email;
import java.io.File;
import javax.annotation.Resource;
import javax.mail.internet.MimeMessage;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.stereotype.Component;
@Component
public class AccountNotifierMailSenderImpl implements AccountNotifier {
@Resource
private JavaMailSender mailSender;
@Resource
private SimpleMailMessage mailMessage;
// should be JavaMailSende, not MailSender instead
public void setMailSender(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
public void setMailMessage(SimpleMailMessage mailMessage) {
this.mailMessage = mailMessage;
}
@Override
public void notifyAccount(final String accountId, final String password) {
MimeMessagePreparator preparator = new MimeMessagePreparator() {
public void prepare(MimeMessage mimeMessage) throws Exception {
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,
true);
helper.setFrom(mailMessage.getFrom());
helper.setTo(mailMessage.getTo());
helper.setSubject(mailMessage.getSubject());
helper.setText(String.format(mailMessage.getText(), accountId,
password), true); // must set true,otherwise, will show plain text in email
// add attachment
FileSystemResource file = new FileSystemResource(new File(
"d:/logo.jpg"));
helper.addAttachment("mylog.jpg", file);
}
};
mailSender.send(preparator);
}
}
In the prepare() method, you can prepare the MimeMessage object, which is precreated for JavaMailSender. If there’s any exception thrown, it will be converted into Spring’s mail runtime exception automatically.
Thursday, October 14, 2010
Spring v3.0.2 Learning Note 13 - AOP Example
- 命名空间的支持
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<context:component-scan base-package="com.spring.test" />
<aop:aspectj-autoproxy />
// AOP中这2个Bean的定义不可少!不明为什么,我定义了类路径的自动扫描了
<bean id="personAopImpl" class="com.spring.test.aop.PersonAopImpl" />
<bean id="personAopProxy" class="com.spring.test.aop.PersonAopProxy" />
</beans>
- Aspect 注解
- 原代码
package com.spring.test.aop;
public interface IPersonAop {
public void sayHello(String name);
}
实现类:
package com.spring.test.aop;
import org.springframework.stereotype.Service;
@Service
public class PersonAopImpl implements IPersonAop {
@Override
public void sayHello(String name) {
System.out.println("Hello," + name);
}
}
代理类:
package com.spring.test.aop;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class PersonAopProxy {
@Before("execution (* com.spring.test.aop..*.*(..))")
public void before() {
System.out.println("before...");
}
@After("execution (* com.spring.test.aop..*.*(..))")
public void after() {
System.out.println("after...");
}
}
测试类:
package com.spring.test.junit;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.spring.test.aop.IPersonAop;
public class AopTest {
@BeforeClass
public static void setUpBeforeClass() throws Exception {
}
@AfterClass
public static void tearDownAfterClass() throws Exception {
}
@Test
public void test(){
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"spring.xml");
IPersonAop aop = (IPersonAop)ctx.getBean("personAopImpl");
aop.sayHello("xxxxx");
}
}
Monday, October 11, 2010
Spring v3.0.2 Learning Note 12 - Integrate with Hiberate
与hibernate的集成
drop database if exists `springlearn`;
create database `springlearn`;
use `springlearn`;
create table `springlearn`.`spring_seq_no` (
seq_no int(20) not null primary key,
table_code varchar(20) not null
)
ENGINE=INNODB
DEFAULT CHARSET=utf8;
create table `springlearn`.`spring_product` (
product_id int(20) not null primary key,
product_name varchar(20) not null
)
ENGINE=INNODB
DEFAULT CHARSET=utf8;
package com.spring.jdbc.dao.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* SpringSeqNo generated by hbm2java
*/
@Entity
@Table(name = "spring_seq_no", catalog = "springlearn")
public class SpringSeqNo implements java.io.Serializable {
private static final long serialVersionUID = -198923549321643996L;
private Long seqNo;
private String tableCode;
public SpringSeqNo() {
}
public SpringSeqNo(Long seqNo, String tableCode) {
this.seqNo = seqNo;
this.tableCode = tableCode;
}
@Id
@Column(name = "seq_no", unique = true, nullable = false)
public Long getSeqNo() {
return this.seqNo;
}
public void setSeqNo(Long seqNo) {
this.seqNo = seqNo;
}
@Column(name = "table_code", nullable = false, length = 20)
public String getTableCode() {
return this.tableCode;
}
public void setTableCode(String tableCode) {
this.tableCode = tableCode;
}
public String toString(){
return "SpringSeqNo{seqNo="+seqNo+",tableCode="+tableCode+"}";
}
}
--------------------------------------------------------------------------------------------
// Generated Oct 10, 2010 9:45:03 AM by Hibernate Tools 3.2.4.GA
package com.spring.jdbc.dao.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* SpringProduct generated by hbm2java
*/
@Entity
@Table(name = "spring_product", catalog = "springlearn")
public class SpringProduct implements java.io.Serializable {
private static final long serialVersionUID = -4565317465342411183L;
private Long productId;
private String productName;
public SpringProduct() {
}
public SpringProduct(Long productId, String productName) {
this.productId = productId;
this.productName = productName;
}
@Id
@Column(name = "product_id", unique = true, nullable = false)
public Long getProductId() {
return this.productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
@Column(name = "product_name", nullable = false, length = 20)
public String getProductName() {
return this.productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String toString() {
return "SpringProduct{productId=" + productId + ",productName="
+ productName + "}";
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="com.spring.jdbc" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/springlearn" />
<property name="username" value="root" />
<property name="password" value="" />
<!-- 连接池启动时的初始值 -->
<property name="initialSize" value="5" />
<!-- 连接池的最大值 -->
<property name="maxActive" value="500" />
<!-- 最大空闲值.当经过一个高峰时间后,连接池可以慢慢将已经用不到的连接慢慢释放一部分,一直减少到maxIdle为止 -->
<property name="maxIdle" value="10" />
<!-- 最小空闲值.当空闲的连接数少于阀值时,连接池就会预申请去一些连接,以免洪峰来时来不及申请 -->
<property name="minIdle" value="5" />
</bean>
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="annotatedClasses">
<list>
<value>
com.spring.jdbc.dao.domain.SpringProduct
</value>
<value>
com.spring.jdbc.dao.domain.SpringSeqNo
</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
<prop key="hibernate.show_sql">false</prop>
<prop key="hibernate.current_session_context">thread</prop>
</props>
</property>
</bean>
<bean id="txManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<tx:annotation-driven transaction-manager="txManager" />
</beans>
无需传统的hibernate.cfg.xml
import com.spring.jdbc.dao.domain.SpringProduct;
public interface IProductDao {
public void saveProduct(SpringProduct product) ;
public Long getSeqNo(String tableCode, int increment);
}
--------------------------------------------------------------------------------------
package com.spring.jdbc.dao.impl;
import javax.annotation.Resource;
import org.hibernate.Query;
import org.hibernate.SessionFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import com.spring.jdbc.dao.IProductDao;
import com.spring.jdbc.dao.domain.SpringProduct;
import com.spring.jdbc.dao.domain.SpringSeqNo;
@Repository
@Transactional
public class ProductDao implements IProductDao {
@Resource
private SessionFactory sessionFactory;
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
private static final int FIRST_SEQ_NUM = 1;// init No
private static final String SQL_GET_SEQ_NUM = "select t from SpringSeqNo as t where t.tableCode=:table_code";
private static final String SQL_UPDATE_SEQ_NUM = "update SpringSeqNo set seqNo=seqNo+:increment where tableCode=:table_code";
@Override
@Transactional
public void saveProduct(SpringProduct product) {
Long currSeqNo = getSeqNo("PRODUCT", 1);//取下一个ID,该值由表spring_seq_no存储
product.setProductId(currSeqNo);
sessionFactory.getCurrentSession().save(product);
System.out.println("save " + product);
}
@Override
@Transactional
public synchronized Long getSeqNo(String tableCode, int increment) {
SpringSeqNo result = null;
int row = 0;
// Important, update the table first to lock the record.
Query queryUpdate = sessionFactory.getCurrentSession().createQuery(
SQL_UPDATE_SEQ_NUM); //先update数据库加上锁,而不是先查!!
queryUpdate.setString("table_code", tableCode);
queryUpdate.setInteger("increment", increment);
row = queryUpdate.executeUpdate();
if (row == 0) {
// row=0 means it is the first time to get seq no.
// insert a new record which seq_no=1
SpringSeqNo seqNo = new SpringSeqNo();
seqNo.setTableCode(tableCode);
seqNo.setSeqNo(Long.valueOf(increment + FIRST_SEQ_NUM));
sessionFactory.getCurrentSession().save(seqNo);
result = new SpringSeqNo();
result.setTableCode(tableCode);
result.setSeqNo(Long.valueOf(FIRST_SEQ_NUM));
} else {
/** get current sequence no. */
Query query = sessionFactory.getCurrentSession().createQuery(
SQL_GET_SEQ_NUM);
query.setString("table_code", tableCode);
SpringSeqNo temp = (SpringSeqNo) query.setMaxResults(1)
.uniqueResult();
result = new SpringSeqNo();
result.setTableCode(tableCode);
result.setSeqNo(temp.getSeqNo() - increment);
}
return result.getSeqNo();
}
}
----------------------------------------------------------------------------------------------------
写一个多线程测试看是否可正确取到sequence number
package com.spring.jdbc.test;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.spring.jdbc.dao.IProductDao;
import com.spring.jdbc.dao.domain.SpringProduct;
public class ProductProcessor extends Thread {
private int number = 100;
AbstractApplicationContext ctx = new ClassPathXmlApplicationContext(
"spring.xml");
public ProductProcessor() {
}
public void run() {
while (number > 0) {
IProductDao prodDao = (IProductDao) ctx.getBean("productDao");
String threadName = Thread.currentThread().getName();
SpringProduct product = new SpringProduct();
product.setProductName(threadName);
prodDao.saveProduct(product);
number--;
}
}
}
-----------------------------------------
package com.spring.jdbc.test;
public class Tester {
public static void main(String[] args) {
// TODO Auto-generated method stub
Thread t1 = new ProductProcessor();
Thread t2 = new ProductProcessor();
Thread t3 = new ProductProcessor();
Thread t4 = new ProductProcessor();
Thread t5 = new ProductProcessor();
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
注意:
先在spring_seq_no给一个初始值,
insert into spring_seq_no(seq_no,table_code) value (1,'PRODUCT');
运行Tester.java,可以看到每个线程可以拿到正确的ID.
如果不给初始值, 会碰到这个异常。
Caused by: java.sql.BatchUpdateException: Deadlock found when trying to get lock; try restarting transaction
at com.mysql.jdbc.PreparedStatement.executeBatchSerially(PreparedStatement.java:1684)
at com.mysql.jdbc.PreparedStatement.executeBatch(PreparedStatement.java:1108)
at org.apache.commons.dbcp.DelegatingStatement.executeBatch(DelegatingStatement.java:297)
at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:70)
at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:268)
在ProductDao.java中的saveProduct()和getSeqNo()中任意位置抛出RuntimeException,事务会回滚;两个方法中的 sessionFactory.getCurrentSession() 对应的session是同一个session,事务也是同一个事务。
以上代码也在Oralce 10g环境下测试,可得同样的结果。
即便不用spring来管理事务,而是通过hibernate单独处理,也可得同样的结果。
- 环境
- Spring v3.0.2
- Hibernate V3.3.1
- MySql v5.0.22
- JDK 1.6.x
drop database if exists `springlearn`;
create database `springlearn`;
use `springlearn`;
create table `springlearn`.`spring_seq_no` (
seq_no int(20) not null primary key,
table_code varchar(20) not null
)
ENGINE=INNODB
DEFAULT CHARSET=utf8;
create table `springlearn`.`spring_product` (
product_id int(20) not null primary key,
product_name varchar(20) not null
)
ENGINE=INNODB
DEFAULT CHARSET=utf8;
- 参照http://wangxiangblog.blogspot.com/2010/04/hibernate-tools-with-eclipse.html 自动生成域对象
package com.spring.jdbc.dao.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* SpringSeqNo generated by hbm2java
*/
@Entity
@Table(name = "spring_seq_no", catalog = "springlearn")
public class SpringSeqNo implements java.io.Serializable {
private static final long serialVersionUID = -198923549321643996L;
private Long seqNo;
private String tableCode;
public SpringSeqNo() {
}
public SpringSeqNo(Long seqNo, String tableCode) {
this.seqNo = seqNo;
this.tableCode = tableCode;
}
@Id
@Column(name = "seq_no", unique = true, nullable = false)
public Long getSeqNo() {
return this.seqNo;
}
public void setSeqNo(Long seqNo) {
this.seqNo = seqNo;
}
@Column(name = "table_code", nullable = false, length = 20)
public String getTableCode() {
return this.tableCode;
}
public void setTableCode(String tableCode) {
this.tableCode = tableCode;
}
public String toString(){
return "SpringSeqNo{seqNo="+seqNo+",tableCode="+tableCode+"}";
}
}
--------------------------------------------------------------------------------------------
// Generated Oct 10, 2010 9:45:03 AM by Hibernate Tools 3.2.4.GA
package com.spring.jdbc.dao.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* SpringProduct generated by hbm2java
*/
@Entity
@Table(name = "spring_product", catalog = "springlearn")
public class SpringProduct implements java.io.Serializable {
private static final long serialVersionUID = -4565317465342411183L;
private Long productId;
private String productName;
public SpringProduct() {
}
public SpringProduct(Long productId, String productName) {
this.productId = productId;
this.productName = productName;
}
@Id
@Column(name = "product_id", unique = true, nullable = false)
public Long getProductId() {
return this.productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
@Column(name = "product_name", nullable = false, length = 20)
public String getProductName() {
return this.productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String toString() {
return "SpringProduct{productId=" + productId + ",productName="
+ productName + "}";
}
}
- 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="com.spring.jdbc" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/springlearn" />
<property name="username" value="root" />
<property name="password" value="" />
<!-- 连接池启动时的初始值 -->
<property name="initialSize" value="5" />
<!-- 连接池的最大值 -->
<property name="maxActive" value="500" />
<!-- 最大空闲值.当经过一个高峰时间后,连接池可以慢慢将已经用不到的连接慢慢释放一部分,一直减少到maxIdle为止 -->
<property name="maxIdle" value="10" />
<!-- 最小空闲值.当空闲的连接数少于阀值时,连接池就会预申请去一些连接,以免洪峰来时来不及申请 -->
<property name="minIdle" value="5" />
</bean>
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="annotatedClasses">
<list>
<value>
com.spring.jdbc.dao.domain.SpringProduct
</value>
<value>
com.spring.jdbc.dao.domain.SpringSeqNo
</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
<prop key="hibernate.show_sql">false</prop>
<prop key="hibernate.current_session_context">thread</prop>
</props>
</property>
</bean>
<bean id="txManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<tx:annotation-driven transaction-manager="txManager" />
</beans>
无需传统的hibernate.cfg.xml
- 源代码
import com.spring.jdbc.dao.domain.SpringProduct;
public interface IProductDao {
public void saveProduct(SpringProduct product) ;
public Long getSeqNo(String tableCode, int increment);
}
--------------------------------------------------------------------------------------
package com.spring.jdbc.dao.impl;
import javax.annotation.Resource;
import org.hibernate.Query;
import org.hibernate.SessionFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import com.spring.jdbc.dao.IProductDao;
import com.spring.jdbc.dao.domain.SpringProduct;
import com.spring.jdbc.dao.domain.SpringSeqNo;
@Repository
@Transactional
public class ProductDao implements IProductDao {
@Resource
private SessionFactory sessionFactory;
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
private static final int FIRST_SEQ_NUM = 1;// init No
private static final String SQL_GET_SEQ_NUM = "select t from SpringSeqNo as t where t.tableCode=:table_code";
private static final String SQL_UPDATE_SEQ_NUM = "update SpringSeqNo set seqNo=seqNo+:increment where tableCode=:table_code";
@Override
@Transactional
public void saveProduct(SpringProduct product) {
Long currSeqNo = getSeqNo("PRODUCT", 1);//取下一个ID,该值由表spring_seq_no存储
product.setProductId(currSeqNo);
sessionFactory.getCurrentSession().save(product);
System.out.println("save " + product);
}
@Override
@Transactional
public synchronized Long getSeqNo(String tableCode, int increment) {
SpringSeqNo result = null;
int row = 0;
// Important, update the table first to lock the record.
Query queryUpdate = sessionFactory.getCurrentSession().createQuery(
SQL_UPDATE_SEQ_NUM); //先update数据库加上锁,而不是先查!!
queryUpdate.setString("table_code", tableCode);
queryUpdate.setInteger("increment", increment);
row = queryUpdate.executeUpdate();
if (row == 0) {
// row=0 means it is the first time to get seq no.
// insert a new record which seq_no=1
SpringSeqNo seqNo = new SpringSeqNo();
seqNo.setTableCode(tableCode);
seqNo.setSeqNo(Long.valueOf(increment + FIRST_SEQ_NUM));
sessionFactory.getCurrentSession().save(seqNo);
result = new SpringSeqNo();
result.setTableCode(tableCode);
result.setSeqNo(Long.valueOf(FIRST_SEQ_NUM));
} else {
/** get current sequence no. */
Query query = sessionFactory.getCurrentSession().createQuery(
SQL_GET_SEQ_NUM);
query.setString("table_code", tableCode);
SpringSeqNo temp = (SpringSeqNo) query.setMaxResults(1)
.uniqueResult();
result = new SpringSeqNo();
result.setTableCode(tableCode);
result.setSeqNo(temp.getSeqNo() - increment);
}
return result.getSeqNo();
}
}
----------------------------------------------------------------------------------------------------
写一个多线程测试看是否可正确取到sequence number
package com.spring.jdbc.test;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.spring.jdbc.dao.IProductDao;
import com.spring.jdbc.dao.domain.SpringProduct;
public class ProductProcessor extends Thread {
private int number = 100;
AbstractApplicationContext ctx = new ClassPathXmlApplicationContext(
"spring.xml");
public ProductProcessor() {
}
public void run() {
while (number > 0) {
IProductDao prodDao = (IProductDao) ctx.getBean("productDao");
String threadName = Thread.currentThread().getName();
SpringProduct product = new SpringProduct();
product.setProductName(threadName);
prodDao.saveProduct(product);
number--;
}
}
}
-----------------------------------------
package com.spring.jdbc.test;
public class Tester {
public static void main(String[] args) {
// TODO Auto-generated method stub
Thread t1 = new ProductProcessor();
Thread t2 = new ProductProcessor();
Thread t3 = new ProductProcessor();
Thread t4 = new ProductProcessor();
Thread t5 = new ProductProcessor();
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
注意:
先在spring_seq_no给一个初始值,
insert into spring_seq_no(seq_no,table_code) value (1,'PRODUCT');
运行Tester.java,可以看到每个线程可以拿到正确的ID.
如果不给初始值, 会碰到这个异常。
Caused by: java.sql.BatchUpdateException: Deadlock found when trying to get lock; try restarting transaction
at com.mysql.jdbc.PreparedStatement.executeBatchSerially(PreparedStatement.java:1684)
at com.mysql.jdbc.PreparedStatement.executeBatch(PreparedStatement.java:1108)
at org.apache.commons.dbcp.DelegatingStatement.executeBatch(DelegatingStatement.java:297)
at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:70)
at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:268)
在ProductDao.java中的saveProduct()和getSeqNo()中任意位置抛出RuntimeException,事务会回滚;两个方法中的 sessionFactory.getCurrentSession() 对应的session是同一个session,事务也是同一个事务。
以上代码也在Oralce 10g环境下测试,可得同样的结果。
即便不用spring来管理事务,而是通过hibernate单独处理,也可得同样的结果。
Saturday, October 9, 2010
Spring v3.0.2 Learning Note 11 - XML-based Transaction Management
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<context:property-placeholder location="classpath:jdbc.properties" /> // 占位符的设置
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="${driverClassName}" />
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
<!-- 连接池启动时的初始值 -->
<property name="initialSize" value="${initialSize}" />
<!-- 连接池的最大值 -->
<property name="maxActive" value="${maxActive}" />
<!-- 最大空闲值.当经过一个高峰时间后,连接池可以慢慢将已经用不到的连接慢慢释放一部分,一直减少到maxIdle为止 -->
<property name="maxIdle" value="${maxIdle}" />
<!-- 最小空闲值.当空闲的连接数少于阀值时,连接池就会预申请去一些连接,以免洪峰来时来不及申请 -->
<property name="minIdle" value="${minIdle}" />
</bean>
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<aop:config>
<aop:pointcut id="transactionPointcut" expression="execution(* com.spring.test.dao..*.*(..))"/> // 对该包及其子包应用事务管理
<aop:advisor advice-ref="txAdvice" pointcut-ref="transactionPointcut"/>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
// 适用于以get开头的方法
<tx:method name="get*" read-only="true" propagation="NOT_SUPPORTED"/>
// 表明其他方法使用默认的事务行为
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<bean id="personService" class="com.spring.test.manager.impl.PersonManager">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<context:property-placeholder location="classpath:jdbc.properties" /> // 占位符的设置
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="${driverClassName}" />
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
<!-- 连接池启动时的初始值 -->
<property name="initialSize" value="${initialSize}" />
<!-- 连接池的最大值 -->
<property name="maxActive" value="${maxActive}" />
<!-- 最大空闲值.当经过一个高峰时间后,连接池可以慢慢将已经用不到的连接慢慢释放一部分,一直减少到maxIdle为止 -->
<property name="maxIdle" value="${maxIdle}" />
<!-- 最小空闲值.当空闲的连接数少于阀值时,连接池就会预申请去一些连接,以免洪峰来时来不及申请 -->
<property name="minIdle" value="${minIdle}" />
</bean>
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<aop:config>
<aop:pointcut id="transactionPointcut" expression="execution(* com.spring.test.dao..*.*(..))"/> // 对该包及其子包应用事务管理
<aop:advisor advice-ref="txAdvice" pointcut-ref="transactionPointcut"/>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
// 适用于以get开头的方法
<tx:method name="get*" read-only="true" propagation="NOT_SUPPORTED"/>
// 表明其他方法使用默认的事务行为
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<bean id="personService" class="com.spring.test.manager.impl.PersonManager">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
Spring v3.0.2 Learning Note 10 - Annotation-based Transaction Management
基于注解的事务管理
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<context:property-placeholder location="classpath:jdbc.properties" /> // 占位符的设置
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="${driverClassName}" />
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
<!-- 连接池启动时的初始值 -->
<property name="initialSize" value="${initialSize}" />
<!-- 连接池的最大值 -->
<property name="maxActive" value="${maxActive}" />
<!-- 最大空闲值.当经过一个高峰时间后,连接池可以慢慢将已经用不到的连接慢慢释放一部分,一直减少到maxIdle为止 -->
<property name="maxIdle" value="${maxIdle}" />
<!-- 最小空闲值.当空闲的连接数少于阀值时,连接池就会预申请去一些连接,以免洪峰来时来不及申请 -->
<property name="minIdle" value="${minIdle}" />
</bean>
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:annotation-driven transaction-manager="txManager" />
<bean id="personService" class="com.spring.test.manager.impl.PersonManager">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 指定采用@Transactional 注解的方式使用事务 -->
注意:@Transactional只能用于public方法上。如果用在private、protected,系统不会抱错,但是配置的事务设置将失效。如果一定要在非public方法上用这个注解,需要引入AspectJ.
public class PersonDao implements IPersonDao {
//.......
}
NOT_SUPPORTED:声明方法不需要事务。如果方法没有关联到一个事务,容器不会为它开启事务。如果方法在一个事务中被调用,该事物会被挂起,在方法调用结束后,原先的事务恢复运行。
REQUEST_NEW:不管是否存在事务,业务方法总会为自己发起一个新的事务。如果方法运行在一个事务中,则原有事务挂起,新的事务创建,直到方法执行结束,新事务才算结束,原先的事务才会恢复执行。
MANDATORY:指定业务方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务。如果业务方法在没有事务的环境下调用,容器就会抛出异常。
SUPPORTS:如果业务方法在某个事务范围内被调用,则方法称为该事物的一部分。如果业务方法在事务范围外被调用,则方法在没有事务的环境下执行。
NEVER:指定业务方法不能再事务范围内执行。如果业务方法在某个事务中执行,容器会抛出例外,如果业务方法没有关联到任何事务,才能正常执行。
NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中,如果没有活动事务,则按REQUESTED属性执行。它使用了一个独立的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会影响到外部事务。
jdbcTemplate.update("delete from person where id=?", new Object[]{personid},
new int[]{java.sql.Types.INTEGER});
throw new RuntimeException("This is unchecked-exception.");
}
运行此方法,默认状态下,数据库中的数据不会删除,spring容器会回滚事务。
public void delete(Integer personid) throws Exception {
jdbcTemplate.update("delete from person where id=?", new Object[]{personid},
new int[]{java.sql.Types.INTEGER});
throw new Exception("This is checked-exception.");
}
运行此方法,默认状态下,数据库中的数据会被删除,spring容器不会回滚事务。
修改默认情况,指定事务中碰到某个checked-exception,spring容器会回滚事务。
jdbcTemplate.update("delete from person where id=?", new Object[]{personid},
new int[]{java.sql.Types.INTEGER});
throw new Exception("This is checked-exception.");
}运行此方法,spring容器会回滚事务。
同理,也可修改属性,使得事务中碰到unchecked-exception不回滚事务。
jdbcTemplate.update("delete from person where id=?", new Object[]{personid},
new int[]{java.sql.Types.INTEGER});
throw new RuntimeException("This is unchecked-exception.");
}
可以按如下方法明确指定传播属性。
@Transactional(propagation=Propagation.NOT_SUPPORTED)
public List<Person> getPersons() {
return (List<Person>)jdbcTemplate.query("select * from person", new PersonRowMapper());
}
Read Uncommitted:读未提交的数据,会出现脏读,不可重复读和幻读
Read Committed:读已提交的数据,会出现不可重复读和幻读
Repeatable Read:可重复读,会出现幻读
Serializable:最高隔离级别,效率也最低,不会出现以上的脏读,不可重复读和幻读。
脏读:一个事务读到另一个未提交事务的更新数据。
不可重复读:在同一个事物中,多次读取同一数据返回的结果不同。即后续读取可以读到另一个事务已提交的更新事务。
幻读:一个事务读到另一个事务已提交的insert数据。
- 配置数据源:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<context:property-placeholder location="classpath:jdbc.properties" /> // 占位符的设置
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="${driverClassName}" />
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
<!-- 连接池启动时的初始值 -->
<property name="initialSize" value="${initialSize}" />
<!-- 连接池的最大值 -->
<property name="maxActive" value="${maxActive}" />
<!-- 最大空闲值.当经过一个高峰时间后,连接池可以慢慢将已经用不到的连接慢慢释放一部分,一直减少到maxIdle为止 -->
<property name="maxIdle" value="${maxIdle}" />
<!-- 最小空闲值.当空闲的连接数少于阀值时,连接池就会预申请去一些连接,以免洪峰来时来不及申请 -->
<property name="minIdle" value="${minIdle}" />
</bean>
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:annotation-driven transaction-manager="txManager" />
<bean id="personService" class="com.spring.test.manager.impl.PersonManager">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
jdbc.properties内容:
driverClassName=org.gjt.mm.mysql.Driver
url=jdbc\:mysql\://localhost\:3306/myproj?useUnicode\=true&characterEncoding\=UTF-8
username=root
password=123456
initialSize=1
maxActive=500
maxIdle=2
minIdle=1
url=jdbc\:mysql\://localhost\:3306/myproj?useUnicode\=true&characterEncoding\=UTF-8
username=root
password=123456
initialSize=1
maxActive=500
maxIdle=2
minIdle=1
注意,如果直接在spring的XML配置文件中配置URL,其写法为(注意&的写法)
<property name="url" value="jdbc:mysql://localhost:3306/myproj?useUnicode=true&characterEncoding=UTF-8"/>
- 配置事务,需要在xml配置文件中引入声明事务的tx命名空间,支持注解的方式和XML配置方式。
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
- 注解方式用法
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 指定采用@Transactional 注解的方式使用事务 -->
<tx:annotation-driven transaction-manager="txManager" />
注意:@Transactional只能用于public方法上。如果用在private、protected,系统不会抱错,但是配置的事务设置将失效。如果一定要在非public方法上用这个注解,需要引入AspectJ.
- 基本用法
public class PersonDao implements IPersonDao {
//.......
}
- Spring事务管理的传播属性
NOT_SUPPORTED:声明方法不需要事务。如果方法没有关联到一个事务,容器不会为它开启事务。如果方法在一个事务中被调用,该事物会被挂起,在方法调用结束后,原先的事务恢复运行。
REQUEST_NEW:不管是否存在事务,业务方法总会为自己发起一个新的事务。如果方法运行在一个事务中,则原有事务挂起,新的事务创建,直到方法执行结束,新事务才算结束,原先的事务才会恢复执行。
MANDATORY:指定业务方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务。如果业务方法在没有事务的环境下调用,容器就会抛出异常。
SUPPORTS:如果业务方法在某个事务范围内被调用,则方法称为该事物的一部分。如果业务方法在事务范围外被调用,则方法在没有事务的环境下执行。
NEVER:指定业务方法不能再事务范围内执行。如果业务方法在某个事务中执行,容器会抛出例外,如果业务方法没有关联到任何事务,才能正常执行。
NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中,如果没有活动事务,则按REQUESTED属性执行。它使用了一个独立的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会影响到外部事务。
- Spring容器在默认状态下,碰到unchecked-exception会回滚,checked-exception则不会。
jdbcTemplate.update("delete from person where id=?", new Object[]{personid},
new int[]{java.sql.Types.INTEGER});
throw new RuntimeException("This is unchecked-exception.");
}
运行此方法,默认状态下,数据库中的数据不会删除,spring容器会回滚事务。
public void delete(Integer personid) throws Exception {
jdbcTemplate.update("delete from person where id=?", new Object[]{personid},
new int[]{java.sql.Types.INTEGER});
throw new Exception("This is checked-exception.");
}
运行此方法,默认状态下,数据库中的数据会被删除,spring容器不会回滚事务。
修改默认情况,指定事务中碰到某个checked-exception,spring容器会回滚事务。
@Transactional(rollbackFor=Exception.class)
public void delete(Integer personid) throws Exception {jdbcTemplate.update("delete from person where id=?", new Object[]{personid},
new int[]{java.sql.Types.INTEGER});
throw new Exception("This is checked-exception.");
}运行此方法,spring容器会回滚事务。
同理,也可修改属性,使得事务中碰到unchecked-exception不回滚事务。
@Transactional(noRollbackFor=RuntimeException.class)
public void delete(Integer personid) {jdbcTemplate.update("delete from person where id=?", new Object[]{personid},
new int[]{java.sql.Types.INTEGER});
throw new RuntimeException("This is unchecked-exception.");
}
- 注解中指定传播属性
可以按如下方法明确指定传播属性。
@Transactional(propagation=Propagation.NOT_SUPPORTED)
public List<Person> getPersons() {
return (List<Person>)jdbcTemplate.query("select * from person", new PersonRowMapper());
}
- 隔离级别
Read Uncommitted:读未提交的数据,会出现脏读,不可重复读和幻读
Read Committed:读已提交的数据,会出现不可重复读和幻读
Repeatable Read:可重复读,会出现幻读
Serializable:最高隔离级别,效率也最低,不会出现以上的脏读,不可重复读和幻读。
脏读:一个事务读到另一个未提交事务的更新数据。
不可重复读:在同一个事物中,多次读取同一数据返回的结果不同。即后续读取可以读到另一个事务已提交的更新事务。
幻读:一个事务读到另一个事务已提交的insert数据。
解惑 spring 嵌套事务
转自http://www.javaeye.com/topic/35907
/**
* @author 王政
* @date 2006-11-24
* @note 转载请注明出处
*/
在所有使用 spring 的应用中, 声明式事务管理可能是使用率最高的功能了, 但是, 从我观察到的情况看, 绝大多数人并不能深刻理解事务声明中不同事务传播属性配置的的含义, 让我们来看一下 TransactionDefinition 接口中的定义 。
我们可以看到, 在 spring 中一共定义了六种事务传播属性, 如果你觉得看起来不够直观, 那么我来转贴一个满大街都有的翻译
PROPAGATION_REQUIRED -- 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
PROPAGATION_SUPPORTS -- 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY -- 支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW -- 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED -- 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER -- 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED -- 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
前六个策略类似于EJB CMT,第七个(PROPAGATION_NESTED)是Spring所提供的一个特殊变量。
它要求事务管理器或者使用JDBC 3.0 Savepoint API提供嵌套事务行为(如Spring的DataSourceTransactionManager)
在我所见过的误解中, 最常见的是下面这种:
假如有两个业务接口 ServiceA 和 ServiceB, 其中 ServiceA 中有一个方法实现如下
/**
* 事务属性配置为 PROPAGATION_REQUIRED
*/
void methodA() {
// 调用 ServiceB 的方法
ServiceB.methodB();
}
那么如果 ServiceB 的 methodB 如果配置了事务, 就必须配置为 PROPAGATION_NESTED
这种想法可能害了不少人, 认为 Service 之间应该避免互相调用, 其实根本不用担心这点,PROPAGATION_REQUIRED 已经说得很明白,
如果当前线程中已经存在事务, 方法调用会加入此事务, 果当前没有事务,就新建一个事务, 所以 ServiceB#methodB() 的事务只要遵循最普通的规则配置为 PROPAGATION_REQUIRED 即可, 如果 ServiceB#methodB (我们称之为内部事务, 为下文打下基础) 抛了异常, 那么 ServiceA#methodA(我们称之为外部事务) 如果没有特殊配置此异常时事务提交 (即 +MyCheckedException的用法), 那么整个事务是一定要 rollback 的, 什么 Service 只能调 Dao 之类的言论纯属无稽之谈, spring 只负责配置了事务属性方法的拦截, 它怎么知道你这个方法是在 Service 还是 Dao 里 ?
说了这么半天, 那到底什么是真正的事务嵌套呢, 解释之前我们来看一下 Juergen Hoeller 的原话
PROPAGATION_REQUIRES_NEW starts a new, independent "inner" transaction for the given scope. This transaction will be committed or rolled back completely independent from the outer transaction, having its own isolation scope, its own set of locks, etc. The outer transaction will get suspended at the beginning of the inner one, and resumed once the inner one has completed.
Such independent inner transactions are for example used for id generation through manual sequences, where the access to the sequence table should happen in its own transactions, to keep the lock there as short as possible. The goal there is to avoid tying the sequence locks to the (potentially much longer running) outer transaction, with the sequence lock not getting released before completion of the outer transaction.
PROPAGATION_NESTED on the other hand starts a "nested" transaction, which is a true subtransaction of the existing one. What will happen is that a savepoint will be taken at the start of the nested transaction. íf the nested transaction fails, we will roll back to that savepoint. The nested transaction is part of of the outer transaction, so it will only be committed at the end of of the outer transaction.
Nested transactions essentially allow to try some execution subpaths as subtransactions: rolling back to the state at the beginning of the failed subpath, continuing with another subpath or with the main execution path there - all within one isolated transaction, and not losing any previous work done within the outer transaction.
For example, consider parsing a very large input file consisting of account transfer blocks: The entire file should essentially be parsed within one transaction, with one single commit at the end. But if a block fails, its transfers need to be rolled back, writing a failure marker somewhere. You could either start over the entire transaction every time a block fails, remembering which blocks to skip - or you mark each block as a nested transaction, only rolling back that specific set of operations, keeping the previous work of the outer transaction. The latter is of course much more efficient, in particular when a block at the end of the file fails.
Rolling back the entire transaction is the choice of the demarcation code/config that started the outer transaction.
So if an inner transaction throws an exception and is supposed to be rolled back (according to the rollback rules), the transaction will get rolled back to the savepoint taken at the start of the inner transaction. The immediate calling code can then decide to catch the exception and proceed down some other path within the outer transaction.
If the code that called the inner transaction lets the exception propagate up the call chain, the exception will eventually reach the demarcation code of the outer transaction. At that point, the rollback rules of the outer transaction decide whether to trigger a rollback. That would be a rollback of the entire outer transaction then.
So essentially, it depends on your exception handling. If you catch the exception thrown by the inner transaction, you can proceed down some other path within the outer transaction. If you let the exception propagate up the call chain, it's eventually gonna cause a rollback of the entire outer transaction.
也就是说, 最容易弄混淆的其实是 PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED, 那么这两种方式又有何区别呢? 我简单的翻译一下 Juergen Hoeller 的话 :
PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 "内部" 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行.
另一方面, PROPAGATION_NESTED 开始一个 "嵌套的" 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交.
由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于, PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 潜套事务也会被 commit, 这个规则同样适用于 roll back.
那么外部事务如何利用嵌套事务的 savepoint 特性呢, 我们用代码来说话
这种情况下, 因为 ServiceB#methodB 的事务属性为 PROPAGATION_REQUIRES_NEW, 所以两者不会发生任何关系, ServiceA#methodA 和 ServiceB#methodB 不会因为对方的执行情况而影响事务的结果, 因为它们根本就是两个事务, 在 ServiceB#methodB 执行时 ServiceA#methodA 的事务已经挂起了 (关于事务挂起的内容已经超出了本文的讨论范围, 有时间我会再写一些挂起的文章) .
那么 PROPAGATION_NESTED 又是怎么回事呢? 继续看代码
现在的情况就变得比较复杂了, ServiceB#methodB 的事务属性被配置为ROPAGATION_NESTED, 此时两者之间又将如何协作呢? 从 Juergen Hoeller 的原话中我们可以找到答案, ServiceB#methodB 如果 rollback, 那么内部事务(即 ServiceB#methodB) 将回滚到它执行前的 SavePoint(注意, 这是本文中第一次提到它, 潜套事务中最核心的概念), 而外部事务(即 ServiceA#methodA) 可以有以下两种处理方式:
1. 改写 ServiceA 如下
这种方式也是潜套事务最有价值的地方, 它起到了分支执行的效果, 如果 ServiceB.methodB 失败, 那么执行 ServiceC.methodC(), 而 ServiceB.methodB 已经回滚到它执行之前的 SavePoint, 所以不会产生脏数据(相当于此方法从未执行过), 这种特性可以用在某些特殊的业务中, 而 PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 都没有办法做到这一点. (题外话 : 看到这种代码, 似乎似曾相识, 想起了 prototype.js 中的 Try 函数 )
2. 代码不做任何修改, 那么如果内部事务(即 ServiceB#methodB) rollback, 那么首先 ServiceB.methodB 回滚到它执行之前的 SavePoint(在任何情况下都会如此), 外部事务(即 ServiceA#methodA) 将根据具体的配置决定自己是 commit 还是 rollback (+MyCheckedException).
上面大致讲述了潜套事务的使用场景, 下面我们来看如何在 spring 中使用PROPAGATION_NESTED, 首先来看 AbstractPlatformTransactionManager
一目了然
1. 我们要设置 transactionManager 的 nestedTransactionAllowed 属性为 true, 注意, 此属性默认为 false!!!
再看 AbstractTransactionStatus#createAndHoldSavepoint() 方法
可以看到 Savepoint 是 SavepointManager.createSavepoint 实现的, 再看 SavepointManager 的层次结构, 发现其 Template 实现是 JdbcTransactionObjectSupport, 常用的 DatasourceTransactionManager, HibernateTransactionManager 中的 TransactonObject 都是它的子类 :
JdbcTransactionObjectSupport 告诉我们必须要满足两个条件才能 createSavepoint :
2. java.sql.Savepoint 必须存在, 即 jdk 版本要 1.4+
3. Connection.getMetaData().supportsSavepoints() 必须为 true, 即 jdbc drive 必须支持 JDBC 3.0
确保以上条件都满足后, 你就可以尝试使用 PROPAGATION_NESTED 了.
/**
* @author 王政
* @date 2006-11-24
* @note 转载请注明出处
*/
在所有使用 spring 的应用中, 声明式事务管理可能是使用率最高的功能了, 但是, 从我观察到的情况看, 绝大多数人并不能深刻理解事务声明中不同事务传播属性配置的的含义, 让我们来看一下 TransactionDefinition 接口中的定义 。
Java代码
- /**
- * Support a current transaction, create a new one if none exists.
- * Analogous to EJB transaction attribute of the same name.
- * <p>This is typically the default setting of a transaction definition.
- */
- int PROPAGATION_REQUIRED = 0;
- /**
- * Support a current transaction, execute non-transactionally if none exists.
- * Analogous to EJB transaction attribute of the same name.
- * <p>Note: For transaction managers with transaction synchronization,
- * PROPAGATION_SUPPORTS is slightly different from no transaction at all,
- * as it defines a transaction scopp that synchronization will apply for.
- * As a consequence, the same resources (JDBC Connection, Hibernate Session, etc)
- * will be shared for the entire specified scope. Note that this depends on
- * the actual synchronization configuration of the transaction manager.
- * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization
- */
- int PROPAGATION_SUPPORTS = 1;
- /**
- * Support a current transaction, throw an exception if none exists.
- * Analogous to EJB transaction attribute of the same name.
- */
- int PROPAGATION_MANDATORY = 2;
- /**
- * Create a new transaction, suspend the current transaction if one exists.
- * Analogous to EJB transaction attribute of the same name.
- * <p>Note: Actual transaction suspension will not work on out-of-the-box
- * on all transaction managers. This in particular applies to JtaTransactionManager,
- * which requires the <code>javax.transaction.TransactionManager</code> to be
- * made available it to it (which is server-specific in standard J2EE).
- * @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
- */
- int PROPAGATION_REQUIRES_NEW = 3;
- /**
- * Execute non-transactionally, suspend the current transaction if one exists.
- * Analogous to EJB transaction attribute of the same name.
- * <p>Note: Actual transaction suspension will not work on out-of-the-box
- * on all transaction managers. This in particular applies to JtaTransactionManager,
- * which requires the <code>javax.transaction.TransactionManager</code> to be
- * made available it to it (which is server-specific in standard J2EE).
- * @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
- */
- int PROPAGATION_NOT_SUPPORTED = 4;
- /**
- * Execute non-transactionally, throw an exception if a transaction exists.
- * Analogous to EJB transaction attribute of the same name.
- */
- int PROPAGATION_NEVER = 5;
- /**
- * Execute within a nested transaction if a current transaction exists,
- * behave like PROPAGATION_REQUIRED else. There is no analogous feature in EJB.
- * <p>Note: Actual creation of a nested transaction will only work on specific
- * transaction managers. Out of the box, this only applies to the JDBC
- * DataSourceTransactionManager when working on a JDBC 3.0 driver.
- * Some JTA providers might support nested transactions as well.
- * @see org.springframework.jdbc.datasource.DataSourceTransactionManager
- */
- int PROPAGATION_NESTED = 6;
/** * Support a current transaction, create a new one if none exists. * Analogous to EJB transaction attribute of the same name. * <p>This is typically the default setting of a transaction definition. */ int PROPAGATION_REQUIRED = 0; /** * Support a current transaction, execute non-transactionally if none exists. * Analogous to EJB transaction attribute of the same name. * <p>Note: For transaction managers with transaction synchronization, * PROPAGATION_SUPPORTS is slightly different from no transaction at all, * as it defines a transaction scopp that synchronization will apply for. * As a consequence, the same resources (JDBC Connection, Hibernate Session, etc) * will be shared for the entire specified scope. Note that this depends on * the actual synchronization configuration of the transaction manager. * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization */ int PROPAGATION_SUPPORTS = 1; /** * Support a current transaction, throw an exception if none exists. * Analogous to EJB transaction attribute of the same name. */ int PROPAGATION_MANDATORY = 2; /** * Create a new transaction, suspend the current transaction if one exists. * Analogous to EJB transaction attribute of the same name. * <p>Note: Actual transaction suspension will not work on out-of-the-box * on all transaction managers. This in particular applies to JtaTransactionManager, * which requires the <code>javax.transaction.TransactionManager</code> to be * made available it to it (which is server-specific in standard J2EE). * @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager */ int PROPAGATION_REQUIRES_NEW = 3; /** * Execute non-transactionally, suspend the current transaction if one exists. * Analogous to EJB transaction attribute of the same name. * <p>Note: Actual transaction suspension will not work on out-of-the-box * on all transaction managers. This in particular applies to JtaTransactionManager, * which requires the <code>javax.transaction.TransactionManager</code> to be * made available it to it (which is server-specific in standard J2EE). * @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager */ int PROPAGATION_NOT_SUPPORTED = 4; /** * Execute non-transactionally, throw an exception if a transaction exists. * Analogous to EJB transaction attribute of the same name. */ int PROPAGATION_NEVER = 5; /** * Execute within a nested transaction if a current transaction exists, * behave like PROPAGATION_REQUIRED else. There is no analogous feature in EJB. * <p>Note: Actual creation of a nested transaction will only work on specific * transaction managers. Out of the box, this only applies to the JDBC * DataSourceTransactionManager when working on a JDBC 3.0 driver. * Some JTA providers might support nested transactions as well. * @see org.springframework.jdbc.datasource.DataSourceTransactionManager */ int PROPAGATION_NESTED = 6;
我们可以看到, 在 spring 中一共定义了六种事务传播属性, 如果你觉得看起来不够直观, 那么我来转贴一个满大街都有的翻译
引用
PROPAGATION_REQUIRED -- 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
PROPAGATION_SUPPORTS -- 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY -- 支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW -- 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED -- 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER -- 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED -- 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
前六个策略类似于EJB CMT,第七个(PROPAGATION_NESTED)是Spring所提供的一个特殊变量。
它要求事务管理器或者使用JDBC 3.0 Savepoint API提供嵌套事务行为(如Spring的DataSourceTransactionManager)
在我所见过的误解中, 最常见的是下面这种:
引用
假如有两个业务接口 ServiceA 和 ServiceB, 其中 ServiceA 中有一个方法实现如下
/**
* 事务属性配置为 PROPAGATION_REQUIRED
*/
void methodA() {
// 调用 ServiceB 的方法
ServiceB.methodB();
}
那么如果 ServiceB 的 methodB 如果配置了事务, 就必须配置为 PROPAGATION_NESTED
这种想法可能害了不少人, 认为 Service 之间应该避免互相调用, 其实根本不用担心这点,PROPAGATION_REQUIRED 已经说得很明白,
如果当前线程中已经存在事务, 方法调用会加入此事务, 果当前没有事务,就新建一个事务, 所以 ServiceB#methodB() 的事务只要遵循最普通的规则配置为 PROPAGATION_REQUIRED 即可, 如果 ServiceB#methodB (我们称之为内部事务, 为下文打下基础) 抛了异常, 那么 ServiceA#methodA(我们称之为外部事务) 如果没有特殊配置此异常时事务提交 (即 +MyCheckedException的用法), 那么整个事务是一定要 rollback 的, 什么 Service 只能调 Dao 之类的言论纯属无稽之谈, spring 只负责配置了事务属性方法的拦截, 它怎么知道你这个方法是在 Service 还是 Dao 里 ?
说了这么半天, 那到底什么是真正的事务嵌套呢, 解释之前我们来看一下 Juergen Hoeller 的原话
Juergen Hoeller 写道
PROPAGATION_REQUIRES_NEW starts a new, independent "inner" transaction for the given scope. This transaction will be committed or rolled back completely independent from the outer transaction, having its own isolation scope, its own set of locks, etc. The outer transaction will get suspended at the beginning of the inner one, and resumed once the inner one has completed.
Such independent inner transactions are for example used for id generation through manual sequences, where the access to the sequence table should happen in its own transactions, to keep the lock there as short as possible. The goal there is to avoid tying the sequence locks to the (potentially much longer running) outer transaction, with the sequence lock not getting released before completion of the outer transaction.
PROPAGATION_NESTED on the other hand starts a "nested" transaction, which is a true subtransaction of the existing one. What will happen is that a savepoint will be taken at the start of the nested transaction. íf the nested transaction fails, we will roll back to that savepoint. The nested transaction is part of of the outer transaction, so it will only be committed at the end of of the outer transaction.
Nested transactions essentially allow to try some execution subpaths as subtransactions: rolling back to the state at the beginning of the failed subpath, continuing with another subpath or with the main execution path there - all within one isolated transaction, and not losing any previous work done within the outer transaction.
For example, consider parsing a very large input file consisting of account transfer blocks: The entire file should essentially be parsed within one transaction, with one single commit at the end. But if a block fails, its transfers need to be rolled back, writing a failure marker somewhere. You could either start over the entire transaction every time a block fails, remembering which blocks to skip - or you mark each block as a nested transaction, only rolling back that specific set of operations, keeping the previous work of the outer transaction. The latter is of course much more efficient, in particular when a block at the end of the file fails.
Juergen Hoeller 写道
Rolling back the entire transaction is the choice of the demarcation code/config that started the outer transaction.
So if an inner transaction throws an exception and is supposed to be rolled back (according to the rollback rules), the transaction will get rolled back to the savepoint taken at the start of the inner transaction. The immediate calling code can then decide to catch the exception and proceed down some other path within the outer transaction.
If the code that called the inner transaction lets the exception propagate up the call chain, the exception will eventually reach the demarcation code of the outer transaction. At that point, the rollback rules of the outer transaction decide whether to trigger a rollback. That would be a rollback of the entire outer transaction then.
So essentially, it depends on your exception handling. If you catch the exception thrown by the inner transaction, you can proceed down some other path within the outer transaction. If you let the exception propagate up the call chain, it's eventually gonna cause a rollback of the entire outer transaction.
也就是说, 最容易弄混淆的其实是 PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED, 那么这两种方式又有何区别呢? 我简单的翻译一下 Juergen Hoeller 的话 :
PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 "内部" 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行.
另一方面, PROPAGATION_NESTED 开始一个 "嵌套的" 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交.
由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于, PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 潜套事务也会被 commit, 这个规则同样适用于 roll back.
那么外部事务如何利用嵌套事务的 savepoint 特性呢, 我们用代码来说话
Java代码
- ServiceA {
- /**
- * 事务属性配置为 PROPAGATION_REQUIRED
- */
- void methodA() {
- ServiceB.methodB();
- }
- }
- ServiceB {
- /**
- * 事务属性配置为 PROPAGATION_REQUIRES_NEW
- */
- void methodB() {
- }
- }
ServiceA {
/**
* 事务属性配置为 PROPAGATION_REQUIRED
*/
void methodA() {
ServiceB.methodB();
}
}
ServiceB {
/**
* 事务属性配置为 PROPAGATION_REQUIRES_NEW
*/
void methodB() {
}
}
这种情况下, 因为 ServiceB#methodB 的事务属性为 PROPAGATION_REQUIRES_NEW, 所以两者不会发生任何关系, ServiceA#methodA 和 ServiceB#methodB 不会因为对方的执行情况而影响事务的结果, 因为它们根本就是两个事务, 在 ServiceB#methodB 执行时 ServiceA#methodA 的事务已经挂起了 (关于事务挂起的内容已经超出了本文的讨论范围, 有时间我会再写一些挂起的文章) .
那么 PROPAGATION_NESTED 又是怎么回事呢? 继续看代码
Java代码
- ServiceA {
- /**
- * 事务属性配置为 PROPAGATION_REQUIRED
- */
- void methodA() {
- ServiceB.methodB();
- }
- }
- ServiceB {
- /**
- * 事务属性配置为 PROPAGATION_NESTED
- */
- void methodB() {
- }
- }
ServiceA {
/**
* 事务属性配置为 PROPAGATION_REQUIRED
*/
void methodA() {
ServiceB.methodB();
}
}
ServiceB {
/**
* 事务属性配置为 PROPAGATION_NESTED
*/
void methodB() {
}
}
现在的情况就变得比较复杂了, ServiceB#methodB 的事务属性被配置为ROPAGATION_NESTED, 此时两者之间又将如何协作呢? 从 Juergen Hoeller 的原话中我们可以找到答案, ServiceB#methodB 如果 rollback, 那么内部事务(即 ServiceB#methodB) 将回滚到它执行前的 SavePoint(注意, 这是本文中第一次提到它, 潜套事务中最核心的概念), 而外部事务(即 ServiceA#methodA) 可以有以下两种处理方式:
1. 改写 ServiceA 如下
Java代码
- ServiceA {
- /**
- * 事务属性配置为 PROPAGATION_REQUIRED
- */
- void methodA() {
- try {
- ServiceB.methodB();
- } catch (SomeException) {
- // 执行其他业务, 如 ServiceC.methodC();
- }
- }
- }
ServiceA {
/**
* 事务属性配置为 PROPAGATION_REQUIRED
*/
void methodA() {
try {
ServiceB.methodB();
} catch (SomeException) {
// 执行其他业务, 如 ServiceC.methodC();
}
}
}
这种方式也是潜套事务最有价值的地方, 它起到了分支执行的效果, 如果 ServiceB.methodB 失败, 那么执行 ServiceC.methodC(), 而 ServiceB.methodB 已经回滚到它执行之前的 SavePoint, 所以不会产生脏数据(相当于此方法从未执行过), 这种特性可以用在某些特殊的业务中, 而 PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 都没有办法做到这一点. (题外话 : 看到这种代码, 似乎似曾相识, 想起了 prototype.js 中的 Try 函数 )
2. 代码不做任何修改, 那么如果内部事务(即 ServiceB#methodB) rollback, 那么首先 ServiceB.methodB 回滚到它执行之前的 SavePoint(在任何情况下都会如此), 外部事务(即 ServiceA#methodA) 将根据具体的配置决定自己是 commit 还是 rollback (+MyCheckedException).
上面大致讲述了潜套事务的使用场景, 下面我们来看如何在 spring 中使用PROPAGATION_NESTED, 首先来看 AbstractPlatformTransactionManager
Java代码
- /**
- * Create a TransactionStatus for an existing transaction.
- */
- private TransactionStatus handleExistingTransaction(
- TransactionDefinition definition, Object transaction, boolean debugEnabled)
- throws TransactionException {
- ... 省略
- if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
- if (!isNestedTransactionAllowed()) {
- throw new NestedTransactionNotSupportedException(
- "Transaction manager does not allow nested transactions by default - " +
- "specify 'nestedTransactionAllowed' property with value 'true'");
- }
- if (debugEnabled) {
- logger.debug("Creating nested transaction with name [" + definition.getName() + "]");
- }
- if (useSavepointForNestedTransaction()) {
- // Create savepoint within existing Spring-managed transaction,
- // through the SavepointManager API implemented by TransactionStatus.
- // Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization.
- DefaultTransactionStatus status =
- newTransactionStatus(definition, transaction, false, false, debugEnabled, null);
- status.createAndHoldSavepoint();
- return status;
- }
- else {
- // Nested transaction through nested begin and commit/rollback calls.
- // Usually only for JTA: Spring synchronization might get activated here
- // in case of a pre-existing JTA transaction.
- doBegin(transaction, definition);
- boolean newSynchronization = (this.transactionSynchronization != SYNCHRONIZATION_NEVER);
- return newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, null);
- }
- }
- }
/**
* Create a TransactionStatus for an existing transaction.
*/
private TransactionStatus handleExistingTransaction(
TransactionDefinition definition, Object transaction, boolean debugEnabled)
throws TransactionException {
... 省略
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
if (!isNestedTransactionAllowed()) {
throw new NestedTransactionNotSupportedException(
"Transaction manager does not allow nested transactions by default - " +
"specify 'nestedTransactionAllowed' property with value 'true'");
}
if (debugEnabled) {
logger.debug("Creating nested transaction with name [" + definition.getName() + "]");
}
if (useSavepointForNestedTransaction()) {
// Create savepoint within existing Spring-managed transaction,
// through the SavepointManager API implemented by TransactionStatus.
// Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization.
DefaultTransactionStatus status =
newTransactionStatus(definition, transaction, false, false, debugEnabled, null);
status.createAndHoldSavepoint();
return status;
}
else {
// Nested transaction through nested begin and commit/rollback calls.
// Usually only for JTA: Spring synchronization might get activated here
// in case of a pre-existing JTA transaction.
doBegin(transaction, definition);
boolean newSynchronization = (this.transactionSynchronization != SYNCHRONIZATION_NEVER);
return newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, null);
}
}
}
一目了然
1. 我们要设置 transactionManager 的 nestedTransactionAllowed 属性为 true, 注意, 此属性默认为 false!!!
再看 AbstractTransactionStatus#createAndHoldSavepoint() 方法
Java代码
- /**
- * Create a savepoint and hold it for the transaction.
- * @throws org.springframework.transaction.NestedTransactionNotSupportedException
- * if the underlying transaction does not support savepoints
- */
- public void createAndHoldSavepoint() throws TransactionException {
- setSavepoint(getSavepointManager().createSavepoint());
- }
/**
* Create a savepoint and hold it for the transaction.
* @throws org.springframework.transaction.NestedTransactionNotSupportedException
* if the underlying transaction does not support savepoints
*/
public void createAndHoldSavepoint() throws TransactionException {
setSavepoint(getSavepointManager().createSavepoint());
}
可以看到 Savepoint 是 SavepointManager.createSavepoint 实现的, 再看 SavepointManager 的层次结构, 发现其 Template 实现是 JdbcTransactionObjectSupport, 常用的 DatasourceTransactionManager, HibernateTransactionManager 中的 TransactonObject 都是它的子类 :
JdbcTransactionObjectSupport 告诉我们必须要满足两个条件才能 createSavepoint :
2. java.sql.Savepoint 必须存在, 即 jdk 版本要 1.4+
3. Connection.getMetaData().supportsSavepoints() 必须为 true, 即 jdbc drive 必须支持 JDBC 3.0
确保以上条件都满足后, 你就可以尝试使用 PROPAGATION_NESTED 了.
Subscribe to:
Posts (Atom)