一站式应用开发框架-Spring使用方法总结

Spring 4.x

文章目录

  • Spring 4.x
    • 1. Spring Boot
    • 2. IoC容器
      • 2.1 ApplicationContext
        • 2.1.1 AnnotationConfigApplicationContext
        • 2.1.2 ClassPathXmlApplicationContext
      • 2.2 生命周期
    • 3. 在IoC容器中装配Bean
      • 3.1 Setter注入
      • 3.2 Construct注入
      • 3.3 Factory注入
      • 3.4 内部Bean
      • 3.5 注入null
      • 3.6 注入集合
      • 3.7 级联
      • 3.8 继承
      • 3.9 依赖
      • 3.10 注解
    • 4. Spring AOP
      • 4.1 JDK动态代理
      • 4.2 CGLib动态代理
      • 4.3 增强
        • 4.3.1 前置增强
        • 4.3.2 后置增强
        • 4.3.3 环绕增强
        • 4.3.4 异常抛出增强
        • 4.3.5 引介增强
      • 4.4 基于@AspectJ的AOP
        • 4.4.1 @After和@Before等
        • 4.4.2 @DeclareParent
        • 4.4.3 切点表达式函数
    • 5. Spring的事务管理
      • 5.1 三类数据读问题
        • 5.1.1 脏读(dirty read)
        • 5.1.2 不可重复读(unrepeatable read)
        • 5.1.3 幻象读(phantom read)
      • 5.2 两类数据更新问题
        • 5.2.1 第一类丢失更新
        • 5.2.2 第二类丢失更新
      • 5.3 事务隔离级别
        • 5.3.1 乐观锁和悲观锁
        • 5.3.2 对MySQL中四种事务隔离级别实现原理的猜想
      • 5.4 aop/tx方式进行配置
      • 5.4 注解@Transactional进行配置
    • 6. 整合SSH与SSM
      • 6.1 Hibernate5.x
      • 6.2 MyBatis
    • 7. Spring Cache
    • 8. Spring MVC
    • 9. Spring-Android联合项目开发
    • 10. Spring-Android-Web三端联合开发

1. Spring Boot

Spring Boot用于快速开启一个Spring项目:

  1. 通过Maven方式新建一个项目
  2. pom.xml中指定parent为spring-boot-starter-parent
<parent>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-parentartifactId>
    <version>2.1.3.RELEASEversion>
parent>
  1. 依赖starter,书本p51-p52有介绍不同的starter,此处为了支持Web应用和数据库开发导入了spring-boot-starter-webspring-boot-starter-jdbc(connector-j需要另外导入,右键Diagrams显示依赖,可以看到依赖了什么库):
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
    <version>${boot.version}version>
dependency>

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-jdbcartifactId>
    <version>2.1.3.RELEASEversion>
dependency>
  1. 定义Application,使用@SpringBootApplication代替@Configuration@ComponentScan@EnableAutoConfiguration三个注解,@EnableTransactionManagement表示支持事务,继承SpringBootServletInitializer重写confiura()方法以支持Spring MVC
@SpringBootApplication
@EnableTransactionManagement
public class Application extends SpringBootServletInitializer {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(Application.class);
    }
}
  1. 在resource下创建application.properties,配置数据库链接、tomcat、jsp:
spring.datasource.url=jdbc:mysql://localhost:3306/sampledb?serverTimezone=GMT
spring.datasource.username=zhangyijun
spring.datasource.password=mysql1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.datasource.tomcat.max-idle=10
spring.datasource.tomcat.min-idle=8
spring.datasource.tomcat.max-wait=1000
spring.datasource.tomcat.max-active=100
spring.datasource.tomcat.test-on-borrow=true
spring.datasource.tomcat.validation-query=select 1

spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
  1. 编写Controller,使用注解@RestController@RequestMapping
@RestController
public class LoginController {
    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping(value = {"/hello", "/index.html"})
    public ModelAndView loginPage() {
        return new ModelAndView("login");
    }

    @RequestMapping(value = "/loginCheck.html")
    public ModelAndView loginCheck(HttpServletRequest request, LoginCommand loginCommand) {
        boolean isValidUser = userService.hasMatchUser(loginCommand.getUserName(), loginCommand.getPassword());
        if (!isValidUser) {
            return new ModelAndView("login", "error", "用户名或密码错误");
        } else {
            User user = userService.findUserByUserName(loginCommand.getUserName());
            user.setLastIp(request.getLocalAddr());
            user.setLastVisit(new Date());
            userService.loginSuccess(user);
            request.getSession().setAttribute("user", user);
            return new ModelAndView("main");
        }
    }

}
  1. 编写剩余代码,JdbcTemplate注入到Dao中,Dao注入到Service中,Service注入到Controller中

2. IoC容器

2.1 ApplicationContext

2.1.1 AnnotationConfigApplicationContext

  • 拥有@Bean注解方法的类 User:
@Bean(name = "DefaultUser")
public User buildUser() {
    return User.builder()
            .userId(123)
            .userName("in annotation @Bean")
            .password("123")
            .credits(123)
            .lastIp("127.0.0.1")
            .lastVisit(new Date())
            .build();
}
  • 通过构造器传入上面的类
  • 使用 T getBean(String name, Class requiredType)方法获取bean:
ApplicationContext context = new AnnotationConfigApplicationContext(User.class);
User user = context.getBean("DefaultUser", User.class);

2.1.2 ClassPathXmlApplicationContext

public ClassPathXmlApplicationContext(String... configLocations) throws BeansException {

}
  • 通过构造器传入xml配置信息的路径:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="date" class="java.util.Date"/>

    <bean id="user" class="com.smart.domain.User"
          p:userId="1"
          p:userName="in xml file: beans.xml"
          p:password="asd"
          p:credits="123"
          p:lastIp="1.1.1.1"
          p:lastVisit-ref="date"/>

