Spring 通知类型按切面功能调用的不同时刻,可以分为提供了 5 种 Advice 类型
1、前置通知 Before advice:在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)
2、后置通知 After returning advice:在某连接点正常完成后执行的通知
3、异常通知 After throwing advice:在方法抛出异常退出时执行的通知
4、最终通知 After (finally) advice:当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)
5、环绕通知 Around Advice:包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知
可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行
通知类型的选择
1、环绕通知是最常用的通知类型
2、推荐使用尽可能简单的通知类型来实现需要的功能
3、如果你只是需要一个方法的返回值来更新缓存,最好使用后置通知而不是环绕通知
4、用最合适的通知类型可以使得编程模型变得简单,并且能够避免很多潜在的错误
依赖:spring-aop 和 spring-aspects
应用开发 1:在访问 UserServImpl 具体实现类中方法时需要记录日志【应该使用日志记录器,例如 log4j,这里简化为控制台输出】
public class LogAdvice{
public void before(){
System.out.println("before...");
}
}
配置这个通知类对象,同时引入 aop 名空间用于配置拦截规则
<bean id = "LodAdvice" class = "com.ma.aop.LogAdvice"/>
<aop:config>
<aop:aspect ref="logAdvice">
<aop:pointcut id = "bbc" expression = "execution(* com.ma.biz.*.*(..))"/>
<aop:before method = "before" pointcut-ref="bbc"/>
aop:aspect>
aop:config>
可以使用连接点对象 JoinPoint 获取当前拦截的连接点相关信息,但是除非是原地修改否则修改传入数据无效;不能决定程序是否继续执行,除非人为抛出异常阻止继续执行
public class LogAdvice(){
public void before(JoinPoint joinPoint){
System.out.println("before...");
Object target = joinPoint.getTarget();
System.out.println("调用的目标对象为"+target);
Signature signature = joinPoint.getSignature();
System.out.println("方法签名为:"+ signature.getName());
Object[] args = joinPoint.gteArgs();
System.out.println("调用的方法参数为:" + args);
}
}
应用 2:修改业务方法的返回值,将口令修改为等长星号
AfterReturning 可以原地修改返回值,但是如果不是原地修改,则修改无效
public class PasswordAspect{
public void changePassword(List<User> userList){
for(User tmp:userList){
tmp.setPassword(changePwd(tmp.getPassword()));
}
}
private String changePwd(String Pwd){
StringBuilder sb = new StringBuilder();
for(int i = 0;i < pwd.length();i++)
sb.append("*");
return sb.toString();
}
}
applicationContext.xml 配置
<bean id = "PwdAspect" class = "com.ma.aop.PasswordAspect"/>
<aop:config>
<aop:aspect ref="PwdAspect">
<aop:pointcut id = "bbc" expression = "execution(java.util.List com.ma.biz.*.getAllUsers())"/>
<aop:before method = "changePassword" pointcut-ref="bbc" returning = "userList"/>
aop:aspect>
aop:config>
应用 3:无所不能的环绕通知
环绕通知可以在方法执行前执行,可以修改传入参数,可以决定是否执行目标程序,可以在方法执行后执行,可以修改返回值,可以捕获异常并消费掉,可以使用 try/finally 结构定义最终需要执行的程序
@Override
public String hello(String name){
System.out.println("业务方法参数:" + name);
String res = "Hello"+name+"!";
System.out.println("业务方法返回值:" + res);
return res;
}
定义对应的切面类
public class AroundAspect{
//如果需要获取请求相关信息,则需要在方法中添加一个参数ProceedingJoinPoint
public Object aaa(ProceedingJoinPoint pjp){
Object res = null;
try{
System.out.println("前置处理程序....");
Signature signature = pjp.getSignature();//方法签名
Object target = pjp.getTarget();//目标对象
Object[] args = pjp.getArgs();//获取调用方法的请求参数
if(args!=null && args.length>0){
args[0] = "修改传入参数";
}
//pjp.proceed() 用于不修改参数的继续向后执行,下一个是目标对象,还是下一个切面
res = pjp.proceed(args);//用于修改请求参数
System.out.println("返回后置处理程序...");
res = "修改返回值:"+res;
}catch(Throwable exception){
System.out.println("异常处理程序...");
}finally{
System.out.println("最终处理程序...");
}
return res;
}
}
对应配置
<bean id = "aroundAspect" class = "com.ma.aop.AroundAspect"/>
<aop:config>
<aop:aspect ref="aroundAspect">
<aop:pointcut id = "bbc" expression = "execution(java.lang.String *.he*.*(java.lang.String))"/>
<aop:before method = "aaa" pointcut-ref="bbc"/>
aop:aspect>
aop:config>
注意:事实上在环绕通知中甚至可以执行其它程序,而不执行真正调用的方法
斑鸠蛋模式:阳奉阴违
具体应用:
一般针对控制层建议使用 Filter 之类的 AOP 实现;针对业务层建议使用 Spring AOP,针对持久层的 Mybatis还是优先考虑 MyBatis 的拦截器 Interceptor
1、通过 Spring 的 API 实现 AOP。实现前置通知接口 MethodBeforeAdvice / 后置通知 AfterReturningAdvice / 环绕通知 MethodInterceptor / 最终通知 AfterAdvice / 异常通知 AfterThrowingAdvice 等特定的接口
<bean id="log" class="com.ma.Log"/>
<bean id="afterLog" class="com.ma.AfterLog">bean>
<aop:config>
切入点,需要告诉方法在什么去执行。expression="execution(* com.ma..(…))"其中第一个* 表示所有的返回值,然后就是包名。第二个表示所有的类对象。第三个表示类对象所有的方法。第四个*表示所有方法下面的带参数的方法或者是不带参数的方法
<aop:pointcut expression="execution(* com.ma.*.*(..))" id="pointcut"/>
<aop:advisor advice-ref="log" pointcut-ref="pointcut"/>
aop:config>
Spring AOP 的工作原理【面试】
Spring 框架中的 AOP 拦截技术是 POJO 的方法层面的拦截【拦截的颗粒度较粗】。其底层实现原理是动态代理技术。对于面向接口的方法拦截,依赖于 jdk 的动态代理技术,即 java.lang.reflect.Proxy#newProxyInstance,
将对被代理的目标对象的调用,委托到代理对象,触发拦截通知;而当被拦截的方法, 不是在接口中定义时,使用的是 cglib,对字节码进行动态增强,生成被代理类的子对象,以实现代理spring 实现 aop,动态代理技术的两种实现是 jdk 动态代理、cglib 代理,根据被通知的方法是否为接口方法,来选择使用哪种代理生成策略
1、jdk 动态代理,原理是实现接口的实例,拦截定义于接口中的目标方法,性能更优,是 spring 生成代理的优先选择
2、cglib 代理,原理是使用 cglib 库中的字节码动态生成技术,生成被代理类的子类实例,可以拦截代理类中的任一 public 方法的调用,无论目标方法是否定义于接口中,更通用,但性能相对 jdk 代理差一些;
2、自定义类来实现 AOP,不实现 spring 的自带的通知
<bean id="userService" class="com.ma.UserServiceImpl"/>
<bean id="log" class="com.yan.Log"/>切面也要配置成 bean
<aop:config>
<aop:aspect ref="log">
<aop:pointcut expression="execution(* com.ma.*.*(..))切入点表达式" id="pointcut"/>
<aop:before method="before 切面类中的方法名" pointcut-ref="pointcut"/>
<aop:after method="after 切面类中的方法名" pointcut-ref="pointcut"/>
aop:aspect>
aop:config>
3、通过注解实现 AOP
首先切面类需要定义为受管 bean,也就是 xml 配置或者使用@Component 注解+自动扫描
@Aspect
public class Log {
@Before("execution(* com.ma.*.*(..))") //前置处理程序
public void before(JoinPoint jp){
System.out.println("方法执行前"); }
@After("execution(* com.ma.*.*(..))") //最终处理程序
public void after(){
System.out.println("方法执行后"); }
@Around("execution(* com.ma.*.*(..))") //环绕处理程序
public Object around(ProceedingJoinPoint jp) throws Throwable{
System.out.println("环绕前");
System.out.println("方法"+jp.getSignature());
Object result=jp.proceed();
System.out.println("环绕后");
return result; }
}
对应配置
<bean id="log" class="com.ma.Log"/>
<aop:aspectj-autoproxy/> 打开自动代理
5 种常见注解
1、@Before 前置通知, 在方法执行之前执行
2、@After 最终通知, 在方法执行之后执行
3、@AfterRunning 后置通知, 在方法返回结果之后执行
4、@AfterThrowing 异常通知, 在方法抛出异常之后
5、@Around 环绕通知, 围绕着方法执行
1、添加依赖 mybatis、mybatis-spring,另外不使用 mybatis 提供的连接池,而是使用产品级连接池 druid,所以添加依赖 druid 以及数据库驱动
2、定义表结构,从输入页面上进行总结
create table if not exist tbl_users(
id bigint primary key auto_increment,
username varchar(32) not null unique,
password varchar(32) not null,
hiredate timestamp default current_timestamp comment '入职时间',
sex boolean default 1
)engine=innodb default charset utf8;
3、使用反向映射插件 mybatis-generator-maven-plugin 进行反向映射,生成实体类、mapper 接口和 mapper.xml映射元文件
添加反向映射的配置文件,可以从网络中获取,也可以从旧有项目中进行拷贝修改
生成的映射元文件 UserMapper.xml、实体类 User 和映射接口 UserMapper
4、修改映射生成的类、接口和 xml 文件
实体类
@Data
public class User implements Serializable{
private Long id;
private String username;
private String password;
private Data hiredata;
private Boolean sex;
}
利用泛型定义通用接口
public interface BaseMapper<T extends Serializable,ID extends Serializable>{
int deleteByPrimaryKey(ID id);
int insertSelective(T row);
T selectByPrimaryKey(ID id);
int updateByPrimaryKeySelective(T row);
List<T> selectByExample(T row);
int countByExample(T row);
}
定义 UserMapper 继承于 BaseMapper,一般 BaseMapper 中声明通用常见的方法,UserMapper 中定义特殊的方法
public interface UserMapper extends BaseMapper<User ,Long>{
}
修 改 映 射 元 文 件 UserMapper.xml , 删 除 2 个 方 法 insert 和 updateByPrimaryKey 并 新 增 两 个 方 法selectByExample 和 countByExample
<mapper namespace="com.ma.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.ma.entity.User">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="password" jdbcType="VARCHAR" property="password" />
<result column="hiredate" jdbcType="DATE" property="hiredate" />
<result column="sex" jdbcType="BOOLEAN" property="sex" />
resultMap>
<sql id="Base_Column_List">
定义 sql 代码块,因为这里的查询条件生成在 selectByExample 和 countByExample 方法中都需要使用
<sql id="condition">
<if test="username != null">
and username = #{username,jdbcType=VARCHAR}
if>
<if test="password != null">
and password = #{password,jdbcType=VARCHAR}
if>
<if test="hiredate != null">
and hiredate = #{hiredate,jdbcType=DATE}
if>
<if test="sex != null">
and sex = #{sex,jdbcType=BOOLEAN}
if>
<if test="id!=null">
and id = #{id,jdbcType=BIGINT}
if>
sql>
新增 selectByExample 和 countByExample
<select id="selectByExample" parameterType="com.ma.entity.User" resultMap="BaseResultMap">
select <include refid="Base_Column_List"/> from tbl_users where 1=1
<include refid="condition"/>
select>
<select id="countByExample" parameterType="com.ma.entity.User" resultType="int">
select count(1) from tbl_users where 1=1
<include refid="condition"/>
select>
5、在 IoC 容器中添加配置
datasource–sqlSessionFactory
将 MyBatis 与 Spring 进行整合,主要解决的问题就是将 SqlSessionFactory 对象交由 Spring 容器来管理,所以,该整合,只需要将 SqlSessionFactory 的对象生成器 SqlSessionFactoryBean 注册在 Spring 容器中,再将其注入给 Dao 的实现类即可完成整合。
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
p:driverClassName="com.mysql.cj.jdbc.Driver"
p:url="jdbc:mysql:///test?serverTimezone=UTC" p:username="root"
p:password="10086" destroy-method="close"/>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"
p:dataSource-ref="dataSource"
p:mapperLocations="classpath:mapper/*.xml"/>
mybatis-spring 提供了@MapperScan 注解,其可以取代 MapperScannerConfigurer
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<property name="basePackage" value="com.yan.dao"/>
bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"
p:sqlSessionFactoryBeanName="sqlSessionFactory" p:basePackage="com.yan.mapper"/>
SqlSessionTemplate 是 mybatis-spring 的核心,其实现 SqlSession 接口,且线程安全。使用 SqlSessionTemplate后,不再需要通过 SqlSessionFactory.openSession()方法来创建 SqlSession 实例;使用完成之后,也不要调用SqlSession.close()方法进行关闭。另外对于事务,SqlSessionTemplate 将会保证使用的 SqlSession 是和当前Spring 的事务相关的。
6、使用单元测试验证代码的正确
依赖 spring-test、junit
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<version>${spring.version}version>
<scope>testscope>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.13.2version>
<scope>testscope>
dependency>
使用 main 方法执行测试,不能保留测试痕迹,所以引入 junit 进行回归测试
ApplicationContext ac=new ClassPathXmlApplicationContext
("classpath:applicationContext.xml");
UserMapper userMapper=ac.getBean(UserMapper.class);
User tmp=new User();
tmp.setUsername("zhangsanfeng");
tmp.setPassword("666666");
int selective = userMapper.insertSelective(tmp);
定义测试类时,一般测试谁和谁同包
@RunWith(SpringJUnit4ClassRunner.class)//使用 Spring-test 则必须设置运行器,否则默认 Junit4 运行器
@ContextConfiguration(locations = "classpath:applicationContext.xml") //设置核心配置文件的位置
public class UserMapperTest {
@Autowired //按照类型自动装配
private UserMapper userMapper;
@Test //单元测试方法
public void testInsert(){
User user=new User();
user.setUsername("李四光");
user.setPassword("444444");
int len = userMapper.insertSelective(user);
Assert.assertEquals(1,len);
}
}
7、添加业务类和业务接口
public interface IUserServ {
public List<User> getAllUsers();
}
public class UserServImpl implements IUserServ {
private UserMapper userMapper;
@Override
public List<User> getAllUsers() {
return userMapper.selectByExample(null);
}
public UserMapper getUserMapper() {
return userMapper;
}
public void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}
}
将业务类对象交由 Spring IoC 容器负责管理,所以添加对应的配置
<bean id="userService" class="com.ma.biz.UserServImpl" p:userMapper-ref="userMapper"/>
8、在 Servlet 中获取 IoC 容器的引用,再通过受管 bean 的名称获取对应的业务实现类对象
注意 spring 和 Servlet 整合:1、添加依赖 spring-web 以及 Servlet-api 依赖 javax.servlet-api。2、添加 web.xml配置监听器,在应用启动时会自动初始化 springIoC 容器,并存储在 application 对象中
public class UserServlet extends HttpServlet {
private IUserServ userService;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws
ServletException, IOException {
if(userService==null){
WebApplicationContext wac=
WebApplicationContextUtils.getRequiredWebApplicationContext(this.getServletContext());
userService=wac.getBean("userService",IUserServ.class);
}
List<User> userList=userService.getAllUsers();
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.println("");
out.println("<tr><th>用户编号</th><th>用户名称</th><th>用户口令</th><th>入职时间
</th><th>性别</th><th>操作</th></tr>");
for(User tmp:userList){
out.println(""+tmp.getId()+" "+tmp.getUsername()+" ");
out.println(tmp.getPassword()+" "+tmp.getHiredate()+" "
+tmp.getSex()+" 删除 修改 ");
}
out.println("
");
}
}
web.xml 中针对 Servlet 进行地址映射
<servlet>
<servlet-name>userServletservlet-name>
<servlet-class>com.yan.action.UserServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>userServletservlet-name>
<url-pattern>/users.dourl-pattern>
servlet-mapping>
启动 jetty 后打开浏览器进行访问
事务管理是企业级应用程序开发中必不可少的技术, 用来确保数据的完整性和一致性。事务是由多步数据库操作序列组成的逻辑执行单元,这系列操作要么全部执行,要么全部放弃执行。
事务的 ACID 特性:原子性、一致性、持久性和隔离性
数据库的并发问题:更新丢失、脏读、不可重复读和幻读
为了避免出现并发访问问题,在标准 SQL 规范中定义了 4 个事务隔离级别,不同的隔离级别对事务的处理不同。
1、读未提交 Read Uncommitted,只处理更新丢失。如果一个事务已经开始写数据,则不允许其他事务同时进行写操作,但允许其他事务读此行数据。可通过排他写锁实现。加写锁直到事务结束后才释放。
2、读提交 Read Committed。处理更新丢失、脏读。读取数据的事务允许其他事务继续访问改行数据,但是未提交的写事务将会禁止其他事务访问改行。可通过瞬间共享读锁和排他写锁实现。一般情况下,使用此级别即可。加写锁,直到事务结束后才释放;加读锁,读完之后立刻释放。
3、可重复读取 Repeatable Read。处理更新丢失、脏读和不可重复读取。读取数据的事务将会禁止写事务,但允许读事务,写事务则禁止任何其他事务。可通过共享读锁和排他写锁实现。加写锁,直到事务结束后才释放;加读锁,直到事务结束后才释放。
4、序列化 Serializable。提供严格的事务隔离。要求失去序列化执行,事务只能一个接一个地执行,不能并发执行。仅仅通过行级锁是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。