本系列文章将简单的学习SpringCloud微服务相关知识,其实也是因为时间的原因,一直拖到现在,遂打算趁着假期,决定记录下来。
从天气预报微服务系统的单体架构——>分布式架构的演变过程中,一步一步,由浅及深的学习SpringCloud微服务的思想与其实现的组件。
本系列文章分为以下几个章节:
项目源码已上传至Github.
//依赖关系
dependencies {
//该依赖用于编译阶段
compile('org.springframework.boot:spring-boot-starter-web')
//HttpClient
compile('org.apache.httpcomponents:httpclient:4.5.3')
//Redis
compile('org.springframework.boot:spring-boot-starter-data-redis')
//Quartz
compile('org.springframework.boot:spring-boot-starter-quartz')
//该依赖用于测试阶段
testCompile('org.springframework.boot:spring-boot-starter-test')
}
Quartz,是任务定时框架。早些时候,我有简单的介绍过它。SpringBoot也对Quartz进行了良好的集成,让我们可以很方便的使用它。
在天气预报系统中,我们为什么要用到定时任务呢?其实上一篇在介绍Redis的时候,就已经介绍过了。Quartz结合Redis,定时请求天气数据,存入缓存。
其实在本章节中,还有重要的一步,便是读取XML文件中的城市列表,将其转换为实体类,用Quartz和Redis一起来同步所有城市的数据。
http://mobile.weather.com.cn/js/citylist.xml
很遗憾,现在已经打不开这个链接了。
但是,依然找到了部分的城市列表XML(广东省)。
位于项目资源目录下,之后会使用到。
<c c1="0">
<d d1="101280101" d2="广州" d3="guangzhou" d4="广东"/>
<d d1="101280102" d2="番禺" d3="panyu" d4="广东"/>
<d d1="101280103" d2="从化" d3="conghua" d4="广东"/>
<d d1="101280104" d2="增城" d3="zengcheng" d4="广东"/>
<d d1="101280105" d2="花都" d3="huadu" d4="广东"/>
<d d1="101280201" d2="韶关" d3="shaoguan" d4="广东"/>
<d d1="101280202" d2="乳源" d3="ruyuan" d4="广东"/>
<d d1="101280203" d2="始兴" d3="sJhixing" d4="广东"/>
<d d1="101280204" d2="翁源" d3="wengyuan" d4="广东"/>
<d d1="101280205" d2="乐昌" d3="lechang" d4="广东"/>
<d d1="101280206" d2="仁化" d3="renhua" d4="广东"/>
<d d1="101280207" d2="南雄" d3="nanxiong" d4="广东"/>
.....
c>
我们可以从中创建两个实体类,这个涉及到Xml字符串与Java对象之间的转换(可以用[JAXB实现](https://blog.csdn.net/qq_33764491/article/details/79582880))。
/**
* @Author: cfx
* @Description: 将Xml解析成对应字段@XmlAccessorType(XmlAccessType.FIELD)通过字段访问
* @Date: Created in 2018/4/5 22:14
*/
@XmlRootElement(name = "d")
@XmlAccessorType(XmlAccessType.FIELD)
public class City {
@XmlAttribute(name = "d1")
private String cityId;
@XmlAttribute(name = "d2")
private String cityName;
@XmlAttribute(name = "d3")
private String cityCode;
@XmlAttribute(name = "d4")
private String province;
...
/**
* @Author: cfx
* @Description: 城市列表
* @Date: Created in 2018/4/5 22:22
*/
@XmlRootElement(name = "c")
@XmlAccessorType(XmlAccessType.FIELD)
public class CityList {
@XmlElement(name = "d")
private List cityList;
...
public interface CityDataService {
/**
* 获取City列表
* @return
* @throws Exception
*/
List listCity() throws Exception;
}
public class CityDataServiceImpl implements CityDataService {
private static final Logger logger = LoggerFactory.getLogger(CityDataServiceImpl.class);
@Override
public List listCity() throws Exception {
//读取xml文件
Resource resource = new ClassPathResource("citylist.xml");
BufferedReader br = new BufferedReader(new InputStreamReader
(resource.getInputStream(),"utf-8"));
StringBuffer buffer = new StringBuffer();
String line = "";
while ((line = br.readLine()) != null) {
buffer.append(line);
}
br.close();
//把xml转为java对象
CityList cityList = (CityList)XmlBuilder.xmlStrToObject(CityList.class,buffer.toString());
return cityList.getCityList();
}
}
public class XmlBuilder {
/**
* 将XML转换为指定的POJO
* @param clazz
* @param xmlStr
* @return
* @throws Exception
*/
public static Object xmlStrToObject(Class> clazz,String xmlStr) throws Exception {
Object xmlObject = null;
Reader reader = null;
JAXBContext context = JAXBContext.newInstance(clazz);
//XML转为对象接口
Unmarshaller unmarshaller = context.createUnmarshaller();
reader = new StringReader(xmlStr);
xmlObject = unmarshaller.unmarshal(reader);
if (null != reader) {
reader.close();
}
return xmlObject;
}
}
如果你了解Quartz,就会知道它的三大要素:Job、Trigger、Schedule。
定义一个Job是用来说明具体要定时的任务是什么。
在天气预报系统中,我们的定时任务是定时获取所有城市的天气任务。
所以需要在WeatherDataService接口中定义根据城市ID同步天气的方法。
* WeatherDataService
public interface WeatherDataService {
/**
* 根据城市Id查询天气
* @param cityId
* @return
*/
WeatherResponse getDataByCityId(String cityId);
/**
* 根据城市名称查询天气
* @param cityName
* @return
*/
WeatherResponse getDataByCityName(String cityName);
/**
* 根据城市Id来同步天气
* @param cityId
*/
void syncDataByCityId(String cityId);
}
@Service
public class WeatherDataServiceImpl implements WeatherDataService {
private static final Logger logger = LoggerFactory.getLogger(WeatherDataServiceImpl.class);
private static final String WEATHER_URI = "http://wthrcdn.etouch.cn/weather_mini?";
private static final long TIME_OUT = 1800L;//1800s
@Autowired
private RestTemplate restTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public WeatherResponse getDataByCityId(String cityId) {
String uri = WEATHER_URI + "citykey=" + cityId;
return this.doGetWeather(uri);
}
@Override
public WeatherResponse getDataByCityName(String cityName) {
String uri = WEATHER_URI + "city=" + cityName;
return this.doGetWeather(uri);
}
@Override
public void syncDataByCityId(String cityId) {
String uri = WEATHER_URI + "citykey=" + cityId;
this.saveWeatherData(uri);
}
/**
* 把天气数据放入缓存
* 相当于更新缓存
* @param uri
*/
private void saveWeatherData(String uri) {
String key = uri;
String strBody = null;
ValueOperations ops = stringRedisTemplate.opsForValue();
//调用服务接口来获取
ResponseEntity respString = restTemplate.getForEntity(uri,String.class);
//将接口返回的Json字符串转换成对象
if (respString.getStatusCodeValue() == 200) {
strBody = respString.getBody();
}
//数据写入缓存
ops.set(uri,strBody,TIME_OUT, TimeUnit.SECONDS);
}
/**
* 重构代码
* @param uri
* @return
*/
private WeatherResponse doGetWeather(String uri) {
String key = uri;
String strBody = null;
ObjectMapper mapper = new ObjectMapper();
WeatherResponse resp = null;
ValueOperations ops = stringRedisTemplate.opsForValue();
//先查缓存,如果缓存中有天气信息就在缓存中取
if (stringRedisTemplate.hasKey(key)) {
logger.info("Redis has data");
strBody = ops.get(key);
} else {
logger.info("Redis don't has data");
//如果缓存没有,再去调用服务接口来获取
ResponseEntity respString = restTemplate.getForEntity(uri,String.class);
//将接口返回的Json字符串转换成对象
if (respString.getStatusCodeValue() == 200) {
strBody = respString.getBody();
}
//数据写入缓存
ops.set(uri,strBody,TIME_OUT, TimeUnit.SECONDS);
}
try {
resp = mapper.readValue(strBody,WeatherResponse.class);
} catch (IOException e) {
logger.info("Error!!",e);
}
return resp;
}
}
public class WeatherDataSyncJob extends QuartzJobBean{
private static final Logger logger = LoggerFactory.getLogger(WeatherDataSyncJob.class);
@Autowired
private CityDataService cityDataService;
@Autowired
private WeatherDataService weatherDataService;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
logger.info("天气数据同步Job,Start!");
//获取城市ID列表
List cityList = null;
try {
cityList = cityDataService.listCity();
} catch (Exception e) {
logger.error("Exception!!",e);
}
//遍历城市ID获取天气
for (City city : cityList) {
String cityId = city.getCityId();
logger.info("天气数据同步Job,cityId:" + cityId);
weatherDataService.syncDataByCityId(cityId);
}
logger.info("天气数据同步Job,End!");
}
}
@Configuration
public class QuartzConfiguration {
private static final int TIME = 1800;//更新频率
//JobDetail
@Bean
public JobDetail weatherDataSyncJobDetail() {
return JobBuilder.newJob(WeatherDataSyncJob.class).withIdentity("weatherDataSyncJob")
.storeDurably().build();
}
//Trigger触发器
//还可以使用CronScheduleBuilder来自定义cron表达式,更加灵活
@Bean
public Trigger weatherDataSyncTrigger() {
SimpleScheduleBuilder schedBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(TIME).repeatForever();
return TriggerBuilder.newTrigger().forJob(weatherDataSyncJobDetail())
.withIdentity("weatherDataSyncTrigger").withSchedule(schedBuilder).build();
}
}