beans>
val context = new ClassPathXmlApplicationContext("classpath:beans.xml");
val user = context.getBean("user", User.class);
  • "classpath:beans.xml"此处可以简写为"beans.xml"
  • beans.xml放在了resources文件夹下

2.2 生命周期

BeanFactoryApplicationContext的生命周期类似,BeanFactory在调用getBean(beanName)的时候开始生命周期,ApplicationContext在容器开启的时候一次性初始化所有Bean(可以通过@Lazy注解实现懒加载):

  1. InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation()
  2. 实例化
  3. InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation()
  4. InstantiationAwareBeanPostProcessor#postProcessPropertyValues()
  5. 设置属性值
  6. BeanNameAware#setBeanName()
  7. BeanFactoryAware#setBeanFactory()
  8. BeanPostProcessor#postProcessBeforeInitialization()
  9. InitializingBean#afterPropertiesSet()
  10. init-method
  11. BeanPostProcessor#postProcessAfterInitialization()
  12. scope
    1. singleton Spring缓存池中准备就绪的Bean
    2. prototype 将准备就绪的Bean交给调用者
  13. 容器销毁,调用DisposableBean中的destroy()方法
  14. destory-method
  • 通过在标签中定义init-methoddestory-method,因为对象实例化和属性初始化的原因,这两个方法的生命周期都比较靠后
  • 通过注解@PreDestroy@PostConstrut修饰方法

3. 在IoC容器中装配Bean

3.1 Setter注入

  • 需要提供对应的setter方法,并且id或name前两个字母要么都是大写,要么都是小写
public class User{
    private String name;
    public void setName(String name){
        this.name = name;
    }
}
<bean id="user" class="com.smart.domain.User" p:name="1"/>

3.2 Construct注入

  • 最好用index+type指定入参
<bean id="user1" class="com.smart.domain.User">
    <constructor-arg index="0" type="int" value="1"/>
    <constructor-arg index="1" type="java.lang.String" value="1"/>
    <constructor-arg index="2" type="java.lang.String" value="1"/>
    <constructor-arg index="3" type="int" value="1"/>
    <constructor-arg index="4" type="java.lang.String" value="1123"/>
    <constructor-arg index="5" type="java.util.Date" ref="date"/>
bean>

3.3 Factory注入

  • 如果工厂提供静态方法,则把factory-bean换成class指定工厂类就行了
<bean id="userFactory" class="com.smart.factory.UserFactory"/>
<bean id="user" factory-bean="userFactory" factory-method="createUser"/>

<bean id="user" class="com.smart.factory.UserFactory" factory-method="createUser"/>

3.4 内部Bean

  • 作用域为prototype
<bean id="boss" class="com.smart.attr.Boss">
    <property name="car">
        <bean class="com.smart.attr.Car">
            <property name="maxSpeed" value="200"/>
            <property name="price" value="2000.00"/>
        bean>
    property>
bean>

3.5 注入null

<bean id="boss" class="com.smart.attr.Boss">
    <property name="car">
        <null/>
    property>
bean>

3.6 注入集合

  • 可以引入util命名空间来显式指定实现类
<bean id="boss" class="com.smart.attr.Boss">
    
    
    
    
    
    
    
    
    
    
    <util:list id="carList" list-class="java.util.LinkedList">
       <value>ferrarivalue>
       <value>maserativalue>
       <value>porschevalue>
    util:list>
    
    
    <property name="carSet">
        <set>
            <value>ferrarivalue>
            <value>maserativalue>
            <value>porschevalue>
        set>
    property>
    
    
    <property name="carMap">
        <map>
            <entry key="Ferrari" value-ref="ferrari"/>
        map>
    property>
    
bean>

3.7 级联

<bean id="boss" class="com.smart.attr.Car">
    <property name="car.price" value="2000.00"/>
bean>

此时的car必须有一个实例,所以需要为car属性声明一个初始化对象:

public class Boss{
    private Car car = new Car();
}

3.8 继承

  • abstract=true表示user是一个模板,并不会实例化
<bean id="user" class="com.smart.domain.User"
      p:userId="1"
      p:userName="in classpath beans.xml"
      p:password="asd"
      p:credits="123"
      p:lastIp="1.1.1.1"
      p:lastVisit-ref="date"
      abstract="true"/>

<bean id="childUser" parent="user" p:userId="2"/>

3.9 依赖

  • 使用depends-on指定两个bean之间的实例化顺序,假设UserUserConstanst中读取配置信息,而此时有一个类ConstantInit在初始化修改了UserConstanst的配置信息,如果想User实例化时得到UserConstanst中的最新配置信息,可以使用depends-onUser的初始化放到ConstantInit
<bean id="user" class="com.smart.domain.User"
      p:userId="1"
      p:userName="in classpath beans.xml"
      p:password="asd"
      p:credits="123"
      p:lastIp="1.1.1.1"
      p:lastVisit-ref="date"
      depends-on="ConstantInit"/>

3.10 注解

@Lazy
@Scope("prototype")
@Component("userDao")
public class UserDao{
    ...
}
  • @Component可以用@Repository替代标注Dao,用@Service替代标注Service,用@Controller替代标注Controller,因此此处等同于
@Lazy
@Scope("prototype")
@Repository
public class UserDao{
    ...
}
  • 等同于
<bean id="userDao" 
    class="com.smart.dao.UserDao" 
    lazy-init="true" 
    scope="prototype"/>
  • 等同于
@Lazy
@Scope("prototype")
@Bean("userDao")
public UserDao createUserDao(){
    return new UserDao();
}
  • 引用。有变量注入、构造器注入、setter注入,不推荐用变量注入,此处使用了setter注入
