java常用注解大致是spring、mybatis、mybatisplus、jpa、springboot、springclould提供的一些注解。
一、日志类注解
@Slf4j 用于类上面,整个类里面就可以使用
log.info("错误信息,{}", 用户名称为空);
log.err("错误信息,{}", 用户名称为空);
此注解效果等同于 每个类里面写 private final Logger logger = LoggerFactory.getLogger(当前类名.class);
二、业务层类注解
@Service 用于类上面,将自动注册到Spring容器, 不需要再在applicationContext.xml文件定义bean
@Controller、@RestController 用于controller层,**controller上
@Service 用于service层,**seviceImpl上
@Repository 用于dao层,**daoImpl上
@Component 用于mapper层,**mapper上
@Mapper用于接口类上面,编译之后会生成相应的接口实现类,每个类都要加
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;
// @Controller // 或者用下面这个
@RestController // 推荐这个
public class UserController {
}
############################
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class UserService {
}
############################
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserDAO {
}
############################
import org.springframework.stereotype.Repository;
@Repository // 基于 @Component 实现
public interface UserRepository extends Repository<Users, Long>{
}
或者
@RepositoryDefinition(domainClass = User.class, idClass = Long.class)
public interface UserRepository {
}
三、请求方法注解
@RequestBody 用于方法上面,请求参数格式化,转入javabean(用于POST请求)
@RequestParam 用于方法上面,传入单个参数,可以是否必传,设置默认值(用于GET请求)
@PathVariable 用于方法上面,是spring3.0的一个新功能:接收请求路径中占位符的值(分解url作为参数传入action内,用于GET请求)
@ResponseBody 用于方法上面,返回参数格式化(json格式),若@RestController用于类上,则不用该类里面的每个方法上都写上@ResponseBody 注解了,有点类似批量之意
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
// @Controller // 或者用下面这个
@RestController // 推荐这个
public class UserController {
//@ResponseBody // 类上面的注解如果是@RestController 这个可省省略,因为@RestController已包含@ResponseBody的功效
@PostMapping("/add")
public void addUser(@RequestBody User user) {
}
/**
* value="name" 调用放传入的参数名称是 name
* defaultValue = “张三” 可省略。没传为空时,可以进行设置默认值为张三。
* required = true 可省略。是否必传,默认是。所以非必填时,需要用这个设为false
* @param name
* @return
*/
@GetMapping("/list")
public List<User> listUser(@RequestParam(value = "name", defaultValue = "", required = false) String name){
return new ArrayList<>();
}
@RequestMapping("showUser/{id}/{name}")
public List<User> showUser(@PathVariable("id") Long id , @PathVariable("name") String name){
return new ArrayList<>();
}
}
@Param 一般用于dao接口层。有多个参数或者需要起别名时使用
import feign.Param;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserDAO {
/**
* select * from t_user where user_id = #{userId} and user_name = #{userName}
* 只有一个参数时候,@Param注解可以不写, 有多个参数时如果没写则会报错
* 他另外一个作用就是起别名,如下,上一层传进来是user_id,传到下一层去是userId,所以sql里面是#{userId}而不是#{user_id}
* @param user_id
* @param user_name
* @return
*/
User queryUser(@Param("userId") int user_id, @Param("userName") String user_name);
/**
* select * from t_user where user_name = #{vo.userName}
* <if test="userIds != null and userIds.size!=0"> // userIds就是userIdList的别名
* and user_id in
* <foreach collection="userIds" close=")" open="(" separator="," item="id">
* #{id}
* </foreach>
* </if>
* 对象、list也可以用别名
* @param user
* @param userIdList
* @return
*/
List<User> queryUserList(@Param("vo") User user, @Param("userIds") List<Integer> userIdList);
}
四、注入类
@Autowired spring框架提供;先根据类型再根据名称查询;当接口有多个实现类时,搭配@Primary(优先使用哪一个) 或者 @Qualifier(指定使用哪一个)
@Resource java的jdk提供;先根据名称再根据类型查询;当接口有多个实现类时,通过name参数来指定使用哪一个
定义用户服务接口和两个实现类UserServiceImpl1和UserServiceImpl2
public interface UserService {
void getName();
}
#####################################
@Service
public class UserServiceImpl1 implements UserService{
@Override
public void getName() {
System.out.println("UserService1Impl");
}
}
@Service
public class UserServiceImpl2 implements UserService {
@Override
public void getName() {
System.out.println("userServiceImpl2");
}
}
注入测试结果
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class UserController {
/**
* 报错:当UserService有多个实现类UserServiceImpl1和UserServiceImpl2同时存在时
* 因为@Autowired先按照byType匹配,匹配到了“userServiceImpl1”和“userServiceImpl2”,结果不唯一。
* 接下来只能按照byName注入,此时的name为“userService”,结果无法唯一确定,只能抛出异常
*/
//@Autowired
//private UserService userService;
/**
* 成功:因为先按照byType匹配不唯一,在根据byName,成功匹配到唯一的实现者 userServiceImpl1
*/
//@Autowired
//private UserService userServiceImpl1;
/**
* 成功:因为先按照byType匹配不唯一,在根据byName,而@Qualifier注解指定使用名称为 userServiceImpl1 的这一个
*/
//@Autowired
//@Qualifier("userServiceImpl1")
//private UserService userService;
/**
* 报错:当UserService有多个实现类UserServiceImpl1和UserServiceImpl2同时存在时
* 因为@Resource先按照byName匹配,此时的name为“userService”,没有匹配结果.
* 接下来只能按照byType进行配置,此时又匹配到了“userServiceImpl1”和“userServiceImpl2”,结果不唯一,只能抛出异常
*/
//@Resource
//private UserService userService;
/**
* 成功:因为先按照byName匹配,直接匹配到唯一的实现者 userServiceImpl1
*/
//@Resource
//private UserService userServiceImpl1;
/**
* 成功:注解里面的参数name指定了实现者的名称
*/
//@Resource(name = "userServiceImpl1")
//private UserService userService;
public void getName(){
userService.getName();
}
}
四、实体类以及对象bean
lombok里面的注解( ideal需要下载此插件),简化实体javabean工作量,只需要定义私有字段即可,编译时自动生成get/set等方法,如下注解常见
@Setter @Getter 写在类上面是给全部字段生成,写在某字段上只给某字段生成
@EqualsAndHashCode(callSuper = true)
@Accessors(fluent = true) 此注解有三个属性
fluent(值为布尔型),默认false,为true时省略get/set前缀,则是.name()而不是.getName()
chain(值为布尔型),默认false,为true生成的set方法返回this,false时set是void类型
prefix(值为字符型),get/set去掉指定的前缀。如@Accessors(prefix = “user”)是.getName()而不是.getUserName()
@ToString
@Data 集成以上几个注解 ,一般使用这一个注解就足够了。
比如用于接收请求参数的请求对象
import lombok.*;
import lombok.experimental.Accessors;
import java.util.Date;
//@Setter
//@Getter
//@Accessors(fluent = true)
//@ToString
@Data // 用这一个就行了
@EqualsAndHashCode(callSuper = true) // 默认为false, 若new两个user对象里面字段属性值不同,当false时候比较结果相同,当true时比较结果不相同
public class UserDTO {
private Integer id;
private String name;
private Date createDate;
private String notExistStr;
private String sex;
}
所需依赖是
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
实体类的注解,取决于持久层用的是什么技术,比如Hibernate、jpa、mybatis、mybatis-plus等,用的不同,其注解也略有差异。就像数据库mysql、oracle、sqlserver一样,语法总体一样,但个别语法、函数有差别。
对象映射到数据库的数据表
@Entity 说明是实体类,并且使用默认的orm规则(即类名即数据库表中表名,class字段名即表中的字段名)
@Table(name = “t_user”) 用于实体类上 ,Hibernate使用。一般搭配@Entity一起用
@TableName(“t_user”) 用于实体类上 ,mybatis-plus使用
若类名和数据库的表名相同,则可以省略括号;不相同则需用括号来绑定影射表名
@TableName // 说明数据库的表名称就是user,和类名一致
@TableName(“t_user”) // 数据库的表名称就是t_user,和类名user不一致,所以需要告知t_user的字段都映射到User这个实体类。这个比较常见通用
@TableName(value = “t_user”) // 上面是简略写法,这个是完整的写法
表示主键字段
@Id Hibernate使用。一般搭配@GeneratedValue使用
@GeneratedValue(strategy = GenerationType.AUTO) 用于标注主键的生成策略,通过strategy 属性指定策略。默认是AUTO自动增长,全部值如下
@TableId(value = “id”, type = IdType.AUTO) mybatis-plus使用
其中GenerationType的全部枚举如下
IDENTITY:采用数据库ID自增长的方式来自增主键字段,Oracle 不支持这种方式;
AUTO:JPA自动选择合适的策略,是默认选项;
SEQUENCE:通过序列产生主键,通过@SequenceGenerator 注解指定序列名,MySql不支持这种方式
TABLE:通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植。
其中IdType的全部枚举如下
AUTO(0)数据库ID自增,确保数据库设置了 ID自增 否则无效
NONE(1) 为未设置主键类型(注解里等于跟随全局,全局里约等于INPUT)
INPUT(2) 用户输入ID 该类型可以通过自己注册自动填充插件进行填充
以下2种类型、只有当插入对象ID 为空,才自动填充。
ASSIGN_ID(3) 分配ID (主键类型为number或string) 默认实现类 {@link
com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(雪花算法)
ASSIGN_UUID(4) 分配UUID (主键类型为 string) 默认实现类 {@link
com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(UUID.replace(“-”,“”))
表示非主键的普通字段,映射表字段名称和实体变量名称的对应关系。
@TableField 用于变量上,(一般表字段是下划线格式,实体变量是驼峰格式,用于mybatis plus)
属性如下
- exist 表示是否为是数据库字段 非数据库字段设置这个属性为false,不设置则插入查询试会报错
- select 表示是否查询该字段。加上不参与查询,并且返回null
- 其他
@Column 用于变量上面,表示表字段和实体变量的对应关系,类似TableField
其他属性如下
- name 定义了被标注字段在数据库表中所对应字段的名称;
- unique 表示该字段是否为唯一标识,默认为false
- nullable 表示该字段是否可以为null值,默认为true
- length 表示字段的长度,当字段的类型为varchar时,该属性才有效,默认为255个字符
- precision和scale 表示精度,当字段类型为double时,precision表示数值的总长度,scale表示小数点所占的位数。
如果实体变量存在数据库字段,且没其他特殊要求,这两个注解可以省略不写。
@JsonIgnoreProperties(ignoreUnknown = true) 用于类上,用来在实体类序列化和反序列化的时候忽略该字段字段,数据转换时候如果找不到对应的字段会报错,此注解忽略对应不上的,就不会报错了
hebernate实体注解demo如下
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.walmart.aloha.canary.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;
import org.springframework.data.annotation.Transient;
import org.springframework.format.annotation.DateTimeFormat;
import javax.persistence.*;
import javax.validation.constraints.*;
import java.math.BigDecimal;
import java.util.Date;
//@Setter
//@Getter
//@Accessors(fluent = true)
//@ToString
@Data // 用这一个就行了
@EqualsAndHashCode(callSuper = false)
//@DynamicInsert //置为true,表示insert对象的时候,生成动态的insert语句,如果这个字段的值是null就不会加入到insert语句中。
//@DynamicUpdate //设置为true,表示update对象的时候,生成动态的update语句,如果这个字段的值是null就不会被加入到update语句中。
//@Table // 说明数据库的表名称就是user,和类名一致
@Table(name = "t_user") // 只能完整的写法,不能省略name这个属性
@Entity // 有它就得必然有@Id注解
public class User extends BaseEntity {
/**
* 表示这个字段是表的主键
*/
@Id
@GeneratedValue // 默认主键自增(也就是GenerationType.AUTO),所以可以省略
//@GeneratedValue(strategy = GenerationType.AUTO) // 或者指定主键策略
private Long id;
/**
* 这个注解一般不使用,因为数据库是怎么字段名称,我们就定义为什么实体变量
*/
//@Column 一般省略不写
//@Column(name = "user_name", length = 100, nullable = false) // 数据库的字段名称是user_name不是name, 长度不允许超过100, 不允许为空
private String name;
/**
* 表示notExistStr这个字段不存在t_user表里面
* 非表里面的字段需要添加此注解, 否则,ORM框架默认其注解为@Basic,当查询、插入时sql会报错这个字段找不到
*/
@Transient
private String notExistStr;
/**
* 时间类型字段,设置时间和格式。非必须, 但建议使用
* 但没设置时间格式, 返回的值年月日和时分秒之间有个字母T
* 但没设置时间时区,返回的值可能不正确,比如数据库是20210103 00:00:00,但接口返回是20210102 00:00:00,相差了一天
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") // 将Date转换成String
//@DateTimeFormat(pattern = "yyyy-MM-dd") // 将String转换成Date 用这个也可以进行时间格式化
private Date createDate;
/**
* 忽略这个字段
*/
@JsonIgnore
@Enumerated
private String sex;
/**
* 用于变量上,改变返回字段名称,比如实体定义的变量是nn但接口返回的却是ww
*/
@JsonProperty("ww")
private String nn;
// 以下是校验注解
/**
* 最大最小值校验 年纪要求是18-150岁
*/
@Min(18) // 类似的 @DecimalMin("45")
@Max(150) // 类似的 @DecimalMax("45")
// @Size(min = 18 ,max = 150) 或者这样写 在或者用 @Range
private int age;
/**
* 必须是一个过去的时间。比当前时间小
* 类似的还有 @Future 必须是一个未来的时间。比当前时间大
*
* 要求数据类型必须是date类型 表明在持久化过程中将birthday对应的数据存为DATE(即只有“年月日”)
*/
@Past
@Temporal(TemporalType.DATE) // 或者 @Column(columnDefinition="DATE") columnDefinition的值和数据库一样的
private Date birthDay;
/**
* 长度校验
* 正则校验
*/
@Length(min = 11, max = 11, message = "手机号码必须11位")
@Pattern(regexp = "1\d{10}", message = "手机号码格式不正确")
private String mobile;
/**
* 必填,不允许为空
* 必须是邮箱格式
*/
@NotNull(message = "邮箱不能为空")
@Email
private String email;
//@Digits(Integer, fraction) // 必须是数字
//@AssertFalse // 值必须是 false
}
mybatis-plus实体注解demo如下
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.walmart.aloha.canary.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import javax.persistence.Entity;
import java.util.Date;
//@Setter
//@Getter
//@Accessors(fluent = true)
//@ToString
@Data // 用这一个就行了
@EqualsAndHashCode(callSuper = true) // 默认为false, 若new两个user对象里面字段属性值不同,当false时候比较结果相同,当true时比较结果不相同
//@TableName // 说明数据库的表名称就是user,和类名一致
@TableName("t_user") // 推荐用这个。数据库的表名称就是t_user,和类名user不一致,所以需要告知t_user的字段都映射到User这个实体类。
//@TableName(value = "t_user") // 上面是简略写法,这个是完整的写法
public class User extends BaseEntity {
/**
* 表示这个字段是表的主键,AUTO表示是自增的
*/
@TableId(value = "id", type = IdType.AUTO) // 推荐用这个。比较常见
//@TableId(value = "id") // 省略type表示用默认的主键自增
//@TableId // 表示数据库的主键字段名称就id, 和实体变量名id完全一致
private Integer id;
/**
* 这个注解一般不使用,因为数据库是怎么字段名称,我们就定义为什么实体变量
*/
//@TableField // 一般省略不写
//@TableField(value = "user_name") // 说明数据库的字段名是user_name 不是 name
//@TableField(value = "user_name", updateStrategy = FieldStrategy.IGNORED) // 对于更新操作,不做任何处理(可以设置不允许空,不更新这个字段)
private String name;
/**
* 表示notExistStr这个字段不存在t_user表里面
* 非表里面的字段需要添加此注解, 否则查询、插入时sql会报错这个字段找不到
*/
@TableField(exist = false)
private String notExistStr;
/**
* 自动填充值(默认不填充) 在使用 fill 字段填充策略时,需要实现处理才能生效
* 插入操作时,自动填充createTime字段,需要自己写个实现类。参考我写的 MybatisObjectHandler
*/
@TableField(value = "create_time", fill = FieldFill.INSERT)
private Date createTime;
}
填充实现类
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 将MybatisObjectHandler注入spring容器中,操作数据添加或者修改时,自动给指定字段添加数据
*/
@Component
public class MybatisObjectHandler implements MetaObjectHandler {
// 指定的字段名称,表示在insert场合可以操作该方法
@Override
public void insertFill(MetaObject metaObject) {
setFieldValByName("createtime", new Date(),metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
setFieldValByName("updateTime",new Date(),metaObject);
}
}
五、枚举类
基于实体类的注解,枚举也可以省点代码
@NoArgsConstructor //无参注解,也可以用于实体对象
@AllArgsConstructor //全参注解,也可以用于实体对象
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public enum SexEnum {
MAN(1, "男"),
WOMAN(2, "女");
private int code;
private String value;
}
六、配置类
@Value(“${}”) 用于变量上,读取配置文件里面的值赋给给变量
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Setter
@Getter
@Component
public class MyConfig { // 读取配置的类,需要用配置项的service,通过@Autowired MyConfig使用这里的变量
@Value("${my.app.name}") // 取配置文件里面my.app.name的值
private String appName;
@Value("${my.app.name:test}") // 取配置文件里面my.app.name的值,如果取不到默认为test
private String appName1;
}
或者直接读取整个配置文件,映射到字段上面
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration // 表示这是一个配置文件的类
@PropertySource(value="classpath:merchant.properties") // 表示这个类对应的配置文件是merchant.properties
@ConfigurationProperties(prefix="sdk") // 表示配置文件里面的参数都有前缀sdk, 例如sdk.requestUrl=www.123.com
@Setter
@Getter
public class FileConfig {
private String requestUrl;
}
七、启动类注解
直接看启动类代码demo
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication // springboot的启动类注解,包含了@SpringBootConfiguration、@EnableAutoConfiguration 和@ComponentScan,开启了包扫描、配置和自动配置的功能
@Configuration // 相当于传统的xml配置文
@ImportResource(locations = { "classpath:jdbcTemplate.xml", "classpath : yyyy.xml"} }) // 加载文件 若只有一个文件 @ImportSource("classpath : jdbcTemplate.xml")这样也可. 绝对路径写法是 @ImportSource(locations= {"file : d:/test/jdbcTemplate.xml"})
@MapperScan("com.yulisao.dao") // 如果没有,则每个mapper接口类上需添加@Mapper注解
@ServletComponentScan
// 表示将该类自动发现(扫描)并注册为Bean,可以自动收集所有的Spring组件,包括@Configuration类。我们经常使用@ComponentScan注解搜索beans,并结合@Autowired注解导入。
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class, MybatisAutoConfiguration.class}) //排除默认的DatasourceAutoConfiguration
// 以下是在启动类上开启,具体的方法上还有对应的注解与之搭配。若启动类上没开启,则对应的注解不起作用
@EnableAsync // 开启异步
@EnableFeignClients // 开启远程调用Feign
@EnableScheduling // 开启定时任务
@EnableTransactionManagement // 开启事务
@EnableCaching // 开启缓存
@EnableRetry // 开启请求重试机制
public class MyDemoApplication {
public static void main(String[] args) {
SpringApplication.run(MyDemoApplication.class, args);
}
}
八、功能类注解
@EnableFeignClients 用于启动类上,表示启动feign客户端
@FeignClient 用于接口类上,表示这个是feign客户端,无需写实现类。
@FeignClient(name = "yourName")
public interface MyTestClient {
@PostMapping("/***/***/queryCount") // post请求
Long queryCount(@RequestBody ReqDto reqDto);
@GetMapping(value ="/***/***/queryUser") //get请求
Long queryUser(@RequestParam("name") String name);
}
调用的时候
@Autowired
private MyTestClient myTestClient;
方法里面写 Long count = myTestClient.queryCount(reqDto);
@EnableScheduling 用于启动类上,表示项目支持定时任务
@Scheduled 用于方法上,表示该方法定时执行
用法
@Scheduled(fixedRate = 5000) 每隔5秒执行一次
@Scheduled(cron = “0 23 20 ? * *” ) 每天23.20执行一次
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
public class TestService {
@Scheduled(cron = "0/10 * * * * ?") // 每隔10秒执行
//@Scheduled(fixedRate = 5000) //每隔5秒执行一次
public void process() {
System.out.println(new Date());
}
}
@EnableAsync 用于启动类上,表示项目支持异步方法调用
@Async 用于方法上,表示该方法为异步方法,即该方法和调用者不在一个线程中进行
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
public class TestService {
public void test() {
System.out.println("主线程执行中");
process(); // 异步执行后续逻辑
}
@Async
public void process() {
System.out.println(new Date());
}
}
@EnableRetry 用于启动类上, 表示接口支持重试
@Retryable 用于方法上,表示这个方法如果没请求成功会自动重新发起请求。支持设置重试次数,每次的时间间隔。底层通过while循环实现,若不支持幂等性的接口慎用
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import java.util.Date;
@Slf4j
@Service
public class UserService {
/**
* value:抛出异常时触发重试
* include:抛出指定异常时候,触发重试。比如连接超时异常,不同时刻重试有可能成功。他和value一样,默认为空,当exclude也为空时,默认所有异常都触发 exclude = TimeoutException.class
* exclude:抛出指定异常时候,不触发重试。 比如空指针异常一般是程序逻辑或者数据问题,重试再多次也一样失败。 include = {NullPointerException.class, IndexOutOfBoundsException.class}
* maxAttempts:最大重试次数,默认3次。第一次请求也算一次,所以如果失败了,后续还会重试2次
* backoff:重试等待策略,默认使用@Backoff,
* @Backoff的value默认为1000L,单位是毫秒。如下是设置为2000毫秒,表示第一次失败后,休眠2000毫秒进行第一次重试
* @Backoff的multiplier表示指定延迟倍数,默认为0,根据这个计算出第二次、第三次....重试的时间。
* 例如 首次失败后,则第一次重试为2秒,第二次为2*1.5=3秒,第三次为3*1.5=4.5秒
* 建议合理设置重试次数和间隔。设置过多的重试次数可能会导致系统资源浪费,设置过小的间隔可能导致重试失败。
* @param user
* @throws Exception
*/
@Retryable(value = Exception.class, maxAttempts = 5, backoff = @Backoff(delay = 2000L,multiplier = 1.5))
public void add(User user) throws Exception {
System.out.println("执行中,时间是:" + new Date());
// dosomething 业务逻辑略
throw new Exception(); // testing
}
/**
* 上面的最大重试次数maxAttempts用完后还没请求成功,则会执行这个方法(如果没有这个方法则抛出异常结束)
* @Retryable注解会找@Recover注解进行匹配,所以这两个注解修饰的方法的返回类型,入参需要一致才能匹配上,且在同一个类中
* 第一个入参需是异常类,和@Retryable的保持一致即可
* @param e
*/
@Recover
public void recover1(Exception e) {
log.info("回调方法执行,比如发个邮件告警");
}
/**
* 这一个不会被匹配执行, 因为返回数据类型是int,和上面@Retryable注解修饰的add方法的返回类型void不一致
* @param e
* @return
*/
@Recover
public int recover2(Exception e) {
log.info("回调方法执行,比如发个短信告警");
return 0;
}
}
他的所需依赖是
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
@EnableRetry 用于启动类上, 表示支持数据结果缓存
@Cacheable 用于方法上,表示当请求参数完全一致时,就不执行方法了直接从缓存返回结果即可。
@CachePut 类似@Cacheable,但是总会执行方法并把最新结果更新到缓存
@CacheEvict 清理掉缓存
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
/**
* 请求参数一致时,直接从缓存中拿,就不执行方法去查库了
* value / cacheNames 用来指定缓存名,二者选其一即可。 多个缓存空间时,value = {"name1", "name2"}
* key / keyGenerator 用来指定缓存的key是什么 缺省时则会自动生成一个key。
* 比如 key = "#userName" 表示用userName这个入参做key,比如 key = "#p0" 表示用该方法的第一个参数做key,key = "'id-' + #user.id"表示用id-用户id做key,比如 key = "'hash' + #user.hashCode()"用整个user对象做key
* cacheManager / cacheResolver 指定缓存管理器(spring 默认提供是的SimpleCacheResolver)。比如你想用redis的缓存
* sync 是否同步。多线程同时刻调用,都请求到数据库了,也就是稍微的缓存击穿。 设置为ture可以避免
* condition 对请求参数判断在决定是否要进行缓存。 比如请求参数中年纪小于18的不缓存 condition = ”#age > 18“
* unless 对返回参数判断在决定是否要进行缓存。 比如返回结果中年纪小于18的不缓存 unless = ”#age < 18“
*
* @param userName
* @return
*/
@Cacheable(value = {"yourCacheName"}, key = "#userName")
public User queryUser(String userName) {
return null;
}
// 会执行方法查库并把最新结果存缓存里 根据用户的id作为key缓存在yourCacheName里面,但是用户名称是zhang的数据不缓存起来
@CachePut(value = {"yourCacheName"}, key = "#user.id", unless = "#result.username eq 'zhang'")
public User queryUser2(User user) {
return null;
}
//beforeInvocation=false表示在方法执行之后且用户名称是zhang则移除缓存
@CacheEvict(value = {"yourCacheName"}, key = "#user.id", beforeInvocation = false, condition = "#result.username eq 'zhang'")
public User queryUser3(User user) {
return null;
}
@EnableTransactionManagement 用于启动类上, 表示项目支持事务
@Transactional用于方法上
用法
@Transactional
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
@ServletComponentScan 用于启动类上,
Servlet、Filter、Listener可以直接通过@WebServlet、@WebFilter、@WebListener注解自动注册实现
@WebServlet(name="TestServlet",urlPatterns="/mytest")
public class TestServlet extends HttpServlet {
}
写法很多
@ComponentScan({“com.yulisao.dao”,“com.yulisao.dao1”})
@ComponentScan(basePackages = {“com.yulisao.dao”,“com.yulisao.dao1”})
@ComponentScan(“com.yulisao”)
@ComponentScan(value = “com.yulisao”)
@ComponentScan(basePackageClasses = 要扫描类.class)
测试类注解
写一个单一的测试类,下面的执行顺序是 @BeforeClass -> @Before -> @Test -> @After -> @Before -> @Test -> @After -> @AfterClass
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Before;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import java.io.IOException;
@AfterClass
@RunWith(SpringRunner.class) // 固定
//@TestPropertySource("classpath:${test.prop:my-test.properties}") // 使用的配置文件路径
@AutoConfigureMockMvc // mvc的web项目需要
@SpringBootTest // 也可以加上 (classes = { 启动类.class })
public class MyTest {
protected final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
protected MockMvc mockMvc;
protected MockHttpSession mockHttpSession;
/**
* 整个测试类第一个执行,只执行一次,且方法必然被static修饰
* @throws IOException
*/
// @BeforeAll Junit 5写法
@BeforeClass // Junit 4写法
public static void beforeClass() throws IOException {
System.out.println("运行开始时只执行一次 beforeClass ");
}
/**
* 每个@test方法执行之前都会被执行一次
* @throws IOException
*/
//@BeforeEach // Junit 5写法
@Before("") // Junit 4写法
public void Before() throws IOException {
System.out.println("每个方法执行之前都执行一次 Before ");
}
/**
* 测试方法,一个类中可以有多个测试方法,按从上至下顺序执行
* @throws Exception
*/
@Test
public void test1() throws Exception {
System.out.println("测试方法1");
}
@Test
public void test2() throws Exception {
System.out.println("测试方法2");
}
/**
* 每个@test方法执行之后都会被执行一次
* @throws IOException
*/
//@AfterEach // Junit 5写法
@After("") // Junit 4写法
public void After() throws IOException {
System.out.println("每个方法执行之后都执行一次 After ");
}
/**
* 整个测试类最后已个执行,只执行一次,且方法必然被static修饰
* @throws IOException
*/
// @AfterAll Junit 5写法
@AfterClass
public static void afterClass() throws IOException {
System.out.println("运行结束时只执行一次 afterClass ");
}
}
或者写一个抽象的测试类(里面可以写点公共通用的方法、日志打印等), 测试不同的功能时候继承实现这个抽象测试类
import org.junit.jupiter.api.BeforeEach;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
@RunWith(SpringRunner.class) // 固定
//@TestPropertySource("classpath:${test.prop:my-test.properties}") // 使用的配置文件路径
@AutoConfigureMockMvc // mvc的web项目需要
@SpringBootTest // 也可以加上 (classes = { 启动类.class })
public abstract class MyTest {
protected final Logger LOG = LoggerFactory.getLogger(getClass());
@Autowired
protected MockMvc mockMvc;
protected MockHttpSession mockHttpSession;
@BeforeEach
public void setupMockMvc() {
mockHttpSession = new MockHttpSession();
}
/**
* 发起请求的公共方法
* @param url 请求地址
* @param content 请求参数
* @return
* @throws Exception
*/
protected String post(String url, String content) throws Exception {
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post(url)
.accept(MediaType.ALL).session(mockHttpSession)
.contentType(MediaType.APPLICATION_JSON).content(content);
MockHttpServletResponse response = mockMvc.perform(builder).andReturn().getResponse();
response.setCharacterEncoding("UTF-8");
return response.getContentAsString();
}
}
#### 实现者 ###
import com.alibaba.fastjson.JSONObject;
import com.yulisao.MyTest;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.springframework.beans.factory.annotation.Autowired;
public class UserControllerTest extends MyTest {
/**
* 实现类里面直接注入 Controller 进行调用, 跟调用service方法类似
*/
@Autowired
@InjectMocks
UserController userController;
@Test
public void addUser() throws Exception {
// 组装请求参数
User param = new User();
param.setUserName("aa");
// 直接通过 Controller调用 发起请求
userController.addUser(param);
// 或者 模拟调用接口地址 发起请求
post("/user/addUser", JSONObject.toJSONString(param));
}
}
切面类注解(功能类似拦截器)
@Aspect 用于类上, 声明这个是一个切面类
@Pointcut 定义切入点
@Around 定义切入的功能方法
比如为全部请求打印请求返回参数并登记数据库形成请求日志
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.yulisao.common.http.RespBody;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
@Aspect // 声明是切面类
@Configuration
public class ControllerLogAspect {
private static final Logger LOG = LoggerFactory.getLogger(ControllerLogAspect.class);
@Autowired
private LogsService logsService;
// 拦截全部action请求,也就是项目里面的全部的控制类
final String regex = "execution(* com.yulisao.controller..*.*(..))";
// 定义切点Pointcut
@Pointcut(regex)
public void excudeService() {
}
// 切入后在这里做什么事情,实现什么功能
@Around("excudeService()") // 上面切点注解修饰的方法名称以及入参
public Object Around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
RespBody<?> resp = null; // 全部控制类的返回结果的公共部分 ?表示每个接口返回的具体对象数据
try {
LOG.info("执行目标方法之前...");
Date startTime = new Date();
resp = (RespBody<?>)proceedingJoinPoint.proceed(); // 执行目标方法
Date endTime = new Date();
LOG.info("执行目标方法之后...");
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
LOG.info("目标对象为:" + proceedingJoinPoint.getTarget());
LOG.info("返回结果为:" + resp.getMessage());
// 组装参数入库记录保存
LogsAspectDTO logsAspectDTO = new LogsAspectDTO();
logsAspectDTO.setArgs(proceedingJoinPoint.getArgs());
logsAspectDTO.setRequest(request);
logsAspectDTO.setStartTime(startTime);
logsAspectDTO.setEndTime(endTime);
logsAspectDTO.setResult(resp.getMessage());
logsService.insertOperateLogs(logsAspectDTO);
} catch (Exception e) {
LOG.error("环绕增强方法异常:{}",e);
}finally {
return resp;
}
}
}
再比如请求次数限制注解拦截切面
import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringMap;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Aspect
@Component
public class LimitRequestAspect {
private Logger log = LoggerFactory.getLogger(this.getClass());
private static ConcurrentHashMap<String, ExpiringMap<String, Integer>> book = new ConcurrentHashMap<>();
private List<String> spcUrlList = Arrays.asList(
"/file/upload",
"/register"
);
@Pointcut("@annotation(limitRequest)") // 切入点是@LimitRequest这个注解
public void excudeService(LimitRequest limitRequest) {
}
@Around("excudeService(limitRequest)") // 上面的方法名称以及入参limitRequest
public Object doAround(ProceedingJoinPoint pjp, LimitRequest limitRequest) throws Throwable {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
String ip = getIpAddr(request);
String url = request.getServletPath();
log.info("request url is " + url);
log.info("request ip is " + ip);
String prefix = getPathPrefix(url);
if (StringUtils.isNotBlank(prefix)) {
url = prefix;
}
ExpiringMap<String, Integer> uc = book.getOrDefault(url, ExpiringMap.builder().variableExpiration().build());
Integer uCount = uc.getOrDefault(ip, 0);
log.info("request uCount is " + uCount);
if (uCount >= limitRequest.count()) { // 超过次数,不执行目标方法
throw new Exception("请求过于频繁,请稍后在试");
} else if (uCount == 0){ // 第一次请求时,设置有效时间
uc.put(ip, uCount + 1, ExpirationPolicy.CREATED, limitRequest.time(), TimeUnit.MILLISECONDS);
} else { // 未超过次数, 记录加一
uc.put(ip, uCount + 1);
}
book.put(url, uc);
return pjp.proceed();
}
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
if (inet!=null)
ipAddress = inet.getHostAddress();
}
}
if (ipAddress != null && ipAddress.length() > 15) {
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
return ipAddress;
}
private String getPathPrefix(String url) {
for(int i=0; i < spcUrlList.size(); i++){
Pattern pattern = Pattern.compile(spcUrlList.get(i));
Matcher matcher = pattern.matcher(url);
if(matcher.find()){
return spcUrlList.get(i);
}
}
return null;
}
}
配置文件类注解
@ActiveProfiles 一般用于测试类上,表示使用哪个配置文件
@Profiles 用于类或者方法上面,表示是配置之一
@ActiveProfiles("dev") // 表示使用 dev 这个配置
public class MyTest {
@Test
public void test() {
action.execute("use dev config");
}
}
#### 配置文件, 也可能是写在pom文件里面 ###
@Configuration
public class ProjectConfig {
@Bean
@Profile("dev") // 开发环境配置
public AppConfig config1(){
// 略
}
@Bean
@Profile("uat") // uat环境配置
public AppConfig config2(){
// 略
}
}
sql查询类注解
# Repository
@Query("from t_user p where p.name = ?1 and p.age = ?2")
User queryUser(String name, String age); // 顺序必须和上面一致
@Query("from t_user p where p.name = :name and p.age = :age")
User queryUser(@Param("age") Integer age, @Param("name") String name); // 顺序不要求上面一致
@Query(value = "select count(*) from t_user p where p.name = :name and p.age = :age", nativeQuery = true) // nativeQuery = true表示支持原生sql查询
Long queryUserCount(@Param("age") Integer age, @Param("name") String name);
# mybatis 类似的还有@insert @update @delete
@Select("<script>select * from t_user where age=#{age} <if test='name != null'> and name=#{name} </if> order by create_time desc </script>")
User queryUser(@Param("age") Integer age, @Param("name") String name);
注解内的注解(自己写自定义注解时候才会用到)
@Documented:表示该注解是否可以生成到 API文档中。可以理解为导出API文档时文档化
@Target :用于描述注解的使用范围
@Target 配合ElementType枚举类型一起使用。
TYPE : 类型上面 用于描述类、接口(包括注解类型) 或enum声明
FIELD : 用于描述字段
METHOD :方法
PARAMETER : 参数 【参数名】
CONSTRUCTOR : 构造方法
LOCAL_VARIABLE : 局部变量
ANNOTATION_TYPE : 可以打在注解上面
PACKAGE :可以打在包上面
TYPE_PARAMETER : 参数类型【形式参数类型】
TYPE_USE : 任何位置都可以
@Retention:用于描述注解的生命周期,也就是说这个注解在什么范围内有效。
@Retention配合RetentionPolicy枚举类型一起使用。
SOURCE :源码中
CLASS :表示 一个注解可以在源码中,并且可以在字节码文件中
RUNTIME :表示 一个注解可以在源码、字节码、及运行时期该注解都会存在
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface TransactionCode {
String value() default "";
}
如上的Target 表示作用于方法之内,Retention表示运行时候也有效。