@Autowired
@Qualifier("userDao")
public void setUserDao(UserDao userDao) {
    this.userDao = userDao;
}

4. Spring AOP

4.1 JDK动态代理

  • jdk动态代理是Spring中ProxyFactory的内部实现技术之一
  • 首先要定义接口,返回的代理只能转型为某一接口(因此无法对一个类中所有的方法进行增强)
public interface Hello {
    void hello();
}
  • 然后让User类实现接口,里面增加了长达1秒的耗时操作
public class User{
    @Override
    public void hello() {
        try {
            Thread.sleep(1000);
            System.out.println(getClass().getName() + ":hello");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 性能监控类
public class MethodPerformance {
    private long begin;
    private String serviceMethod;

    private static final ThreadLocal<MethodPerformance> performanceRecord = new ThreadLocal<>();

    public static void begin(String method) {
        System.out.println("begin monitor");
        MethodPerformance methodPerformance = new MethodPerformance(method);
        performanceRecord.set(methodPerformance);
    }

    public static void end() {
        System.out.println("end monitor");
        MethodPerformance methodPerformance = performanceRecord.get();
        methodPerformance.printPerformance();
    }

    private MethodPerformance(String serviceMethod) {
        this.serviceMethod = serviceMethod;
        begin = System.currentTimeMillis();
    }

    private void printPerformance() {
        long end = System.currentTimeMillis();
        System.out.println(serviceMethod + " cost " + (end - begin) + " millis.");
    }

}
  • 为了加入对invoke()的监听,需要实现到InvocationHandler接口,在invoke()中,方法调用的前后加入了性能监控的代码。获得代理对象调用的是Proxy.newProxyInstance()方法,第一个入参为类加载器,第二个为类实现的接口,第三个参数传入InvocationHandler接口的实现。
public class JdkProxy implements InvocationHandler {

    private Object target;

    public JdkProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        MethodPerformance.begin(method.getClass().getName() + "." + method.getName());
        Object result = method.invoke(target, args);
        MethodPerformance.end();
        return result;
    }

    public static Object proxy(Object target) {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new JdkProxy(target));
    }

}
  • 调用代码,此处转型为Hello类型,否则抛出无法转型的异常。(这里的转型只能转为User实现的接口之一)
public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    ApplicationContext context = new AnnotationConfigApplicationContext(User.class);
    
    User target = context.getBean("DefaultUser", User.class);
    Hello proxy = (Hello) JdkProxy.proxy(target);
    proxy.hello();
}
  • log输出
begin monitor
com.smart.domain.User:hello
end monitor
java.lang.reflect.Method.hello cost 1000 millis.

4.2 CGLib动态代理

  • 实现接口MethodInterceptor,以设置监听器的方式将MethodInterceptor设置到Enhancer中,当enhaner.create()返回的代理调用方法的时候就会被拦截执行intercept()
public class CglibProxy implements MethodInterceptor {

    private Enhancer enhancer = new Enhancer();

    public static <T> T proxy(Class<T> clazz) {
        CglibProxy cglibProxy = new CglibProxy();
        Enhancer enhancer = cglibProxy.enhancer;
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(cglibProxy);
        return (T) enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        MethodPerformance.begin(method.getClass().getName() + "." + method.getName());
        Object result = methodProxy.invokeSuper(o, objects);
        MethodPerformance.end();
        return result;
    }
}
  • 调用代码
public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    val context = new AnnotationConfigApplicationContext(User.class);
    User target = context.getBean("DefaultUser",User.class);
    
    User proxy = CglibProxy.proxy(User.class);
    proxy.hello();
    proxy.setUserId(123);
    
    target.print();
}
  • log输出,可见使用CGLib进行代理会自动生成子类com.smart.domain.User$$EnhancerByCGLIB$$17f04a1c,这是因为CGLib使用了字节码技术。由于是以动态创建子类的方法生成代理对象,private和final的方法无法被增强
begin monitor
com.smart.domain.User$$EnhancerByCGLIB$$17f04a1c:hello
end monitor
java.lang.reflect.Method.hello cost 1003 millis.

begin monitor
end monitor
java.lang.reflect.Method.setUserId cost 0 millis.

User(userId=123, userName=in annotation @Bean, password=123, credits=123, lastIp=127.0.0.1, lastVisit=Tue Apr 02 08:58:41 GMT+08:00 2019)

4.3 增强

  • Spring使用Advice来增强方法。调用ProxyFactory.setInterferces()方法后默认以Jdk的动态代理技术来生成代理对象。(如果仍要使用CGLib,可以使用ProxyFactory.setOptimize(true)方法)
public class SpringProxy {

    public static Object proxy(Object target, boolean useJdkProxy, Advice... advice) {
        ProxyFactory factory = new ProxyFactory();
        if (useJdkProxy) {
            factory.setInterfaces(target.getClass().getInterfaces());
        }
        factory.setTarget(target);
        for (int i = 0; i < advice.length; ++i) {
            factory.addAdvice(i, advice[i]);
        }
        return factory.getProxy();
    }

}

4.3.1 前置增强

  • 实现MethodBeforeAdvice
public class LogBeforeAdvice implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println(target.getClass() + "在" + new Date() + "调用了" + method.getName());
    }
}
  • 调用
User target = context.getBean("DefaultUser", User.class);
User proxy = (User) SpringProxy.proxy(target, false, new LogBeforeAdvice());
proxy.hello();
  • 输出
class com.smart.domain.User 在 Tue Apr 02 09:27:51 GMT+08:00 2019 调用了 hello
com.smart.domain.User:hello
  • 以XML的方法获得代理对象,proxyTargetClass="false"表示对接口进行代理,也就是使用JDK动态代理技术
<bean id="user" class="com.smart.domain.User"
      p:userId="1"
      p:userName="in classpath beans.xml"
      p:password="asd"
      p:credits="123"
      p:lastIp="127.0.0.1"
      p:lastVisit-ref="date"/>

<bean id="logAdvice" class="com.smart.aop.LogBeforeAdvice"/>

<bean id="userProxy" class="org.springframework.aop.framework.ProxyFactoryBean"
      p:proxyInterfaces="com.smart.domain.Hello"
      p:interceptorNames="logAdvice"
      p:proxyTargetClass="false"
      p:target-ref="user"/>
public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    val context = new ClassPathXmlApplicationContext("beans.xml");
    Hello proxy = context.getBean("userProxy", Hello.class);
    proxy.hello();
}

4.3.2 后置增强

  • 方法调用完毕后进行增强
public class LogAfterAdvice implements AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println(target.getClass() + " 在 " + new Date() + " 调用了 " + method.getName() + (returnValue == null ? "" : " 返回值为" + returnValue));
    }
}
  • p:interceptorNames中用逗号隔开多个Advice
<bean id="logAdvice" class="com.smart.aop.advices.LogBeforeAdvice"/>
<bean id="logAdvice2" class="com.smart.aop.advices.LogAfterAdvice"/>
<bean id="userProxy" class="org.springframework.aop.framework.ProxyFactoryBean"
      p:proxyInterfaces="com.smart.domain.Hello"
      p:interceptorNames="logAdvice,logAdvice2"
      p:proxyTargetClass="false"
      p:target-ref="user"/>
  • 也可以不简写,分开写编译器会及时发现增强存不存在或者名字有没有写错
<property name="interceptorNames">
    <list>
        <idref bean="logAdvice"/>
        <idref bean="logAdvice2"/>
    list>
property>
  • 控制台输出
class com.smart.domain.User 在 Tue Apr 02 12:21:17 GMT+08:00 2019 调用了 hello
com.smart.domain.User:hello
class com.smart.domain.User 在 Tue Apr 02 12:21:18 GMT+08:00 2019 调用了 hello

4.3.3 环绕增强

public class LogInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        MethodPerformance.begin(invocation.getMethod().getName());
        Object result = invocation.proceed();
        MethodPerformance.end();
        return result;
    }
}
class com.smart.domain.User 在 Tue Apr 02 12:38:08 GMT+08:00 2019 调用了 hello 
begin monitor
com.smart.domain.User:hello
end monitor
hello cost 1001 millis.
class com.smart.domain.User 在 Tue Apr 02 12:38:09 GMT+08:00 2019 调用了 hello

4.3.4 异常抛出增强

  • 修改hello()代码,令其抛出一个运行时异常
@Override
public void hello() throws RuntimeException {
    throw new RuntimeException("hello runtime exception!");
}
  • 实现ThrowsAdvice,这是一个空接口,方法按照下面的例子来定义
public class AfterThrowsAdvice implements ThrowsAdvice {

    public void afterThrowing(Method method, Object[] args, Object target, Exception e) {
        System.out.println("1");
    }

    public void afterThrowing(Exception e) {
        System.out.println("2");
    }

    public void afterThrowing() {
        System.out.println("3");
    }

    public void afterThrowing(RuntimeException e) {
        System.out.println("4");
    }

}
  • 2和4中,优先选用入参的异常和抛出的异常最相似的,即打印4
  • 1和2中,优先选用入参列表最详细的,即打印1
  • 没有形参的不会被调用,即不可能打印3

4.3.5 引介增强

  • 定义一个新的接口PerformanceAccessible,其中有方法setAccessible()用于实现对性能监控的开关
public interface PerformanceAccessible {
    void setAccessible(boolean accessible);
}
  • 继承DelegatingIntroductionInterceptor并实现上面的PerformanceAccessible接口。这样不仅方法setAccessible()被嵌入到代理类中,而且invoke()方法中还通过变量accessibleStatus.get()来判断是否开启性能监控。
public class LogIntroductionAdvice extends DelegatingIntroductionInterceptor implements PerformanceAccessible {

    private ThreadLocal<Boolean> accessibleStatus = new ThreadLocal<>();

    @Override
    public void setAccessible(boolean accessible) {
        accessibleStatus.set(accessible);
    }

    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        Object o;
        if (accessibleStatus.get() != null && accessibleStatus.get()) {
            MethodPerformance.begin(mi.getMethod().getName());
            o = super.invoke(mi);
            MethodPerformance.end();
        } else {
            o = super.invoke(mi);
        }
        return o;
    }
}
  • 由于这里用到了动态创建子类的技术,方法的执行应该是o = super.invoke(mi);而不是o = mi.proceed();
  • 修改xml中的代码,特别注意p:proxyTargetClass="true"
    p:proxyInterfaces="com.smart.domain.PerformanceAccessible",因为引介增强只能通过创建子类的方法,于是要切换动态代理技术为CGLib
<bean id="logAdvice" class="com.smart.aop.advices.LogIntroductionAdvice"/>
<bean id="userProxy" class="org.springframework.aop.framework.ProxyFactoryBean"
      p:proxyInterfaces="com.smart.domain.PerformanceAccessible"
      p:proxyTargetClass="true"
      p:interceptorNames="logAdvice"
      p:target-ref="user">
bean>
  • 如果发生了强转错误,可能是p:interfaces中指定了引介增强的接口,而p:proxyInterfaces中又设置了其他接口
  • 调用代码
public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    val context = new ClassPathXmlApplicationContext("beans.xml");
    
    Hello proxy = context.getBean("userProxy", User.class);
    proxy.hello();
    
    ((PerformanceAccessible) proxy).setAccessible(true);
    System.out.println("");
    proxy.hello();
}
  • 此处强转成功说明动态代理产生的子类确实实现了PerformanceAccessible接口。log:
hello

begin monitor
hello
end monitor
com.smart.domain.User.hello cost 1000 millis.

4.4 基于@AspectJ的AOP

4.4.1 @After和@Before等

  • 导库

<dependency>
    <groupId>org.aspectjgroupId>
    <artifactId>aspectjweaverartifactId>
    <version>1.9.2version>
dependency>
  • 编写切面,并使用@Aspect注解。使用@Before@After注解来增强hello()方法(用JoinPoint来获取连接点信息),这里开启了一个性能监控
@Aspect
public class PerformanceAspect {

    @Before("execution(* hello(..))")
    public void beforeHello(JoinPoint point) {
        System.out.println(Arrays.toString(point.getArgs()));  //[]
        System.out.println(point.getSignature()); //void com.smart.domain.User.hello()
        System.out.println(point.getTarget().getClass().getName()); //com.smart.domain.User
        System.out.println(point.getThis().getClass().getName()); //com.smart.domain.User$$EnhancerBySpringCGLIB$$8956fe50
        
        MethodPerformance.begin("com.smart.domain.User.hello()");
    }

    @After("execution(* hello(..))")
    public void afterHello() {
        MethodPerformance.end();
    }
}
  • @Berfore对应MethodBeforeAdvice,即前置增强
  • @AfterReturning对应AfterReturningAdvice,即后置增强,通过returning属性绑定返回值
  • @Around对应MethodInterceptor,即环绕增强,用ProceedingJoinPoint代替JoinPoint访问连接点信息
  • @AfterThrowing对应ThrowAdvice,即异常抛出增强,通过throwing属性绑定异常对象
  • @After@AfterReturning@AfterThrowing的结合
  • 调用代码
public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    ApplicationContext context = new AnnotationConfigApplicationContext(User.class);
    User user = context.getBean("DefaultUser", User.class);

    AspectJProxyFactory factory = new AspectJProxyFactory();
    factory.setTarget(user);
    factory.addAspect(PerformanceAspect.class);
    Hello proxy = factory.getProxy();

    proxy.hello();
}
  • 使用配置来替代,加入下面两句后,自动为匹配@Aspect切面的Bean创建代理并增强hello()方法
<aop:aspectj-autoproxy proxy-target-class="true"/>
<bean class="com.smart.aop.aspect.PerformanceAspect"/>
  • 调用代码,打印结果为class com.smart.domain.User$$EnhancerBySpringCGLIB$$8738de70,获取的对象直接变成了代理对象
public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    User user = context.getBean("user", User.class);
    System.out.println(user.getClass());
}

4.4.2 @DeclareParent

  • @DeclareParent对应IntroductionInterceptor,即引介增强,在@Aspect注解的类中加入用@DeclaerParent注解的接口,defaultImpl传入该接口的默认实现类。(这种方法实现的引介增强还不知道如何做到Advice那样控制的效果,好像没有对方法的监听)
@DeclareParents(value = "com.smart.domain.User", defaultImpl = ByeImpl.class)
public Bye bye;
  • 接口默认实现类
public class ByeImpl implements Bye {
    @Override
    public void bye() {
        System.out.println("bye");
    }
}
  • 调用代码
User user = context.getBean("user", User.class);
((Bye) user).bye();
  • 打印结果
bye

4.4.3 切点表达式函数

切点可以通过&&!||运算符来进行复合运算

  • execution(* hello(…))匹配类中方法的连接点,支持所有通配符。
@Before("execution(* hello(..))") //匹配所有类中方法名为 hello的连接点,*号与方法名分开,表示任意返回值

@Before("execution(public * *(..))") //匹配所有public方法,第一个*指任意返回值,第二个指任意方法名

@Before("execution(* *To(..))") //匹配所有以To结尾的方法,第一个*指任意返回值,第二个指任意方法名前缀

@Before("execution(* com.smart.domain.User.*(..))") //匹配所有User的方法和User所有子类中继承自User的方法,第一个*指任意返回值,第二个指任意方法名

@Before("execution(* com.smart.domain.User+.*(..))") //匹配User和User所有子类中的所有方法

@Before("execution(* hello(String,*,Object+,..))") //方法入参指定第一个为String,第二个为任意一个类型,第三个为Object及其子类,第四个为任意长度任意类型
  • @annotation()匹配所有标注了某个注解的方法
@Before("@annotation(com.smart.domain.Test)")

  • args()匹配方法入参
@Before("args(com.smart.domain.User)")
//等同于:
@Before("execution(* *(com.smart.domain.User+)")

也可以用于获取连接点参数:

// User中添加新的方法
public boolean update(String name, int id) {
    this.userName = name;
    this.userId = id;
    return true;
}

此处args(name,id)根据下面的增强方法的参数自动换为args(String,int),然后匹配到update(String name,int id)方法

@AfterReturning(value = "target(com.smart.domain.User) && args(name,id)", returning = "result", argNames = "name,id,result")
public void afterUpdate(String name, int id, boolean result) {
    System.out.println("name=" + name);
    System.out.println("id=" + id);
    System.out.println("result=" + result);
}
  • @args()匹配标注了某个注解的入参,标注点需要低于入参类型点,例如下面有继承关系:
@Test
public class A{
    
}

public class B extends A{
    
}

需要增强的方法:

public void func(B b){
    
}

进行增强的代码:

@Before("@args(com.smart.domain.anno.Test)")
public void beforeFunc(){
    
}

此时@Test的标注点A高于func(B b)的入参B,此时切面不匹配任何类。


  • within()匹配目标类,不能匹配接口
@Before("within(com.smart.domain.User)") //匹配User

@Before("within(com.smart.domain.*)") //匹配domain包中的所有类

@Before("within(com.smart.domain..*)") //匹配domain包以及子孙包中的所有类
  • @within()匹配标注了某个注解的类,也能获取连接点的注解
@Before("@within(t)")
public void getAnnotation(Test t) {
    System.out.println(t.getClass().getName()); //com.sun.proxy.$Proxy48,注解也被代理了
}

  • target()匹配目标类
@Before("target(com.smart.domain.Hello)") //匹配实现了Hello接口的所有类的所有方法
  • @target()匹配标注了某个注解的类以及其子类

  • this()判断代理对象的类是否按类型匹配于指定类,这里匹配上之后会增强除了Bye接口的所有方法(不知道为什么,本来想用这种方法来获取引介增强连接点信息的,结果现在还是只能老实用IntroductionInterceptor
@Aspect
public class PerformanceAspect {

    @AfterReturning("this(com.smart.domain.Bye)")
    public void testThis() {
        System.out.println("im Bye!");
    }

    @DeclareParents(value = "com.smart.domain.User", defaultImpl = ByeImpl.class)
    public Bye bye;

}
public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    User user = context.getBean("user", User.class);
    user.hello();
    ((Bye) user).bye();
}
hello
im Bye! 

ByeBye
//此处没有输出im Bye!

此处相当于:(将代理对象绑定到参数bye上)

@AfterReturning(value = "this(bye)", argNames = "bye")
public void testThis(Bye bye) {
    System.out.println("im Bye!");
}

5. Spring的事务管理

5.1 三类数据读问题

5.1.1 脏读(dirty read)

脏读即一个事务读取另一个事务未提交的更改数据。

  • A事务实际没有更改余额,B实际存了100,最终的结果应该是1100,而这里B读了无效的余额500,导致结果为600:
gantt
dateFormat h

section 事务A
查询余额为1000: 1, 2h
修改余额为500: 3, 2h
撤销事务,修改余额回1000: 7, 3h
section 事务B
查询余额为500: 5, 2h
汇入100,修改余额为600: 9, 2h

5.1.2 不可重复读(unrepeatable read)

不可重复读即一个事务因为另外一个事务的提交而读到了两次不一样的数据。(行级锁)

  • B事务准备读取两次余额,第一次结束后A事务修改余额为500并提交,B在同一事务读到两次余额不一致:
gantt
dateFormat h

section 事务A
查询余额为1000: 1, 1h
修改余额为500: 3, 1h
section 事务B
查询余额为1000: 2, 1h
查询余额为500: 4, 1h

5.1.3 幻象读(phantom read)

幻象读即一个事务在统计、查询等时候将另一个事务提交的新数据也算了进来。(表级锁)

  • A事务准备统计两次表中所有账号并求和,第一次结束后B事务插入了新数据,A在同一事务中统计数据不一致:
gantt
dateFormat h

section 事务A
统计表中所有账户的总额为10000: 1, 2h
统计表中所有账户的总额为10100: 4, 2h
section 事务B
表中插入新数据,存款为100: 3, 1h

5.2 两类数据更新问题

5.2.1 第一类丢失更新

第一类数据更新即一个事务撤销时把另一个事务提交的有效数据覆盖了。

  • A事务实际没有更改余额,但不小心把B事务存的100吞掉了,其实B在查询余额时发生了脏读:
gantt
dateFormat h

section 事务A
查询余额为1000: 1, 2h
取出500: 3, 2h
撤销事务,余额恢复1000: 9, 3h
section 事务B
查询余额为500: 5, 2h
汇入100并提交: 7, 2h

5.2.2 第二类丢失更新

第二类数据更新即一个事务提交时把另一个事务提交的有效数据覆盖了。

  • A事务取出100,B事务汇入100,最终结果应该是余额不变,但这里B事务将A事务的结果覆盖了,余额变为了1100,其实B发生了不可重复读:
gantt
dateFormat h

section 事务A
查询余额为1000: 1, 2h
取出100并提交(修改为900): 5, 2h
section 事务B
查询余额为1000: 3, 2h
汇入100并提交(修改为1100): 7, 2h

5.3 事务隔离级别

Read uncommitted 读未提交
  • 读未提交,顾名思义,一个事务可以读到另外一个事务没有提交的数据
  • 允许三类读问题
  • 能避免第一类丢失更新,无法避免第二类
  • Oracle的read uncommitted由于使用多版本的原因,不允许脏读
Read committed 读提交
  • 读提交,只能读另外一个事务提交完的数据
  • 可以避免脏读,无法避免另外两种读问题
  • 能避免第一类丢失更新,无法避免第二类
  • Oracle的read committed由于使用多版本的原因,已经满足了Repeatable read的级别
Repeatable read 重复读
  • 重复读,其他事务结束前不进行数据的更改
  • 避免了脏读和不可重复读问题,允许幻象读
  • 避免了一类和二类两种数据丢失更新问题
  • MySQL的repeatable read已经解决了幻象读的问题
Serializable 序列化
  • 避免所有读问题和数据更新问题
  • 并发效率最低

5.3.1 乐观锁和悲观锁

  • 锁1:悲观锁-独占:(Oracle的lock table in row exclusive mode或者MySQL的select .. for update获得的行独占锁,update、delete、insert默认获得独占锁)不允许其他事务读和写。
  • 锁2:悲观锁-共享:(Oracle的select .. for updatelock table in row share mode或者MySQL的select .. lock in share mode获得的行共享锁)允许其他事务读取,不允许写。
  • 锁3:乐观锁,加入一个版本号或者时间戳,在事务提交的时候对比版本号或时间戳,检查有没有其他事务修改数据,避免了不可重复读的同时也解决了第二类问题。

5.3.2 对MySQL中四种事务隔离级别实现原理的猜想

Mysql数据库事务的隔离级别和锁的实现原理分析

数据库并发的五个问题以及四级封锁协议与事务隔离的四个级别

  • RU:A事务在修改数据时对其上X锁,直到事务COMMIT或ROLLBACK才释放,因此可以防止丢失更新。在读取数据时不加锁,所以不能避免三种读问题
  • RC:在RU的基础上,读取数据时加上S锁,读完即释放,因此在RU的基础上避免了脏读
  • RR:在RU的基础上,读取数据时加上S锁,事务结束才释放,因此在RU的基础上避免了不可重复读和脏读
  • Serializable:在RR的基础上加上表锁,防止幻读

5.4 aop/tx方式进行配置

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
          p:dataSource-ref="dataSource"/>

    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="*User*" rollback-for="Exception" isolation="SERIALIZABLE" propagation="REQUIRED"
                       read-only="false" no-rollback-for="" timeout="-1"/>
        tx:attributes>
    tx:advice>

    <aop:config>
        <aop:pointcut id=" helloPoint " expression="execution(* com.smart.service.UserService.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref=" helloPoint "/>
    aop:config>

就像使用增强一样使用事务管理。其中tx:method的属性:

  • name:方法名,可以使用通配符*
  • propagation:事务传播行为
事务传播行为类型 说明
PROPAGATION_REQUIRED 如果当前没有事务则新建一个,有则加入到这个事务中
PROPAGATION_SUPPORTS 支持当前事务,如果没有则以非事务方式执行
PROPAGATION_MANDATORY 使用当前事务,如果没有则抛出异常
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务则把当前事务挂起
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务则把当前事务挂起
PROPAGATION_NEVER 以非事务方式执行操作,如果当前存在事务则抛出异常
PROPAGATION_NESTED 若当前存在事务则在嵌套事务内执行,若没有则新建
  • isolation:四种隔离级别和DEFAULT默认级别
  • timeout:超时后回滚,单位为秒
  • read-only:设置只读后在某些仅读取数据的事务中将会提高性能
  • rollback-for:需要回滚的异常,多个异常用逗号隔开
  • no-rollback-for:不触发回滚的异常

5.4 注解@Transactional进行配置

配置文件中加入以下两句:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
  p:dataSource-ref="dataSource"/>

<tx:annotation-driven proxy-target-class="true" transaction-manager="transactionManager"/>
  • 其中事务管理器的id为"transactionManager"的话可以不用指定transaction-manager="transactionManager"
  • proxy-target-class指定代理方式是按类还是接口
  • order指定织入顺序

@Transactional的属性和使用xml配置相差无几:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}

@Transactional可以标注在类或方法上,此处方法标注的timeout = 5会覆盖类注解中的timeout = -1(默认值)

@Transactional
@Service
public class UserService {

    @Transactional(timeout = 5)
    public void loginSuccess(User user) {
        user.setCredits(5 + user.getCredits());
        userDao.updateLoginInfo(user);

        loginLogDao.insertLoginLog(LoginLog.builder()
                .userId(user.getUserId())
                .ip(user.getLastIp())
                .loginDate(user.getLastVisit())
                .build());

    }
}

6. 整合SSH与SSM

6.1 Hibernate5.x

  • Hibernate是一个优秀的ORM框架,它为开发人员减少了大部分的SQL语句,安卓的新生ORM框架Room的使用方法便是借鉴了Hibernate。而Spring为开发者提供了HibernateTemplate,实现了对Session进一步的封装。
  1. 导入Spring Boot Data JPA Starter

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-jpaartifactId>
    <version>2.1.3.RELEASEversion>
dependency>
  1. resource文件夹下创建hibernate.xml配置文件,p:mappingLocations为对象映射文件,路径在resource下要加classpath:、在java包里面写要加classpath*:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/tx
           http://www.springframework.org/schema/tx/spring-tx.xsd ">

    <bean id="sessionFactory"
          class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"
          p:mappingLocations="classpath:hbm/*.xml"
          p:dataSource-ref="dataSource"/>

    <bean id="hibernateTemplate"
          class="org.springframework.orm.hibernate5.HibernateTemplate"
          p:sessionFactory-ref="sessionFactory"/>

    <bean id="transactionManager"
          class="org.springframework.orm.hibernate5.HibernateTransactionManager"
          p:sessionFactory-ref="sessionFactory"/>

    <tx:annotation-driven proxy-target-class="true"/>

beans>
  1. 编写对象映射文件,指明类型name、表名table、主键id等等,type是映射类型


<hibernate-mapping>

    <class name="com.smart.domain.User" table="t_user">
        <meta attribute="user-description">
            这里是使用Hibernate时,User的映射文件
        meta>
        <id name="userId" type="int" column="user_id">
            <generator class="native"/>
        id>
        <property name="userName" type="string" column="user_name"/>
        <property name="credits" type="int" column="credits"/>
        <property name="password" type="string" column="password"/>
        <property name="lastVisit" type="date" column="last_visit"/>
        <property name="lastIp" type="string" column="last_ip"/>
    class>

hibernate-mapping>
  1. 编写Dao基类,使用@ImportResource("classpath:hibernate.xml")导入hibernate配置文件,使用泛型提供增删查改方法。整合SSH的时候可能出现update、save、delete等方法执行无效的问题,hibernate需要在事务环境下运行,所以此处标注上了@Transactional
@Transactional
@ImportResource("classpath:hibernate.xml")
public class BaseDao<T> implements BaseDaoOperation<T> {

    private HibernateTemplate hibernateTemplate;

    @Autowired
    public void setHibernateTemplate(HibernateTemplate hibernateTemplate) {
        this.hibernateTemplate = hibernateTemplate;
    }

    public HibernateTemplate getHibernateTemplate() {
        return hibernateTemplate;
    }

    @Override
    public Serializable save(T t) {
        return hibernateTemplate.save(t);
    }

    @Override
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void update(T t) {
        hibernateTemplate.update(t);
    }

    @Override
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void delete(T t) {
        hibernateTemplate.delete(t);
    }

    @Override
    @SuppressWarnings("unchecked")
    public T find(Serializable id) {
        Class<T> clazz = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        return hibernateTemplate.get(clazz, id);
    }

    @Override
    public List<T> find(T example) {
        return hibernateTemplate.findByExample(example);
    }

}
  • 执行update(t)的时候,hibernate只能将从数据库取出来对象进行update,也就是new出来的对象使用update是无效的
  • 正因为需要先从数据库中读取出实体类,然后再update,所以update的事务隔离级别需要达到REPEATABLE_READ这一点十分重要
  1. 编写实现类HbnUserDao,标注上@Repository
@Repository
public class UserDao extends BaseDao<User> {

}
  1. Controller或Service中注入UserDao,包括增、删、查、改的完整代码:
@RestController
public class Controller {

    private UserDao userDao;

    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    /**
     * GET --> 使用id来查找用户
     *
     * @param id 用户id
     * @return 用户数据
     */
    @RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
    @ResponseBody
    public ResultBean<User> find(@PathVariable("id") int id) {
        User user = userDao.find(id);
        return user != null ? ResultBean.ok(user) : ResultBean.no("用户不存在");
    }

    /**
     * POST JSON --> 保存用户
     *
     * @param entity 用户
     * @return 保存结果
     */
    @RequestMapping(value = "/user/save", method = RequestMethod.POST, produces = "application/json")
    @ResponseBody
    public ResultBean<String> save(HttpEntity<User> entity) {
        User user = entity.getBody();
        if (user != null) {
            int key = (int) userDao.save(user);
            return ResultBean.ok("主键为" + key);
        } else {
            return ResultBean.no("请检查输入");
        }
    }

    /**
     * GET --> 修改用户名
     *
     * @param name 新用户名
     * @param id   要修改的用户id
     * @return 新的用户数据
     */
    @RequestMapping(value = "/user/update/{id}", method = RequestMethod.GET)
    @ResponseBody
    public ResultBean<User> update(@RequestParam("name") String name, @PathVariable("id") int id) {
        User user = userDao.find(id);
        if (user != null && !name.equals("")) {
            user.setName(name);
            userDao.update(user);
            return ResultBean.ok(user);
        } else if (user == null) {
            return ResultBean.no("用户不存在");
        } else if (name.equals("")) {
            return ResultBean.no("名字不可为空");
        } else {
            return ResultBean.no("失败");
        }
    }

    /**
     * GET --> 用id删除用户
     *
     * @param id 用户id
     * @return 删除用户的数据
     */
    @RequestMapping(value = "user/delete/{id}", method = RequestMethod.GET)
    @ResponseBody
    public ResultBean<User> delete(@PathVariable("id") int id) {
        User user = userDao.find(id);
        if (user != null) {
            userDao.delete(user);
            return ResultBean.ok(user);
        } else {
            return ResultBean.no("用户不存在");
        }
    }

    /**
     * GET --> 用名字寻找用户
     *
     * @param key 名字
     * @return 符合的用户列表
     */
    @RequestMapping(value = "user/list", method = RequestMethod.GET)
    @ResponseBody
    public ResultBean<List<User>> list(@RequestParam(value = "key", required = false) String key) {
        if (key == null) {
            return ResultBean.ok(userDao.all());
        } else {
            List<User> users = userDao.find(User.builder().name(key).build());
            return users.size() == 0 ? ResultBean.no("用户列表为空") : ResultBean.ok(users);
        }
    }


}
  1. 使用注解配置,注解全都在javax包下,小心别导错包了。类上标注@Entity@Table("表名"),属性标注@Column("列名"),主键标注上@Id,自增的话还需要标注上@GeneratedValue(strategy = GenerationType.IDENTITY)
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "t_user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private int id;

    @Column(name = "user_name")
    private String name;

    @Column(name = "password")
    private String password;

}
  • 然后在hibernate中加入p:packagesToScan="com.smart.domain"指定要扫描注解的包,这时使用xml配置的User.hbm.xml和使用注解配置的User可以同时工作,冲突的时候会默认使用xml的配置

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/tx
           http://www.springframework.org/schema/tx/spring-tx.xsd ">

    <bean id="sessionFactory"
          class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"
          p:packagesToScan="com.smart.domain"
          p:mappingLocations="classpath:hbm/*.xml"
          p:dataSource-ref="dataSource"/>

    <bean id="hibernateTemplate"
          class="org.springframework.orm.hibernate5.HibernateTemplate"
          p:sessionFactory-ref="sessionFactory"/>

    <bean id="transactionManager"
          class="org.springframework.orm.hibernate5.HibernateTransactionManager"
          p:sessionFactory-ref="sessionFactory"/>

    <tx:annotation-driven proxy-target-class="true"/>

beans>
  1. 如果在使用的时候出现了强转错误java.lang.ClassCastException: org.springframework.orm.jpa.EntityManagerHolder cannot be cast to org.springframework.orm.hibernate5.SessionHolder,请在@SpringBootApplication()中加上exclude = HibernateJpaAutoConfiguration.class

6.2 MyBatis

  • Hibernate虽然替开发人员减少了90%的数据库操作代码,但是它的过程是全封闭的,当开发人员希望使用更高级的数据库操作、或者进行数据库优化的时候,需要学习HQL的使用,这无疑加重了开发者的负担。MyBatis的出现弥补了这一不足,它将SQL语句的编写交由开发者来完成,相比Hibernate更加灵活。

7. Spring Cache

8. Spring MVC

9. Spring-Android联合项目开发

10. Spring-Android-Web三端联合开发

你可能感兴趣的:(后台,框架,spring,web)